@ordergroove/smi-precompile
Advanced tools
Comparing version 1.7.11-alpha-PR-766-9.48 to 1.7.11-alpha-PR-819-4.45
export * as loader from './loader'; | ||
export * as precompile from './precompile'; |
@@ -344,33 +344,20 @@ const nunjucks = require('nunjucks'); | ||
const array = this.wrap(node.arr); | ||
const renderArrayItem = t.arrowFunctionExpression( | ||
[ | ||
node.name instanceof n.Array | ||
? t.objectPattern( | ||
node.name.children.map(it => | ||
t.objectProperty(t.identifier(it.value), t.identifier(it.value), false, true) | ||
const repeatCallExpression = t.callExpression(t.memberExpression(array, t.identifier('map')), [ | ||
t.arrowFunctionExpression( | ||
[ | ||
node.name instanceof n.Array | ||
? t.objectPattern( | ||
node.name.children.map(it => | ||
t.objectProperty(t.identifier(it.value), t.identifier(it.value), false, true) | ||
) | ||
) | ||
) | ||
: this.wrap(node.name), | ||
t.identifier('index') | ||
], | ||
this.wrapTemplate(node.body) | ||
); | ||
const mapCallExpression = t.callExpression(t.memberExpression(array, t.identifier('map')), [renderArrayItem]); | ||
const repeatCallExpression = t.callExpression(t.identifier('repeat'), [ | ||
array, | ||
t.arrowFunctionExpression( | ||
[t.identifier('item')], | ||
// using the | key filter sets this property on each item | ||
// we use it as a unique key to allow Lit to update lists correctly when the order changes | ||
t.memberExpression(t.identifier('item'), t.identifier('__og_key')) | ||
), | ||
renderArrayItem | ||
: this.wrap(node.name), | ||
t.identifier('index') | ||
], | ||
this.wrapTemplate(node.body) | ||
) | ||
]); | ||
const isArray = t.callExpression(t.identifier('Array.isArray'), [array]); | ||
// when the | key filter is used in the template, the resulting array will have this property set | ||
const shouldKey = t.memberExpression(array, t.identifier('__og_should_key')); | ||
const conditionalMapOrRepeat = t.conditionalExpression(shouldKey, repeatCallExpression, mapCallExpression); | ||
return t.conditionalExpression( | ||
@@ -381,3 +368,3 @@ node.else_ | ||
conditionalMapOrRepeat, | ||
repeatCallExpression, | ||
node.else_ ? this.wrapTemplate(node.else_) : t.stringLiteral('') | ||
@@ -384,0 +371,0 @@ ); |
@@ -56,9 +56,5 @@ /* eslint-disable */ | ||
describe('forloop', () => { | ||
function expectedForLoop(renderCode, renderElse = '""') { | ||
return `{ return Array.isArray(list) ? list.__og_should_key ? repeat(list, item => item.__og_key, ${renderCode}) : list.map(${renderCode}) : ${renderElse}; }`; | ||
} | ||
test('should repeat items', () => { | ||
expect(compile(`{%for item in list %}{{item}}{% endfor%}`)).toEqual( | ||
toCode(expectedForLoop('(item, index) => item'), 'list') | ||
toCode('{ return Array.isArray(list) ? list.map((item, index) => item) : "" }', 'list') | ||
); | ||
@@ -68,3 +64,3 @@ }); | ||
expect(compile(`{%for name, value in list %}{{name}}{% endfor%}`)).toEqual( | ||
toCode(expectedForLoop('({ name, value }, index) => name'), 'list') | ||
toCode('{ return Array.isArray(list) ? list.map(({name, value}, index) => name) : ""; }', 'list') | ||
); | ||
@@ -74,3 +70,3 @@ }); | ||
expect(compile(`{%for item in list %}<div>{{item}}</div>{% endfor%}`)).toEqual( | ||
toCode(expectedForLoop('(item, index) => html`<div>${item}</div>`'), 'list') | ||
toCode('{ return Array.isArray(list) ? list.map((item, index) => html`<div>${item}</div>`) : ""; }', 'list') | ||
); | ||
@@ -80,6 +76,3 @@ }); | ||
expect(compile(`{%for item in list %}{{item}}{%else%}nothing{% endfor%}`)).toEqual( | ||
toCode( | ||
'{ return Array.isArray(list) && list.length ? list.__og_should_key ? repeat(list, item => item.__og_key, (item, index) => item) : list.map((item, index) => item) : html`nothing`; }', | ||
'list' | ||
) | ||
toCode('{return Array.isArray(list) && list.length ? list.map((item, index) => item) : html`nothing`; }', 'list') | ||
); | ||
@@ -89,7 +82,7 @@ }); | ||
expect(compile(`{% for i in "foobar" | list %}{{ i }},{% endfor %}`)).toEqual( | ||
toCode( | ||
'{ return Array.isArray(_F.list("foobar")) ? _F.list("foobar").__og_should_key ? repeat(_F.list("foobar"), item => item.__og_key, (i, index) => html`${i},`) : _F.list("foobar").map((i, index) => html`${i},`) : ""; }' | ||
) | ||
toCode('{return Array.isArray(_F.list("foobar")) ? _F.list("foobar").map((i, index) => html`${i},`):"";}') | ||
); | ||
}); | ||
it('should loop thru objects', () => {}); | ||
}); | ||
@@ -96,0 +89,0 @@ describe('group', () => { |
{ | ||
"name": "@ordergroove/smi-precompile", | ||
"version": "1.7.11-alpha-PR-766-9.48+0c765774", | ||
"version": "1.7.11-alpha-PR-819-4.45+00ece7c6", | ||
"description": "Prcompilers for smi-core", | ||
@@ -50,3 +50,3 @@ "author": "Brian Lewis <brian.lewis@ordergroove.com>", | ||
}, | ||
"gitHead": "0c7657749f2aab940967db17b32e030c88105b93" | ||
"gitHead": "00ece7c65a893961279fb5aaa7445ca65533cd62" | ||
} |
@@ -0,1 +1,2 @@ | ||
const { nodes: n } = require('nunjucks'); | ||
const babel = require('@babel/parser'); | ||
@@ -5,3 +6,3 @@ const { default: generate } = require('@babel/generator'); | ||
const t = require('@babel/types'); | ||
const { parse } = require('./lit-nunjucks'); | ||
const { Parser } = require('./lit-nunjucks'); | ||
const { get, snakeCase, last, mapKeys, has } = require('lodash'); | ||
@@ -13,2 +14,41 @@ const path = require('path'); | ||
const validCurrencies = new Set(Object.values(i18nCurrency)); | ||
const validCurrencyDisplays = new Set(['symbol', 'narrowSymbol', 'code', 'name']); | ||
class SmiParser extends Parser { | ||
handleFunCall(node) { | ||
if (node.name instanceof n.Symbol) { | ||
switch (node.name.value) { | ||
case 't': | ||
return this.handleTranslation(node); | ||
case 'setting': | ||
return this.handleSetting(node); | ||
case 'js': | ||
return this.handleJs(node); | ||
case 'currency': | ||
return this.handleCurrency(node); | ||
default: | ||
return super.handleFunCall(node); | ||
} | ||
} | ||
} | ||
handleTranslation(node) { | ||
return t.callExpression(t.identifier(`_F.t`), [].concat(this.wrap(node.args))); | ||
} | ||
handleSetting(node) { | ||
return t.callExpression(t.identifier(`_F.setting`), [].concat(this.wrap(node.args))); | ||
} | ||
handleJs(node) { | ||
return t.callExpression(t.identifier(`_F.js`), [].concat(this.wrap(node.args))); | ||
} | ||
handleCurrency(node) { | ||
return t.callExpression(t.identifier(`_F.currency`), [].concat(this.wrap(node.args))); | ||
} | ||
} | ||
function parse(source, opts) { | ||
return new SmiParser(opts).parse(source); | ||
} | ||
const filenameWithoutExt = (content, file) => { | ||
@@ -66,132 +106,136 @@ const ext = path.extname(file); | ||
/** | ||
* @param source string representing the main template liquid file | ||
* @param partials string object where keys refer to the template partials name and values are the template content | ||
* @param locales array locale objects { locale, translations, file } | ||
* @param defaultLocale string default to en | ||
*/ | ||
function precompileCompat(source, partials = {}, locales = [], settings = {}, defaultLocale = 'en', script, css) { | ||
const ast = t.file(t.program([parse(source, { partials: mapKeys(partials, filenameWithoutExt) })])); | ||
function addCallExpressions(ast, locales, file, settings) { | ||
traverse(ast, { | ||
// replace | t expressions | ||
CallExpression(path) { | ||
const { arguments: args } = path.node; | ||
const [firstArg] = args; | ||
const result = []; | ||
const localizedTeamplates = []; | ||
const validCurrencies = new Set(Object.values(i18nCurrency)); | ||
const validCurrencyDisplays = new Set(['symbol', 'narrowSymbol', 'code', 'name']); | ||
if (isTranslationCallExpression(path) && t.isStringLiteral(firstArg)) { | ||
processTranslationCallExpression(path, firstArg, locales, file); | ||
} else if (isJsCallExpression(path) && t.isStringLiteral(firstArg)) { | ||
processJsCallExpression(path, firstArg); | ||
} else if (isSettingCallExpression(path) && t.isStringLiteral(firstArg)) { | ||
processSettingCallExpression(path, firstArg, settings); | ||
} else if (isCurrencyCallExpression(path)) { | ||
processCurrencyCallExpression(path, locales, validCurrencies, validCurrencyDisplays); | ||
} | ||
}, | ||
Identifier(path) { | ||
const translationsKey = path.node.name; | ||
if (translationsKey in locales) { | ||
processIdentifier(path, translationsKey, locales, file); | ||
} | ||
} | ||
}); | ||
} | ||
if (!locales.length) { | ||
locales.push({ | ||
locale: defaultLocale, | ||
translations: {} | ||
}); | ||
function processTranslationCallExpression(path, firstArg, locales, file) { | ||
const translationsKey = firstArg.value.toLowerCase(); | ||
const translationArguments = path.node.arguments[1]; | ||
if (!(translationsKey in locales) || locales[translationsKey] === '') { | ||
path.replaceWith(t.stringLiteral('')); | ||
} else if (!translationArguments) { | ||
path.replaceWith(parseTranslation(locales[translationsKey], translationsKey, file)); | ||
} else if (t.isObjectExpression(translationArguments)) { | ||
replaceTranslationWithArguments(path, translationArguments, locales[translationsKey], translationsKey, file); | ||
} else { | ||
throw new Error('Translation expression not supported'); | ||
} | ||
} | ||
let defaultLocaleIx = locales.findIndex(({ locale }) => locale.startsWith(defaultLocale)); | ||
if (defaultLocaleIx < 0) defaultLocaleIx = 0; | ||
function processJsCallExpression(path, firstArg) { | ||
try { | ||
const subprogram = babel.parse(firstArg.value); | ||
path.replaceWith(subprogram.program.body[0]); | ||
} catch (err) { | ||
throw new Error(`Can't parse JS expression: ${firstArg.value}`); | ||
} | ||
} | ||
locales.forEach(({ locale, translations, file }, ix, arr) => { | ||
const localeName = locale; | ||
const locales = | ||
ix !== defaultLocaleIx ? { ...arr[defaultLocaleIx].translations, ...translations } : translations || {}; | ||
const clonedAst = t.cloneNode(ast, true); | ||
traverse(clonedAst, { | ||
// replace | t expressions | ||
CallExpression(path) { | ||
if (isTranslationCallExpression(path) && t.isStringLiteral(path.node.arguments[0])) { | ||
const translationsKey = path.node.arguments[0].value.toLowerCase(); | ||
function processSettingCallExpression(path, firstArg, settings) { | ||
const settingKey = firstArg.value.toLowerCase(); | ||
const defaultValueNode = path.node.arguments[1]; | ||
if (!(translationsKey in locales) || locales[translationsKey] === '') { | ||
path.replaceWith(t.stringLiteral('')); | ||
} else if (path.node.arguments.length === 1) { | ||
path.replaceWith(parseTranslation(locales[translationsKey], translationsKey, file)); | ||
} else if (path.node.arguments.length === 2 && t.isObjectExpression(path.node.arguments[1])) { | ||
// Support translations arguments t(every=subscription.every, period=subscription.period) | ||
const argumentReplacement = path.node.arguments[1]; | ||
if (!argumentReplacement.properties.every(v => t.isStringLiteral(v.key))) { | ||
throw new Error('Translation arguments keys should be string'); | ||
} | ||
if (has(settings, settingKey)) { | ||
const value = get(settings, settingKey); | ||
path.replaceWithSourceString(JSON.stringify(value)); | ||
} else if (defaultValueNode) { | ||
path.replaceWith(defaultValueNode); | ||
} else { | ||
path.replaceWithSourceString('undefined'); | ||
} | ||
} | ||
const [argumentNames, callValues] = argumentReplacement.properties.reduce( | ||
(acc, cur) => [ | ||
[...acc[0], t.identifier(cur.key.value)], | ||
[...acc[1], cur.value] | ||
], | ||
[[], []] | ||
); | ||
const body = parseTranslation(locales[translationsKey], translationsKey, file); | ||
const translationWithValues = t.callExpression(t.arrowFunctionExpression(argumentNames, body), callValues); | ||
path.replaceWith(translationWithValues); | ||
} else { | ||
throw new Error('translation expression not supported'); | ||
} | ||
} | ||
if (isJsCallExpression(path) && t.isStringLiteral(path.node.arguments[0])) { | ||
try { | ||
const subprogram = babel.parse(path.node.arguments[0].value); | ||
path.replaceWith(subprogram.program.body[0]); | ||
} catch (err) { | ||
throw new Error(`Cant parse expression, ${path.node.arguments[0].value}`); | ||
} | ||
} | ||
if (isSettingCallExpression(path) && t.isStringLiteral(path.node.arguments[0])) { | ||
const settingKey = path.node.arguments[0].value.toLowerCase(); | ||
if (has(settings, settingKey)) { | ||
const value = get(settings, settingKey); | ||
path.replaceWithSourceString(JSON.stringify(value)); | ||
} else if (path.node.arguments[1]) { | ||
path.replaceWith(path.node.arguments[1]); | ||
} else { | ||
path.replaceWithSourceString('undefined'); | ||
} | ||
} | ||
function processCurrencyCallExpression(path, locales, validCurrencies, validCurrencyDisplays) { | ||
// there should always be a value being piped into the filter | ||
if (path.node.arguments.length === 1) { | ||
path.node.arguments.push( | ||
validCurrencies.has(locales['currency_code']) | ||
? t.stringLiteral(locales['currency_code']) | ||
: t.identifier('undefined') | ||
); | ||
path.node.arguments.push( | ||
validCurrencyDisplays.has(locales['currency_display']) | ||
? t.stringLiteral(locales['currency_display']) | ||
: t.identifier('undefined') | ||
); | ||
} | ||
} | ||
// there should always be a value being piped into the filter | ||
if (isCurrencyCallExpression(path) && path.node.arguments.length === 1) { | ||
path.node.arguments.push( | ||
validCurrencies.has(locales['currency_code']) | ||
? t.stringLiteral(locales['currency_code']) | ||
: t.identifier('undefined') | ||
); | ||
path.node.arguments.push( | ||
validCurrencyDisplays.has(locales['currency_display']) | ||
? t.stringLiteral(locales['currency_display']) | ||
: t.identifier('undefined') | ||
); | ||
} | ||
}, | ||
Identifier(path) { | ||
const translationsKey = path.node.name; | ||
if (translationsKey in locales) { | ||
if (propertyIsInTemplateArg(path)) { | ||
path.parentPath.remove(); | ||
} else if (t.isTemplateLiteral(path.parent) || isTranslation(path)) { | ||
if (locales[translationsKey] === '') { | ||
path.replaceWith(t.stringLiteral('')); | ||
} else { | ||
path.replaceWith(parseTranslation(locales[translationsKey], translationsKey, file)); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
function processIdentifier(path, translationsKey, locales, file) { | ||
if (propertyIsInTemplateArg(path)) { | ||
path.parentPath.remove(); | ||
} else if (t.isTemplateLiteral(path.parent) || isTranslation(path)) { | ||
if (locales[translationsKey] === '') { | ||
path.replaceWith(t.stringLiteral('')); | ||
} else { | ||
path.replaceWith(parseTranslation(locales[translationsKey], translationsKey, file)); | ||
} | ||
} | ||
} | ||
traverse(clonedAst, { | ||
Identifier(path) { | ||
if (t.isFunctionDeclaration(path.parent) && path.node.name === 'template') { | ||
const name = `template_${snakeCase(localeName)}`; | ||
localizedTeamplates.push(t.objectProperty(t.stringLiteral(localeName), t.identifier(name))); | ||
result.push( | ||
t.functionDeclaration( | ||
t.identifier(name), | ||
path.parent.params.map(p => t.cloneNode(p, true)), | ||
t.cloneNode(path.parent.body, true) | ||
) | ||
); | ||
} | ||
function replaceTranslationWithArguments(path, argumentReplacement, translation, translationsKey, file) { | ||
// Support translations arguments t(every=subscription.every, period=subscription.period) | ||
if (!argumentReplacement.properties.every(prop => t.isStringLiteral(prop.key))) { | ||
throw new Error('Translation argument keys should be strings'); | ||
} | ||
const [argumentNames, callValues] = argumentReplacement.properties.reduce( | ||
([names, values], prop) => [ | ||
[...names, t.identifier(prop.key.value)], | ||
[...values, prop.value] | ||
], | ||
[[], []] | ||
); | ||
const body = parseTranslation(translation, translationsKey, file); | ||
const translationWithValues = t.callExpression(t.arrowFunctionExpression(argumentNames, body), callValues); | ||
path.replaceWith(translationWithValues); | ||
} | ||
function addLocaleFunctions(ast, localeName, result, localizedTemplates) { | ||
traverse(ast, { | ||
Identifier(path) { | ||
if (t.isFunctionDeclaration(path.parent) && path.node.name === 'template') { | ||
const name = `template_${snakeCase(localeName)}`; | ||
localizedTemplates.push(t.objectProperty(t.stringLiteral(localeName), t.identifier(name))); | ||
result.push( | ||
t.functionDeclaration( | ||
t.identifier(name), | ||
path.parent.params.map(p => t.cloneNode(p, true)), | ||
t.cloneNode(path.parent.body, true) | ||
) | ||
); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
function handleExtras(settings, script, css) { | ||
let extras = []; | ||
if (script) { | ||
const scriptAst = babel.parse(script); | ||
extras = [...extras, ...scriptAst.program.body]; | ||
extras.push(...scriptAst.program.body); | ||
} | ||
@@ -207,4 +251,33 @@ if (css) { | ||
`); | ||
extras = [...extras, ...cssAst.program.body]; | ||
extras.push(...cssAst.program.body); | ||
} | ||
return extras; | ||
} | ||
/** | ||
* @param source string representing the main template liquid file | ||
* @param partials string object where keys refer to the template partials name and values are the template content | ||
* @param locales array locale objects { locale, translations, file } | ||
* @param defaultLocale string default to en | ||
*/ | ||
function precompileCompat(source, partials = {}, locales = [], settings = {}, defaultLocale = 'en', script, css) { | ||
const ast = t.file(t.program([parse(source, { partials: mapKeys(partials, filenameWithoutExt) })])); | ||
const result = []; | ||
const localizedTemplates = []; | ||
if (!locales.length) locales.push({ locale: defaultLocale, translations: {} }); | ||
let defaultLocaleIx = locales.findIndex(({ locale }) => locale.startsWith(defaultLocale)); | ||
if (defaultLocaleIx < 0) defaultLocaleIx = 0; | ||
locales.forEach(({ locale, translations, file }, ix, arr) => { | ||
const localeName = locale; | ||
const mergedLocales = | ||
ix !== defaultLocaleIx ? { ...arr[defaultLocaleIx].translations, ...translations } : translations || {}; | ||
const clonedAst = t.cloneNode(ast, true); | ||
addCallExpressions(clonedAst, mergedLocales, file, settings); | ||
addLocaleFunctions(clonedAst, localeName, result, localizedTemplates); | ||
}); | ||
const extras = handleExtras(settings, script, css); | ||
const program = t.file( | ||
@@ -215,8 +288,4 @@ t.program([ | ||
[t.identifier('html'), t.identifier('repeat'), t.identifier('unsafeHTML')], | ||
t.blockStatement([ | ||
// Custom script should be before templates_en_us but inside template(){} generator. | ||
...extras, | ||
...result, | ||
t.returnStatement(t.objectExpression(localizedTeamplates)) | ||
]) | ||
// Custom script should be before templates_en_us but inside template(){} generator. | ||
t.blockStatement([...extras, ...result, t.returnStatement(t.objectExpression(localizedTemplates))]) | ||
) | ||
@@ -303,2 +372,3 @@ ]) | ||
} | ||
/** | ||
@@ -305,0 +375,0 @@ * new signature |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
5920240
6383