UNPKG

@esmx/core

Version:

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

1,169 lines (1,115 loc) 40 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 { createClientImportMap, createImportMap } 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'; /** * Core configuration options interface for the Esmx framework */ export interface EsmxOptions { /** * Project root directory path * - Can be absolute or relative path * - Defaults to current working directory (process.cwd()) */ root?: string; /** * Whether it is production environment * - true: Production environment * - false: Development environment * - Defaults to process.env.NODE_ENV === 'production' */ isProd?: boolean; /** * Base path placeholder configuration * - string: Custom placeholder * - false: Disable placeholder * - Default value is '[[[___ESMX_DYNAMIC_BASE___]]]' * - Used for dynamically replacing the base path of assets at runtime */ basePathPlaceholder?: string | false; /** * Module configuration options * - Used to configure module resolution rules for the project * - Includes module aliases, external dependencies, etc. */ modules?: ModuleConfig; /** * Package configuration options * - Used to package build artifacts into standard npm .tgz format packages * - Includes output path, package.json handling, packaging hooks, etc. */ packs?: PackConfig; /** * Development environment application creation function * - Only used in development environment * - Used to create application instance for development server * @param esmx Esmx instance */ devApp?: (esmx: Esmx) => Promise<App>; /** * Server startup configuration function * - Used to configure and start HTTP server * - Can be used in both development and production environments * @param esmx Esmx instance */ server?: (esmx: Esmx) => Promise<void>; /** * Post-build processing function * - Executed after project build is completed * - Can be used to perform additional resource processing, deployment, etc. * @param esmx Esmx instance */ postBuild?: (esmx: Esmx) => Promise<void>; } /** * Application build target types. * - client: Client build target, used to generate code that runs in the browser * - server: Server build target, used to generate code that runs in Node.js environment */ export type BuildEnvironment = 'client' | 'server'; /** * Command enumeration for the Esmx framework. * Used to control the runtime mode and lifecycle of the framework. */ export enum COMMAND { /** * Development mode * Starts development server with hot reload support */ dev = 'dev', /** * Build mode * Generates production build artifacts */ build = 'build', /** * Preview mode * Preview build artifacts */ preview = 'preview', /** * Start mode * Starts production environment server */ start = 'start' } export type { ImportMap, SpecifierMap, ScopesMap }; /** * Initialization status interface for Esmx framework instance * @internal For framework internal use only * * @description * This interface defines the status data after framework instance initialization, including: * - Application instance: Handles requests and rendering * - Current command: Controls runtime mode * - Module configuration: Parsed module settings * - Package configuration: Parsed build settings * - Cache handling: Framework internal caching mechanism */ interface Readied { /** Application instance, providing middleware and rendering functionality */ app: App; /** Currently executing framework command */ command: COMMAND; /** Parsed module configuration information */ moduleConfig: ParsedModuleConfig; /** Parsed package configuration information */ packConfig: ParsedPackConfig; /** Cache handler */ cache: CacheHandle; } export class Esmx { // Basic properties and constructor 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(); } /** * Get module name * @returns {string} The name of the current module, sourced from module configuration * @throws {NotReadyError} Throws error when framework instance is not initialized */ public get name(): string { return this.moduleConfig.name; } /** * Get module variable name * @returns {string} A valid JavaScript variable name generated based on the module name * @throws {NotReadyError} Throws error when framework instance is not initialized */ public get varName(): string { return '__' + this.name.replace(/[^a-zA-Z]/g, '_') + '__'; } /** * Get the absolute path of the project root directory * @returns {string} The absolute path of the project root directory * If the configured root is a relative path, it is resolved to an absolute path based on the current working directory */ public get root(): string { const { root = cwd() } = this._options; if (path.isAbsolute(root)) { return root; } return path.resolve(cwd(), root); } /** * Determine if currently in production environment * @returns {boolean} Environment flag * Prioritizes the isProd in configuration, if not configured, judges based on process.env.NODE_ENV */ public get isProd(): boolean { return this._options?.isProd ?? process.env.NODE_ENV === 'production'; } /** * Get the base path of the module * @returns {string} The base path of the module starting and ending with a slash * Used to construct the access path for module assets */ public get basePath(): string { return `/${this.name}/`; } /** * Get the base path placeholder * @returns {string} Base path placeholder or empty string * Used for dynamically replacing the base path of the module at runtime, can be disabled through configuration */ public get basePathPlaceholder(): string { const varName = this._options.basePathPlaceholder; if (varName === false) { return ''; } return varName ?? '[[[___ESMX_DYNAMIC_BASE___]]]'; } /** * Get the currently executing command * @returns {COMMAND} The command enumeration value currently being executed * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized */ public get command(): COMMAND { return this.readied.command; } /** * Get the command enumeration type * @returns {typeof COMMAND} Command enumeration type definition */ public get COMMAND(): typeof COMMAND { return COMMAND; } /** * Get module configuration information * @returns {ParsedModuleConfig} Complete configuration information of the current module */ public get moduleConfig(): ParsedModuleConfig { return this.readied.moduleConfig; } /** * Get package configuration information * @returns {ParsedPackConfig} Package-related configuration of the current module */ public get packConfig(): ParsedPackConfig { return this.readied.packConfig; } /** * Get the static asset processing middleware for the application. * * This middleware is responsible for handling static asset requests for the application, * providing different implementations based on the runtime environment: * - Development environment: Supports real-time compilation and hot reloading of source code, uses no-cache strategy * - Production environment: Handles built static assets, supports long-term caching for immutable files * * @returns {Middleware} Returns the static asset processing middleware function * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * const server = http.createServer((req, res) => { * // Use middleware to handle static asset requests * 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; } /** * Get the server-side rendering function for the application. * * This function is responsible for executing server-side rendering, * providing different implementations based on the runtime environment: * - Development environment: Loads server entry file from source code, supports hot reloading and real-time preview * - Production environment: Loads built server entry file, provides optimized rendering performance * * @returns {(options?: RenderContextOptions) => Promise<RenderContext>} Returns the server-side rendering function * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // Basic usage * const rc = await esmx.render({ * params: { url: req.url } * }); * res.end(rc.html); * * // Advanced configuration * const rc = await esmx.render({ * base: '', // Set base path * importmapMode: 'inline', // Set import map mode * entryName: 'default', // Specify render entry * 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; } /** * Initialize the Esmx framework instance. * * This method executes the following core initialization process: * 1. Parse project configuration (package.json, module configuration, package configuration, etc.) * 2. Create application instance (development or production environment) * 3. Execute corresponding lifecycle methods based on the command * * @param command - Framework running command * - dev: Start development server with hot reload support * - build: Build production artifacts * - preview: Preview build artifacts * - start: Start production environment server * * @returns Returns true for successful initialization * @throws {Error} Throws error when initializing repeatedly * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // Development environment configuration * async devApp(esmx) { * return import('@esmx/rspack').then((m) => * m.createRspackHtmlApp(esmx, { * config(context) { * // Custom Rspack configuration * } * }) * ); * }, * * // HTTP server configuration * async server(esmx) { * const server = http.createServer((req, res) => { * // Static file handling * esmx.middleware(req, res, async () => { * // Pass rendering parameters * const render = await esmx.render({ * params: { url: req.url } * }); * // Respond with HTML content * res.end(render.html); * }); * }); * * // Listen to port * 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; } /** * Destroy the Esmx framework instance, performing resource cleanup and connection closing operations. * * This method is mainly used for resource cleanup in development environment, including: * - Closing development servers (such as Rspack Dev Server) * - Cleaning up temporary files and cache * - Releasing system resources * * Note: In general, the framework automatically handles resource release, users do not need to manually call this method. * Only use it when custom resource cleanup logic is needed. * * @returns Returns a Promise that resolves to a boolean value * - true: Cleanup successful or no cleanup needed * - false: Cleanup failed * * @example * ```ts * // Use when custom cleanup logic is needed * process.once('SIGTERM', async () => { * await esmx.destroy(); // Clean up resources * process.exit(0); * }); * ``` */ public async destroy(): Promise<boolean> { const { readied } = this; if (readied.app?.destroy) { return readied.app.destroy(); } return true; } /** * Execute the application's build process. * * This method is responsible for executing the entire application build process, including: * - Compiling source code * - Generating production build artifacts * - Optimizing and compressing code * - Generating asset manifests * * The build process prints start and end times, as well as total duration and other information. * * @returns Returns a Promise that resolves to a boolean value * - true: Build successful or build method not implemented * - false: Build failed * * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // Development environment configuration * async devApp(esmx) { * return import('@esmx/rspack').then((m) => * m.createRspackHtmlApp(esmx, { * config(context) { * // Custom Rspack configuration * } * }) * ); * }, * * // Post-build processing * async postBuild(esmx) { * // Generate static HTML after build completion * 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; } /** * Start HTTP server and configure server instance. * * This method is called in the following lifecycle of the framework: * - Development environment (dev): Start development server, providing features like hot reload * - Production environment (start): Start production server, providing production-grade performance * * The specific implementation of the server is provided by the user through the server configuration function in EsmxOptions. * This function is responsible for: * - Creating HTTP server instance * - Configuring middleware and routes * - Handling requests and responses * - Starting server listening * * @returns Returns a Promise that resolves when the server startup is complete * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // entry.node.ts * import http from 'node:http'; * import type { EsmxOptions } from '@esmx/core'; * * export default { * // Server configuration * async server(esmx) { * const server = http.createServer((req, res) => { * // Handle static assets * esmx.middleware(req, res, async () => { * // Server-side rendering * const render = await esmx.render({ * params: { url: req.url } * }); * res.end(render.html); * }); * }); * * // Start server * server.listen(3000, () => { * console.log('Server running at http://localhost:3000'); * }); * } * } satisfies EsmxOptions; * ``` */ public async server(): Promise<void> { await this._options?.server?.(this); } /** * Execute post-build processing logic. * * This method is called after the application build is completed, used to perform additional resource processing, such as: * - Generating static HTML files * - Processing build artifacts * - Executing deployment tasks * - Sending build notifications * * The method automatically captures and handles exceptions during execution, ensuring it does not affect the main build process. * * @returns Returns a Promise that resolves to a boolean value * - true: Post-processing successful or no processing needed * - false: Post-processing failed * * @example * ```ts * // entry.node.ts * import type { EsmxOptions } from '@esmx/core'; * * export default { * // Post-build processing * async postBuild(esmx) { * // Generate static HTML for multiple pages * const pages = ['/', '/about', '/404']; * * for (const url of pages) { * const render = await esmx.render({ * params: { url } * }); * * // Write static HTML file * 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; } } /** * Resolve project relative path to absolute path * * @param projectPath - Project path type, such as 'dist/client', 'dist/server', etc. * @param args - Path segments to be concatenated * @returns Resolved absolute path * * @example * ```ts * // Used in entry.node.ts * async postBuild(esmx) { * const outputPath = esmx.resolvePath('dist/client', 'index.html'); * // Output: /project/root/dist/client/index.html * } * ``` */ public resolvePath(projectPath: ProjectPath, ...args: string[]): string { return resolvePath(this.root, projectPath, ...args); } /** * Write file content synchronously * * @param filepath - Absolute path of the file * @param data - Data to be written, can be string, Buffer or object * @returns Whether the write was successful * * @example * ```ts * // Used in 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 { // Ensure the target directory exists fs.mkdirSync(path.dirname(filepath), { recursive: true }); // Write file fs.writeFileSync(filepath, data); return true; } catch { return false; } } /** * Write file content asynchronously * * @param filepath - Absolute path of the file * @param data - Data to be written, can be string, Buffer or object * @returns Promise<boolean> Whether the write was successful * * @example * ```ts * // Used in 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 { // Ensure the target directory exists await fsp.mkdir(path.dirname(filepath), { recursive: true }); // Write file await fsp.writeFile(filepath, data); return true; } catch { return false; } } /** * Read and parse JSON file synchronously * * @template T - Expected JSON object type to return * @param filename - Absolute path of the JSON file * @returns {T} Parsed JSON object * @throws Throws exception when file does not exist or JSON format is incorrect * * @example * ```ts * // Used in entry.node.ts * async server(esmx) { * const manifest = esmx.readJsonSync<Manifest>(esmx.resolvePath('dist/client', 'manifest.json')); * // Use manifest object * } * ``` */ public readJsonSync<T = any>(filename: string): T { return JSON.parse(fs.readFileSync(filename, 'utf-8')); } /** * Read and parse JSON file asynchronously * * @template T - Expected JSON object type to return * @param filename - Absolute path of the JSON file * @returns {Promise<T>} Parsed JSON object * @throws Throws exception when file does not exist or JSON format is incorrect * * @example * ```ts * // Used in entry.node.ts * async server(esmx) { * const manifest = await esmx.readJson<Manifest>(esmx.resolvePath('dist/client', 'manifest.json')); * // Use manifest object * } * ``` */ public async readJson<T = any>(filename: string): Promise<T> { return JSON.parse(await fsp.readFile(filename, 'utf-8')); } /** * Get build manifest list * * @description * This method is used to get the build manifest list for the specified target environment, including the following features: * 1. **Cache Management** * - Uses internal caching mechanism to avoid repeated loading * - Returns immutable manifest list * * 2. **Environment Adaptation** * - Supports both client and server environments * - Returns corresponding manifest information based on the target environment * * 3. **Module Mapping** * - Contains module export information * - Records resource dependency relationships * * @param env - Target environment type * - 'client': Client environment * - 'server': Server environment * @returns Returns read-only build manifest list * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // Used in entry.node.ts * async server(esmx) { * // Get client build manifest * const manifests = await esmx.getManifestList('client'); * * // Find build information for a specific module * 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( env: BuildEnvironment ): Promise<readonly ManifestJson[]> { return this.readied.cache(`getManifestList-${env}`, async () => Object.freeze(await getManifestList(env, this.moduleConfig)) ); } /** * Get import map object * * @description * This method is used to generate ES module import maps with the following features: * 1. **Module Resolution** * - Generate module mappings based on build manifests * - Support both client and server environments * - Automatically handle module path resolution * * 2. **Cache Optimization** * - Use internal caching mechanism * - Return immutable mapping objects * * 3. **Path Handling** * - Automatically handle module paths * - Support dynamic base paths * * @param env - Target environment type * - 'client': Generate import map for browser environment * - 'server': Generate import map for server environment * @returns Returns read-only import map object * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // Used in entry.node.ts * async server(esmx) { * // Get client import map * const importmap = await esmx.getImportMap('client'); * * // Custom HTML template * const html = ` * <!DOCTYPE html> * <html> * <head> * <script type="importmap"> * ${JSON.stringify(importmap)} * </script> * </head> * <body> * <!-- Page content --> * </body> * </html> * `; * } * ``` */ public async getImportMap( env: BuildEnvironment ): Promise<Readonly<ImportMap>> { return this.readied.cache(`getImportMap-${env}`, async () => { const { moduleConfig } = this.readied; const manifests = await this.getManifestList(env); let json: ImportMap = {}; switch (env) { case 'client': { json = createClientImportMap({ manifests, getScope(name, scope) { return `/${name}${scope}`; }, getFile(name, file) { return `/${name}/${file}`; } }); break; } case 'server': json = createImportMap({ manifests, getScope: (name: string, scope: 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, scope)) .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); }); } /** * Get client import map information * * @description * This method is used to generate import map code for client environment, supporting two modes: * 1. **Inline Mode (inline)** * - Inline import map directly into HTML * - Reduce additional network requests * - Suitable for scenarios with smaller import maps * * 2. **JS File Mode (js)** * - Generate standalone JS file * - Support browser caching * - Suitable for scenarios with larger import maps * * Core Features: * - Automatically handle dynamic base paths * - Support module path runtime replacement * - Optimize caching strategy * - Ensure module loading order * * @param mode - Import map mode * - 'inline': Inline mode, returns HTML script tag * - 'js': JS file mode, returns information with file path * @returns Returns import map related information * - src: URL of the JS file (only in js mode) * - filepath: Local path of the JS file (only in js mode) * - code: HTML script tag content * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // Used in entry.node.ts * async server(esmx) { * const server = express(); * server.use(esmx.middleware); * * server.get('*', async (req, res) => { * // Use JS file mode * const result = await esmx.render({ * importmapMode: 'js', * params: { url: req.url } * }); * res.send(result.html); * }); * * // Or use inline mode * 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, unsafe: true })}</script>` }; } ); } /** * Get the list of static import paths for a module. * * @param env - Build target ('client' | 'server') * @param specifier - Module specifier * @returns Returns the list of static import paths, returns null if not found * @throws {NotReadyError} Throws error when calling this method if the framework instance is not initialized * * @example * ```ts * // Get static import paths for client entry module * const paths = await esmx.getStaticImportPaths( * 'client', * `your-app-name/src/entry.client` * ); * ``` */ public async getStaticImportPaths( env: BuildEnvironment, specifier: string ) { return this.readied.cache( `getStaticImportPaths-${env}-${specifier}`, async () => { const result = await getStaticImportPaths( specifier, await this.getImportMap(env), this.moduleConfig ); if (!result) { return null; } return Object.freeze(Object.values(result)); } ); } } /** * Default development environment application creation function * * @description * This is a default placeholder function that throws an error when the development environment application creation function is not configured. * In actual use, the actual application creation function should be configured through EsmxOptions.devApp. * * @throws {Error} Throws an error when devApp is not configured, prompting the user to set up the development environment application creation function * @returns {Promise<App>} Will not actually return, always throws an error * * @example * ```ts * // Correct usage is to provide devApp in the configuration * 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 framework not initialized error * * @description * This error is thrown in the following situations: * - Accessing methods or properties that require initialization before calling init() * - Attempting to use core functionality when the framework is not fully initialized * - Continuing to use framework functionality after destroying the instance * * @extends Error * * @example * ```ts * const esmx = new Esmx(); * try { * // This will throw NotReadyError because it's not initialized yet * 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`); } } /** * Calculate SHA-256 hash value of content * * @description * This function is used for: * - Generating unique identifiers for file content * - Cache invalidation judgment * - Generating filenames with content hash * * Features: * - Uses SHA-256 algorithm to ensure hash uniqueness * - Truncates to first 12 characters to balance uniqueness and length * - Suitable for cache control and file version management * * @param {string} text - Text content to calculate hash for * @returns {string} Returns 12-character hexadecimal hash string * * @example * ```ts * const content = 'some content'; * const hash = contentHash(content); * // Output similar to: 'a1b2c3d4e5f6' * ``` */ function contentHash(text: string) { const hash = crypto.createHash('sha256'); hash.update(text); return hash.digest('hex').substring(0, 12); }