UNPKG

utquidem

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

192 lines (159 loc) 5.34 kB
import path from 'path'; import { createDebugger } from '@modern-js/utils'; import { Plugin as RollupPlugin } from 'rollup'; import MagicString from 'magic-string'; import { Parser } from 'acorn'; import acornClassFields from 'acorn-class-fields'; import { CallExpression, MemberExpression, Identifier, Literal } from 'estree'; import { simple as esWalk } from 'acorn-walk'; import type { IAppContext, NormalizedConfig } from '@modern-js/core'; import { isJsRequest, isCSSRequest } from '../utils'; import { DEV_CLIENT_URL } from '../constants'; import { fileToModules } from '../AssetModule'; type AcceptDep = Literal & { start: number; end: number }; export type HMRAcceptDeps = { selfAccepted: boolean; acceptDeps: Array<AcceptDep>; }; const debug = createDebugger('esm:hmr-plugin'); const hasHotContext = (code: string) => code.includes('module.hot') || code.includes(`import.meta.hot`); const shouldApplyHmr = ( code: string, filename: string, internalDirectory: string, ) => { if (filename.startsWith(internalDirectory)) { return false; } if (isCSSRequest(filename)) { return true; } if (isJsRequest(filename) && hasHotContext(code)) { return true; } return false; }; // append hmr api export const hmrPlugin = ( _config: NormalizedConfig, appContext: IAppContext, ): RollupPlugin => { const { appDirectory } = appContext; return { name: 'esm-hmr', async transform(code: string, importer: string) { const { internalDirectory } = appContext; if (!shouldApplyHmr(code, importer, internalDirectory)) { return null; } const s = new MagicString(code); const assetModule = fileToModules.get(importer); const { id } = assetModule!; s.prepend( `import { createHotContext} from "${DEV_CLIENT_URL}";\nimport.meta.hot = createHotContext(${JSON.stringify( id, )});\n`, ); // replace webpack-style `module.hot` to `import.meta.hot`: if (code.includes('module.hot')) { const matches = code.matchAll(/module\.hot/g); for (const match of matches) { s.overwrite( match.index as number, (match.index as number) + 10, 'import.meta.hot', ); } } // auto wrapper import.hot.accept for css if (isCSSRequest(importer)) { s.append( [ `\nimport.meta.hot.accept();`, `import.meta.hot.prune(() => { removeStyle(${JSON.stringify(importer)}) });`, ].join('\n'), ); assetModule!.selfAccepted = true; return { code: s.toString() }; } if (s.toString().includes(`import.meta.hot.`)) { const finalStr = new MagicString(s.toString()); const { selfAccepted, acceptDeps } = getHotAcceptDeps(s.toString()); for (const { value, start, end } of acceptDeps) { const resolved = await this.resolve( value as string, path.dirname(importer), ); // TODO: resolved 为 null时 错误提示 if (resolved) { const filePath = typeof resolved === 'object' ? resolved.id : resolved; const relative = path.relative(appDirectory, filePath); const depId = relative.startsWith('.') ? filePath : `/${relative}`; assetModule!.acceptIds.add(depId); finalStr.overwrite(start, end, JSON.stringify(depId)); } } assetModule!.selfAccepted = selfAccepted; debug(`file ${importer} selfAccepted & acceptDeps:`, { acceptDeps, selfAccepted, }); return { code: finalStr.toString() }; } return { code: s.toString() }; }, }; }; const isHotImportMetaNode = (node: MemberExpression) => node.object && node.object.type === 'MetaProperty' && node.object.meta && node.object.meta.name === 'import' && node.object.property && node.object.property.name === 'meta' && node.property && (node.property as Identifier).name === 'hot'; const getHotAcceptDeps = (code: string): HMRAcceptDeps => { let selfAccepted = false; const deps: Array<AcceptDep> = []; const ast = Parser.extend(acornClassFields).parse(code, { sourceType: 'module', ecmaVersion: '2020' as any, }); esWalk(ast, { // @ts-expect-error CallExpression(node: CallExpression) { if ( node.callee.type === 'MemberExpression' && isHotImportMetaNode(node.callee.object as MemberExpression) && node.callee.property && (node.callee.property as Identifier).name === 'accept' ) { const args = node.arguments; if ( args.length === 0 || args[0].type === 'ArrowFunctionExpression' || args[0].type === 'FunctionExpression' ) { selfAccepted = true; } else if (args[0].type === 'ArrayExpression') { for (const specifier of args[0].elements) { if ((specifier as Literal).type === 'Literal') { deps.push(specifier as AcceptDep); } } } else if (args[0].type === 'Literal') { deps.push(args[0] as AcceptDep); } } }, }); return { selfAccepted, acceptDeps: deps, }; };