@shopify/theme-check-common
Advanced tools
Comparing version 1.16.0 to 1.16.1
# @shopify/theme-check-common | ||
## 1.16.1 | ||
### Patch Changes | ||
- 8d35241: Fix `paginate` object completion, hover and `UndefinedObject` reporting | ||
- 201f30c: Add support for `{% layout none %}` | ||
- c0298e7: Fix `recommendations` completion, hover and `UndefinedObject` reporting | ||
- 6fad756: Fix `predictive_search` completion, hover and `UndefinedObject` reporting | ||
- fc86c91: Fix `form` object completion, hover and `UndefinedObject` reporting | ||
## 1.16.0 | ||
@@ -4,0 +14,0 @@ |
@@ -26,3 +26,4 @@ "use strict"; | ||
*/ | ||
if (context.relativePath(context.file.absolutePath).startsWith('snippets/')) { | ||
const relativePath = context.relativePath(context.file.absolutePath); | ||
if (relativePath.startsWith('snippets/')) { | ||
return {}; | ||
@@ -48,3 +49,3 @@ } | ||
async LiquidTag(node) { | ||
var _a, _b, _c; | ||
var _a, _b, _c, _d; | ||
if (isLiquidTagAssign(node)) { | ||
@@ -60,10 +61,34 @@ indexVariableScope(node.markup.name, { | ||
} | ||
/** | ||
* {% form 'cart', cart %} | ||
* {{ form }} | ||
* {% endform %} | ||
*/ | ||
if (['form', 'paginate'].includes(node.name)) { | ||
indexVariableScope(node.name, { | ||
start: node.blockStartPosition.end, | ||
end: (_b = node.blockEndPosition) === null || _b === void 0 ? void 0 : _b.start, | ||
}); | ||
} | ||
/* {% layout none %} */ | ||
if (node.name === 'layout') { | ||
indexVariableScope('none', { | ||
start: node.position.start, | ||
end: node.position.end, | ||
}); | ||
} | ||
/** | ||
* {% for x in y %} | ||
* {{ forloop }} | ||
* {{ x }} | ||
* {% endfor %} | ||
*/ | ||
if (isLiquidForTag(node) || isLiquidTableRowTag(node)) { | ||
indexVariableScope(node.markup.variableName, { | ||
start: node.blockStartPosition.end, | ||
end: (_b = node.blockEndPosition) === null || _b === void 0 ? void 0 : _b.start, | ||
end: (_c = node.blockEndPosition) === null || _c === void 0 ? void 0 : _c.start, | ||
}); | ||
indexVariableScope(node.name === 'for' ? 'forloop' : 'tablerowloop', { | ||
start: node.blockStartPosition.end, | ||
end: (_c = node.blockEndPosition) === null || _c === void 0 ? void 0 : _c.start, | ||
end: (_d = node.blockEndPosition) === null || _d === void 0 ? void 0 : _d.start, | ||
}); | ||
@@ -79,3 +104,3 @@ } | ||
async onCodePathEnd() { | ||
const objects = await globalObjects(themeDocset); | ||
const objects = await globalObjects(themeDocset, relativePath); | ||
objects.forEach((obj) => variableScopes.set(obj.name, [])); | ||
@@ -98,9 +123,19 @@ variables.forEach((variable) => { | ||
}; | ||
async function globalObjects(themeDocset) { | ||
async function globalObjects(themeDocset, relativePath) { | ||
const objects = await themeDocset.objects(); | ||
const contextualObjects = getContextualObjects(relativePath); | ||
const globalObjects = objects.filter(({ access, name }) => { | ||
return name === 'section' || !access || access.global === true || access.template.length > 0; | ||
return (contextualObjects.includes(name) || | ||
!access || | ||
access.global === true || | ||
access.template.length > 0); | ||
}); | ||
return globalObjects; | ||
} | ||
function getContextualObjects(relativePath) { | ||
if (relativePath.startsWith('sections/')) { | ||
return ['section', 'predictive_search', 'recommendations']; | ||
} | ||
return []; | ||
} | ||
function isDefined(variableName, variablePosition, scopedVariables) { | ||
@@ -107,0 +142,0 @@ const scopes = scopedVariables.get(variableName); |
@@ -153,2 +153,32 @@ "use strict"; | ||
}); | ||
(0, vitest_1.it)('should contextually report on the undefined nature of the paginate object (defined in paginate tag, undefined outside)', async () => { | ||
const sourceCode = ` | ||
{% assign col = 'string' | split: '' %} | ||
{% paginate col by 5 %} | ||
{{ paginate }} | ||
{% endpaginate %}{{ paginate }} | ||
`; | ||
const offenses = await (0, test_1.runLiquidCheck)(index_1.UndefinedObject, sourceCode); | ||
(0, vitest_1.expect)(offenses).toHaveLength(1); | ||
(0, vitest_1.expect)(offenses.map((e) => e.message)).toEqual(["Unknown object 'paginate' used."]); | ||
}); | ||
(0, vitest_1.it)('should contextually report on the undefined nature of the form object (defined in form tag, undefined outside)', async () => { | ||
const sourceCode = ` | ||
{% form "cart" %} | ||
{{ form }} | ||
{% endform %}{{ form }} | ||
`; | ||
const offenses = await (0, test_1.runLiquidCheck)(index_1.UndefinedObject, sourceCode); | ||
(0, vitest_1.expect)(offenses).toHaveLength(1); | ||
(0, vitest_1.expect)(offenses.map((e) => e.message)).toEqual(["Unknown object 'form' used."]); | ||
}); | ||
(0, vitest_1.it)('should support {% layout none %}', async () => { | ||
const sourceCode = ` | ||
{% layout none %} | ||
{{ none }} | ||
`; | ||
const offenses = await (0, test_1.runLiquidCheck)(index_1.UndefinedObject, sourceCode); | ||
(0, vitest_1.expect)(offenses).toHaveLength(1); | ||
(0, vitest_1.expect)(offenses.map((e) => e.message)).toEqual(["Unknown object 'none' used."]); | ||
}); | ||
(0, vitest_1.it)('should not report an offense when object is undefined in a "snippet" file', async () => { | ||
@@ -180,2 +210,16 @@ const sourceCode = ` | ||
}); | ||
(0, vitest_1.it)('should support contextual exceptions', async () => { | ||
let offenses; | ||
const contexts = [ | ||
['section', 'sections/section.liquid'], | ||
['predictive_search', 'sections/predictive-search.liquid'], | ||
['recommendations', 'sections/recommendations.liquid'], | ||
]; | ||
for (const [object, goodPath] of contexts) { | ||
offenses = await (0, test_1.runLiquidCheck)(index_1.UndefinedObject, `{{ ${object} }}`, goodPath); | ||
(0, vitest_1.expect)(offenses).toHaveLength(0); | ||
offenses = await (0, test_1.runLiquidCheck)(index_1.UndefinedObject, `{{ ${object} }}`, 'file.liquid'); | ||
(0, vitest_1.expect)(offenses).toHaveLength(1); | ||
} | ||
}); | ||
(0, vitest_1.it)('should report an offense for forloop/tablerowloop used outside of context', async () => { | ||
@@ -182,0 +226,0 @@ const sourceCode = ` |
import { Config, Dependencies, Offense, Theme } from './types'; | ||
export * from './AugmentedThemeDocset'; | ||
export * from './AugmentedSchemaValidators'; | ||
export * from './fixes'; | ||
@@ -7,2 +9,4 @@ export * from './types'; | ||
export * from './ignore'; | ||
export * from './utils/types'; | ||
export * from './utils/memo'; | ||
export declare function check(sourceCodes: Theme, config: Config, dependencies: Dependencies): Promise<Offense[]>; |
@@ -36,2 +36,6 @@ "use strict"; | ||
const ignore_1 = require("./ignore"); | ||
const AugmentedThemeDocset_1 = require("./AugmentedThemeDocset"); | ||
const AugmentedSchemaValidators_1 = require("./AugmentedSchemaValidators"); | ||
__exportStar(require("./AugmentedThemeDocset"), exports); | ||
__exportStar(require("./AugmentedSchemaValidators"), exports); | ||
__exportStar(require("./fixes"), exports); | ||
@@ -42,2 +46,4 @@ __exportStar(require("./types"), exports); | ||
__exportStar(require("./ignore"), exports); | ||
__exportStar(require("./utils/types"), exports); | ||
__exportStar(require("./utils/memo"), exports); | ||
const defaultErrorHandler = (_error) => { | ||
@@ -50,2 +56,9 @@ // Silently ignores errors by default. | ||
const { DisabledChecksVisitor, isDisabled } = (0, disabled_checks_1.createDisabledChecksModule)(); | ||
// We're memozing those deps here because they shouldn't change within a run. | ||
if (dependencies.themeDocset && !dependencies.themeDocset.isAugmented) { | ||
dependencies.themeDocset = new AugmentedThemeDocset_1.AugmentedThemeDocset(dependencies.themeDocset); | ||
} | ||
if (dependencies.schemaValidators && !dependencies.schemaValidators.isAugmented) { | ||
dependencies.schemaValidators = new AugmentedSchemaValidators_1.AugmentedSchemaValidators(dependencies.schemaValidators); | ||
} | ||
for (const type of Object.values(types_1.SourceCodeType)) { | ||
@@ -52,0 +65,0 @@ switch (type) { |
@@ -110,2 +110,26 @@ "use strict"; | ||
}, | ||
{ | ||
name: 'section', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
{ | ||
name: 'predictive_search', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
{ | ||
name: 'recommendations', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
]; | ||
@@ -112,0 +136,0 @@ }, |
@@ -333,3 +333,1 @@ import { NodeTypes as LiquidHtmlNodeTypes, LiquidHtmlNode } from '@shopify/liquid-html-parser'; | ||
} | ||
export type WithRequired<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; | ||
export type WithOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; |
@@ -5,13 +5,9 @@ /** | ||
export interface ThemeDocset { | ||
/** | ||
* Returns Liquid filters available on themes. | ||
*/ | ||
/** Whether it was augmented prior to being passed. */ | ||
isAugmented?: boolean; | ||
/** Returns Liquid filters available on themes. */ | ||
filters(): Promise<FilterEntry[]>; | ||
/** | ||
* Returns objects (or Liquid variables) available on themes. | ||
*/ | ||
/** Returns objects (or Liquid variables) available on themes. */ | ||
objects(): Promise<ObjectEntry[]>; | ||
/** | ||
* Returns Liquid tags available on themes. | ||
*/ | ||
/** Returns Liquid tags available on themes. */ | ||
tags(): Promise<TagEntry[]>; | ||
@@ -23,5 +19,5 @@ } | ||
export interface JsonSchemaValidators { | ||
/** | ||
* Retrieves the JSON schema validator for theme sections. | ||
*/ | ||
/** Whether it was augmented prior to being passed. */ | ||
isAugmented?: boolean; | ||
/** Retrieves the JSON schema validator for theme sections. */ | ||
validateSectionSchema(): Promise<ValidateFunction>; | ||
@@ -28,0 +24,0 @@ } |
export * from './array'; | ||
export * from './position'; | ||
export * from './error'; | ||
export * from './types'; | ||
export * from './memo'; |
@@ -20,2 +20,4 @@ "use strict"; | ||
__exportStar(require("./error"), exports); | ||
__exportStar(require("./types"), exports); | ||
__exportStar(require("./memo"), exports); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@shopify/theme-check-common", | ||
"version": "1.16.0", | ||
"version": "1.16.1", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "homepage": "https://github.com/Shopify/theme-tools/blob/main/packages/theme-check-common/README.md", |
import { expect, describe, it } from 'vitest'; | ||
import { UndefinedObject } from './index'; | ||
import { runLiquidCheck, highlightedOffenses } from '../../test'; | ||
import { Offense } from '../../types'; | ||
@@ -196,2 +197,41 @@ describe('Module: UndefinedObject', () => { | ||
it('should contextually report on the undefined nature of the paginate object (defined in paginate tag, undefined outside)', async () => { | ||
const sourceCode = ` | ||
{% assign col = 'string' | split: '' %} | ||
{% paginate col by 5 %} | ||
{{ paginate }} | ||
{% endpaginate %}{{ paginate }} | ||
`; | ||
const offenses = await runLiquidCheck(UndefinedObject, sourceCode); | ||
expect(offenses).toHaveLength(1); | ||
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'paginate' used."]); | ||
}); | ||
it('should contextually report on the undefined nature of the form object (defined in form tag, undefined outside)', async () => { | ||
const sourceCode = ` | ||
{% form "cart" %} | ||
{{ form }} | ||
{% endform %}{{ form }} | ||
`; | ||
const offenses = await runLiquidCheck(UndefinedObject, sourceCode); | ||
expect(offenses).toHaveLength(1); | ||
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'form' used."]); | ||
}); | ||
it('should support {% layout none %}', async () => { | ||
const sourceCode = ` | ||
{% layout none %} | ||
{{ none }} | ||
`; | ||
const offenses = await runLiquidCheck(UndefinedObject, sourceCode); | ||
expect(offenses).toHaveLength(1); | ||
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'none' used."]); | ||
}); | ||
it('should not report an offense when object is undefined in a "snippet" file', async () => { | ||
@@ -232,2 +272,17 @@ const sourceCode = ` | ||
it('should support contextual exceptions', async () => { | ||
let offenses: Offense[]; | ||
const contexts: [string, string][] = [ | ||
['section', 'sections/section.liquid'], | ||
['predictive_search', 'sections/predictive-search.liquid'], | ||
['recommendations', 'sections/recommendations.liquid'], | ||
]; | ||
for (const [object, goodPath] of contexts) { | ||
offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, goodPath); | ||
expect(offenses).toHaveLength(0); | ||
offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, 'file.liquid'); | ||
expect(offenses).toHaveLength(1); | ||
} | ||
}); | ||
it('should report an offense for forloop/tablerowloop used outside of context', async () => { | ||
@@ -234,0 +289,0 @@ const sourceCode = ` |
@@ -38,3 +38,4 @@ import { | ||
*/ | ||
if (context.relativePath(context.file.absolutePath).startsWith('snippets/')) { | ||
const relativePath = context.relativePath(context.file.absolutePath); | ||
if (relativePath.startsWith('snippets/')) { | ||
return {}; | ||
@@ -75,2 +76,28 @@ } | ||
/** | ||
* {% form 'cart', cart %} | ||
* {{ form }} | ||
* {% endform %} | ||
*/ | ||
if (['form', 'paginate'].includes(node.name)) { | ||
indexVariableScope(node.name, { | ||
start: node.blockStartPosition.end, | ||
end: node.blockEndPosition?.start, | ||
}); | ||
} | ||
/* {% layout none %} */ | ||
if (node.name === 'layout') { | ||
indexVariableScope('none', { | ||
start: node.position.start, | ||
end: node.position.end, | ||
}); | ||
} | ||
/** | ||
* {% for x in y %} | ||
* {{ forloop }} | ||
* {{ x }} | ||
* {% endfor %} | ||
*/ | ||
if (isLiquidForTag(node) || isLiquidTableRowTag(node)) { | ||
@@ -97,3 +124,3 @@ indexVariableScope(node.markup.variableName, { | ||
async onCodePathEnd() { | ||
const objects = await globalObjects(themeDocset); | ||
const objects = await globalObjects(themeDocset, relativePath); | ||
@@ -119,7 +146,13 @@ objects.forEach((obj) => variableScopes.set(obj.name, [])); | ||
async function globalObjects(themeDocset: ThemeDocset) { | ||
async function globalObjects(themeDocset: ThemeDocset, relativePath: string) { | ||
const objects = await themeDocset.objects(); | ||
const contextualObjects = getContextualObjects(relativePath); | ||
const globalObjects = objects.filter(({ access, name }) => { | ||
return name === 'section' || !access || access.global === true || access.template.length > 0; | ||
return ( | ||
contextualObjects.includes(name) || | ||
!access || | ||
access.global === true || | ||
access.template.length > 0 | ||
); | ||
}); | ||
@@ -130,2 +163,10 @@ | ||
function getContextualObjects(relativePath: string): string[] { | ||
if (relativePath.startsWith('sections/')) { | ||
return ['section', 'predictive_search', 'recommendations']; | ||
} | ||
return []; | ||
} | ||
function isDefined( | ||
@@ -132,0 +173,0 @@ variableName: string, |
@@ -1,2 +0,3 @@ | ||
import { FixApplicator, Offense, SourceCodeType, Theme, WithRequired } from '../types'; | ||
import { FixApplicator, Offense, SourceCodeType, Theme } from '../types'; | ||
import { WithRequired } from '../utils/types'; | ||
import { createCorrector } from './correctors'; | ||
@@ -3,0 +4,0 @@ import { flattenFixes } from './utils'; |
@@ -27,3 +27,7 @@ import { | ||
import { isIgnored } from './ignore'; | ||
import { AugmentedThemeDocset } from './AugmentedThemeDocset'; | ||
import { AugmentedSchemaValidators } from './AugmentedSchemaValidators'; | ||
export * from './AugmentedThemeDocset'; | ||
export * from './AugmentedSchemaValidators'; | ||
export * from './fixes'; | ||
@@ -34,2 +38,4 @@ export * from './types'; | ||
export * from './ignore'; | ||
export * from './utils/types'; | ||
export * from './utils/memo'; | ||
@@ -49,2 +55,11 @@ const defaultErrorHandler = (_error: Error): void => { | ||
// We're memozing those deps here because they shouldn't change within a run. | ||
if (dependencies.themeDocset && !dependencies.themeDocset.isAugmented) { | ||
dependencies.themeDocset = new AugmentedThemeDocset(dependencies.themeDocset); | ||
} | ||
if (dependencies.schemaValidators && !dependencies.schemaValidators.isAugmented) { | ||
dependencies.schemaValidators = new AugmentedSchemaValidators(dependencies.schemaValidators); | ||
} | ||
for (const type of Object.values(SourceCodeType)) { | ||
@@ -51,0 +66,0 @@ switch (type) { |
@@ -144,2 +144,26 @@ import { | ||
}, | ||
{ | ||
name: 'section', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
{ | ||
name: 'predictive_search', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
{ | ||
name: 'recommendations', | ||
access: { | ||
global: false, | ||
parents: [], | ||
template: [], | ||
}, | ||
}, | ||
]; | ||
@@ -146,0 +170,0 @@ }, |
@@ -434,4 +434,1 @@ import { NodeTypes as LiquidHtmlNodeTypes, LiquidHtmlNode } from '@shopify/liquid-html-parser'; | ||
} | ||
export type WithRequired<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; | ||
export type WithOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; |
@@ -5,15 +5,12 @@ /** | ||
export interface ThemeDocset { | ||
/** | ||
* Returns Liquid filters available on themes. | ||
*/ | ||
/** Whether it was augmented prior to being passed. */ | ||
isAugmented?: boolean; | ||
/** Returns Liquid filters available on themes. */ | ||
filters(): Promise<FilterEntry[]>; | ||
/** | ||
* Returns objects (or Liquid variables) available on themes. | ||
*/ | ||
/** Returns objects (or Liquid variables) available on themes. */ | ||
objects(): Promise<ObjectEntry[]>; | ||
/** | ||
* Returns Liquid tags available on themes. | ||
*/ | ||
/** Returns Liquid tags available on themes. */ | ||
tags(): Promise<TagEntry[]>; | ||
@@ -26,5 +23,6 @@ } | ||
export interface JsonSchemaValidators { | ||
/** | ||
* Retrieves the JSON schema validator for theme sections. | ||
*/ | ||
/** Whether it was augmented prior to being passed. */ | ||
isAugmented?: boolean; | ||
/** Retrieves the JSON schema validator for theme sections. */ | ||
validateSectionSchema(): Promise<ValidateFunction>; | ||
@@ -31,0 +29,0 @@ } |
export * from './array'; | ||
export * from './position'; | ||
export * from './error'; | ||
export * from './types'; | ||
export * from './memo'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1143193
456
19277