UNPKG

@esmx/core

Version:

A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Federation capabilities.

1,165 lines (1,111 loc) 37.2 kB
import crypto from 'node:crypto'; import fs from 'node:fs'; import fsp from 'node:fs/promises'; import path from 'node:path'; import { cwd } from 'node:process'; import { pathToFileURL } from 'node:url'; import type { ImportMap, ScopesMap, SpecifierMap } from '@esmx/import'; import serialize from 'serialize-javascript'; import { type App, createApp } from './app'; import { type ManifestJson, getManifestList } from './manifest-json'; import { type ModuleConfig, type ParsedModuleConfig, parseModuleConfig } from './module-config'; import { type PackConfig, type ParsedPackConfig, parsePackConfig } from './pack-config'; import type { ImportmapMode } from './render-context'; import type { RenderContext, RenderContextOptions } from './render-context'; import { type CacheHandle, createCache } from './utils/cache'; import { getImportMap } from './utils/import-map'; import type { Middleware } from './utils/middleware'; import { type ProjectPath, resolvePath } from './utils/resolve-path'; import { getImportPreloadInfo as getStaticImportPaths } from './utils/static-import-lexer'; /** * Esmx 框架的核心配置选项接口 */ export interface EsmxOptions { /** * 项目根目录路径 * - 可以是绝对路径或相对路径 * - 默认为当前工作目录 (process.cwd()) */ root?: string; /** * 是否为生产环境 * - true: 生产环境 * - false: 开发环境 * - 默认根据 process.env.NODE_ENV === 'production' 判断 */ isProd?: boolean; /** * 基础路径占位符配置 * - string: 自定义占位符 * - false: 禁用占位符 * - 默认值为 '[[[___GEZ_DYNAMIC_BASE___]]]' * - 用于运行时动态替换资源的基础路径 */ basePathPlaceholder?: string | false; /** * 模块配置选项 * - 用于配置项目的模块解析规则 * - 包括模块别名、外部依赖等配置 */ modules?: ModuleConfig; /** * 打包配置选项 * - 用于将构建产物打包成标准的 npm .tgz 格式软件包 * - 包括输出路径、package.json 处理、打包钩子等配置 */ packs?: PackConfig; /** * 开发环境应用创建函数 * - 仅在开发环境中使用 * - 用于创建开发服务器的应用实例 * @param esmx Esmx实例 */ devApp?: (esmx: Esmx) => Promise<App>; /** * 服务器启动配置函数 * - 用于配置和启动 HTTP 服务器 * - 在开发环境和生产环境中都可使用 * @param esmx Esmx实例 */ server?: (esmx: Esmx) => Promise<void>; /** * 构建后置处理函数 * - 在项目构建完成后执行 * - 可用于执行额外的资源处理、部署等操作 * @param esmx Esmx实例 */ postBuild?: (esmx: Esmx) => Promise<void>; } /** * 应用程序构建目标类型。 * - client: 客户端构建目标,用于生成浏览器端运行的代码 * - server: 服务端构建目标,用于生成 Node.js 环境运行的代码 */ export type BuildSsrTarget = 'client' | 'server'; /** * Esmx 框架的命令枚举。 * 用于控制框架的运行模式和生命周期。 */ export enum COMMAND { /** * 开发模式 * 启动开发服务器并支持热更新 */ dev = 'dev', /** * 构建模式 * 生成生产环境构建产物 */ build = 'build', /** * 预览模式 * 预览构建产物 */ preview = 'preview', /** * 启动模式 * 启动生产环境服务器 */ start = 'start' } export type { ImportMap, SpecifierMap, ScopesMap }; /** * Esmx 框架实例的初始化状态接口 * @internal 仅供框架内部使用 * * @description * 该接口定义了框架实例初始化后的状态数据,包含: * - 应用实例:处理请求和渲染 * - 当前命令:控制运行模式 * - 模块配置:解析后的模块设置 * - 打包配置:解析后的构建设置 * - 缓存处理:框架内部缓存机制 */ interface Readied { /** 应用程序实例,提供中间件和渲染功能 */ app: App; /** 当前执行的框架命令 */ command: COMMAND; /** 解析后的模块配置信息 */ moduleConfig: ParsedModuleConfig; /** 解析后的打包配置信息 */ packConfig: ParsedPackConfig; /** 缓存处理器 */ cache: CacheHandle; } export class Esmx { // 基础属性和构造函数 private readonly _options: EsmxOptions; private _readied: Readied | null = null; private _importmapHash: string | null = null; private get readied() { if (this._readied) { return this._readied; } throw new NotReadyError(); } /** * 获取模块名称 * @returns {string} 当前模块的名称,来源于模块配置 * @throws {NotReadyError} 在框架实例未初始化时抛出错误 */ public get name(): string { return this.moduleConfig.name; } /** * 获取模块变量名 * @returns {string} 基于模块名称生成的合法 JavaScript 变量名 * @throws {NotReadyError} 在框架实例未初始化时抛出错误 */ public get varName(): string { return '__' + this.name.replace(/[^a-zA-Z]/g, '_') + '__'; } /** * 获取项目根目录的绝对路径 * @returns {string} 项目根目录的绝对路径 * 如果配置的 root 为相对路径,则基于当前工作目录解析为绝对路径 */ public get root(): string { const { root = cwd() } = this._options; if (path.isAbsolute(root)) { return root; } return path.resolve(cwd(), root); } /** * 判断当前是否为生产环境 * @returns {boolean} 环境标识 * 优先使用配置项中的 isProd,若未配置则根据 process.env.NODE_ENV 判断 */ public get isProd(): boolean { return this._options?.isProd ?? process.env.NODE_ENV === 'production'; } /** * 获取模块的基础路径 * @returns {string} 以斜杠开头和结尾的模块基础路径 * 用于构建模块资源的访问路径 */ public get basePath(): string { return `/${this.name}/`; } /** * 获取基础路径占位符 * @returns {string} 基础路径占位符或空字符串 * 用于运行时动态替换模块的基础路径,可通过配置禁用 */ public get basePathPlaceholder(): string { const varName = this._options.basePathPlaceholder; if (varName === false) { return ''; } return varName ?? '[[[___GEZ_DYNAMIC_BASE___]]]'; } /** * 获取当前执行的命令 * @returns {COMMAND} 当前正在执行的命令枚举值 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 */ public get command(): COMMAND { return this.readied.command; } /** * 获取命令枚举类型 * @returns {typeof COMMAND} 命令枚举类型定义 */ public get COMMAND(): typeof COMMAND { return COMMAND; } /** * 获取模块配置信息 * @returns {ParsedModuleConfig} 当前模块的完整配置信息 */ public get moduleConfig(): ParsedModuleConfig { return this.readied.moduleConfig; } /** * 获取打包配置信息 * @returns {ParsedPackConfig} 当前模块的打包相关配置 */ public get packConfig(): ParsedPackConfig { return this.readied.packConfig; } /** * 获取应用程序的静态资源处理中间件。 * * 该中间件负责处理应用程序的静态资源请求,根据运行环境提供不同的实现: * - 开发环境:支持源码的实时编译、热更新,使用 no-cache 缓存策略 * - 生产环境:处理构建后的静态资源,支持不可变文件的长期缓存 * * @returns {Middleware} 返回静态资源处理中间件函数 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * const server = http.createServer((req, res) => { * // 使用中间件处理静态资源请求 * esmx.middleware(req, res, async () => { * const rc = await esmx.render({ url: req.url }); * res.end(rc.html); * }); * }); * ``` */ public get middleware(): Middleware { return this.readied.app.middleware; } /** * 获取应用程序的服务端渲染函数。 * * 该函数负责执行服务端渲染,根据运行环境提供不同的实现: * - 开发环境:加载源码中的服务端入口文件,支持热更新和实时预览 * - 生产环境:加载构建后的服务端入口文件,提供优化的渲染性能 * * @returns {(options?: RenderContextOptions) => Promise<RenderContext>} 返回服务端渲染函数 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // 基本用法 * const rc = await esmx.render({ * params: { url: req.url } * }); * res.end(rc.html); * * // 高级配置 * const rc = await esmx.render({ * base: '', // 设置基础路径 * importmapMode: 'inline', // 设置导入映射模式 * entryName: 'default', // 指定渲染入口 * params: { * url: req.url, * state: { user: 'admin' } * } * }); * ``` */ public get render(): ( options?: RenderContextOptions ) => Promise<RenderContext> { return this.readied.app.render; } public constructor(options: EsmxOptions = {}) { this._options = options; } /** * 初始化 Esmx 框架实例。 * * 该方法执行以下核心初始化流程: * 1. 解析项目配置(package.json、模块配置、打包配置等) * 2. 创建应用实例(开发环境或生产环境) * 3. 根据命令执行相应的生命周期方法 * * @param command - 框架运行命令 * - dev: 启动开发服务器,支持热更新 * - build: 构建生产环境产物 * - preview: 预览构建产物 * - start: 启动生产环境服务器 * * @returns 初始化成功返回 true * @throws {Error} 重复初始化时抛出错误 * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // 开发环境配置 * async devApp(esmx) { * return import('@esmx/rspack').then((m) => * m.createRspackHtmlApp(esmx, { * config(context) { * // 自定义 Rspack 配置 * } * }) * ); * }, * * // HTTP 服务器配置 * async server(esmx) { * const server = http.createServer((req, res) => { * // 静态文件处理 * esmx.middleware(req, res, async () => { * // 传入渲染的参数 * const render = await esmx.render({ * params: { url: req.url } * }); * // 响应 HTML 内容 * res.end(render.html); * }); * }); * * // 监听端口 * server.listen(3000, () => { * console.log('http://localhost:3000'); * }); * } * } satisfies EsmxOptions; * ``` */ public async init(command: COMMAND): Promise<boolean> { if (this._readied) { throw new Error('Cannot be initialized repeatedly'); } const { name } = await this.readJson( path.resolve(this.root, 'package.json') ); const moduleConfig = parseModuleConfig( name, this.root, this._options.modules ); const packConfig = parsePackConfig(this._options.packs); this._readied = { command, app: { middleware() { throw new NotReadyError(); }, async render() { throw new NotReadyError(); } }, moduleConfig, packConfig, cache: createCache(this.isProd) }; const devApp = this._options.devApp || defaultDevApp; const app: App = [COMMAND.dev, COMMAND.build].includes(command) ? await devApp(this) : await createApp(this, command); this.readied.app = app; switch (command) { case COMMAND.dev: case COMMAND.start: await this.server(); break; case COMMAND.build: return this.build(); case COMMAND.preview: break; } return true; } /** * 销毁 Esmx 框架实例,执行资源清理和连接关闭等操作。 * * 该方法主要用于开发环境下的资源清理,包括: * - 关闭开发服务器(如 Rspack Dev Server) * - 清理临时文件和缓存 * - 释放系统资源 * * 注意:一般情况下,框架会自动处理资源的释放,用户无需手动调用此方法。 * 仅在需要自定义资源清理逻辑时才需要使用。 * * @returns 返回一个 Promise,resolve 为 boolean 值 * - true: 清理成功或无需清理 * - false: 清理失败 * * @example * ```ts * // 在需要自定义清理逻辑时使用 * process.once('SIGTERM', async () => { * await esmx.destroy(); // 清理资源 * process.exit(0); * }); * ``` */ public async destroy(): Promise<boolean> { const { readied } = this; if (readied.app?.destroy) { return readied.app.destroy(); } return true; } /** * 执行应用程序的构建流程。 * * 该方法负责执行整个应用的构建过程,包括: * - 编译源代码 * - 生成生产环境的构建产物 * - 优化和压缩代码 * - 生成资源清单 * * 构建过程会打印开始和结束时间,以及总耗时等信息。 * * @returns 返回一个 Promise,resolve 为 boolean 值 * - true: 构建成功或构建方法未实现 * - false: 构建失败 * * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // 开发环境配置 * async devApp(esmx) { * return import('@esmx/rspack').then((m) => * m.createRspackHtmlApp(esmx, { * config(context) { * // 自定义 Rspack 配置 * } * }) * ); * }, * * // 构建后处理 * async postBuild(esmx) { * // 构建完成后生成静态 HTML * const render = await esmx.render({ * params: { url: '/' } * }); * esmx.writeSync( * esmx.resolvePath('dist/client', 'index.html'), * render.html * ); * } * } satisfies EsmxOptions; * ``` */ public async build(): Promise<boolean> { const startTime = Date.now(); const successful = await this.readied.app.build?.(); const endTime = Date.now(); const duration = endTime - startTime; const status = successful ? '\x1b[32m✓\x1b[0m'.padEnd(3) : '\x1b[31m✗\x1b[0m'.padEnd(3); console.log( `${status.padEnd(2)} Build ${successful ? 'completed' : 'failed'} in ${duration}ms` ); return successful ?? true; } /** * 启动 HTTP 服务器并配置服务器实例。 * * 该方法在框架的以下生命周期中被调用: * - 开发环境(dev):启动开发服务器,提供热更新等功能 * - 生产环境(start):启动生产服务器,提供生产级性能 * * 服务器的具体实现由用户通过 EsmxOptions 的 server 配置函数提供。 * 该函数负责: * - 创建 HTTP 服务器实例 * - 配置中间件和路由 * - 处理请求和响应 * - 启动服务器监听 * * @returns 返回一个 Promise,在服务器启动完成后 resolve * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // entry.node.ts * import http from 'node:http'; * import type { EsmxOptions } from '@esmx/core'; * * export default { * // 服务器配置 * async server(esmx) { * const server = http.createServer((req, res) => { * // 处理静态资源 * esmx.middleware(req, res, async () => { * // 服务端渲染 * const render = await esmx.render({ * params: { url: req.url } * }); * res.end(render.html); * }); * }); * * // 启动服务器 * server.listen(3000, () => { * console.log('Server running at http://localhost:3000'); * }); * } * } satisfies EsmxOptions; * ``` */ public async server(): Promise<void> { await this._options?.server?.(this); } /** * 执行构建后的处理逻辑。 * * 该方法在应用构建完成后被调用,用于执行额外的资源处理,如: * - 生成静态 HTML 文件 * - 处理构建产物 * - 执行部署任务 * - 发送构建通知 * * 方法会自动捕获并处理执行过程中的异常,确保不会影响主构建流程。 * * @returns 返回一个 Promise,resolve 为 boolean 值 * - true: 后处理成功或无需处理 * - false: 后处理失败 * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // 构建后处理 * async postBuild(esmx) { * // 生成多个页面的静态 HTML * const pages = ['/', '/about', '/404']; * * for (const url of pages) { * const render = await esmx.render({ * params: { url } * }); * * // 写入静态 HTML 文件 * esmx.writeSync( * esmx.resolvePath('dist/client', url.substring(1), 'index.html'), * render.html * ); * } * } * } satisfies EsmxOptions; * ``` */ public async postBuild(): Promise<boolean> { try { await this._options.postBuild?.(this); return true; } catch (e) { console.error(e); return false; } } /** * 解析项目相对路径为绝对路径 * * @param projectPath - 项目路径类型,如 'dist/client'、'dist/server' 等 * @param args - 需要拼接的路径片段 * @returns 解析后的绝对路径 * * @example * ```ts * // 在 entry.node.ts 中使用 * async postBuild(esmx) { * const outputPath = esmx.resolvePath('dist/client', 'index.html'); * // 输出: /project/root/dist/client/index.html * } * ``` */ public resolvePath(projectPath: ProjectPath, ...args: string[]): string { return resolvePath(this.root, projectPath, ...args); } /** * 同步写入文件内容 * * @param filepath - 文件的绝对路径 * @param data - 要写入的数据,可以是字符串、Buffer 或对象 * @returns 写入是否成功 * * @example * ```ts * // 在 entry.node.ts 中使用 * async postBuild(esmx) { * const htmlPath = esmx.resolvePath('dist/client', 'index.html'); * const success = esmx.writeSync(htmlPath, '<html>...</html>'); * } * ``` */ public writeSync(filepath: string, data: any): boolean { try { // 确保目标目录存在 fs.mkdirSync(path.dirname(filepath), { recursive: true }); // 写入文件 fs.writeFileSync(filepath, data); return true; } catch { return false; } } /** * 异步写入文件内容 * * @param filepath - 文件的绝对路径 * @param data - 要写入的数据,可以是字符串、Buffer 或对象 * @returns Promise<boolean> 写入是否成功 * * @example * ```ts * // 在 entry.node.ts 中使用 * async postBuild(esmx) { * const htmlPath = esmx.resolvePath('dist/client', 'index.html'); * const success = await esmx.write(htmlPath, '<html>...</html>'); * } * ``` */ public async write(filepath: string, data: any): Promise<boolean> { try { // 确保目标目录存在 await fsp.mkdir(path.dirname(filepath), { recursive: true }); // 写入文件 await fsp.writeFile(filepath, data); return true; } catch { return false; } } /** * 同步读取并解析 JSON 文件 * * @template T - 期望返回的JSON对象类型 * @param filename - JSON 文件的绝对路径 * @returns {T} 解析后的 JSON 对象 * @throws 当文件不存在或 JSON 格式错误时抛出异常 * * @example * ```ts * // 在 entry.node.ts 中使用 * async server(esmx) { * const manifest = esmx.readJsonSync<Manifest>(esmx.resolvePath('dist/client', 'manifest.json')); * // 使用 manifest 对象 * } * ``` */ public readJsonSync<T = any>(filename: string): T { return JSON.parse(fs.readFileSync(filename, 'utf-8')); } /** * 异步读取并解析 JSON 文件 * * @template T - 期望返回的JSON对象类型 * @param filename - JSON 文件的绝对路径 * @returns {Promise<T>} 解析后的 JSON 对象 * @throws 当文件不存在或 JSON 格式错误时抛出异常 * * @example * ```ts * // 在 entry.node.ts 中使用 * async server(esmx) { * const manifest = await esmx.readJson<Manifest>(esmx.resolvePath('dist/client', 'manifest.json')); * // 使用 manifest 对象 * } * ``` */ public async readJson<T = any>(filename: string): Promise<T> { return JSON.parse(await fsp.readFile(filename, 'utf-8')); } /** * 获取构建清单列表 * * @description * 该方法用于获取指定目标环境的构建清单列表,包含以下功能: * 1. **缓存管理** * - 使用内部缓存机制避免重复加载 * - 返回不可变的清单列表 * * 2. **环境适配** * - 支持客户端和服务端两种环境 * - 根据目标环境返回对应的清单信息 * * 3. **模块映射** * - 包含模块导出信息 * - 记录资源依赖关系 * * @param target - 目标环境类型 * - 'client': 客户端环境 * - 'server': 服务端环境 * @returns 返回只读的构建清单列表 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // 在 entry.node.ts 中使用 * async server(esmx) { * // 获取客户端构建清单 * const manifests = await esmx.getManifestList('client'); * * // 查找特定模块的构建信息 * const appModule = manifests.find(m => m.name === 'my-app'); * if (appModule) { * console.log('App exports:', appModule.exports); * console.log('App chunks:', appModule.chunks); * } * } * ``` */ public async getManifestList( target: BuildSsrTarget ): Promise<readonly ManifestJson[]> { return this.readied.cache(`getManifestList-${target}`, async () => Object.freeze(await getManifestList(target, this.moduleConfig)) ); } /** * 获取导入映射对象 * * @description * 该方法用于生成 ES 模块导入映射(Import Map),具有以下特点: * 1. **模块解析** * - 基于构建清单生成模块映射 * - 支持客户端和服务端两种环境 * - 自动处理模块路径解析 * * 2. **缓存优化** * - 使用内部缓存机制 * - 返回不可变的映射对象 * * 3. **路径处理** * - 自动处理模块路径 * - 支持动态基础路径 * * @param target - 目标环境类型 * - 'client': 生成浏览器环境的导入映射 * - 'server': 生成服务端环境的导入映射 * @returns 返回只读的导入映射对象 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // 在 entry.node.ts 中使用 * async server(esmx) { * // 获取客户端导入映射 * const importmap = await esmx.getImportMap('client'); * * // 自定义 HTML 模板 * const html = ` * <!DOCTYPE html> * <html> * <head> * <script type="importmap"> * ${JSON.stringify(importmap)} * </script> * </head> * <body> * <!-- 页面内容 --> * </body> * </html> * `; * } * ``` */ public async getImportMap( target: BuildSsrTarget ): Promise<Readonly<ImportMap>> { return this.readied.cache(`getImportMap-${target}`, async () => { const { moduleConfig } = this.readied; const manifests = await this.getManifestList(target); let json: ImportMap = {}; switch (target) { case 'client': json = getImportMap({ manifests, getScope(name) { return `/${name}/`; }, getFile(name, file) { return `/${name}/${file}`; } }); break; case 'server': json = getImportMap({ manifests, getScope: (name: string) => { const linkPath = moduleConfig.links[name].server; // Get the real physical path instead of symbolic link // This is crucial when generating import maps on the server side. // If we use symbolic link paths as scopes, it would cause module resolution errors at runtime // because the actual accessed paths are real physical paths, not the symbolic links. // Using realpathSync ensures path consistency between import map generation and runtime resolution. const realPath = fs.realpathSync(linkPath); return pathToFileURL(path.join(realPath, '/')).href; }, getFile: (name: string, file: string) => { const linkPath = moduleConfig.links[name].server; // Get the real physical path instead of symbolic link // This is crucial to maintain consistency with getScope function // and ensure proper module resolution at runtime const realPath = fs.realpathSync(linkPath); return pathToFileURL(path.resolve(realPath, file)) .href; } }); break; } return Object.freeze(json); }); } /** * 获取客户端导入映射信息 * * @description * 该方法用于生成客户端环境的导入映射代码,支持两种模式: * 1. **内联模式 (inline)** * - 将导入映射直接内联到 HTML 中 * - 减少额外的网络请求 * - 适合导入映射较小的场景 * * 2. **JS 文件模式 (js)** * - 生成独立的 JS 文件 * - 支持浏览器缓存 * - 适合导入映射较大的场景 * * 核心功能: * - 自动处理动态基础路径 * - 支持模块路径运行时替换 * - 优化缓存策略 * - 确保模块加载顺序 * * @param mode - 导入映射模式 * - 'inline': 内联模式,返回 HTML script 标签 * - 'js': JS 文件模式,返回带有文件路径的信息 * @returns 返回导入映射的相关信息 * - src: JS 文件的 URL(仅在 js 模式下) * - filepath: JS 文件的本地路径(仅在 js 模式下) * - code: HTML script 标签内容 * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // 在 entry.node.ts 中使用 * async server(esmx) { * const server = express(); * server.use(esmx.middleware); * * server.get('*', async (req, res) => { * // 使用 JS 文件模式 * const result = await esmx.render({ * importmapMode: 'js', * params: { url: req.url } * }); * res.send(result.html); * }); * * // 或者使用内联模式 * server.get('/inline', async (req, res) => { * const result = await esmx.render({ * importmapMode: 'inline', * params: { url: req.url } * }); * res.send(result.html); * }); * } * ``` */ public async getImportMapClientInfo<T extends ImportmapMode>( mode: T ): Promise< T extends 'js' ? { src: string; filepath: string; code: string; } : { src: null; filepath: null; code: string; } > { return this.readied.cache( `getImportMap-${mode}`, async (): Promise<any> => { const importmap = await this.getImportMap('client'); const { basePathPlaceholder } = this; let filepath: string | null = null; if (this._importmapHash === null) { let wrote = false; const code = `(() => { const base = document.currentScript.getAttribute("data-base"); const importmap = ${serialize(importmap, { isJSON: true })}; const set = (data) => { if (!data) return; Object.entries(data).forEach(([k, v]) => { data[k] = base + v; }); }; set(importmap.imports); if (importmap.scopes) { Object.values(importmap.scopes).forEach(set); } const script = document.createElement("script"); script.type = "importmap"; script.innerText = JSON.stringify(importmap); document.head.appendChild(script); })();`; const hash = contentHash(code); filepath = this.resolvePath( 'dist/client/importmap', `${hash}.final.mjs` ); try { const existingContent = await fsp.readFile( filepath, 'utf-8' ); if (existingContent === code) { wrote = true; } else { wrote = await this.write(filepath, code); } } catch { wrote = await this.write(filepath, code); } this._importmapHash = wrote ? hash : ''; } if (mode === 'js' && this._importmapHash) { const src = `${basePathPlaceholder}${this.basePath}importmap/${this._importmapHash}.final.mjs`; return { src, filepath, code: `<script data-base="${basePathPlaceholder}" src="${src}"></script>` }; } if (basePathPlaceholder) { const set = (data?: Record<string, string>) => { if (!data) return; Object.entries(data).forEach(([k, v]) => { data[k] = basePathPlaceholder + v; }); }; set(importmap.imports); if (importmap.scopes) { Object.values(importmap.scopes).forEach(set); } } return { src: null, filepath: null, code: `<script type="importmap">${serialize(importmap, { isJSON: true })}</script>` }; } ); } /** * 获取模块的静态导入路径列表。 * * @param target - 构建目标('client' | 'server') * @param specifier - 模块标识符 * @returns 返回静态导入路径列表,如果未找到则返回 null * @throws {NotReadyError} 在框架实例未初始化时调用此方法会抛出错误 * * @example * ```ts * // 获取客户端入口模块的静态导入路径 * const paths = await esmx.getStaticImportPaths( * 'client', * `your-app-name/src/entry.client` * ); * ``` */ public async getStaticImportPaths( target: BuildSsrTarget, specifier: string ) { return this.readied.cache( `getStaticImportPaths-${target}-${specifier}`, async () => { const result = await getStaticImportPaths( specifier, await this.getImportMap(target), this.moduleConfig ); if (!result) { return null; } return Object.freeze(Object.values(result)); } ); } } /** * 默认的开发环境应用创建函数 * * @description * 这是一个默认的占位函数,用于在未配置开发环境应用创建函数时抛出错误。 * 实际使用时应当通过 EsmxOptions.devApp 配置实际的应用创建函数。 * * @throws {Error} 当未配置 devApp 时抛出错误,提示用户需要设置开发环境应用创建函数 * @returns {Promise<App>} 不会真正返回,总是抛出错误 * * @example * ```ts * // 正确的使用方式是在配置中提供 devApp * const options: EsmxOptions = { * devApp: async (esmx) => { * return import('@esmx/rspack').then(m => * m.createRspackHtmlApp(esmx) * ); * } * }; * ``` */ async function defaultDevApp(): Promise<App> { throw new Error("'devApp' function not set"); } /** * Esmx 框架未初始化错误 * * @description * 该错误在以下情况下抛出: * - 在调用 init() 之前访问需要初始化的方法或属性 * - 在框架未完全初始化时尝试使用核心功能 * - 在销毁实例后继续使用框架功能 * * @extends Error * * @example * ```ts * const esmx = new Esmx(); * try { * // 这会抛出 NotReadyError,因为还未初始化 * await esmx.render(); * } catch (e) { * if (e instanceof NotReadyError) { * console.error('Framework not initialized'); * } * } * ``` */ class NotReadyError extends Error { constructor() { super(`The Esmx has not been initialized yet`); } } /** * 计算内容的 SHA-256 哈希值 * * @description * 该函数用于: * - 生成文件内容的唯一标识符 * - 用于缓存失效判断 * - 生成具有内容哈希的文件名 * * 特点: * - 使用 SHA-256 算法确保哈希值的唯一性 * - 截取前 12 位以平衡唯一性和长度 * - 适用于缓存控制和文件版本管理 * * @param {string} text - 要计算哈希的文本内容 * @returns {string} 返回 12 位的十六进制哈希字符串 * * @example * ```ts * const content = 'some content'; * const hash = contentHash(content); * // 输出类似:'a1b2c3d4e5f6' * ``` */ function contentHash(text: string) { const hash = crypto.createHash('sha256'); hash.update(text); return hash.digest('hex').substring(0, 12); }