diff --git a/packages/theme/src/cli/services/package.test.ts b/packages/theme/src/cli/services/package.test.ts index 04a07462ef4..24300fbc4b6 100644 --- a/packages/theme/src/cli/services/package.test.ts +++ b/packages/theme/src/cli/services/package.test.ts @@ -15,9 +15,11 @@ describe('packageTheme', () => { const inputDirectory = joinPath(tmpDir, 'theme') await mkdir(inputDirectory) const themeRelativePaths = [ + 'AGENTS.md', 'assets/base.css', 'layout/theme.liquid', 'config/settings_schema.json', + 'config/styles.css', 'listings/canine-gourmand/sections/header-group.json', 'listings/dawn/templates/index.json', 'release-notes.md', diff --git a/packages/theme/src/cli/services/package.ts b/packages/theme/src/cli/services/package.ts index b0188516dd1..239865e718d 100644 --- a/packages/theme/src/cli/services/package.ts +++ b/packages/theme/src/cli/services/package.ts @@ -6,6 +6,7 @@ import {resolvePath, relativizePath} from '@shopify/cli-kit/node/path' import {parseJSON} from '@shopify/theme-check-node' const themeFilesPattern = [ + 'AGENTS.md', 'assets/**', 'blocks/**', 'config/**', diff --git a/packages/theme/src/cli/utilities/fixtures/theme/AGENTS.md b/packages/theme/src/cli/utilities/fixtures/theme/AGENTS.md new file mode 100644 index 00000000000..1d7f0819b89 --- /dev/null +++ b/packages/theme/src/cli/utilities/fixtures/theme/AGENTS.md @@ -0,0 +1,3 @@ +# Theme instructions + +Keep the theme accessible. diff --git a/packages/theme/src/cli/utilities/fixtures/theme/config/styles.css b/packages/theme/src/cli/utilities/fixtures/theme/config/styles.css new file mode 100644 index 00000000000..bdad4af40c3 --- /dev/null +++ b/packages/theme/src/cli/utilities/fixtures/theme/config/styles.css @@ -0,0 +1,3 @@ +:root { + --color-primary: #000; +} diff --git a/packages/theme/src/cli/utilities/theme-fs.test.ts b/packages/theme/src/cli/utilities/theme-fs.test.ts index 8c09df84edd..f2ded99a372 100644 --- a/packages/theme/src/cli/utilities/theme-fs.test.ts +++ b/packages/theme/src/cli/utilities/theme-fs.test.ts @@ -66,16 +66,18 @@ describe('theme-fs', () => { // Then expect(themeFileSystem.root).toBe(root) - expect(themeFileSystem.files.size).toBe(10) + expect(themeFileSystem.files.size).toBe(12) expect(themeFileSystem.unsyncedFileKeys).toEqual(new Set()) expect(themeFileSystem.uploadErrors).toEqual(new Map()) // Check that all expected files are present with correct checksums const expectedFiles = [ + {checksum: '6e3520cc5a5c4cdb1267f36406c732a1', key: 'AGENTS.md'}, {checksum: 'b7fbe0ecff2a6c1d6e697a13096e2b17', key: 'assets/base.css'}, {checksum: '7adcd48a3cc215a81fabd9dafb919507', key: 'assets/sparkle.gif'}, {checksum: '22e69af13b7953914563c60035a831bc', key: 'config/settings_data.json'}, {checksum: 'cbe979d3fd3b7cdf2041ada9fdb3af57', key: 'config/settings_schema.json'}, + {checksum: '98fb75d10c4dbf239997ae494581fd7d', key: 'config/styles.css'}, {checksum: '7a92d18f1f58b2396c46f98f9e502c6a', key: 'layout/password.liquid'}, {checksum: '2374357fdadd3b4636405e80e21e87fc', key: 'layout/theme.liquid'}, {checksum: '0b2f0aa705a4eb2b4740e2ed68bc043f', key: 'locales/en.default.json'}, @@ -137,7 +139,7 @@ describe('theme-fs', () => { // Given const root = tmpDir await copyDirectoryContents(joinPath(locationOfThisFile, 'fixtures/theme'), root) - const watchSpy = vi.spyOn(chokidar, 'watch') + const watchSpy = vi.spyOn(chokidar, 'watch').mockReturnValue(new EventEmitter() as any) // When const themeFileSystem = mountThemeFileSystem(root, {listing: 'modern'}) @@ -157,7 +159,7 @@ describe('theme-fs', () => { // Given const root = tmpDir await copyDirectoryContents(joinPath(locationOfThisFile, 'fixtures/theme'), root) - const watchSpy = vi.spyOn(chokidar, 'watch') + const watchSpy = vi.spyOn(chokidar, 'watch').mockReturnValue(new EventEmitter() as any) // When const themeFileSystem = mountThemeFileSystem(root) @@ -511,11 +513,13 @@ describe('theme-fs', () => { {key: 'templates/404.json', checksum: '7'}, {key: 'config/settings_schema.json', checksum: '8'}, {key: 'config/settings_data.json', checksum: '9'}, + {key: 'config/styles.css', checksum: '16'}, {key: 'sections/announcement-bar.liquid', checksum: '10'}, {key: 'snippets/language-localization.liquid', checksum: '11'}, {key: 'templates/404.context.uk.json', checksum: '12'}, {key: 'templates/404.liquid', checksum: '13'}, {key: 'blocks/block.liquid', checksum: '14'}, + {key: 'AGENTS.md', checksum: '17'}, ] // When const { @@ -525,10 +529,12 @@ describe('theme-fs', () => { otherJsonFiles, configSchemaFile, configDataFile, + configStylesheetFiles, staticAssetFiles, contextualizedJsonFiles, blockLiquidFiles, layoutFiles, + agentInstructionFiles, } = partitionThemeFiles(files) // Then @@ -542,6 +548,7 @@ describe('theme-fs', () => { expect(templateJsonFiles).toEqual([{key: 'templates/404.json', checksum: '7'}]) expect(configSchemaFile).toEqual([{key: 'config/settings_schema.json', checksum: '8'}]) expect(configDataFile).toEqual([{key: 'config/settings_data.json', checksum: '9'}]) + expect(configStylesheetFiles).toEqual([{key: 'config/styles.css', checksum: '16'}]) expect(staticAssetFiles).toEqual([ {key: 'assets/base.css', checksum: '1'}, {key: 'assets/sparkle.gif', checksum: '3'}, @@ -553,6 +560,7 @@ describe('theme-fs', () => { {key: 'layout/theme.liquid', checksum: '5'}, {key: 'layout/custom.liquid', checksum: '15'}, ]) + expect(agentInstructionFiles).toEqual([{key: 'AGENTS.md', checksum: '17'}]) }) test('should handle empty file array', () => { @@ -567,7 +575,9 @@ describe('theme-fs', () => { otherJsonFiles, configSchemaFile, configDataFile, + configStylesheetFiles, staticAssetFiles, + agentInstructionFiles, } = partitionThemeFiles(files) // Then @@ -577,7 +587,9 @@ describe('theme-fs', () => { expect(otherJsonFiles).toEqual([]) expect(configSchemaFile).toEqual([]) expect(configDataFile).toEqual([]) + expect(configStylesheetFiles).toEqual([]) expect(staticAssetFiles).toEqual([]) + expect(agentInstructionFiles).toEqual([]) }) }) @@ -590,6 +602,7 @@ describe('theme-fs', () => { expect(isTextFile('assets/icon.svg')).toBeTruthy() expect(isTextFile('sections/template.liquid')).toBeTruthy() expect(isTextFile('templates/cart.json')).toBeTruthy() + expect(isTextFile('AGENTS.md')).toBeTruthy() }) test(`returns false when it's not a text file`, async () => { diff --git a/packages/theme/src/cli/utilities/theme-fs.ts b/packages/theme/src/cli/utilities/theme-fs.ts index 9692a5c9ac2..4cc4e3c3f7e 100644 --- a/packages/theme/src/cli/utilities/theme-fs.ts +++ b/packages/theme/src/cli/utilities/theme-fs.ts @@ -29,8 +29,10 @@ import type { const FILE_EVENT_DEBOUNCE_TIME_IN_MS = 250 const THEME_DIRECTORY_PATTERNS = [ + 'AGENTS.md', 'assets/**/*.*', 'config/**/*.json', + 'config/styles.css', 'layout/**/*.liquid', 'locales/**/*.json', 'sections/**/*.{liquid,json}', @@ -41,11 +43,13 @@ const THEME_DIRECTORY_PATTERNS = [ ] const THEME_PARTITION_REGEX = { + agentInstructionsRegex: /^AGENTS\.md$/, layoutLiquidRegex: /^layout\/.+\.liquid$/, sectionLiquidRegex: /^sections\/.+\.liquid$/, blockLiquidRegex: /^blocks\/.+\.liquid$/, configSchemaRegex: /^config\/settings_schema\.json$/, configDataRegex: /^config\/settings_data\.json$/, + configStylesheetRegex: /^config\/styles\.css$/, sectionJsonRegex: /^sections\/.+\.json$/, templateJsonRegex: /^templates\/.+\.json$/, jsonRegex: /^(?!config\/).*\.json$/, @@ -468,13 +472,17 @@ export function partitionThemeFiles(files: T[]) { const contextualizedJsonFiles: T[] = [] const configSchemaFile: T[] = [] const configDataFile: T[] = [] + const configStylesheetFiles: T[] = [] const staticAssetFiles: T[] = [] const blockLiquidFiles: T[] = [] const layoutFiles: T[] = [] + const agentInstructionFiles: T[] = [] files.forEach((file) => { const fileKey = file.key - if (fileKey.endsWith('.liquid')) { + if (THEME_PARTITION_REGEX.agentInstructionsRegex.test(fileKey)) { + agentInstructionFiles.push(file) + } else if (fileKey.endsWith('.liquid')) { if (THEME_PARTITION_REGEX.sectionLiquidRegex.test(fileKey)) { sectionLiquidFiles.push(file) } else if (THEME_PARTITION_REGEX.blockLiquidRegex.test(fileKey)) { @@ -488,6 +496,8 @@ export function partitionThemeFiles(files: T[]) { configSchemaFile.push(file) } else if (THEME_PARTITION_REGEX.configDataRegex.test(fileKey)) { configDataFile.push(file) + } else if (THEME_PARTITION_REGEX.configStylesheetRegex.test(fileKey)) { + configStylesheetFiles.push(file) } else if (THEME_PARTITION_REGEX.jsonRegex.test(fileKey)) { if (THEME_PARTITION_REGEX.contextualizedJsonRegex.test(fileKey)) { contextualizedJsonFiles.push(file) @@ -512,9 +522,11 @@ export function partitionThemeFiles(files: T[]) { otherJsonFiles, configSchemaFile, configDataFile, + configStylesheetFiles, staticAssetFiles, blockLiquidFiles, layoutFiles, + agentInstructionFiles, } } @@ -533,6 +545,7 @@ export function isTextFile(path: string) { 'text/x-sass', 'text/x-scss', 'image/svg+xml', + 'text/markdown', ] return textFileTypes.includes(lookupMimeType(path)) diff --git a/packages/theme/src/cli/utilities/theme-uploader.test.ts b/packages/theme/src/cli/utilities/theme-uploader.test.ts index b97e7c68cc0..396c6b8b8f3 100644 --- a/packages/theme/src/cli/utilities/theme-uploader.test.ts +++ b/packages/theme/src/cli/utilities/theme-uploader.test.ts @@ -255,6 +255,8 @@ describe('theme-uploader', () => { {key: 'templates/index.liquid', checksum: '4'}, {key: 'assets/liquid.liquid', checksum: '5'}, {key: 'config/settings_data.json', checksum: '6'}, + {key: 'config/styles.css', checksum: '9'}, + {key: 'AGENTS.md', checksum: '10'}, {key: 'assets/image.png', checksum: '7'}, {key: 'layout/custom.liquid', checksum: '8'}, ] @@ -282,6 +284,8 @@ describe('theme-uploader', () => { 'templates/index.liquid', 'assets/liquid.liquid', 'config/settings_data.json', + 'config/styles.css', + 'AGENTS.md', 'assets/image.png', ], adminSession, @@ -305,6 +309,8 @@ describe('theme-uploader', () => { ['templates/product.context.uk.json', {key: 'templates/product.context.uk.json', checksum: '8'}], ['blocks/block.liquid', {key: 'blocks/block.liquid', checksum: '9'}], ['layout/theme.liquid', {key: 'layout/theme.liquid', checksum: '10'}], + ['config/styles.css', {key: 'config/styles.css', checksum: '11', value: ':root { --color-primary: #000; }'}], + ['AGENTS.md', {key: 'AGENTS.md', checksum: '12', value: '# Theme instructions'}], ]), ) @@ -345,6 +351,14 @@ describe('theme-uploader', () => { { key: 'assets/image.png', }, + { + key: 'config/styles.css', + value: ':root { --color-primary: #000; }', + }, + { + key: 'AGENTS.md', + value: '# Theme instructions', + }, ], adminSession, ) diff --git a/packages/theme/src/cli/utilities/theme-uploader.ts b/packages/theme/src/cli/utilities/theme-uploader.ts index 68b9941e47e..6a4254cf248 100644 --- a/packages/theme/src/cli/utilities/theme-uploader.ts +++ b/packages/theme/src/cli/utilities/theme-uploader.ts @@ -201,7 +201,7 @@ function getRemoteFilesToBeDeleted(remoteChecksums: Checksum[], themeFileSystem: return filesToBeDeleted } -// Contextual Json Files -> Json Files -> Liquid Files -> Config Files -> Static Asset Files +// Contextual JSON files -> JSON files -> Liquid files -> Config files -> Other independent files function orderFilesToBeDeleted(files: Checksum[]): Checksum[] { const fileSets = partitionThemeFiles(files) return [ @@ -214,6 +214,8 @@ function orderFilesToBeDeleted(files: Checksum[]): Checksum[] { ...fileSets.layoutFiles, ...fileSets.otherLiquidFiles, ...fileSets.configDataFile, + ...fileSets.configStylesheetFiles, + ...fileSets.agentInstructionFiles, ...fileSets.configSchemaFile, ...fileSets.staticAssetFiles, ] @@ -334,6 +336,8 @@ function selectUploadableFiles(themeFileSystem: ThemeFileSystem, remoteChecksums * - The other Liquid files (for example, snippets, and liquid templates) * - The other JSON files (for example, locales) * - The static assets + * - Config stylesheets + * - Agent instructions * */ function orderFilesToBeUploaded(files: ChecksumWithSize[]): { @@ -344,7 +348,13 @@ function orderFilesToBeUploaded(files: ChecksumWithSize[]): { return { // Most JSON files here are locales. Since we filter locales out in `replaceTemplates`, // and assets can be served locally, we can give priority to the unique Liquid files: - independentFiles: [fileSets.otherLiquidFiles, fileSets.otherJsonFiles, fileSets.staticAssetFiles], + independentFiles: [ + fileSets.otherLiquidFiles, + fileSets.otherJsonFiles, + fileSets.staticAssetFiles, + fileSets.configStylesheetFiles, + fileSets.agentInstructionFiles, + ], // Follow order of dependencies: dependentFiles: [ fileSets.configSchemaFile,