diff --git a/.changeset/good-plums-fail.md b/.changeset/good-plums-fail.md new file mode 100644 index 00000000000..892abf1c4bb --- /dev/null +++ b/.changeset/good-plums-fail.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme': patch +'@shopify/app': patch +--- + +Bump Shopify/theme-tools packages diff --git a/.changeset/nervous-terms-invite.md b/.changeset/nervous-terms-invite.md new file mode 100644 index 00000000000..b3accf4acc2 --- /dev/null +++ b/.changeset/nervous-terms-invite.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme': minor +'@shopify/cli': minor +--- + +Developers can now use the `shopify theme metafields pull` command to download metafields, which can then be used for more refined code completion. diff --git a/.changeset/quick-eggs-end.md b/.changeset/quick-eggs-end.md new file mode 100644 index 00000000000..02aaa148b7e --- /dev/null +++ b/.changeset/quick-eggs-end.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Introduce method to fetch metafield definitions by ownerType from Admin API diff --git a/.changeset/seven-seahorses-bake.md b/.changeset/seven-seahorses-bake.md new file mode 100644 index 00000000000..9117786fef0 --- /dev/null +++ b/.changeset/seven-seahorses-bake.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Use Shopify Functions Javy plugin instead of default Javy plugin for building JS Shopify Functions diff --git a/.changeset/shaggy-cheetahs-think.md b/.changeset/shaggy-cheetahs-think.md new file mode 100644 index 00000000000..e1fef71e286 --- /dev/null +++ b/.changeset/shaggy-cheetahs-think.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': patch +--- + +Bump cli-hydrogen package to 9.0.3 diff --git a/.changeset/silver-mice-thank.md b/.changeset/silver-mice-thank.md new file mode 100644 index 00000000000..3cb763c622f --- /dev/null +++ b/.changeset/silver-mice-thank.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-kit': patch +'@shopify/app': patch +--- + +Remove all template lockfiles, except the one used to install dependencies diff --git a/packages/app/src/cli/constants.ts b/packages/app/src/cli/constants.ts index 1f9a91d1319..c3ddd8b2bfe 100644 --- a/packages/app/src/cli/constants.ts +++ b/packages/app/src/cli/constants.ts @@ -11,6 +11,8 @@ export const configurationFileNames = { web: 'shopify.web.toml', appEnvironments: 'shopify.environments.toml', lockFile: '.shopify.lock', + hiddenConfig: 'project.json', + hiddenFolder: '.shopify', } as const export const dotEnvFileNames = { diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 082522d78a6..f3418214c28 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -119,6 +119,7 @@ export function testApp(app: Partial = {}, schemaType: 'current' | specifications: app.specifications ?? [], configSchema: (app.configSchema ?? AppConfigurationSchema) as any, remoteFlags: app.remoteFlags ?? [], + hiddenConfig: app.hiddenConfig ?? {}, }) if (app.updateDependencies) { diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index 106210c3625..ba603bdc3a5 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -11,10 +11,11 @@ import {UIExtensionSchema} from '../extensions/specifications/ui_extension.js' import {Flag} from '../../utilities/developer-platform-client.js' import {AppAccessSpecIdentifier} from '../extensions/specifications/app_config_app_access.js' import {WebhookSubscriptionSchema} from '../extensions/specifications/app_config_webhook_schemas/webhook_subscription_schema.js' +import {configurationFileNames} from '../../constants.js' import {ZodObjectOf, zod} from '@shopify/cli-kit/node/schema' import {DotEnvFile} from '@shopify/cli-kit/node/dot-env' import {getDependencies, PackageManager, readAndParsePackageJson} from '@shopify/cli-kit/node/node-package-manager' -import {fileRealPath, findPathUp} from '@shopify/cli-kit/node/fs' +import {fileRealPath, findPathUp, writeFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' import {normalizeDelimitedString} from '@shopify/cli-kit/common/string' @@ -80,6 +81,15 @@ export const AppSchema = zod.object({ web_directories: zod.array(zod.string()).optional(), }) +/** + * Hidden configuration for an app. Stored inside ./shopify/project.json + * This is a set of values that are needed by the CLI that are not part of the app configuration. + * These are not meant to be git tracked and the user doesn't need to know about their existence. + */ +export interface AppHiddenConfig { + dev_store_url?: string +} + /** * Utility schema that matches freshly minted or normal, linked, apps. */ @@ -179,6 +189,10 @@ export function usesLegacyScopesBehavior(config: AppConfiguration) { return false } +export function appHiddenConfigPath(appDirectory: string) { + return joinPath(appDirectory, configurationFileNames.hiddenFolder, configurationFileNames.hiddenConfig) +} + /** * Get the field names from the configuration that aren't found in the basic built-in app configuration schema. */ @@ -256,6 +270,7 @@ export interface AppInterface< realExtensions: ExtensionInstance[] draftableExtensions: ExtensionInstance[] errors?: AppErrors + hiddenConfig: AppHiddenConfig includeConfigOnDeploy: boolean | undefined updateDependencies: () => Promise extensionsForType: (spec: {identifier: string; externalIdentifier: string}) => ExtensionInstance[] @@ -274,6 +289,7 @@ export interface AppInterface< creationDefaultOptions(): AppCreationDefaultOptions manifest: () => Promise removeExtension: (extensionUid: string) => void + updateHiddenConfig: (values: Partial) => Promise } type AppConstructor< @@ -290,6 +306,7 @@ type AppConstructor< errors?: AppErrors specifications: ExtensionSpecification[] remoteFlags?: Flag[] + hiddenConfig: AppHiddenConfig } export class App< @@ -311,6 +328,7 @@ export class App< configSchema: ZodObjectOf> remoteFlags: Flag[] realExtensions: ExtensionInstance[] + hiddenConfig: AppHiddenConfig constructor({ name, @@ -326,6 +344,7 @@ export class App< specifications, configSchema, remoteFlags, + hiddenConfig, }: AppConstructor) { this.name = name this.directory = directory @@ -340,6 +359,7 @@ export class App< this.specifications = specifications this.configSchema = configSchema ?? AppSchema this.remoteFlags = remoteFlags ?? [] + this.hiddenConfig = hiddenConfig } get allExtensions() { @@ -388,6 +408,11 @@ export class App< this.nodeDependencies = nodeDependencies } + async updateHiddenConfig(values: Partial) { + this.hiddenConfig = {...this.hiddenConfig, ...values} + await writeFile(appHiddenConfigPath(this.directory), JSON.stringify(this.hiddenConfig, null, 2)) + } + async preDeployValidation() { const functionExtensionsWithUiHandle = this.allExtensions.filter( (ext) => ext.isFunctionExtension && (ext.configuration as unknown as FunctionConfigType).ui?.handle, diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index eb340a4fde2..6ac17d41d04 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -17,6 +17,8 @@ import { SchemaForConfig, AppCreationDefaultOptions, AppLinkedInterface, + appHiddenConfigPath, + AppHiddenConfig, } from './app.js' import {showMultipleCLIWarningIfNeeded} from './validation/multi-cli-warning.js' import {configurationFileNames, dotEnvFileNames} from '../../constants.js' @@ -33,7 +35,7 @@ import {WebhooksSchema} from '../extensions/specifications/app_config_webhook_sc import {loadLocalExtensionsSpecifications} from '../extensions/load-specifications.js' import {UIExtensionSchemaType} from '../extensions/specifications/ui_extension.js' import {deepStrict, zod} from '@shopify/cli-kit/node/schema' -import {fileExists, readFile, glob, findPathUp, fileExistsSync} from '@shopify/cli-kit/node/fs' +import {fileExists, readFile, glob, findPathUp, fileExistsSync, writeFile, mkdir} from '@shopify/cli-kit/node/fs' import {readAndParseDotEnv, DotEnvFile} from '@shopify/cli-kit/node/dot-env' import { getDependencies, @@ -342,6 +344,8 @@ class AppLoader { + const hiddenConfigPath = appHiddenConfigPath(appDirectory) + if (fileExistsSync(hiddenConfigPath)) { + return JSON.parse(await readFile(hiddenConfigPath, {encoding: 'utf8'})) + } else { + // If the hidden config file doesn't exist, create an empty one. + await mkdir(dirname(hiddenConfigPath)) + await writeFile(hiddenConfigPath, '{}') + return {} + } +} + export async function loadAppName(appDirectory: string): Promise { const packageJSONPath = joinPath(appDirectory, 'package.json') return (await getPackageName(packageJSONPath)) ?? basename(appDirectory) diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index 3b88ec56922..d8d77bc28d7 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -102,8 +102,9 @@ async function prepareForDev(commandOptions: DevOptions): Promise { organization: commandOptions.organization, }) - // Update the dev_store_url in the app configuration if it doesn't match the store domain - if (app.configuration.build?.dev_store_url !== store.shopDomain) { + // If the dev_store_url is set in the app configuration, keep updating it. + // If not, `store-context.ts` will take care of caching it in the hidden config. + if (app.configuration.build?.dev_store_url) { app.configuration.build = { ...app.configuration.build, dev_store_url: store.shopDomain, diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 9ec1f0057c9..8f1b72d3c6f 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -153,7 +153,10 @@ class AppInfo { ['App name', this.remoteApp.title || NOT_CONFIGURED_TEXT], ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TEXT], ['Access scopes', getAppScopes(this.app.configuration)], - ['Dev store', this.app.configuration.build?.dev_store_url || NOT_CONFIGURED_TEXT], + [ + 'Dev store', + this.app.configuration.build?.dev_store_url ?? this.app.hiddenConfig.dev_store_url ?? NOT_CONFIGURED_TEXT, + ], ['Update URLs', updateUrls], partnersAccountInfo, ] diff --git a/packages/app/src/cli/services/store-context.ts b/packages/app/src/cli/services/store-context.ts index c9d2ea9df88..2b1a4cfc896 100644 --- a/packages/app/src/cli/services/store-context.ts +++ b/packages/app/src/cli/services/store-context.ts @@ -3,8 +3,11 @@ import {convertToTransferDisabledStoreIfNeeded, selectStore} from './dev/select- import {LoadedAppContextOutput} from './app-context.js' import {OrganizationStore} from '../models/organization.js' import metadata from '../metadata.js' +import {configurationFileNames} from '../constants.js' import {hashString} from '@shopify/cli-kit/node/crypto' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {joinPath} from '@shopify/cli-kit/node/path' +import {appendFile, readFile} from '@shopify/cli-kit/node/fs' /** * Input options for the `storeContext` function. @@ -34,8 +37,13 @@ export async function storeContext({ const {app, organization, developerPlatformClient} = appContextResult let selectedStore: OrganizationStore + const devStoreUrlFromAppConfig = app.configuration.build?.dev_store_url + const devStoreUrlFromHiddenConfig = app.hiddenConfig.dev_store_url + + const cachedStoreURL = devStoreUrlFromAppConfig ?? devStoreUrlFromHiddenConfig + // If forceReselectStore is true, ignore the cached storeFqdn in the app configuration. - const cachedStoreInToml = forceReselectStore ? undefined : app.configuration.build?.dev_store_url + const cachedStoreInToml = forceReselectStore ? undefined : cachedStoreURL // An explicit storeFqdn has preference over anything else. const storeFqdnToUse = storeFqdn ?? cachedStoreInToml @@ -53,6 +61,12 @@ export async function storeContext({ await logMetadata(selectedStore, forceReselectStore) selectedStore.shopDomain = await normalizeStoreFqdn(selectedStore.shopDomain) + // Save the selected store in the hidden config file + if (selectedStore.shopDomain !== cachedStoreURL || !devStoreUrlFromHiddenConfig) { + await app.updateHiddenConfig({dev_store_url: selectedStore.shopDomain}) + await addHiddenConfigToGitIgnoreIfNeeded(app.directory) + } + return selectedStore } @@ -66,3 +80,16 @@ async function logMetadata(selectedStore: OrganizationStore, resetUsed: boolean) store_fqdn: selectedStore.shopDomain, })) } + +/** + * Adds the hidden config folder to the .gitignore file if it's not already there. + * + * This should be part of a larger mitration in the future. + */ +async function addHiddenConfigToGitIgnoreIfNeeded(appDirectory: string) { + const gitIgnorePath = joinPath(appDirectory, '.gitignore') + const gitIgnoreContent = await readFile(gitIgnorePath) + if (!gitIgnoreContent.includes(configurationFileNames.hiddenFolder)) { + await appendFile(gitIgnorePath, `\n${configurationFileNames.hiddenFolder}/*\n`) + } +}