UNPKG

@linaria/utils

Version:

Blazing fast zero-runtime CSS in JS library

973 lines (952 loc) 29.1 kB
/* eslint @typescript-eslint/no-use-before-define: ["error", { "functions": false }] */ /* eslint-disable no-restricted-syntax,no-continue */ import { debug } from '@linaria/logger'; import { getScope } from './getScope'; import isExports from './isExports'; import isNotNull from './isNotNull'; import isRequire from './isRequire'; import isTypedNode from './isTypedNode'; import { getTraversalCache } from './traversalCache'; // '*' means re-export all export const sideEffectImport = item => item.imported === 'side-effect'; export const explicitImport = item => item.imported !== 'side-effect'; function getValue({ node }) { return node.type === 'Identifier' ? node.name : node.value; } // We ignore imports and exports of types const isType = p => 'importKind' in p.node && p.node.importKind === 'type' || 'exportKind' in p.node && p.node.exportKind === 'type'; // Force TypeScript to check, that we have implementation for every possible specifier const collectors = { ImportSpecifier(path, source) { if (isType(path)) return []; const imported = getValue(path.get('imported')); const local = path.get('local'); return [{ imported, local, source, type: 'esm' }]; }, ImportDefaultSpecifier(path, source) { const local = path.get('local'); return [{ imported: 'default', local, source, type: 'esm' }]; }, ImportNamespaceSpecifier(path, source) { const local = path.get('local'); return unfoldNamespaceImport({ imported: '*', local, source, type: 'esm' }); } }; function collectFromImportDeclaration(path, state) { // If importKind is specified, and it's not a value, ignore that import if (isType(path)) return; const source = getValue(path.get('source')); const specifiers = path.get('specifiers'); if (specifiers.length === 0) { state.imports.push({ imported: 'side-effect', local: path, source }); } specifiers.forEach(specifier => { if (specifier.isImportSpecifier() && isType(specifier)) return; const collector = collectors[specifier.node.type]; state.imports.push(...collector(specifier, source)); }); } function getAncestorsWhile(path, cond) { const result = []; let current = path; while (current && cond(current)) { result.push(current); current = current.parentPath; } return result; } function whatIsDestructed(objectPattern) { const destructedProps = []; objectPattern.traverse({ Identifier(identifier) { if (identifier.isBindingIdentifier()) { const parent = identifier.parentPath; if (parent.isObjectProperty({ value: identifier.node })) { const chain = getAncestorsWhile(parent, p => p !== objectPattern).filter(isTypedNode('ObjectProperty')).map(p => { const key = p.get('key'); if (!key.isIdentifier()) { // TODO: try to process other type of keys or at least warn about this return null; } return key; }).filter(isNotNull); chain.reverse(); if (chain.length > 0) { destructedProps.push({ what: chain[0].node.name, as: identifier }); } return; } if (parent.isRestElement({ argument: identifier.node })) { destructedProps.push({ what: '*', as: identifier }); } } } }); return destructedProps; } function importFromVariableDeclarator(path, isSync) { const id = path.get('id'); if (id.isIdentifier()) { // It's the simplest case when the full namespace is imported return [{ as: id, what: '*' }]; } if (!isSync) { // Something went wrong // Is it something like `const { … } = import(…)`? debug('evaluator:collectExportsAndImports', '`import` should be awaited'); return []; } if (id.isObjectPattern()) { return whatIsDestructed(id); } // What else it can be? debug('evaluator:collectExportsAndImports:importFromVariableDeclarator', 'Unknown type of id', id.node.type); return []; } const findIIFE = path => { if (path.isCallExpression() && path.get('callee').isFunctionExpression()) { return path; } if (!path.parentPath) { return null; } return findIIFE(path.parentPath); }; function exportFromVariableDeclarator(path) { const id = path.get('id'); const init = path.get('init'); // If there is no init and id is an identifier, we should find IIFE if (!init.node && id.isIdentifier()) { const binding = getScope(path).getBinding(id.node.name); if (!binding) { return {}; } const iife = [...binding.referencePaths, ...binding.constantViolations, binding.path].map(findIIFE).find(isNotNull); if (!iife) { return {}; } return { [id.node.name]: iife }; } if (!init || !init.isExpression()) { return {}; } if (id.isIdentifier()) { // It is `export const a = 1;` return { [id.node.name]: init }; } if (id.isObjectPattern()) { // It is `export const { a, ...rest } = obj;` return whatIsDestructed(id).reduce((acc, destructed) => ({ ...acc, [destructed.as.node.name]: init }), {}); } // What else it can be? debug('evaluator:collectExportsAndImports:exportFromVariableDeclarator', 'Unknown type of id', id.node.type); return {}; } function collectFromDynamicImport(path, state) { const { parentPath: callExpression } = path; if (!callExpression.isCallExpression()) { // It's wrong `import` return; } const [sourcePath] = callExpression.get('arguments'); if (!sourcePath || !sourcePath.isStringLiteral()) { // Import should have at least one argument, and it should be StringLiteral return; } const source = sourcePath.node.value; let { parentPath: container, key } = callExpression; let isAwaited = false; if (container.isAwaitExpression()) { // If it's not awaited import, it imports the full namespace isAwaited = true; key = container.key; container = container.parentPath; } // Is it `const something = await import("something")`? if (key === 'init' && container.isVariableDeclarator()) { importFromVariableDeclarator(container, isAwaited).map(prop => state.imports.push({ imported: prop.what, local: prop.as, source, type: 'dynamic' })); } } function getCalleeName(path) { const callee = path.get('callee'); if (callee.isIdentifier()) { return callee.node.name; } if (callee.isMemberExpression()) { const property = callee.get('property'); if (property.isIdentifier()) { return property.node.name; } } return undefined; } function getImportExportTypeByInteropFunction(path) { const name = getCalleeName(path); if (name === undefined) { return undefined; } if (name.startsWith('__exportStar')) { return 're-export:*'; } if (name.startsWith('_interopRequireDefault') || name.startsWith('__importDefault')) { return 'default'; } if (name.startsWith('_interopRequireWildcard') || name.startsWith('__importStar') || name.startsWith('__toESM')) { return 'import:*'; } if (name.startsWith('__rest') || name.startsWith('__objRest') || name.startsWith('_objectDestructuringEmpty')) { return 'import:*'; } return undefined; } function isAlreadyProcessed(path) { if (path.isCallExpression() && path.get('callee').isIdentifier({ name: '__toCommonJS' })) { // because its esbuild and we already processed all exports return true; } return false; } function collectFromRequire(path, state) { if (!isRequire(path)) return; // This method can be reached many times from multiple visitors for the same path // So we need to check if we already processed it if (state.processedRequires.has(path)) return; state.processedRequires.add(path); const { parentPath: callExpression } = path; if (!callExpression.isCallExpression()) { // It's wrong `require` return; } const [sourcePath] = callExpression.get('arguments'); if (!sourcePath || !sourcePath.isStringLiteral()) { // Import should have at least one argument, and it should be StringLiteral return; } const source = sourcePath.node.value; const { parentPath: container, key } = callExpression; if (container.isCallExpression() && key === 0) { // It may be transpiled import such as // `var _atomic = _interopRequireDefault(require("@linaria/atomic"));` const imported = getImportExportTypeByInteropFunction(container); if (!imported) { // It's not a transpiled import. // TODO: Can we guess that it's a namespace import? debug('evaluator:collectExportsAndImports', 'Unknown wrapper of require', container.node.callee); return; } if (imported === 're-export:*') { state.reexports.push({ exported: '*', imported: '*', local: path, source }); return; } let { parentPath: variableDeclarator } = container; if (variableDeclarator.isCallExpression()) { if (variableDeclarator.get('callee').isIdentifier({ name: '_extends' })) { variableDeclarator = variableDeclarator.parentPath; } } if (!variableDeclarator.isVariableDeclarator()) { // TODO: Where else it can be? debug('evaluator:collectExportsAndImports', 'Unexpected require inside', variableDeclarator.node.type); return; } const id = variableDeclarator.get('id'); if (!id.isIdentifier()) { debug('evaluator:collectExportsAndImports', 'Id should be Identifier', variableDeclarator.node.type); return; } if (imported === 'import:*') { const unfolded = unfoldNamespaceImport({ imported: '*', local: id, source, type: 'cjs' }); state.imports.push(...unfolded); } else { state.imports.push({ imported, local: id, source, type: 'cjs' }); } } if (container.isMemberExpression()) { // It is `require('@linaria/shaker').dep` const property = container.get('property'); if (!property.isIdentifier() && !property.isStringLiteral()) { debug('evaluator:collectExportsAndImports', 'Property should be Identifier or StringLiteral', property.node.type); return; } const { parentPath: variableDeclarator } = container; if (variableDeclarator.isVariableDeclarator()) { // It is `const … = require('@linaria/shaker').dep`; const id = variableDeclarator.get('id'); if (id.isIdentifier()) { state.imports.push({ imported: getValue(property), local: id, source, type: 'cjs' }); } else { debug('evaluator:collectExportsAndImports', 'Id should be Identifier', variableDeclarator.node.type); } } else { // Maybe require is passed as an argument to some function? // Just use the whole MemberExpression as a local state.imports.push({ imported: getValue(property), local: container, source, type: 'cjs' }); } return; } // Is it `const something = require("something")`? if (key === 'init' && container.isVariableDeclarator()) { importFromVariableDeclarator(container, true).forEach(prop => { if (prop.what === '*') { const unfolded = unfoldNamespaceImport({ imported: '*', local: prop.as, source, type: 'cjs' }); state.imports.push(...unfolded); } else { state.imports.push({ imported: prop.what, local: prop.as, source, type: 'cjs' }); } }); } if (container.isExpressionStatement()) { // Looks like standalone require state.imports.push({ imported: 'side-effect', local: container, source }); } } function collectFromVariableDeclarator(path, state) { let found = false; path.traverse({ Identifier(identifierPath) { if (isRequire(identifierPath)) { collectFromRequire(identifierPath, state); found = true; } } }); if (found) { path.skip(); } } function isChainOfVoidAssignment(path) { const right = path.get('right'); if (right.isUnaryExpression({ operator: 'void' })) { return true; } if (right.isAssignmentExpression()) { return isChainOfVoidAssignment(right); } return false; } function getReturnValue(path) { if (path.node.params.length !== 0) return undefined; const body = path.get('body'); if (body.isExpression()) { return body; } if (body.node.body.length === 1) { const returnStatement = body.get('body')?.[0]; if (!returnStatement.isReturnStatement()) return undefined; const argument = returnStatement.get('argument'); if (!argument.isExpression()) return undefined; return argument; } return undefined; } function getGetterValueFromDescriptor(descriptor) { const props = descriptor.get('properties').filter(isTypedNode('ObjectProperty')); const getter = props.find(p => p.get('key').isIdentifier({ name: 'get' })); const value = getter?.get('value'); if (value?.isFunctionExpression() || value?.isArrowFunctionExpression()) { return getReturnValue(value); } const valueProp = props.find(p => p.get('key').isIdentifier({ name: 'value' })); const valueValue = valueProp?.get('value'); return valueValue?.isExpression() ? valueValue : undefined; } function addExport(path, exported, state) { function getRelatedImport() { if (path.isMemberExpression()) { const object = path.get('object'); if (!object.isIdentifier()) { return undefined; } const objectBinding = object.scope.getBinding(object.node.name); if (!objectBinding) { return undefined; } if (objectBinding.path.isVariableDeclarator()) { collectFromVariableDeclarator(objectBinding.path, state); } const found = state.imports.find(i => objectBinding.identifier === i.local.node || objectBinding.referencePaths.some(p => i.local.isAncestor(p))); if (!found) { return undefined; } const property = path.get('property'); let what = '*'; if (path.node.computed && property.isStringLiteral()) { what = property.node.value; } else if (!path.node.computed && property.isIdentifier()) { what = property.node.name; } return { import: { ...found, local: path }, what }; } return undefined; } const relatedImport = getRelatedImport(); if (relatedImport) { // eslint-disable-next-line no-param-reassign state.reexports.push({ local: relatedImport.import.local, imported: relatedImport.import.imported, source: relatedImport.import.source, exported }); } else { // eslint-disable-next-line no-param-reassign state.exports[exported] = path; } } function collectFromExports(path, state) { if (!isExports(path)) return; if (path.parentPath.isMemberExpression({ object: path.node })) { // It is `exports.prop = …` const memberExpression = path.parentPath; const property = memberExpression.get('property'); if (!property.isIdentifier() || memberExpression.node.computed) { return; } const exportName = property.node.name; const saveRef = () => { // Save all export.____ usages for later if (!state.exportRefs.has(exportName)) { state.exportRefs.set(exportName, []); } state.exportRefs.get(exportName).push(memberExpression); }; const assignmentExpression = memberExpression.parentPath; if (!assignmentExpression.isAssignmentExpression({ left: memberExpression.node })) { // If it's not `exports.prop = …`. Just save it. saveRef(); return; } const right = assignmentExpression.get('right'); if (isChainOfVoidAssignment(assignmentExpression)) { // It is `exports.foo = void 0` return; } const { name } = property.node; if (name === '__esModule') { // eslint-disable-next-line no-param-reassign state.isEsModule = true; return; } saveRef(); // eslint-disable-next-line no-param-reassign state.exports[property.node.name] = right; return; } if (path.parentPath.isCallExpression() && path.parentPath.get('callee').matchesPattern('Object.defineProperty')) { const [obj, prop, descriptor] = path.parentPath.get('arguments'); if (obj?.isIdentifier(path.node) && prop?.isStringLiteral() && descriptor?.isObjectExpression()) { if (prop.node.value === '__esModule') { // eslint-disable-next-line no-param-reassign state.isEsModule = true; } else { /** * Object.defineProperty(exports, "token", { * enumerable: true, * get: function get() { * return _unknownPackage.token; * } * }); */ const exported = prop.node.value; const local = getGetterValueFromDescriptor(descriptor); if (local) { addExport(local, exported, state); } } } else if (obj?.isIdentifier(path.node) && prop?.isIdentifier() && descriptor?.isObjectExpression()) { /** * Object.defineProperty(exports, key, { * enumerable: true, * get: function get() { * return _unknownPackage[key]; * } * }); */ const local = getGetterValueFromDescriptor(descriptor); if (local) { addExport(local, '*', state); } } } } function collectFromRequireOrExports(path, state) { if (isRequire(path)) { collectFromRequire(path, state); } else if (isExports(path)) { collectFromExports(path, state); } } function unfoldNamespaceImport(importItem) { const result = []; const { local } = importItem; if (!local.isIdentifier()) { // TODO: handle it return [importItem]; } const binding = getScope(local).getBinding(local.node.name); if (!binding?.referenced) { // Imported namespace is not referenced and probably not used, // but it can have side effects, so we should keep it as is return [importItem]; } for (const referencePath of binding?.referencePaths ?? []) { if (referencePath.find(ancestor => ancestor.isTSType() || ancestor.isFlowType())) { continue; } const { parentPath } = referencePath; if (parentPath?.isMemberExpression() && referencePath.key === 'object') { const property = parentPath.get('property'); const object = parentPath.get('object'); let imported; if (parentPath.node.computed && property.isStringLiteral()) { imported = property.node.value; } else if (!parentPath.node.computed && property.isIdentifier()) { imported = property.node.name; } else { imported = null; } if (object.isIdentifier() && imported) { result.push({ ...importItem, imported, local: parentPath }); } else { result.push(importItem); break; } continue; } if (parentPath?.isVariableDeclarator() && referencePath.key === 'init') { importFromVariableDeclarator(parentPath, true).map(prop => result.push({ ...importItem, imported: prop.what, local: prop.as })); continue; } if (parentPath?.isCallExpression() && referencePath.listKey === 'arguments') { // The defined variable is used as a function argument. Let's try to figure out what is imported. const importType = getImportExportTypeByInteropFunction(parentPath); if (!importType) { // Imported value is used as an unknown function argument, // so we can't predict usage and import it as is. result.push(importItem); break; } if (importType === 'default') { result.push({ ...importItem, imported: 'default', local: parentPath.get('id') }); continue; } if (importType === 'import:*') { result.push(importItem); break; } debug('evaluator:collectExportsAndImports:unfoldNamespaceImports', 'Unknown import type', importType); result.push(importItem); continue; } if (parentPath?.isExportSpecifier() || parentPath?.isExportDefaultDeclaration()) { // The whole namespace is re-exported result.push(importItem); break; } // Otherwise, we can't predict usage and import it as is // TODO: handle more cases debug('evaluator:collectExportsAndImports:unfoldNamespaceImports', 'Unknown reference', referencePath.node.type); result.push(importItem); break; } return result; } function collectFromExportAllDeclaration(path, state) { if (isType(path)) return; const source = path.get('source')?.node?.value; if (!source) return; // It is `export * from './css';` state.reexports.push({ exported: '*', imported: '*', local: path, source }); } function collectFromExportSpecifier(path, source, state) { if (path.isExportSpecifier()) { const exported = getValue(path.get('exported')); if (source) { // It is `export { foo } from './css';` const imported = path.get('local').node.name; state.reexports.push({ exported, imported, local: path, source }); } else { const local = path.get('local'); // eslint-disable-next-line no-param-reassign state.exports[exported] = local; } return; } if (path.isExportDefaultSpecifier() && source) { // It is `export default from './css';` state.reexports.push({ exported: 'default', imported: 'default', local: path, source }); } if (path.isExportNamespaceSpecifier() && source) { const exported = path.get('exported').node.name; // It is `export * as foo from './css';` state.reexports.push({ exported, imported: '*', local: path, source }); } // TODO: handle other cases debug('evaluator:collectExportsAndImports:collectFromExportSpecifier', 'Unprocessed ExportSpecifier', path.node.type); } function collectFromExportNamedDeclaration(path, state) { if (isType(path)) return; const source = path.get('source')?.node?.value; const specifiers = path.get('specifiers'); if (specifiers) { specifiers.forEach(specifier => collectFromExportSpecifier(specifier, source, state)); } const declaration = path.get('declaration'); if (declaration.isVariableDeclaration()) { declaration.get('declarations').forEach(declarator => { // eslint-disable-next-line no-param-reassign state.exports = { ...state.exports, ...exportFromVariableDeclarator(declarator) }; }); } if (declaration.isTSEnumDeclaration()) { // eslint-disable-next-line no-param-reassign state.exports[declaration.get('id').node.name] = declaration; } if (declaration.isFunctionDeclaration()) { const id = declaration.get('id'); if (id.isIdentifier()) { // eslint-disable-next-line no-param-reassign state.exports[id.node.name] = id; } } if (declaration.isClassDeclaration()) { const id = declaration.get('id'); if (id.isIdentifier()) { // eslint-disable-next-line no-param-reassign state.exports[id.node.name] = id; } } } function collectFromExportDefaultDeclaration(path, state) { if (isType(path)) return; // eslint-disable-next-line no-param-reassign state.exports.default = path.get('declaration'); } function collectFromAssignmentExpression(path, state) { if (isChainOfVoidAssignment(path)) { return; } const left = path.get('left'); const right = path.get('right'); let exported; if (left.isMemberExpression() && isExports(left.get('object'))) { const property = left.get('property'); if (property.isIdentifier()) { exported = property.node.name; } } else if (isExports(left)) { // module.exports = ... if (!isAlreadyProcessed(right)) { exported = 'default'; } } if (!exported) return; if (!right.isCallExpression() || !isRequire(right.get('callee'))) { // eslint-disable-next-line no-param-reassign state.exports[exported] = right; return; } const sourcePath = right.get('arguments')?.[0]; const source = sourcePath.isStringLiteral() ? sourcePath.node.value : undefined; if (!source) return; // It is `exports.foo = require('./css');` if (state.exports[exported]) { // eslint-disable-next-line no-param-reassign delete state.exports[exported]; } state.reexports.push({ exported, imported: '*', local: path, source }); path.skip(); } function collectFromExportStarCall(path, state) { const [requireCall, exports] = path.get('arguments'); if (!isExports(exports)) return; if (!requireCall.isCallExpression()) return; const callee = requireCall.get('callee'); const sourcePath = requireCall.get('arguments')?.[0]; if (!isRequire(callee) || !sourcePath.isStringLiteral()) return; const source = sourcePath.node.value; if (!source) return; state.reexports.push({ exported: '*', imported: '*', local: path, source }); path.skip(); } function collectFromMap(map, state) { const properties = map.get('properties'); properties.forEach(property => { if (!property.isObjectProperty()) return; const key = property.get('key'); const value = property.get('value'); if (!key.isIdentifier()) return; const exported = key.node.name; if (!value.isFunction()) return; if (value.node.params.length !== 0) return; const returnValue = getReturnValue(value); if (!returnValue) return; addExport(returnValue, exported, state); }); } function collectFromEsbuildExportCall(path, state) { const [sourceExports, map] = path.get('arguments'); if (!sourceExports.isIdentifier({ name: 'source_exports' })) return; if (!map.isObjectExpression()) return; collectFromMap(map, state); path.skip(); } function collectFromEsbuildReExportCall(path, state) { const [sourceExports, requireCall, exports] = path.get('arguments'); if (!sourceExports.isIdentifier({ name: 'source_exports' })) return; if (!requireCall.isCallExpression()) return; if (!isExports(exports)) return; const callee = requireCall.get('callee'); if (!isRequire(callee)) return; const sourcePath = requireCall.get('arguments')?.[0]; if (!sourcePath.isStringLiteral()) return; state.reexports.push({ exported: '*', imported: '*', local: path, source: sourcePath.node.value }); path.skip(); } function collectFromSwcExportCall(path, state) { const [exports, map] = path.get('arguments'); if (!isExports(exports)) return; if (!map.isObjectExpression()) return; collectFromMap(map, state); path.skip(); } function collectFromCallExpression(path, state) { const maybeExportStart = path.get('callee'); if (!maybeExportStart.isIdentifier()) { return; } const { name } = maybeExportStart.node; // TypeScript if (name.startsWith('__exportStar')) { collectFromExportStarCall(path, state); return; } // swc if (name === '_exportStar') { collectFromExportStarCall(path, state); } if (name === '_export') { collectFromSwcExportCall(path, state); } // esbuild if (name === '__export') { collectFromEsbuildExportCall(path, state); } if (name === '__reExport') { collectFromEsbuildReExportCall(path, state); } } export function collectExportsAndImports(path, cacheMode = 'enabled') { const localState = { deadExports: [], exportRefs: new Map(), exports: {}, imports: [], reexports: [], isEsModule: path.node.sourceType === 'module', processedRequires: new WeakSet() }; const cache = cacheMode !== 'disabled' ? getTraversalCache(path, 'collectExportsAndImports') : undefined; if (cacheMode === 'enabled' && cache?.has(path)) { return cache.get(path) ?? localState; } path.traverse({ AssignmentExpression: collectFromAssignmentExpression, CallExpression: collectFromCallExpression, ExportAllDeclaration: collectFromExportAllDeclaration, ExportDefaultDeclaration: collectFromExportDefaultDeclaration, ExportNamedDeclaration: collectFromExportNamedDeclaration, ImportDeclaration: collectFromImportDeclaration, Import: collectFromDynamicImport, Identifier: collectFromRequireOrExports, VariableDeclarator: collectFromVariableDeclarator }, localState); const { processedRequires, ...state } = localState; cache?.set(path, state); return state; } //# sourceMappingURL=collectExportsAndImports.js.map