Skip to content

Instantly share code, notes, and snippets.

@intrnl
Created February 27, 2023 02:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save intrnl/883ec4ad4bb1c3e6c44a1e8fc41efb6f to your computer and use it in GitHub Desktop.
Save intrnl/883ec4ad4bb1c3e6c44a1e8fc41efb6f to your computer and use it in GitHub Desktop.
import * as acorn from 'acorn';
import { walk } from 'estree-walker';
import MagicString from 'magic-string';
import { analyze } from 'periscopic';
const debuggableConfig = {
'@vanilla-extract/css': {
style: {
maxParams: 2,
},
createTheme: {
maxParams: 3,
},
styleVariants: {
maxParams: 3,
hasDebugId: ({ arguments: args }) => {
const last = args[args.length - 1];
return last && ((last.type === 'Literal' && typeof last.value === 'string') || last.type === 'TemplateLiteral');
},
},
fontFace: {
maxParams: 2,
},
keyframes: {
maxParams: 2,
},
createVar: {
maxParams: 1,
},
createContainer: {
maxParams: 1,
},
},
'@vanilla-extract/recipes': {
recipe: {
maxParams: 2,
},
},
};
/**
* @typedef {Map<string, { ns: string, name: string, config: any }>} DebugImportsMap
*/
/**
* @typedef {Map<string, string>} DebugNamespacesMap
*/
export function transformDebug (source) {
/** @type {DebugImportsMap} */
const debugImports = new Map();
/** @type {DebugNamespacesMap} */
const debugNamespaces = new Map();
/** @type {import('estree').Program} */
const ast = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' });
const magic = new MagicString(source);
const { map, scope: rootScope } = analyze(ast);
let currentScope = rootScope;
// traverse through program to get the imports
for (let idx = 0, len = ast.body.length; idx < len; idx++) {
const node = ast.body[idx];
if (node.type === 'ImportDeclaration') {
const source = node.source.value;
const map = debuggableConfig[source];
if (!map) {
continue;
}
for (let j = 0, len = node.specifiers.length; j < len; j++) {
const specifier = node.specifiers[j];
if (specifier.type === 'ImportSpecifier') {
const imported = specifier.imported;
const local = specifier.local;
const importedName = imported.type === 'Literal' ? imported.value : imported.name;
const config = map[importedName];
if (!config) {
continue;
}
debugImports.set(local.name, { ns: source, name: importedName, config });
}
else if (specifier.type === 'ImportNamespaceSpecifier') {
debugNamespaces.set(specifier.local.name, source);
}
}
}
}
// and now we walk
walk(ast, {
enter: (node, parent) => {
node.path = { parent };
if (map.has(node)) {
currentScope = map.get(node);
}
if (node.type === 'CallExpression') {
const callee = node.callee;
const args = node.arguments;
if (parent.type !== 'VariableDeclarator') {
return;
}
const debugName = parent.id.name;
const match = getMatch(callee, currentScope, debugImports, debugNamespaces);
if (!match) {
return;
}
const { maxParams, hasDebugId } = match.config;
if (args.length < maxParams && (!hasDebugId || !hasDebugId(node))) {
magic.prependRight(node.end - 1, `, "${debugName}"`);
}
}
},
leave: (node) => {
if (map.has(node)) {
currentScope = currentScope.parent;
}
},
});
return magic.toString();
}
/**
* @param {import('estree').Node} node
* @param {import('periscopic').Scope} scope
* @param {DebugImportsMap} debugImports
* @param {DebugNamespacesMap} debugNamespaces
* @returns {{ ns: string, name: string, config: any }}
*/
function getMatch (node, scope, debugImports, debugNamespaces) {
if (node.type === 'MemberExpression') {
const object = node.object;
const property = node.property;
const propertyKey = node.computed ? property.type === 'Literal' && property.value : property.name;
if (object.type !== 'Identifier' || !propertyKey) {
return null;
}
const name = object.name;
const ownScope = scope.find_owner(name);
if (!ownScope) {
return null;
}
const binding = ownScope.declarations.get(name);
if (!binding || binding.type !== 'ImportNamespaceSpecifier') {
return null;
}
const ns = debugNamespaces.get(name);
if (!ns) {
return null;
}
const map = debuggableConfig[ns];
const config = map[propertyKey];
if (!config) {
return null;
}
return { ns: name, name: propertyKey, config };
}
if (node.type === 'Identifier') {
const name = node.name;
const ownScope = scope.find_owner(name);
if (!ownScope) {
return null;
}
const binding = ownScope.declarations.get(name);
if (!binding || binding.type !== 'ImportSpecifier') {
return null;
}
return debugImports.get(name) || null;
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment