vite-preload
Version:
Speed up your Vite application by preloading server rendered lazy modules and stylesheets as early as possible
199 lines (193 loc) • 8.2 kB
JavaScript
;
var parser = require('@babel/parser');
var _traverse = require('@babel/traverse');
var _generate = require('@babel/generator');
var t = require('@babel/types');
var path = require('node:path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var t__namespace = /*#__PURE__*/_interopNamespaceDefault(t);
function importDefault(module) {
return module['default'] || module;
}
const traverse = importDefault(_traverse);
const generate = importDefault(_generate);
const hookFunctionName = '__collectModule';
// Modules to scan for dynamic imports
const include = /\.(jsx?|tsx?)$/;
// Dynamically import modules to try to inject hook into
const includeJsx = /\.(jsx|tsx)$/;
function preloadPlugin({ __internal_importHelperModuleName = 'vite-preload/__internal', debug, } = {}) {
const lazyImportedModules = new Set();
const injectedModules = new Set();
let count = 0;
return {
name: 'vite-preload',
apply(config) {
// Enable on SSR builds (--ssr)
return Boolean(config.build?.ssr);
},
async transform(code, id) {
if (!include.test(id)) {
return null;
}
const relative = getRelativePath(id);
const foundLazyImports = new Set();
let ast;
// Find dynamic imports
if (code.includes(' import(')) {
ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
traverse(ast, {
Import(path) {
if (!path.parent['arguments']) {
return;
}
const importArgument = path.parent['arguments'][0];
if (importArgument) {
// Dynamic import of a dynamic module is not supported
if (importArgument.type === 'StringLiteral') {
foundLazyImports.add(importArgument.value);
}
}
},
});
}
for (const importString of foundLazyImports) {
const relative = path.resolve(path.dirname(id), importString);
const resolved = await this.resolve(importString, id);
if (!resolved) {
throw new Error(`Did not find imported module ${relative}`);
}
if (!includeJsx.test(resolved.id)) {
continue;
}
if (debug) {
this.info(`dynamically imports ${path.relative(process.cwd(), resolved.id)}`);
}
lazyImportedModules.add(resolved.id);
}
if (lazyImportedModules.has(id)) {
let injected = false;
ast || (ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
}));
traverse(ast, {
ExportDefaultDeclaration(path) {
const declaration = path.node.declaration;
// Insert hook in `export default function() { ... }`
if (isReactFunctionComponent(declaration)) {
injectImport(ast, __internal_importHelperModuleName);
injectHook(path.get('declaration'), relative);
injected = true;
}
else if (t__namespace.isIdentifier(declaration)) {
// Insert hook in `function Component() { ... }; export default Component;`
const binding = path.scope.getBinding(declaration.name);
if (binding) {
// Right here we need to check if the binding is a declarator for the ArrowFunctionExpression.
// This code creates the correct NodePath for the if statement and the injectHook function.
const expressionPath = t__namespace.isVariableDeclarator(binding.path.node)
? binding.path.get('init')
: binding.path;
if (expressionPath.node &&
isReactFunctionComponent(expressionPath.node)) {
injectImport(ast, __internal_importHelperModuleName);
injectHook(expressionPath, relative);
injected = true;
}
}
}
},
});
if (injected) {
if (debug) {
this.info('Injected __collectModule in React component');
}
count++;
const output = generate(ast, {}, code);
injectedModules.add(id);
return {
code: output.code,
map: output.map,
};
}
}
return null;
},
buildEnd() {
if (debug) {
const s = lazyImportedModules.difference(injectedModules);
for (const z of s) {
this.warn(`${z} was not injected`);
}
}
this.info(`${count} hook calls injected`);
},
};
}
function injectHook(path, arg) {
if (t__namespace.isFunctionDeclaration(path.node) ||
t__namespace.isArrowFunctionExpression(path.node)) {
const hookCall = t__namespace.expressionStatement(t__namespace.callExpression(t__namespace.identifier(hookFunctionName), [
// t.memberExpression(
// t.metaProperty(t.identifier('import'), t.identifier('meta')),
// t.identifier('filename')
// ),
t__namespace.stringLiteral(arg),
]));
const bodyList = path.get('body');
const body = Array.isArray(bodyList) ? bodyList[0] : bodyList;
// While function declarations only have a block statement as body,
// arrow functions allow both.
if (t__namespace.isBlockStatement(body.node)) {
body.unshiftContainer('body', hookCall);
}
else if (t__namespace.isExpression(body.node)) {
path.set('body', t__namespace.blockStatement([hookCall, t__namespace.returnStatement(body.node)]));
}
}
}
function injectImport(ast, importHelper) {
let alreadyImported = false;
traverse(ast, {
ImportDeclaration(path) {
if (path.node.source.value === importHelper) {
alreadyImported = true;
}
},
});
if (!alreadyImported) {
const importDeclaration = t__namespace.importDeclaration([
t__namespace.importSpecifier(t__namespace.identifier(hookFunctionName), t__namespace.identifier(hookFunctionName)),
], t__namespace.stringLiteral(importHelper));
ast.program.body.unshift(importDeclaration);
}
}
function isReactFunctionComponent(node) {
return (t__namespace.isFunctionDeclaration(node) ||
t__namespace.isFunctionExpression(node) ||
t__namespace.isArrowFunctionExpression(node));
}
function getRelativePath(filePath) {
return path.relative(process.cwd(), filePath).replace(/\\/g, '/');
}
module.exports = preloadPlugin;
//# sourceMappingURL=plugin.cjs.map