diff --git a/.changeset/remote-file-size-dependency.md b/.changeset/remote-file-size-dependency.md new file mode 100644 index 000000000..eca7b3936 --- /dev/null +++ b/.changeset/remote-file-size-dependency.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': minor +--- + +Add an optional `remoteFileSize` dependency: the size of a remote file, such as a script on a CDN. AssetSizeCSS and AssetSizeJavaScript use it, and by default it makes the same HEAD request as before. diff --git a/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts b/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts deleted file mode 100644 index f3bc4180d..000000000 --- a/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { AppBlockMissingSchema } from './index'; - -describe('AppBlockMissingSchema', () => { - it('should report an error when schema tag is missing on a theme app extension (blocks only)', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'blocks/footer.liquid', - ); - expect(offenses).to.have.lengthOf(1); - }); - - it('should not report an error when schema tag is missing on a snippet in a theme app extension )', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'snippets/footer.liquid', - ); - expect(offenses).to.have.lengthOf(0); - }); - - it('should not report when the schema is present on a theme app extension', async () => { - const sourceCode = ` - - -{% schema %} -{ - "name": "Footer", - "max_blocks": 8, - "blocks": [ - { - "type": "link", - "name": "Link", - "settings": [ - { - "id": "linkurl", - "type": "url", - "label": "URL link" - }, - { - "id": "linktext", - "type": "text", - "label": "Link text" - } - ] - }, - { - "type": "custom-text", - "name": "Custom Text", - "settings": [ - { - "id": "custom-text-field", - "type": "textarea", - "label": "Text" - } - ] - } - ] -} -{% endschema %}`; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'blocks/footer.liquid', - ); - expect(offenses).to.have.lengthOf(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts b/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts index 8d87f9c76..5b8f4bec6 100644 --- a/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts +++ b/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts @@ -1,84 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { ForbiddenTag, AppBlockValidTags } from './index'; - -const blocksFilePath = 'blocks/app.liquid'; +import { ForbiddenTag } from './index'; describe('Module: AppBlockValidTags', () => { - it('should report an offense when the forbidden tag is used in blocks directory liquid file', async () => { - const tags = ['include', 'layout', 'section', 'sections']; - for (const tag of tags) { - const sourceCode = ` - {% ${tag} 'test' %} - {% schema %} - { } - {% endschema %} - `; - - const offenses = await runLiquidCheck(AppBlockValidTags, sourceCode, blocksFilePath); - - const expectedStart = { - index: 9, - line: 1, - character: 8, - }; - const tagEndOffset = 13; - const endOffset = tag.length + tagEndOffset; - - const expectedEnd = { - index: expectedStart.index + endOffset, - line: expectedStart.line, - character: expectedStart.character + endOffset, - }; - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: AppBlockValidTags.meta.code, - message: `Theme app extension blocks cannot contain '${tag}' tags`, - uri: 'file:///blocks/app.liquid', - severity: 0, - start: expectedStart, - end: expectedEnd, - }); - } - }); - - it('should report an offense when the forbidden tag is used with an end tag in blocks directory liquid file', async () => { - const tags = ['javascript', 'stylesheet']; - for (const tag of tags) { - const sourceCode = ` - {% ${tag} %} - {% end${tag} %} - {% schema %} - { } - {% endschema %} - `; - - const offenses = await runLiquidCheck(AppBlockValidTags, sourceCode, blocksFilePath); - - const expectedStart = { - index: 9, - line: 1, - character: 8, - }; - const expectedEnd = { - index: expectedStart.index + tag.length + 34, - line: 2, - character: expectedStart.character + tag.length + 9, - }; - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: AppBlockValidTags.meta.code, - message: `Theme app extension blocks cannot contain '${tag}' tags`, - uri: 'file:///blocks/app.liquid', - severity: 0, - start: expectedStart, - end: expectedEnd, - }); - } - }); - it('should contain specifically the following forbidden tags', async () => { const actualTagValues = Object.values(ForbiddenTag); const expectedTagValues = [ diff --git a/packages/theme-check-common/src/checks/asset-preload/index.spec.ts b/packages/theme-check-common/src/checks/asset-preload/index.spec.ts deleted file mode 100644 index b2e065a83..000000000 --- a/packages/theme-check-common/src/checks/asset-preload/index.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; -import { AssetPreload } from './index'; - -describe('Module: AssetPreload', () => { - it('no offense with link element', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(0); - }); - - it('reports stylesheet preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload argument of the stylesheet_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('reports image preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload argument of the image_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('allows high-priority image preloading', async () => { - // The preload_tag filter cannot generate responsive imagesrcset or - // imagesizes attributes, so LCP images may require an explicit - // high-priority preload link. - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(0); - }); - - it('reports image preloading when attribute values contain Liquid', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(2); - }); - - it('reports general preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('should report offenses for manual preloading of assets', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(3); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([ - ``, - ``, - ``, - ]); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts deleted file mode 100644 index 744247871..000000000 --- a/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AssetSizeAppBlockCSS } from '.'; -import { check, MockTheme } from '../../test'; - -describe('Module: AssetSizeAppBlockCSS', () => { - const extensionFiles: MockTheme = { - 'assets/app.css': '* { color: green } ', - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "app.css" - } - {% endschema %} - `, - }; - - it('should not report any offenses if CSS is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeAppBlockCSS], - {}, - { - AssetSizeAppBlockCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `The file size for 'app.css' (19 B) exceeds the configured threshold (1 B)`, - uri: 'file:///blocks/app.liquid', - start: { index: 51 }, - end: { index: 58 }, - }); - }); - - it('should report an offense if the CSS file does not exist', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "nonexistent.css" - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.css' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 72 }, - }); - }); - - it('should reports offense if the CSS file does not exist and the asset has a trailing comma', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "nonexistent.css", - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.css' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 72 }, - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts deleted file mode 100644 index b5b91e6f5..000000000 --- a/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AssetSizeAppBlockJavaScript } from '.'; -import { check, MockTheme } from '../../test'; - -describe('Module: AssetSizeAppBlockJavaScript', () => { - const extensionFiles: MockTheme = { - 'assets/app.js': 'console.log("Hello, world!");', - 'blocks/app.liquid': ` - {% schema %} - { - "javascript": "app.js" - } - {% endschema %} - `, - }; - - it('should not report any offenses if JavaScript is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeAppBlockJavaScript]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if JavaScript is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeAppBlockJavaScript], - {}, - { - AssetSizeAppBlockJavaScript: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `The file size for 'app.js' (29 B) exceeds the configured threshold (1 B)`, - uri: 'file:///blocks/app.liquid', - start: { index: 51 }, - end: { index: 57 }, - }); - }); - - it('should report an offense if the JavaScript file does not exist', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "javascript": "nonexistent.js" - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockJavaScript]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.js' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 71 }, - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts deleted file mode 100644 index eb4382ae0..000000000 --- a/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { vi, expect, describe, it, afterEach } from 'vitest'; -import { AssetSizeCSS } from '.'; -import { check, MockTheme } from '../../test'; -import { SchemaProp } from '../../types'; -import { hasRemoteAssetSizeExceededThreshold } from '../../utils/file-utils'; - -vi.mock('../../utils/file-utils', async (importOriginal) => { - const actual: any = await importOriginal(); - return { - ...actual, - hasRemoteAssetSizeExceededThreshold: vi.fn(), - }; -}); - -describe('Module: AssetSizeCSS', () => { - const extensionFiles: MockTheme = { - 'assets/theme.css': '* { color: green !important; }', - 'templates/index.liquid': ` - -
- - - - `, - }; - - const httpTest: MockTheme = { - 'templates/index.liquid': ` - - - - - - `, - }; - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should not report any offenses if CSS is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 1 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 51 }, - end: { index: 80 }, - }); - }); - - it('should report a warning when the CSS file size exceeds the threshold', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockReturnValue(Promise.resolve(true)); - const offenses = await check( - httpTest, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 1 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 51 }, - end: { index: 122 }, - }); - }); - - it('should not report any offenses if CSS is smaller than threshold 2', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockResolvedValue(false); - const extensionFiles: MockTheme = { - 'assets/theme.css': 'console.log("hello world");', - 'templates/index.liquid': ` - - - {{ 'theme.css' | asset_url | stylesheet_tag }} - {{ "https://example.com" | stylesheet_tag }} - - - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold 2', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockResolvedValue(true); - const extensionFiles: MockTheme = { - 'assets/theme.css': 'console.log("hello world");', - 'templates/index.liquid': ` - - - {{ 'theme.css' | asset_url | stylesheet_tag }} - {{ "https://example.com" | stylesheet_tag }} - - - `, - }; - - const offenses = await check( - extensionFiles, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 2, - }, - }, - ); - - expect(offenses).toHaveLength(2); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 2 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 48 }, - end: { index: 89 }, - }); - expect(offenses[1]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 2 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 107 }, - end: { index: 128 }, - }); - }); - - it('should not report any offenses if there is no stylesheet', async () => { - const extensionFiles: MockTheme = { - 'templates/index.liquid': ` - - - - - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-css/index.ts b/packages/theme-check-common/src/checks/asset-size-css/index.ts index 93796effd..7a8942c5e 100644 --- a/packages/theme-check-common/src/checks/asset-size-css/index.ts +++ b/packages/theme-check-common/src/checks/asset-size-css/index.ts @@ -64,7 +64,7 @@ export const AssetSizeCSS: LiquidCheckDefinition
-
-
-
- `;
-
- const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode);
- expect(offenses).to.have.lengthOf(3);
- expect(offenses[0].message).to.equal('Missing height attribute on img tag');
- expect(offenses[1].message).to.equal('Missing width attribute on img tag');
- expect(offenses[2].message).to.equal('Missing width and height attributes on img tag');
-
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.eql([
- '
',
- '
',
- '
',
- ]);
- });
-
- it('should report missing width and height attributes on img tags', async () => {
- const sourceCode = `
-
- `;
-
- const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode);
- expect(offenses).to.have.lengthOf(1);
- expect(offenses[0].message).to.equal('Missing width and height attributes on img tag');
-
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.eql(['
']);
- });
-
- it('should report missing height attribute on img tags', async () => {
- const sourceCode = `
-
-
- `;
-
- const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode);
- expect(offenses).to.have.lengthOf(1);
- expect(offenses[0].message).to.equal('Missing height attribute on img tag');
-
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.eql(['
']);
- });
-
- it('should report missing width attribute on img tags', async () => {
- const sourceCode = `
-
-
- `;
-
- const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode);
- expect(offenses).to.have.lengthOf(1);
- expect(offenses[0].message).to.equal('Missing width attribute on img tag');
-
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.eql(['
']);
- });
-
- it('should not report missing attributes on img tags if they are not missing', async () => {
- const sourceCode = `
-
- `;
-
- const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode);
- expect(offenses).to.be.empty;
- });
-});
diff --git a/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts b/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts
deleted file mode 100644
index 1082f97c3..000000000
--- a/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts
+++ /dev/null
@@ -1,435 +0,0 @@
-import { expect, describe, it } from 'vitest';
-import { JSONMissingBlock } from './index';
-import { check, MockTheme } from '../../test';
-
-describe('Module: JsonMissingBlock', () => {
- describe('File existence validation', () => {
- it('should report an offense when a block does not exist', async () => {
- const theme: MockTheme = {
- 'templates/product.failing.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "text_tqQTNE": {
- "type": "missing_block"
- }
- },
- "block_order": ["text_tqQTNE"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "missing_block"
- }
- ]
- }
- {% endschema %}
- `,
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Theme block 'blocks/missing_block.liquid' does not exist.",
- );
-
- const content = theme['templates/product.failing.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"missing_block"');
- });
-
- it('should not report an offense when block exists', async () => {
- const theme: MockTheme = {
- 'templates/product.valid.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "text_block": {
- "type": "text"
- }
- },
- "block_order": ["text_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.be.empty;
- });
-
- it('should report an offense when a nested block does not exist', async () => {
- const theme: MockTheme = {
- 'templates/product.nested.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "parent_block": {
- "type": "text",
- "blocks": {
- "child_block": {
- "type": "missing_nested"
- }
- }
- }
- },
- "block_order": ["parent_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': `
- {% schema %}
- {
- "name": "Text Block",
- "blocks": [
- {
- "type": "missing_nested"
- }
- ]
- }
- {% endschema %}
- `,
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Theme block 'blocks/missing_nested.liquid' does not exist.",
- );
-
- const content = theme['templates/product.nested.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"missing_nested"');
- });
- });
-
- describe('Allowed block type validation', () => {
- it('should report an offense when block exists but is not in the section liquid schema', async () => {
- const theme: MockTheme = {
- 'templates/product.valid.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "text_block": {
- "type": "text"
- }
- },
- "block_order": ["text_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "image"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': '',
- 'blocks/image.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Block type 'text' is not allowed in 'sections/custom-section.liquid'.",
- );
-
- const content = theme['templates/product.valid.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"text"');
- });
-
- it('should report an offense when a nested block exists but is not in the block liquid schema', async () => {
- const theme: MockTheme = {
- 'templates/product.nested.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "parent_block": {
- "type": "text",
- "blocks": {
- "child_block": {
- "type": "missing_nested"
- }
- }
- }
- },
- "block_order": ["parent_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': `
- {% schema %}
- {
- "name": "Text Block",
- "blocks": [
- {
- "type": "image"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/image.liquid': '',
- 'blocks/missing_nested.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Block type 'missing_nested' is not allowed in 'blocks/text.liquid'.",
- );
-
- const content = theme['templates/product.nested.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"missing_nested"');
- });
-
- it('should report an offense when a nested private block exists but is not in the block liquid schema', async () => {
- const theme: MockTheme = {
- 'templates/product.nested.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "parent_block": {
- "type": "text",
- "blocks": {
- "child_block": {
- "type": "_private_block"
- }
- }
- }
- },
- "block_order": ["parent_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': `
- {% schema %}
- {
- "name": "Text Block",
- "blocks": [
- {
- "type": "@theme"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/image.liquid': '',
- 'blocks/_private_block.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Block type '_private_block' is not allowed in 'blocks/text.liquid'.",
- );
-
- const content = theme['templates/product.nested.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"_private_block"');
- });
-
- it('should not report an offense when a static block exists and is not in the block liquid schema', async () => {
- const theme: MockTheme = {
- 'templates/product.static.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "child_block": {
- "type": "text"
- },
- "static_block": {
- "type": "static_block",
- "static": true
- }
- },
- "block_order": ["child_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': `
- {% schema %}
- {
- "name": "Text Block",
- "blocks": [
- {
- "type": "@theme"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/static_block.liquid': `
- {% schema %}
- {
- "name": "Static Block",
- }
- {% endschema %}
- `,
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.have.length(0);
- });
- });
-
- describe('Edge case validation', () => {
- it('should ignore non-template JSON files', async () => {
- const theme: MockTheme = {
- 'config/index.json': `{
- "blocks": {
- "text_block": {
- "type": "nonexistent"
- }
- }
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.be.empty;
- });
-
- it('should ignore templates with local blocks', async () => {
- const theme: MockTheme = {
- 'templates/product.nested.json': `{
- "sections": {
- "custom-section": {
- "type": "custom-section",
- "blocks": {
- "parent_block": {
- "type": "text",
- "blocks": {
- "child_block": {
- "type": "_private_block"
- }
- }
- }
- },
- "block_order": ["parent_block"]
- }
- },
- "order": ["custom-section"]
- }`,
- 'sections/custom-section.liquid': `
- {% schema %}
- {
- "name": "Custom Section",
- "blocks": [
- {
- "type": "text",
- "name": "Text Block"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/text.liquid': `
- {% schema %}
- {
- "name": "Text Block",
- "blocks": [
- {
- "type": "image",
- "name": "Image Block"
- }
- ]
- }
- {% endschema %}
- `,
- 'blocks/image.liquid': '',
- 'blocks/_private_block.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingBlock]);
- expect(offenses).to.be.empty;
- });
- });
-});
diff --git a/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts b/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts
deleted file mode 100644
index 084195c73..000000000
--- a/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-import { expect, describe, it } from 'vitest';
-import { JSONMissingSection } from './index';
-import { check, MockTheme } from '../../test';
-
-describe('Module: JSONMissingSection', () => {
- describe('JSON templates', () => {
- it('should report an offense when a section file does not exist', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "hero_AAAAAA": {
- "type": "featured-collection"
- }
- },
- "order": ["hero_AAAAAA"]
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Section type 'featured-collection' does not refer to an existing section file",
- );
-
- const content = theme['templates/index.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"featured-collection"');
- });
-
- it('should not report an offense when the section file exists', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "hero_AAAAAA": {
- "type": "featured-collection"
- }
- },
- "order": ["hero_AAAAAA"]
- }`,
- 'sections/featured-collection.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
-
- it('should report an offense for each missing section', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "hero_AAAAAA": {
- "type": "missing-one"
- },
- "main": {
- "type": "existing-section"
- },
- "footer_BBBBBB": {
- "type": "missing-two"
- }
- },
- "order": ["hero_AAAAAA", "main", "footer_BBBBBB"]
- }`,
- 'sections/existing-section.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.have.length(2);
- expect(offenses[0].message).to.equal(
- "Section type 'missing-one' does not refer to an existing section file",
- );
- expect(offenses[1].message).to.equal(
- "Section type 'missing-two' does not refer to an existing section file",
- );
- });
-
- it('should not report an offense for section types with platform defaults', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "apps_section": {
- "type": "apps"
- },
- "blocks_section": {
- "type": "_blocks"
- }
- },
- "order": ["apps_section", "blocks_section"]
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
-
- it('should not report an offense when a section entry has no type', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "inherited_section": {
- "settings": {}
- }
- },
- "order": ["inherited_section"]
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
- });
-
- describe('Section groups', () => {
- it('should report an offense when a section file does not exist', async () => {
- const theme: MockTheme = {
- 'sections/header-group.json': `{
- "type": "header",
- "name": "Header group",
- "sections": {
- "announcement": {
- "type": "missing-announcement-bar"
- }
- },
- "order": ["announcement"]
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- "Section type 'missing-announcement-bar' does not refer to an existing section file",
- );
-
- const content = theme['sections/header-group.json'];
- const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
- expect(erroredContent).to.equal('"missing-announcement-bar"');
- });
-
- it('should not report an offense when the section file exists', async () => {
- const theme: MockTheme = {
- 'sections/header-group.json': `{
- "type": "header",
- "name": "Header group",
- "sections": {
- "announcement": {
- "type": "announcement-bar"
- }
- },
- "order": ["announcement"]
- }`,
- 'sections/announcement-bar.liquid': '',
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
-
- it('should not report an offense for the section group type itself', async () => {
- const theme: MockTheme = {
- 'sections/header-group.json': `{
- "type": "header",
- "name": "Header group",
- "sections": {},
- "order": []
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
- });
-
- describe('Edge cases', () => {
- it('should ignore JSON files outside templates/ and sections/', async () => {
- const theme: MockTheme = {
- 'config/settings_data.json': `{
- "sections": {
- "main": {
- "type": "nonexistent"
- }
- }
- }`,
- 'locales/en.default.json': `{
- "sections": {
- "main": { "type": "nonexistent" }
- }
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
-
- it('should not report an offense when sections is not an object', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": "not-an-object",
- "order": []
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
-
- it('should report each duplicate section key at its own location', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{
- "sections": {
- "main": {
- "type": "missing-one"
- },
- "main": {
- "type": "missing-two"
- }
- },
- "order": ["main"]
- }`,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.have.length(2);
-
- const content = theme['templates/index.json'];
- expect(offenses[0].message).to.equal(
- "Section type 'missing-one' does not refer to an existing section file",
- );
- expect(content.slice(offenses[0].start.index, offenses[0].end.index)).to.equal(
- '"missing-one"',
- );
- expect(offenses[1].message).to.equal(
- "Section type 'missing-two' does not refer to an existing section file",
- );
- expect(content.slice(offenses[1].start.index, offenses[1].end.index)).to.equal(
- '"missing-two"',
- );
- });
-
- it('should not report an offense when the JSON is invalid', async () => {
- const theme: MockTheme = {
- 'templates/index.json': `{ "sections": { "main": { "type": `,
- };
-
- const offenses = await check(theme, [JSONMissingSection]);
- expect(offenses).to.be.empty;
- });
- });
-});
diff --git a/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts
deleted file mode 100644
index f6365ea60..000000000
--- a/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { expect, describe, it } from 'vitest';
-import { JSONSyntaxError } from './index';
-import { highlightedOffenses, runJSONCheck } from '../../test';
-
-describe('Module: JSONSyntaxError', () => {
- it('should report an error for invalid JSON (0)', async () => {
- const invalidJson = `{
- "key1": "value1",
- "key2": "value2",,
- }`;
-
- const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json');
- expect(offenses).to.have.length(2);
- expect(offenses[0].message).to.equal('Property name expected');
- expect(offenses[1].message).to.equal('Expecting a value');
-
- const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses);
- expect(highlights).to.have.length(2);
- expect(highlights[0]).to.equal(',');
- expect(highlights[1]).to.equal(',');
- });
-
- it('should report an error for invalid JSON (1)', async () => {
- const invalidJson = `{
- "key1": "value1",
- "key2": "value2"
- `;
-
- const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json');
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal('Expecting a closing brace (})');
-
- const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses);
- expect(highlights).to.have.length(1);
- expect(highlights[0]).to.equal('');
- });
-
- it('should report an error for invalid JSON (2)', async () => {
- const invalidJson = `{
- 'key1': "value1",
- "key2": "value2"
- }`;
-
- const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json');
- expect(offenses).to.have.length(3);
- expect(offenses[0].message).to.equal('Invalid symbol');
- expect(offenses[1].message).to.equal('Property name expected');
- expect(offenses[2].message).to.equal('Expecting a value');
-
- const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses);
- expect(highlights).to.have.length(3);
- expect(highlights[0]).to.equal("'key1'");
- });
-
- it('should not report any errors for valid JSON', async () => {
- const validJson = `{
- "key1": "value1",
- "key2": "value2"
- }`;
-
- const offenses = await runJSONCheck(JSONSyntaxError, validJson);
- expect(offenses).to.be.empty;
- });
-});
diff --git a/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts b/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts
deleted file mode 100644
index b11e904d5..000000000
--- a/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { LiquidComplexity } from './index';
-import { Severity } from '../../types';
-import { check, runLiquidCheck } from '../../test';
-import type { Offense } from '../../types';
-
-const DEFAULT_PATH = 'snippets/test.liquid';
-
-async function checkWithMaxComplexity(template: string, maxComplexity: number): Promise
-
-
- {{ url | img_tag }}
- `;
-
- const offenses = await runLiquidCheck(RemoteAsset, sourceCode);
- expect(offenses).to.have.length(4);
-
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.eql([
- 'src="{{ \'image.png\' }}"',
- 'href="{{ \'style.css\' }}"',
- 'src="{{ \'script.js\' }}"',
- 'url | img_tag',
- ]);
- });
-
- it('should not report an offense when asset_url or img_url filters are used', async () => {
- const sourceCode = `
-
-
-
- {{ url | img_tag | asset_url }},
- {{ 'bootstrap.min.css' | asset_url | stylesheet_tag }}
- {{ product | image_url: width: 450 | img_tag }}
- `;
-
- const offenses = await runLiquidCheck(RemoteAsset, sourceCode);
- expect(offenses).to.be.empty;
- const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
- expect(highlights).to.be.empty;
- });
-
- it('should not report an offense for links, videos, iframes', async () => {
- const sourceCode = `
-
-
-
- `;
-
- const offenses = await runLiquidCheck(DeprecateLazysizes, sourceCode);
- expect(offenses).to.have.length(1);
- expect(offenses[0].message).to.equal(
- 'Use the native loading="lazy" attribute instead of lazysizes',
- );
- });
-
- it('should not report srcset and sizes attributes', async () => {
- const sourceCode = `
-