lib/ast-transform.js
// builders ref https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/builders.ts
const HOT_LOAD_HELPER_NAME = 'hot-load';
// for cases like {{#let (component 'foo-bar') as |Boo|}} <Boo />a {{/let}}
var nestedNames = [];
function createSubExpression(params, b) {
return b.sexpr(b.path(HOT_LOAD_HELPER_NAME), params);
}
function looksLikeComponent(nodePath, appHelpers) {
if (typeof nodePath.includes !== 'function') {
// eslint-disable-next-line no-console
console.log(nodePath, typeof nodePath);
return false;
}
// const isCapitalized = nodePath.charAt(0) === nodePath.charAt(0).toUpperCase();
// (isCapitalized || nodePath.includes("-"))
return (
!appHelpers.includes(nodePath) &&
!helperNames.includes(nodePath) &&
nodePath.includes('-') &&
!nodePath.includes('.')
);
}
const helperNames = [
'identity', // glimmer blocks
'render-inverse', // glimmer blocks
'-get-dynamic-var', // glimmer internal helper
'-lf-get-outlet-state', // dunno
'action',
'component',
'link-to',
HOT_LOAD_HELPER_NAME,
'hot-content',
'hot-placeholder',
'if',
'if-unless',
'each',
'each-in',
'format-date',
'format-message',
'format-relative',
'format-time',
'unless',
'in-element',
'query-params',
'-in-element',
'-class',
'-html-safe',
'-input-type',
'-normalize-class',
'concat',
'get',
'mut',
'readonly',
'unbound',
'debugger',
'else',
'let',
'log',
'loc',
'hash',
'input',
'partial',
'yield',
'textarea',
't',
't-for',
'transition-to',
'get-meta',
'get-attr',
'index-of',
//ember-moment
'moment-format',
'moment-from-now',
'moment-to',
'moment-to-now',
'moment-duration',
'moment-calendar',
'moment-diff',
'outlet',
'is-before',
'is-after',
'is-same',
'is-same-or-before',
'is-same-or-after',
'is-between',
'now',
'unix',
//cp-validations
'v-get',
//route-action
'route-action',
// composable-helpers
'map-by',
'sort-by',
'filter-by',
'reject-by',
'find-by',
'object-at',
'hasBlock',
'has-block',
'has-next',
'has-previous',
'group-by',
'not-eq',
'is-array',
'is-empty',
'is-equal',
// liquid
'liquid-unless',
'liquid-container',
'liquid-outlet',
'liquid-versions',
'liquid-bind',
'liquid-spacer',
'liquid-sync',
'liquid-measured',
'liquid-child',
'liquid-if',
//app-version
'app-version',
];
// For compatibility with pre- and post-glimmer
function unwrapNode(node) {
if (node.sexpr) {
return node.sexpr;
} else {
return node;
}
}
function buildNodeHashAndStats(node, componentName, b) {
let hotReloadCUSTOMHasParams = node.params && node.params.length !== 0;
let hotReloadCustomHasHash =
node.hash && (node.hash.pairs || []).length !== 0;
let hashPairs = [
b.pair('hotReloadCUSTOMhlContext', b.path('this')),
b.pair(
'hotReloadCUSTOMName',
b.string(componentName.original || componentName)
),
b.pair('hotReloadCUSTOMhlProperty', b.path(componentName)),
b.pair('hotReloadCUSTOMHasParams', b.boolean(hotReloadCUSTOMHasParams)),
b.pair('hotReloadCUSTOMHasHash', b.boolean(hotReloadCustomHasHash)),
];
if (node.hash && node.hash.pairs && node.hash.pairs.length) {
node.hash = b.hash(hashPairs.concat(node.hash.pairs));
} else {
node.hash = b.hash(hashPairs);
}
}
function convertComponent(
input,
b,
{ helpers } = {
helpers: [],
}
) {
const node = unwrapNode(input);
if (typeof node !== 'object') {
return;
}
if (node.__ignore && node.path && node.path.original !== 'component') {
return;
}
const nodePath = node.path.original;
if (nodePath === 'component') {
let firstParam = node.params[0];
if (firstParam.value === HOT_LOAD_HELPER_NAME) {
return;
}
if (firstParam.path) {
if (firstParam.path.original === HOT_LOAD_HELPER_NAME) {
return;
}
}
let componentName = firstParam || firstParam.path.original;
buildNodeHashAndStats(node, componentName, b);
node.params = [
createSubExpression(
[
firstParam,
b.path('this'),
b.path(componentName),
b.string(componentName.original || componentName),
],
b
),
].concat(node.params.filter((n) => n !== firstParam));
} else if (looksLikeComponent(nodePath, helpers)) {
let componentName = node.path.original;
node.path = b.path('component');
buildNodeHashAndStats(node, componentName, b);
node.params = [
createSubExpression(
[
b.string(componentName),
b.path('this'),
b.path(componentName),
b.string(componentName.original || componentName),
],
b
),
].concat(node.params);
}
return node;
}
function markNodeAsIgnored(node) {
if (!node) {
return;
}
if (typeof node !== 'object') {
return;
}
node.__ignore = true;
if (node.value) {
markNodeAsIgnored(node.value);
}
if (node.path) {
markNodeAsIgnored(node.path);
}
if (node.params) {
node.params.forEach(markNodeAsIgnored);
}
if (node.hash) {
node.hash.pairs.forEach(markNodeAsIgnored);
}
return node;
}
function captureBlockParams(node) {
const params = node.blockParams;
if (node.__ignore) {
return;
}
params.forEach((param) => {
if (!nestedNames.includes(param)) {
nestedNames.push(param);
}
});
}
function extractAngleBrackedComponentName(name) {
return name.split('::').join('/');
}
function safeAngleBrackedTagName(name) {
return name.split('::').join('___');
}
function wrapAngeBrackedComponentWithLetHelper(b, node, options = {}) {
const tagName = node.tag;
const oldLoc = node.loc;
const newNode = JSON.parse(JSON.stringify(node));
captureBlockParams(node);
newNode.cloned = true;
newNode.children = node.children;
const salt = (options && options.salt) || Math.random().toString(36).slice(2);
newNode.tag = 'HotLoad' + safeAngleBrackedTagName(tagName) + salt;
newNode.loc = oldLoc;
const bItself = b.blockItself([newNode], [newNode.tag]);
bItself.__ignore = true;
const returnNode = b.block(
b.path('let'),
[
b.sexpr(b.path('component'), [
b.string(extractAngleBrackedComponentName(tagName)),
]),
],
b.hash([]),
bItself
);
newNode.modifiers = node.modifiers;
newNode.attributes = node.attributes;
newNode.comments = node.comments;
return returnNode;
}
function isAngleBrackedComponent(node) {
const tagName = node.tag;
if (tagName.startsWith(':')) {
return false;
}
if (tagName.charAt(0) === tagName.charAt(0).toUpperCase()) {
if (node.cloned) {
return false;
}
if (nestedNames.includes(tagName)) {
return false;
}
if (tagName === 'Input' || tagName === 'Textarea' || tagName === 'LinkTo') {
return false;
}
if (tagName.indexOf('.') !== -1) {
return false;
}
if (tagName.charAt(0) === '@') {
return false;
}
return true;
}
return false;
}
class BaseASTHotLoadTransform {
constructor(data = {}) {
const meta = data.meta ? data.meta : {};
this.moduleName = data.module || meta.moduleName || null;
this.syntax = null;
}
static createASTPlugin(config, syntax) {
let b = syntax.builders;
let options = config || { helpers: [] };
nestedNames = [];
const visitor = {
ElementModifierStatement(node) {
markNodeAsIgnored(node.path);
node.params.forEach((param) => {
markNodeAsIgnored(param);
});
},
Program(node) {
captureBlockParams(node);
},
ElementNode(node) {
node.attributes.forEach((attr) => {
if (attr.value && attr.value.type === 'MustacheStatement') {
markNodeAsIgnored(attr.value);
}
});
if (isAngleBrackedComponent(node)) {
return wrapAngeBrackedComponentWithLetHelper(b, node, options);
}
},
HashPair(node) {
if (
node.value &&
node.value.path &&
node.value.path.original !== 'component'
) {
markNodeAsIgnored(node.value);
}
},
ConcatStatement(node) {
node.parts
.filter((part) => {
return part.type === 'MustacheStatement';
})
.forEach((item) => {
markNodeAsIgnored(item);
});
},
SubExpression(node) {
if (node.path.original === 'component') {
convertComponent(node, b, options);
}
},
BlockStatement(node) {
convertComponent(node, b, options);
},
MustacheStatement(node) {
if (node.path.original === 'concat') {
node.params.forEach((param) => {
markNodeAsIgnored(param);
});
}
if (node.escaped) {
convertComponent(node, b, options);
}
},
};
return {
name: 'ember-ast-hot-load',
visitor,
};
}
}
// module.exports = ASTHotLoadTransform;
module.exports = function (config) {
return class ASTHotLoadTransform extends BaseASTHotLoadTransform {
transform(ast) {
// let startLoc = ast.loc ? ast.loc.start : {};
// /*
// Checking for line and column to avoid registering the plugin for ProgramNode inside a BlockStatement since transform is called for all ProgramNodes in Ember 2.15.X. Removing this would result in minifying all the TextNodes.
// */
// if (startLoc.line !== 1 || startLoc.column !== 0) {
// return ast;
// }
const astConfig = config.addonContext._OPTIONS || {};
if (!astConfig.enabled) {
return ast;
}
// cover case for blacklisted addon
if (!astConfig.initialized) {
return ast;
}
const opts = Object.assign({ moduleName: this.moduleName }, astConfig);
// // don't process inline templates (without module name, like itemplates in tests, etc)
// if (opts.moduleName === null) {
// return ast;
// }
let plugin = ASTHotLoadTransform.createASTPlugin(opts, this.syntax);
this.syntax.traverse(ast, plugin.visitor);
return ast;
}
};
};