vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
196 lines (195 loc) • 8.73 kB
JavaScript
;
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');
}