UNPKG

vike

Version:

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

196 lines (195 loc) 8.73 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.transformPointerImports = transformPointerImports; exports.parsePointerImportData = parsePointerImportData; exports.isPointerImportData = isPointerImportData; exports.assertPointerImportPath = 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) const acorn_1 = require("acorn"); const utils_js_1 = require("../../utils.js"); const picocolors_1 = __importDefault(require("@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; (0, utils_js_1.assert)(typeof importPath === 'string'); if (pointerImports !== 'all') { (0, utils_js_1.assert)(importPath in pointerImports); const isPointerImport = pointerImports[importPath]; (0, utils_js_1.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 = !utils_js_1.styleFileRE.test(importPath); let quote = indent(importStatementCode); if (isWarning) { quote = picocolors_1.default.cyan(quote); } else { quote = picocolors_1.default.bold(picocolors_1.default.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) { (0, utils_js_1.assertUsage)(false, errMsg); } (0, utils_js_1.assertWarning)(false, errMsg, { onlyOnce: true }); } } let replacement = ''; node.specifiers.forEach((specifier) => { (0, utils_js_1.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 } = (0, acorn_1.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; (0, utils_js_1.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 }; } (0, utils_js_1.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('../'))) { (0, utils_js_1.assert)(!importStringWasGenerated); (0, utils_js_1.assertUsage)(false, `Invalid relative import path ${picocolors_1.default.code(importPath)} defined by ${picocolors_1.default.code(JSON.stringify(importString))} because it should start with ${picocolors_1.default.code('./')} or ${picocolors_1.default.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 (0, utils_js_1.isImportPath)(importPath) || (0, utils_js_1.isFilePathAbsolute)(importPath); } function spliceMany(str, operations) { let strMod = ''; let endPrev; operations.forEach(({ start, end, replacement }) => { if (endPrev !== undefined) { (0, utils_js_1.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'); }