@shopify/theme-check-common
Advanced tools
Comparing version 2.2.2 to 2.3.0
# @shopify/theme-check-common | ||
## 2.3.0 | ||
### Minor Changes | ||
- 8e3c7e2: Add Schema translation checking to `MatchingTranslations` | ||
- 8e3c7e2: Breaking: add `getDefaultSchema{Locale,Translations}(Factory)?` dependencies | ||
To be used to power `MatchingTranslations` for [Schema translations](https://shopify.dev/docs/themes/architecture/locales/schema-locale-files). | ||
To be used to power Schema translations code completion and hover in section and theme block `{% schema %}` JSON blobs. | ||
### Patch Changes | ||
- 8e3c7e2: Unify parseJSON usage | ||
## 2.2.2 | ||
@@ -4,0 +19,0 @@ |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.AssetSizeAppBlockCSS = void 0; | ||
const json_1 = require("../../json"); | ||
const types_1 = require("../../types"); | ||
const utils_1 = require("../../utils"); | ||
const file_utils_1 = require("../../utils/file-utils"); | ||
@@ -31,20 +33,16 @@ const schema = { | ||
return; | ||
let filePath; | ||
try { | ||
filePath = JSON.parse(node.body.value).stylesheet; | ||
} | ||
catch (error) { | ||
const schema = (0, json_1.parseJSON)(node.body.value); | ||
if ((0, utils_1.isError)(schema)) | ||
return; | ||
} | ||
if (!filePath) { | ||
const stylesheet = schema.stylesheet; | ||
if (!stylesheet) | ||
return; | ||
} | ||
const relativePath = `assets/${filePath}`; | ||
const relativePath = `assets/${stylesheet}`; | ||
const thresholdInBytes = context.settings.thresholdInBytes; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(filePath); | ||
const endIndex = startIndex + filePath.length; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(stylesheet); | ||
const endIndex = startIndex + stylesheet.length; | ||
const fileExists = await (0, file_utils_1.doesFileExist)(context, relativePath); | ||
if (!fileExists) { | ||
context.report({ | ||
message: `'${filePath}' does not exist.`, | ||
message: `'${stylesheet}' does not exist.`, | ||
startIndex: startIndex, | ||
@@ -58,3 +56,3 @@ endIndex: endIndex, | ||
context.report({ | ||
message: `The file size for '${filePath}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
message: `The file size for '${stylesheet}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
startIndex: startIndex, | ||
@@ -61,0 +59,0 @@ endIndex: endIndex, |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.AssetSizeAppBlockJavaScript = void 0; | ||
const json_1 = require("../../json"); | ||
const types_1 = require("../../types"); | ||
const utils_1 = require("../../utils"); | ||
const file_utils_1 = require("../../utils/file-utils"); | ||
@@ -31,20 +33,16 @@ const schema = { | ||
return; | ||
let filePath; | ||
try { | ||
filePath = JSON.parse(node.body.value).javascript; | ||
} | ||
catch (error) { | ||
const schema = (0, json_1.parseJSON)(node.body.value); | ||
if ((0, utils_1.isError)(schema)) | ||
return; | ||
} | ||
if (!filePath) { | ||
const javascript = schema.javascript; | ||
if (!javascript) | ||
return; | ||
} | ||
const relativePath = `assets/${filePath}`; | ||
const relativePath = `assets/${javascript}`; | ||
const thresholdInBytes = context.settings.thresholdInBytes; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(filePath); | ||
const endIndex = startIndex + filePath.length; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(javascript); | ||
const endIndex = startIndex + javascript.length; | ||
const fileExists = await (0, file_utils_1.doesFileExist)(context, relativePath); | ||
if (!fileExists) { | ||
context.report({ | ||
message: `'${filePath}' does not exist.`, | ||
message: `'${javascript}' does not exist.`, | ||
startIndex: startIndex, | ||
@@ -58,3 +56,3 @@ endIndex: endIndex, | ||
context.report({ | ||
message: `The file size for '${filePath}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
message: `The file size for '${javascript}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
startIndex: startIndex, | ||
@@ -61,0 +59,0 @@ endIndex: endIndex, |
@@ -25,2 +25,14 @@ "use strict"; | ||
const nodesByPath = new Map(); | ||
const file = context.file; | ||
const absolutePath = file.absolutePath; | ||
const relativePath = context.relativePath(absolutePath); | ||
const ast = file.ast; | ||
const isLocaleFile = relativePath.startsWith('locales/'); | ||
const isDefaultTranslationsFile = absolutePath.endsWith('.default.json') || absolutePath.endsWith('.default.schema.json'); | ||
const isSchemaTranslationFile = absolutePath.endsWith('.schema.json'); | ||
if (!isLocaleFile || isDefaultTranslationsFile || ast instanceof Error) { | ||
// No need to lint a file that isn't a translation file, we return an | ||
// empty object as the check for those. | ||
return {}; | ||
} | ||
// Helpers | ||
@@ -32,8 +44,3 @@ const hasDefaultTranslations = () => defaultTranslations.size > 0; | ||
const hasDefaultTranslation = (translationPath) => { var _a; return (_a = defaultTranslations.has(translationPath)) !== null && _a !== void 0 ? _a : false; }; | ||
const isDefaultTranslationsFile = ({ absolutePath }) => absolutePath.endsWith('.default.json'); | ||
const isPluralizationPath = (path) => [...PLURALIZATION_KEYS].some((key) => path.endsWith(key)); | ||
const isLocaleFile = ({ absolutePath }) => { | ||
const relativePath = context.relativePath(absolutePath); | ||
return relativePath.startsWith('locales/') && !relativePath.endsWith('schema.json'); | ||
}; | ||
const jsonPaths = (json) => { | ||
@@ -65,9 +72,2 @@ const keys = Object.keys(json); | ||
}; | ||
const file = context.file; | ||
const ast = file.ast; | ||
if (!isLocaleFile(file) || isDefaultTranslationsFile(file) || ast instanceof Error) { | ||
// No need to lint a file that isn't a translation file, we return an | ||
// empty object as the check for those. | ||
return {}; | ||
} | ||
const closestTranslationKey = (translationKey) => { | ||
@@ -90,3 +90,6 @@ var _a; | ||
async onCodePathStart() { | ||
const defaultTranslationPaths = await context.getDefaultTranslations().then(jsonPaths); | ||
const getDefaultTranslations = isSchemaTranslationFile | ||
? context.getDefaultSchemaTranslations | ||
: context.getDefaultTranslations; | ||
const defaultTranslationPaths = await getDefaultTranslations().then(jsonPaths); | ||
defaultTranslationPaths.forEach(Set.prototype.add, defaultTranslations); | ||
@@ -93,0 +96,0 @@ // At the `onCodePathStart`, we assume that all translations are missing, |
@@ -8,2 +8,3 @@ "use strict"; | ||
const utils_2 = require("../utils"); | ||
const json_1 = require("../../json"); | ||
const schema = { | ||
@@ -55,11 +56,8 @@ minSize: types_1.SchemaProp.number(1), | ||
if (node.name === 'schema') { | ||
try { | ||
const schema = JSON.parse(node.body.value); | ||
if (schema.settings && Array.isArray(schema.settings)) { | ||
schemaSettings = schema.settings; | ||
} | ||
const schema = (0, json_1.parseJSON)(node.body.value); | ||
if ((0, utils_1.isError)(schema)) | ||
return; | ||
if (schema.settings && Array.isArray(schema.settings)) { | ||
schemaSettings = schema.settings; | ||
} | ||
catch (error) { | ||
// Ignore JSON parsing errors | ||
} | ||
} | ||
@@ -66,0 +64,0 @@ }, |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.TranslationKeyExists = void 0; | ||
const json_1 = require("../../json"); | ||
const types_1 = require("../../types"); | ||
const utils_1 = require("../../utils"); | ||
function keyExists(key, pointer) { | ||
@@ -57,11 +59,6 @@ for (const token of key.split('.')) { | ||
const defaultLocale = await context.getDefaultLocale(); | ||
try { | ||
schemaLocales = (_a = JSON.parse(node.body.value).locales) === null || _a === void 0 ? void 0 : _a[defaultLocale]; | ||
} | ||
catch (error) { | ||
if (error instanceof SyntaxError) { | ||
return; | ||
} | ||
throw error; | ||
} | ||
const schema = (0, json_1.parseJSON)(node.body.value); | ||
if ((0, utils_1.isError)(schema) && schema instanceof SyntaxError) | ||
return; | ||
schemaLocales = (_a = schema.locales) === null || _a === void 0 ? void 0 : _a[defaultLocale]; | ||
}, | ||
@@ -68,0 +65,0 @@ async onCodePathEnd() { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.parseJsonBody = exports.JsonParseError = void 0; | ||
const utils_1 = require("../../utils"); | ||
const json_1 = require("../../json"); | ||
/** | ||
@@ -40,31 +42,30 @@ * Parses the error message from a `SyntaxError` | ||
var _a; | ||
try { | ||
return JSON.parse(node.body.value); | ||
const body = (0, json_1.parseJSON)(node.body.value); | ||
if (!(0, utils_1.isError)(body)) | ||
return body; | ||
const error = body; | ||
const defaultPosition = { | ||
start: node.blockStartPosition.end, | ||
end: node.blockEndPosition.start, | ||
}; | ||
if (error instanceof SyntaxError) { | ||
const schemaCharIndex = getErrorPositionFromSyntaxError(error); | ||
const offset = node.blockStartPosition.end; | ||
const position = schemaCharIndex | ||
? { | ||
/** | ||
* Offset the error indicies by the position where the syntax error occurred. | ||
* A single character position can be hard to see it or hover for details. | ||
* A position length of 3 characters makes a good balance between visibility and accuracy. | ||
*/ | ||
start: offset + schemaCharIndex - 1, | ||
end: offset + schemaCharIndex + 1, | ||
} | ||
: defaultPosition; | ||
const sanitizedMessage = substringBefore(error.message, ' in JSON at position'); | ||
return new JsonParseError(sanitizedMessage, position); | ||
} | ||
catch (error) { | ||
const defaultPosition = { | ||
start: node.blockStartPosition.end, | ||
end: node.blockEndPosition.start, | ||
}; | ||
if (error instanceof SyntaxError) { | ||
const schemaCharIndex = getErrorPositionFromSyntaxError(error); | ||
const offset = node.blockStartPosition.end; | ||
const position = schemaCharIndex | ||
? { | ||
/** | ||
* Offset the error indicies by the position where the syntax error occurred. | ||
* A single character position can be hard to see it or hover for details. | ||
* A position length of 3 characters makes a good balance between visibility and accuracy. | ||
*/ | ||
start: offset + schemaCharIndex - 1, | ||
end: offset + schemaCharIndex + 1, | ||
} | ||
: defaultPosition; | ||
const sanitizedMessage = substringBefore(error.message, ' in JSON at position'); | ||
return new JsonParseError(sanitizedMessage, position); | ||
} | ||
return new JsonParseError((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : 'Unknown error', defaultPosition); | ||
} | ||
return new JsonParseError((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : 'Unknown error', defaultPosition); | ||
}; | ||
exports.parseJsonBody = parseJsonBody; | ||
//# sourceMappingURL=parse-json-body.js.map |
@@ -8,5 +8,7 @@ import { Config, Dependencies, Offense, Theme } from './types'; | ||
export * from './to-source-code'; | ||
export * from './json'; | ||
export * from './ignore'; | ||
export * from './utils/error'; | ||
export * from './utils/types'; | ||
export * from './utils/memo'; | ||
export declare function check(sourceCodes: Theme, config: Config, dependencies: Dependencies): Promise<Offense[]>; |
@@ -44,3 +44,5 @@ "use strict"; | ||
__exportStar(require("./to-source-code"), exports); | ||
__exportStar(require("./json"), exports); | ||
__exportStar(require("./ignore"), exports); | ||
__exportStar(require("./utils/error"), exports); | ||
__exportStar(require("./utils/types"), exports); | ||
@@ -47,0 +49,0 @@ __exportStar(require("./utils/memo"), exports); |
@@ -0,2 +1,5 @@ | ||
import toJSON from 'json-to-ast'; | ||
import { JSONSourceCode, LiquidSourceCode } from './types'; | ||
export declare function toLiquidHTMLAST(source: string): Error | import("@shopify/liquid-html-parser").DocumentNode; | ||
export declare function toJSONAST(source: string): Error | toJSON.ValueNode; | ||
export declare function toSourceCode(absolutePath: string, source: string, version?: number): LiquidSourceCode | JSONSourceCode; |
@@ -6,21 +6,8 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.toSourceCode = void 0; | ||
exports.toSourceCode = exports.toJSONAST = exports.toLiquidHTMLAST = void 0; | ||
const liquid_html_parser_1 = require("@shopify/liquid-html-parser"); | ||
const json_to_ast_1 = __importDefault(require("json-to-ast")); | ||
const types_1 = require("./types"); | ||
function asError(error) { | ||
if (error instanceof Error) { | ||
return error; | ||
} | ||
else if (typeof error === 'string') { | ||
return new Error(error); | ||
} | ||
else if (error && typeof error.toString === 'function') { | ||
return new Error(error.toString()); | ||
} | ||
else { | ||
return new Error('An unknown error occurred'); | ||
} | ||
} | ||
function parseLiquid(source) { | ||
const error_1 = require("./utils/error"); | ||
function toLiquidHTMLAST(source) { | ||
try { | ||
@@ -30,6 +17,7 @@ return (0, liquid_html_parser_1.toLiquidHtmlAST)(source); | ||
catch (error) { | ||
return asError(error); | ||
return (0, error_1.asError)(error); | ||
} | ||
} | ||
function parseJSON(source) { | ||
exports.toLiquidHTMLAST = toLiquidHTMLAST; | ||
function toJSONAST(source) { | ||
try { | ||
@@ -39,5 +27,6 @@ return (0, json_to_ast_1.default)(source); | ||
catch (error) { | ||
return asError(error); | ||
return (0, error_1.asError)(error); | ||
} | ||
} | ||
exports.toJSONAST = toJSONAST; | ||
function toSourceCode(absolutePath, source, version) { | ||
@@ -50,3 +39,3 @@ const isLiquid = absolutePath.endsWith('.liquid'); | ||
type: types_1.SourceCodeType.LiquidHtml, | ||
ast: parseLiquid(source), | ||
ast: toLiquidHTMLAST(source), | ||
version, | ||
@@ -60,3 +49,3 @@ }; | ||
type: types_1.SourceCodeType.JSON, | ||
ast: parseJSON(source), | ||
ast: toJSONAST(source), | ||
version, | ||
@@ -63,0 +52,0 @@ }; |
@@ -198,2 +198,4 @@ import { NodeTypes as LiquidHtmlNodeTypes, LiquidHtmlNode } from '@shopify/liquid-html-parser'; | ||
getDefaultLocale(): Promise<string>; | ||
getDefaultSchemaLocale(): Promise<string>; | ||
getDefaultSchemaTranslations(): Promise<Translations>; | ||
fileExists(absolutePath: string): Promise<boolean>; | ||
@@ -200,0 +202,0 @@ fileSize?(absolutePath: string): Promise<number>; |
export declare function isError(error: unknown): error is Error; | ||
export declare function asError(error: unknown): Error; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isError = void 0; | ||
exports.asError = exports.isError = void 0; | ||
function isError(error) { | ||
@@ -8,2 +8,17 @@ return error instanceof Error; | ||
exports.isError = isError; | ||
function asError(error) { | ||
if (error instanceof Error) { | ||
return error; | ||
} | ||
else if (typeof error === 'string') { | ||
return new Error(error); | ||
} | ||
else if (error && typeof error.toString === 'function') { | ||
return new Error(error.toString()); | ||
} | ||
else { | ||
return new Error('An unknown error occurred'); | ||
} | ||
} | ||
exports.asError = asError; | ||
//# sourceMappingURL=error.js.map |
{ | ||
"name": "@shopify/theme-check-common", | ||
"version": "2.2.2", | ||
"version": "2.3.0", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -0,1 +1,2 @@ | ||
import { parseJSON } from '../../json'; | ||
import { | ||
@@ -8,3 +9,4 @@ ConfigTarget, | ||
} from '../../types'; | ||
import { doesFileExist, doesFileExceedThreshold } from '../../utils/file-utils'; | ||
import { isError } from '../../utils'; | ||
import { doesFileExceedThreshold, doesFileExist } from '../../utils/file-utils'; | ||
@@ -39,18 +41,12 @@ const schema = { | ||
if (node.name !== 'schema') return; | ||
let filePath; | ||
try { | ||
filePath = JSON.parse(node.body.value).stylesheet; | ||
} catch (error) { | ||
return; | ||
} | ||
const schema = parseJSON(node.body.value); | ||
if (isError(schema)) return; | ||
const stylesheet = schema.stylesheet; | ||
if (!stylesheet) return; | ||
if (!filePath) { | ||
return; | ||
} | ||
const relativePath = `assets/${filePath}`; | ||
const relativePath = `assets/${stylesheet}`; | ||
const thresholdInBytes = context.settings.thresholdInBytes; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(filePath); | ||
const endIndex = startIndex + filePath.length; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(stylesheet); | ||
const endIndex = startIndex + stylesheet.length; | ||
@@ -61,3 +57,3 @@ const fileExists = await doesFileExist(context, relativePath); | ||
context.report({ | ||
message: `'${filePath}' does not exist.`, | ||
message: `'${stylesheet}' does not exist.`, | ||
startIndex: startIndex, | ||
@@ -77,3 +73,3 @@ endIndex: endIndex, | ||
context.report({ | ||
message: `The file size for '${filePath}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
message: `The file size for '${stylesheet}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
startIndex: startIndex, | ||
@@ -80,0 +76,0 @@ endIndex: endIndex, |
@@ -0,1 +1,2 @@ | ||
import { parseJSON } from '../../json'; | ||
import { | ||
@@ -8,2 +9,3 @@ ConfigTarget, | ||
} from '../../types'; | ||
import { isError } from '../../utils'; | ||
import { doesFileExist, doesFileExceedThreshold } from '../../utils/file-utils'; | ||
@@ -39,18 +41,12 @@ | ||
if (node.name !== 'schema') return; | ||
let filePath; | ||
try { | ||
filePath = JSON.parse(node.body.value).javascript; | ||
} catch (error) { | ||
return; | ||
} | ||
const schema = parseJSON(node.body.value); | ||
if (isError(schema)) return; | ||
const javascript = schema.javascript; | ||
if (!javascript) return; | ||
if (!filePath) { | ||
return; | ||
} | ||
const relativePath = `assets/${filePath}`; | ||
const relativePath = `assets/${javascript}`; | ||
const thresholdInBytes = context.settings.thresholdInBytes; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(filePath); | ||
const endIndex = startIndex + filePath.length; | ||
const startIndex = node.body.position.start + node.body.value.indexOf(javascript); | ||
const endIndex = startIndex + javascript.length; | ||
@@ -61,3 +57,3 @@ const fileExists = await doesFileExist(context, relativePath); | ||
context.report({ | ||
message: `'${filePath}' does not exist.`, | ||
message: `'${javascript}' does not exist.`, | ||
startIndex: startIndex, | ||
@@ -77,3 +73,3 @@ endIndex: endIndex, | ||
context.report({ | ||
message: `The file size for '${filePath}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
message: `The file size for '${javascript}' (${fileSize} B) exceeds the configured threshold (${thresholdInBytes} B)`, | ||
startIndex: startIndex, | ||
@@ -80,0 +76,0 @@ endIndex: endIndex, |
@@ -9,241 +9,261 @@ import { expect, describe, it } from 'vitest'; | ||
it('should report offenses when the translation file is missing a key', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense("The translation for 'world' is missing"); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense("The translation for 'world' is missing"); | ||
} | ||
}); | ||
it('should report offenses when the default translation is missing a key', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense("A default translation for 'world' does not exist"); | ||
expect(offenses[0]!).to.suggest( | ||
theme['locales/pt-BR.json'], | ||
'Delete unneeded translation key', | ||
{ | ||
startIndex: 0, | ||
endIndex: theme['locales/pt-BR.json'].length, | ||
insert: prettyJSON({ | ||
hello: 'Olá', | ||
}), | ||
}, | ||
); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense("A default translation for 'world' does not exist"); | ||
expect(offenses[0]!).to.suggest( | ||
theme[`locales/pt-BR${prefix}.json`], | ||
'Delete unneeded translation key', | ||
{ | ||
startIndex: 0, | ||
endIndex: theme[`locales/pt-BR${prefix}.json`].length, | ||
insert: prettyJSON({ | ||
hello: 'Olá', | ||
}), | ||
}, | ||
); | ||
} | ||
}); | ||
it('should report offenses when nested translation keys do not exist', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: {}, | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: {}, | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: '/locales/pt-BR.json', | ||
}); | ||
expect(offenses).to.be.of.length(1); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: `/locales/pt-BR${prefix}.json`, | ||
}); | ||
const fixed = await autofix(theme, offenses); | ||
expect(fixed['locales/pt-BR.json']).to.eql( | ||
prettyJSON({ | ||
hello: { | ||
world: 'TODO', | ||
}, | ||
}), | ||
); | ||
const fixed = await autofix(theme, offenses); | ||
expect(fixed[`locales/pt-BR${prefix}.json`]).to.eql( | ||
prettyJSON({ | ||
hello: { | ||
world: 'TODO', | ||
}, | ||
}), | ||
); | ||
} | ||
}); | ||
it('should report offenses when translation shapes do not match', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(2); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello' does not exist", | ||
absolutePath: '/locales/pt-BR.json', | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: '/locales/pt-BR.json', | ||
}); | ||
expect(offenses).to.be.of.length(2); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello' does not exist", | ||
absolutePath: `/locales/pt-BR${prefix}.json`, | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: `/locales/pt-BR${prefix}.json`, | ||
}); | ||
const fixed = await autofix(theme, offenses); | ||
const fixed = await autofix(theme, offenses); | ||
expect(fixed['locales/pt-BR.json']).to.eql( | ||
prettyJSON({ | ||
hello: { world: 'TODO' }, | ||
}), | ||
); | ||
expect(fixed[`locales/pt-BR${prefix}.json`]).to.eql( | ||
prettyJSON({ | ||
hello: { world: 'TODO' }, | ||
}), | ||
); | ||
} | ||
}); | ||
it('should report offenses when nested translation keys do not match', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/fr.json': JSON.stringify({ | ||
hello: { monde: 'Bonjour, monde' }, | ||
}), | ||
'locales/es-ES.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!', mundo: { hola: '¡Hola, mundo!' } }, | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/fr${prefix}.json`]: JSON.stringify({ | ||
hello: { monde: 'Bonjour, monde' }, | ||
}), | ||
[`locales/es-ES${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!', mundo: { hola: '¡Hola, mundo!' } }, | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(3); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello.monde' does not exist", | ||
absolutePath: '/locales/fr.json', | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello.mundo.hola' does not exist", | ||
absolutePath: '/locales/es-ES.json', | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: '/locales/fr.json', | ||
}); | ||
expect(offenses).to.be.of.length(3); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello.monde' does not exist", | ||
absolutePath: `/locales/fr${prefix}.json`, | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "A default translation for 'hello.mundo.hola' does not exist", | ||
absolutePath: `/locales/es-ES${prefix}.json`, | ||
}); | ||
expect(offenses).to.containOffense({ | ||
message: "The translation for 'hello.world' is missing", | ||
absolutePath: `/locales/fr${prefix}.json`, | ||
}); | ||
const fixed = await autofix(theme, offenses); | ||
expect(fixed['locales/fr.json']).to.eql( | ||
prettyJSON({ | ||
hello: { monde: 'Bonjour, monde', world: 'TODO' }, | ||
}), | ||
); | ||
const fixed = await autofix(theme, offenses); | ||
expect(fixed[`locales/fr${prefix}.json`]).to.eql( | ||
prettyJSON({ | ||
hello: { monde: 'Bonjour, monde', world: 'TODO' }, | ||
}), | ||
); | ||
// Default does not exist should be a suggestion and not autofixed. | ||
expect(fixed['locales/es-ES.json']).to.eql(theme['locales/es-ES.json']); | ||
// Default does not exist should be a suggestion and not autofixed. | ||
expect(fixed[`locales/es-ES${prefix}.json`]).to.eql(theme[`locales/es-ES${prefix}.json`]); | ||
} | ||
}); | ||
it('should not report offenses when default translations do not exist', async () => { | ||
const theme = { | ||
'locales/en.json': JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(0); | ||
expect(offenses).to.be.of.length(0); | ||
} | ||
}); | ||
it('should not report offenses when translations match', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(0); | ||
expect(offenses).to.be.of.length(0); | ||
} | ||
}); | ||
it('should not report offenses when nested translations match', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: { world: 'Olá, mundo!' }, | ||
}), | ||
'locales/fr.json': JSON.stringify({ | ||
hello: { world: 'Bonjour, monde' }, | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Olá, mundo!' }, | ||
}), | ||
[`locales/fr${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Bonjour, monde' }, | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(0); | ||
expect(offenses).to.be.of.length(0); | ||
} | ||
}); | ||
it('should not report offenses and ignore pluralization', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { | ||
one: 'Hello, you', | ||
other: "Hello, y'all", | ||
}, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: { | ||
zero: 'Estou sozinho :(', | ||
few: 'Olá, galerinha :)', | ||
}, | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { | ||
one: 'Hello, you', | ||
other: "Hello, y'all", | ||
}, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: { | ||
zero: 'Estou sozinho :(', | ||
few: 'Olá, galerinha :)', | ||
}, | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(0); | ||
expect(offenses).to.be.of.length(0); | ||
} | ||
}); | ||
it('should not report offenses and ignore keys provided by Shopify', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
shopify: { | ||
checkout: { | ||
general: { | ||
page_title: 'Checkout', | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
shopify: { | ||
checkout: { | ||
general: { | ||
page_title: 'Checkout', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
shopify: { | ||
sentence: { | ||
words_connector: 'hello world', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
shopify: { | ||
sentence: { | ||
words_connector: 'hello world', | ||
}, | ||
}, | ||
}, | ||
}), | ||
}; | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.be.of.length(0); | ||
expect(offenses).to.be.of.length(0); | ||
} | ||
}); | ||
@@ -263,108 +283,120 @@ | ||
it('should highlight the proper element when the translation file is missing a key', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
world: 'World', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
expect(elements).to.deep.eq(['{"hello":"Olá"}']); | ||
expect(elements).to.deep.eq(['{"hello":"Olá"}']); | ||
} | ||
}); | ||
it('should highlight the proper element when the default translation is missing a key', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: 'Hello', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
world: 'Mundo', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
expect(elements).to.deep.eq(['"world":"Mundo"']); | ||
expect(elements).to.deep.eq(['"world":"Mundo"']); | ||
} | ||
}); | ||
it('should highlight the proper element when nested translation keys do not exist', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { | ||
world: 'Hello, world!', | ||
}, | ||
welcome: 'Welcome', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: {}, | ||
welcome: 'Bem-vinda', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { | ||
world: 'Hello, world!', | ||
}, | ||
welcome: 'Welcome', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: {}, | ||
welcome: 'Bem-vinda', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
expect(elements).to.deep.eq(['"hello":{}']); | ||
expect(elements).to.deep.eq(['"hello":{}']); | ||
} | ||
}); | ||
it('should highlight the proper element when nested translation keys do not exist and there is a sibling node', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { | ||
shopify: 'Shopify!', | ||
world: 'Hello, world!', | ||
}, | ||
welcome: 'Welcome', | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: { | ||
shopify: 'Shopify!', | ||
}, | ||
welcome: 'Bem-vinda', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { | ||
shopify: 'Shopify!', | ||
world: 'Hello, world!', | ||
}, | ||
welcome: 'Welcome', | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: { | ||
shopify: 'Shopify!', | ||
}, | ||
welcome: 'Bem-vinda', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
expect(elements).to.deep.eq(['"hello":{"shopify":"Shopify!"}']); | ||
expect(elements).to.deep.eq(['"hello":{"shopify":"Shopify!"}']); | ||
} | ||
}); | ||
it('should highlight the proper element when translation shapes do not match', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/pt-BR.json': JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: JSON.stringify({ | ||
hello: 'Olá', | ||
}), | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
const elements = highlightedOffenses(theme, offenses); | ||
// We have two elements because we have two offenses: | ||
// - A default translation for 'hello' does not exist" | ||
// - The translation for 'hello.world' is missing" | ||
expect(elements).to.deep.eq(['"hello":"Olá"', '"hello":"Olá"']); | ||
// We have two elements because we have two offenses: | ||
// - A default translation for 'hello' does not exist" | ||
// - The translation for 'hello.world' is missing" | ||
expect(elements).to.deep.eq(['"hello":"Olá"', '"hello":"Olá"']); | ||
} | ||
}); | ||
it('should not highlight anything if the file is unparseable', async () => { | ||
const theme = { | ||
'locales/en.default.json': JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
'locales/pt-BR.json': `{"hello": }`, | ||
}; | ||
for (const prefix of ['', '.schema']) { | ||
const theme = { | ||
[`locales/en.default${prefix}.json`]: JSON.stringify({ | ||
hello: { world: 'Hello, world!' }, | ||
}), | ||
[`locales/pt-BR${prefix}.json`]: `{"hello": }`, | ||
}; | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.have.length(0); | ||
const offenses = await check(theme, [MatchingTranslations]); | ||
expect(offenses).to.have.length(0); | ||
} | ||
}); | ||
}); |
@@ -32,3 +32,17 @@ import { PropertyNode } from 'json-to-ast'; | ||
const nodesByPath = new Map<string, PropertyNode>(); | ||
const file = context.file; | ||
const absolutePath = file.absolutePath; | ||
const relativePath = context.relativePath(absolutePath); | ||
const ast = file.ast; | ||
const isLocaleFile = relativePath.startsWith('locales/'); | ||
const isDefaultTranslationsFile = | ||
absolutePath.endsWith('.default.json') || absolutePath.endsWith('.default.schema.json'); | ||
const isSchemaTranslationFile = absolutePath.endsWith('.schema.json'); | ||
if (!isLocaleFile || isDefaultTranslationsFile || ast instanceof Error) { | ||
// No need to lint a file that isn't a translation file, we return an | ||
// empty object as the check for those. | ||
return {}; | ||
} | ||
// Helpers | ||
@@ -43,14 +57,5 @@ const hasDefaultTranslations = () => defaultTranslations.size > 0; | ||
const isDefaultTranslationsFile = ({ absolutePath }: JSONSourceCode) => | ||
absolutePath.endsWith('.default.json'); | ||
const isPluralizationPath = (path: string) => | ||
[...PLURALIZATION_KEYS].some((key) => path.endsWith(key)); | ||
const isLocaleFile = ({ absolutePath }: JSONSourceCode) => { | ||
const relativePath = context.relativePath(absolutePath); | ||
return relativePath.startsWith('locales/') && !relativePath.endsWith('schema.json'); | ||
}; | ||
const jsonPaths = (json: any): string[] => { | ||
@@ -90,10 +95,2 @@ const keys = Object.keys(json); | ||
const file = context.file; | ||
const ast = file.ast; | ||
if (!isLocaleFile(file) || isDefaultTranslationsFile(file) || ast instanceof Error) { | ||
// No need to lint a file that isn't a translation file, we return an | ||
// empty object as the check for those. | ||
return {}; | ||
} | ||
const closestTranslationKey = (translationKey: string) => { | ||
@@ -119,3 +116,6 @@ const translationKeyParts = translationKey.split('.'); | ||
async onCodePathStart() { | ||
const defaultTranslationPaths = await context.getDefaultTranslations().then(jsonPaths); | ||
const getDefaultTranslations = isSchemaTranslationFile | ||
? context.getDefaultSchemaTranslations | ||
: context.getDefaultTranslations; | ||
const defaultTranslationPaths = await getDefaultTranslations().then(jsonPaths); | ||
defaultTranslationPaths.forEach(Set.prototype.add, defaultTranslations); | ||
@@ -122,0 +122,0 @@ |
@@ -8,4 +8,5 @@ import { | ||
import { LiquidCheckDefinition, SchemaProp, Severity, SourceCodeType } from '../../types'; | ||
import { last } from '../../utils'; | ||
import { isError, last } from '../../utils'; | ||
import { isNodeOfType } from '../utils'; | ||
import { parseJSON } from '../../json'; | ||
@@ -66,9 +67,6 @@ const schema = { | ||
if (node.name === 'schema') { | ||
try { | ||
const schema = JSON.parse(node.body.value); | ||
if (schema.settings && Array.isArray(schema.settings)) { | ||
schemaSettings = schema.settings; | ||
} | ||
} catch (error) { | ||
// Ignore JSON parsing errors | ||
const schema = parseJSON(node.body.value); | ||
if (isError(schema)) return; | ||
if (schema.settings && Array.isArray(schema.settings)) { | ||
schemaSettings = schema.settings; | ||
} | ||
@@ -75,0 +73,0 @@ } |
@@ -0,2 +1,4 @@ | ||
import { parseJSON } from '../../json'; | ||
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types'; | ||
import { isError } from '../../utils'; | ||
@@ -65,10 +67,5 @@ function keyExists(key: string, pointer: any) { | ||
const defaultLocale = await context.getDefaultLocale(); | ||
try { | ||
schemaLocales = JSON.parse(node.body.value).locales?.[defaultLocale]; | ||
} catch (error) { | ||
if (error instanceof SyntaxError) { | ||
return; | ||
} | ||
throw error; | ||
} | ||
const schema = parseJSON(node.body.value); | ||
if (isError(schema) && schema instanceof SyntaxError) return; | ||
schemaLocales = schema.locales?.[defaultLocale]; | ||
}, | ||
@@ -75,0 +72,0 @@ |
import { LiquidRawTag, Position } from '@shopify/liquid-html-parser'; | ||
import { isError } from '../../utils'; | ||
import { parseJSON } from '../../json'; | ||
@@ -45,33 +47,32 @@ /** | ||
export const parseJsonBody = (node: LiquidRawTag): object | Error => { | ||
try { | ||
return JSON.parse(node.body.value); | ||
} catch (error) { | ||
const defaultPosition = { | ||
start: node.blockStartPosition.end, | ||
end: node.blockEndPosition.start, | ||
}; | ||
const body = parseJSON(node.body.value); | ||
if (!isError(body)) return body; | ||
const error = body; | ||
const defaultPosition = { | ||
start: node.blockStartPosition.end, | ||
end: node.blockEndPosition.start, | ||
}; | ||
if (error instanceof SyntaxError) { | ||
const schemaCharIndex = getErrorPositionFromSyntaxError(error); | ||
if (error instanceof SyntaxError) { | ||
const schemaCharIndex = getErrorPositionFromSyntaxError(error); | ||
const offset = node.blockStartPosition.end; | ||
const position = schemaCharIndex | ||
? { | ||
/** | ||
* Offset the error indicies by the position where the syntax error occurred. | ||
* A single character position can be hard to see it or hover for details. | ||
* A position length of 3 characters makes a good balance between visibility and accuracy. | ||
*/ | ||
start: offset + schemaCharIndex - 1, | ||
end: offset + schemaCharIndex + 1, | ||
} | ||
: defaultPosition; | ||
const offset = node.blockStartPosition.end; | ||
const position = schemaCharIndex | ||
? { | ||
/** | ||
* Offset the error indicies by the position where the syntax error occurred. | ||
* A single character position can be hard to see it or hover for details. | ||
* A position length of 3 characters makes a good balance between visibility and accuracy. | ||
*/ | ||
start: offset + schemaCharIndex - 1, | ||
end: offset + schemaCharIndex + 1, | ||
} | ||
: defaultPosition; | ||
const sanitizedMessage = substringBefore(error.message, ' in JSON at position'); | ||
const sanitizedMessage = substringBefore(error.message, ' in JSON at position'); | ||
return new JsonParseError(sanitizedMessage, position); | ||
} | ||
return new JsonParseError(sanitizedMessage, position); | ||
} | ||
return new JsonParseError((error as Error)?.message ?? 'Unknown error', defaultPosition); | ||
} | ||
return new JsonParseError((error as Error)?.message ?? 'Unknown error', defaultPosition); | ||
}; |
@@ -36,3 +36,5 @@ import { | ||
export * from './to-source-code'; | ||
export * from './json'; | ||
export * from './ignore'; | ||
export * from './utils/error'; | ||
export * from './utils/types'; | ||
@@ -39,0 +41,0 @@ export * from './utils/memo'; |
@@ -20,2 +20,3 @@ import { | ||
ChecksSettings, | ||
parseJSON, | ||
} from '../index'; | ||
@@ -61,2 +62,3 @@ | ||
const defaultTranslationsFileRelativePath = 'locales/en.default.json'; | ||
const defaultSchemaTranslationsFileRelativePath = 'locales/en.default.schema.json'; | ||
const defaultMockDependencies = { | ||
@@ -72,12 +74,15 @@ async fileSize(absolutePath: string) { | ||
async getDefaultTranslations() { | ||
try { | ||
return JSON.parse(themeDesc[defaultTranslationsFileRelativePath] || '{}'); | ||
} catch (e) { | ||
if (e instanceof SyntaxError) return {}; | ||
throw e; | ||
} | ||
return parseJSON(themeDesc[defaultTranslationsFileRelativePath] || '{}', {}); | ||
}, | ||
async getDefaultSchemaTranslations() { | ||
return parseJSON(themeDesc[defaultSchemaTranslationsFileRelativePath] || '{}', {}); | ||
}, | ||
async getDefaultLocale() { | ||
return defaultTranslationsFileRelativePath.match(/locales\/(.*)\.default\.json$/)?.[1]!; | ||
}, | ||
async getDefaultSchemaLocale() { | ||
return defaultSchemaTranslationsFileRelativePath.match( | ||
/locales\/(.*)\.default\.schema\.json$/, | ||
)?.[1]!; | ||
}, | ||
themeDocset: { | ||
@@ -84,0 +89,0 @@ async filters() { |
@@ -5,16 +5,5 @@ import { toLiquidHtmlAST } from '@shopify/liquid-html-parser'; | ||
import { SourceCodeType, JSONSourceCode, LiquidSourceCode } from './types'; | ||
import { asError } from './utils/error'; | ||
function asError(error: unknown): Error { | ||
if (error instanceof Error) { | ||
return error; | ||
} else if (typeof error === 'string') { | ||
return new Error(error); | ||
} else if (error && typeof error.toString === 'function') { | ||
return new Error(error.toString()); | ||
} else { | ||
return new Error('An unknown error occurred'); | ||
} | ||
} | ||
function parseLiquid(source: string) { | ||
export function toLiquidHTMLAST(source: string) { | ||
try { | ||
@@ -27,3 +16,3 @@ return toLiquidHtmlAST(source); | ||
function parseJSON(source: string) { | ||
export function toJSONAST(source: string) { | ||
try { | ||
@@ -48,3 +37,3 @@ return toJSON(source); | ||
type: SourceCodeType.LiquidHtml, | ||
ast: parseLiquid(source), | ||
ast: toLiquidHTMLAST(source), | ||
version, | ||
@@ -57,3 +46,3 @@ }; | ||
type: SourceCodeType.JSON, | ||
ast: parseJSON(source), | ||
ast: toJSONAST(source), | ||
version, | ||
@@ -60,0 +49,0 @@ }; |
@@ -269,2 +269,4 @@ import { NodeTypes as LiquidHtmlNodeTypes, LiquidHtmlNode } from '@shopify/liquid-html-parser'; | ||
getDefaultLocale(): Promise<string>; | ||
getDefaultSchemaLocale(): Promise<string>; | ||
getDefaultSchemaTranslations(): Promise<Translations>; | ||
fileExists(absolutePath: string): Promise<boolean>; | ||
@@ -271,0 +273,0 @@ fileSize?(absolutePath: string): Promise<number>; |
export function isError(error: unknown): error is Error { | ||
return error instanceof Error; | ||
} | ||
export function asError(error: unknown): Error { | ||
if (error instanceof Error) { | ||
return error; | ||
} else if (typeof error === 'string') { | ||
return new Error(error); | ||
} else if (error && typeof error.toString === 'function') { | ||
return new Error(error.toString()); | ||
} else { | ||
return new Error('An unknown error occurred'); | ||
} | ||
} |
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
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
818548
324
14872