UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

191 lines (190 loc) 8.1 kB
export { transformPointerImports }; export { parsePointerImportData }; export { isPointerImportData }; export { assertPointerImportPath }; // Playground: https://github.com/brillout/acorn-playground // Notes about `with { type: 'pointer' }` // - It works well with TypeScript: it doesn't complain upon `with { type: 'unknown-to-typescript' }` and go-to-definition & types are preserved: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes // - Acorn support for import attributes: https://github.com/acornjs/acorn/issues/983 // - Acorn plugin: https://github.com/acornjs/acorn/issues/983 // - Isn't stage 4 yet: https://github.com/tc39/proposal-import-attributes // - Using a import path suffix such as `import { Layout } from './Layout?real` breaks TypeScript, and TypeScript isn't working on supporting query params: https://github.com/microsoft/TypeScript/issues/10988#issuecomment-867135453 // - Node.js >=21 supports import attributes: https://nodejs.org/api/esm.html#import-attributes // - Esbuid supports // - Blocker: https://github.com/evanw/esbuild/issues/3646 // - Ugly hack to make it work: https://github.com/brillout/esbuild-playground/tree/experiment/import-attribute // - Discussion with esbuild maintainer: https://github.com/evanw/esbuild/issues/3384 // - Using a magic comment `// @vike-real-import` is probably a bad idea: // - Esbuild removes comments: https://github.com/evanw/esbuild/issues/1439#issuecomment-877656182 // - Using source maps to track these magic comments is brittle (source maps can easily break) import { parse } from 'acorn'; import { assert, assertUsage, assertWarning, isFilePathAbsolute, isImportPath, styleFileRE } from '../../utils.js'; import pc from '@brillout/picocolors'; function transformPointerImports(code, filePathToShowToUser2, pointerImports, // For ./transformPointerImports.spec.ts skipWarnings) { const spliceOperations = []; // Performance trick if (!code.includes('import')) return null; const imports = getImports(code); if (imports.length === 0) return null; imports.forEach((node) => { if (node.type !== 'ImportDeclaration') return; const importPath = node.source.value; assert(typeof importPath === 'string'); if (pointerImports !== 'all') { assert(importPath in pointerImports); const isPointerImport = pointerImports[importPath]; assert(isPointerImport === true || isPointerImport === false); if (!isPointerImport) return; } const { start, end } = node; const importStatementCode = code.slice(start, end); /* Pointer import without importing any value => doesn't make sense and doesn't have any effect. ```js // Useless import './some.css' // Useless import './Layout.jsx' ``` */ if (node.specifiers.length === 0) { const isWarning = !styleFileRE.test(importPath); let quote = indent(importStatementCode); if (isWarning) { quote = pc.cyan(quote); } else { quote = pc.bold(pc.red(quote)); } const errMsg = [ `The following import in ${filePathToShowToUser2} has no effect:`, quote, 'See https://vike.dev/config#pointer-imports', ].join('\n'); if (!skipWarnings) { if (!isWarning) { assertUsage(false, errMsg); } assertWarning(false, errMsg, { onlyOnce: true }); } } let replacement = ''; node.specifiers.forEach((specifier) => { assert(specifier.type === 'ImportSpecifier' || specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier'); const importLocalName = specifier.local.name; const exportName = (() => { if (specifier.type === 'ImportDefaultSpecifier') return 'default'; if (specifier.type === 'ImportNamespaceSpecifier') return '*'; { const imported = specifier.imported; if (imported) return imported.name; } return importLocalName; })(); const importString = serializePointerImportData({ importPath, exportName, importStringWasGenerated: true }); replacement += `const ${importLocalName} = '${importString}';`; }); spliceOperations.push({ start, end, replacement, }); }); const codeMod = spliceMany(code, spliceOperations); return codeMod; } function getImports(code) { const { body } = parse(code, { ecmaVersion: 'latest', sourceType: 'module', // https://github.com/acornjs/acorn/issues/1136 }); const imports = []; body.forEach((node) => { if (node.type === 'ImportDeclaration') imports.push(node); }); return imports; } const import_ = 'import'; const SEP = ':'; const zeroWidthSpace = '\u200b'; function serializePointerImportData({ importPath, exportName, importStringWasGenerated, }) { const tag = importStringWasGenerated ? zeroWidthSpace : ''; // `import:${importPath}:${importPath}` return `${tag}${import_}${SEP}${importPath}${SEP}${exportName}`; } function isPointerImportData(str) { return str.startsWith(import_ + SEP) || str.startsWith(zeroWidthSpace + import_ + SEP); } function parsePointerImportData(importString) { if (!isPointerImportData(importString)) { return null; } let importStringWasGenerated = false; if (importString.startsWith(zeroWidthSpace)) { importStringWasGenerated = true; assert(zeroWidthSpace.length === 1); importString = importString.slice(1); } const parts = importString.split(SEP).slice(1); if (!importStringWasGenerated && parts.length === 1) { const exportName = 'default'; const importPath = parts[0]; return { importPath, exportName, importStringWasGenerated, importString }; } assert(parts.length >= 2); const exportName = parts[parts.length - 1]; const importPath = parts.slice(0, -1).join(SEP); if (importPath.startsWith('.') && !(importPath.startsWith('./') || importPath.startsWith('../'))) { assert(!importStringWasGenerated); assertUsage(false, `Invalid relative import path ${pc.code(importPath)} defined by ${pc.code(JSON.stringify(importString))} because it should start with ${pc.code('./')} or ${pc.code('../')}, or use an npm package import instead.`); } assertPointerImportPath(importPath); return { importPath, exportName, importStringWasGenerated, importString }; } // `importPath` is one of the following: // - A relative import path // - An npm package import // - A filesystem absolute path, see transpileWithEsbuild() function assertPointerImportPath(importPath) { return isImportPath(importPath) || isFilePathAbsolute(importPath); } function spliceMany(str, operations) { let strMod = ''; let endPrev; operations.forEach(({ start, end, replacement }) => { if (endPrev !== undefined) { assert(endPrev < start); } else { endPrev = 0; } const replaced = str.slice(start, end); strMod += str.slice(endPrev, start) + replacement + // Preserve number of lines for source map Array(replaced.split('\n').length - replacement.split('\n').length) .fill('\n') .join(''); endPrev = end; }); strMod += str.slice(endPrev, str.length); return strMod; } function indent(str) { return str .split('\n') .map((s) => ` ${s}`) .join('\n'); }