UNPKG

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
'use strict'; 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