feat(editor-core): 添加 UserCodeService 就绪信号机制
- 新增 waitForReady()/signalReady() API - 支持等待用户脚本编译完成 - 解决场景加载时组件未注册的时序问题
This commit is contained in:
@@ -71,16 +71,14 @@ export interface UserCodeCompileOptions {
|
|||||||
sourceMap?: boolean;
|
sourceMap?: boolean;
|
||||||
/** Whether to minify output | 是否压缩输出 */
|
/** Whether to minify output | 是否压缩输出 */
|
||||||
minify?: boolean;
|
minify?: boolean;
|
||||||
/** Output format | 输出格式 */
|
/** Output format (default: 'esm') | 输出格式(默认:'esm')*/
|
||||||
format?: 'esm' | 'iife';
|
format?: 'esm' | 'iife';
|
||||||
/**
|
/**
|
||||||
* SDK modules for shim generation.
|
* SDK modules information (reserved for future use).
|
||||||
* 用于生成 shim 的 SDK 模块列表。
|
* SDK 模块信息(保留供将来使用)。
|
||||||
*
|
*
|
||||||
* If provided, shims will be created for these modules.
|
* Currently SDK is handled via external dependencies and global variable.
|
||||||
* Typically obtained from RuntimeResolver.getAvailableModules().
|
* 当前 SDK 通过外部依赖和全局变量处理。
|
||||||
* 如果提供,将为这些模块创建 shim。
|
|
||||||
* 通常从 RuntimeResolver.getAvailableModules() 获取。
|
|
||||||
*/
|
*/
|
||||||
sdkModules?: SDKModuleInfo[];
|
sdkModules?: SDKModuleInfo[];
|
||||||
}
|
}
|
||||||
@@ -382,6 +380,37 @@ export interface IUserCodeService {
|
|||||||
* 检查是否正在监视。
|
* 检查是否正在监视。
|
||||||
*/
|
*/
|
||||||
isWatching(): boolean;
|
isWatching(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for user code to be ready (compiled and loaded).
|
||||||
|
* 等待用户代码准备就绪(已编译并加载)。
|
||||||
|
*
|
||||||
|
* This method is used to synchronize scene loading with user code compilation.
|
||||||
|
* Call this before loading a scene to ensure user components are registered.
|
||||||
|
* 此方法用于同步场景加载与用户代码编译。
|
||||||
|
* 在加载场景之前调用此方法以确保用户组件已注册。
|
||||||
|
*
|
||||||
|
* @returns Promise that resolves when user code is ready | 当用户代码就绪时解决的 Promise
|
||||||
|
*/
|
||||||
|
waitForReady(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that user code is ready.
|
||||||
|
* 发出用户代码就绪信号。
|
||||||
|
*
|
||||||
|
* Called after user code compilation and registration is complete.
|
||||||
|
* 在用户代码编译和注册完成后调用。
|
||||||
|
*/
|
||||||
|
signalReady(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the ready state (for project switching).
|
||||||
|
* 重置就绪状态(用于项目切换)。
|
||||||
|
*
|
||||||
|
* Called when opening a new project to reset the ready promise.
|
||||||
|
* 打开新项目时调用以重置就绪 Promise。
|
||||||
|
*/
|
||||||
|
resetReady(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { EditorConfig } from '../../Config';
|
import { EditorConfig } from '../../Config';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
createLogger,
|
createLogger,
|
||||||
PlatformDetector,
|
PlatformDetector,
|
||||||
ComponentRegistry as CoreComponentRegistry,
|
GlobalComponentRegistry as CoreComponentRegistry,
|
||||||
COMPONENT_TYPE_NAME,
|
COMPONENT_TYPE_NAME,
|
||||||
SYSTEM_TYPE_NAME
|
SYSTEM_TYPE_NAME
|
||||||
} from '@esengine/ecs-framework';
|
} from '@esengine/ecs-framework';
|
||||||
@@ -82,9 +82,27 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
*/
|
*/
|
||||||
private _hotReloadCoordinator: HotReloadCoordinator;
|
private _hotReloadCoordinator: HotReloadCoordinator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就绪状态 Promise
|
||||||
|
* Ready state promise
|
||||||
|
*/
|
||||||
|
private _readyPromise: Promise<void>;
|
||||||
|
private _readyResolve: (() => void) | undefined;
|
||||||
|
|
||||||
constructor(fileSystem: IFileSystem) {
|
constructor(fileSystem: IFileSystem) {
|
||||||
this._fileSystem = fileSystem;
|
this._fileSystem = fileSystem;
|
||||||
this._hotReloadCoordinator = new HotReloadCoordinator();
|
this._hotReloadCoordinator = new HotReloadCoordinator();
|
||||||
|
this._readyPromise = this._createReadyPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ready promise.
|
||||||
|
* 创建新的就绪 Promise。
|
||||||
|
*/
|
||||||
|
private _createReadyPromise(): Promise<void> {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
this._readyResolve = resolve;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,28 +208,20 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
const entryPath = `${outputDir}${sep}_entry_${options.target}.ts`;
|
const entryPath = `${outputDir}${sep}_entry_${options.target}.ts`;
|
||||||
await this._fileSystem.writeFile(entryPath, entryContent);
|
await this._fileSystem.writeFile(entryPath, entryContent);
|
||||||
|
|
||||||
// Create shim files for framework dependencies | 创建框架依赖的 shim 文件
|
// Get external dependencies | 获取外部依赖
|
||||||
// Returns mapping from package name to shim path
|
// SDK marked as external, resolved from global variable at runtime
|
||||||
// 返回包名到 shim 路径的映射
|
// SDK 标记为外部依赖,运行时从全局变量解析
|
||||||
const alias = await this._createDependencyShims(outputDir, options.sdkModules);
|
const external = this._getExternalDependencies(options.target, options.sdkModules);
|
||||||
|
|
||||||
// Determine global name for IIFE output | 确定 IIFE 输出的全局名称
|
|
||||||
const globalName = options.target === UserCodeTarget.Runtime
|
|
||||||
? EditorConfig.globals.userRuntimeExports
|
|
||||||
: EditorConfig.globals.userEditorExports;
|
|
||||||
|
|
||||||
// Compile using esbuild (via Tauri command or direct) | 使用 esbuild 编译
|
// Compile using esbuild (via Tauri command or direct) | 使用 esbuild 编译
|
||||||
// Use IIFE format to avoid ES module import issues in Tauri
|
// Use ESM format for dynamic import() loading | 使用 ESM 格式以支持动态 import() 加载
|
||||||
// 使用 IIFE 格式以避免 Tauri 中的 ES 模块导入问题
|
|
||||||
const compileResult = await this._runEsbuild({
|
const compileResult = await this._runEsbuild({
|
||||||
entryPath,
|
entryPath,
|
||||||
outputPath,
|
outputPath,
|
||||||
format: 'iife', // Always use IIFE for Tauri compatibility | 始终使用 IIFE 以兼容 Tauri
|
format: 'esm', // ESM for standard dynamic import() | ESM 用于标准动态 import()
|
||||||
globalName,
|
|
||||||
sourceMap: options.sourceMap ?? true,
|
sourceMap: options.sourceMap ?? true,
|
||||||
minify: options.minify ?? false,
|
minify: options.minify ?? false,
|
||||||
external: [], // Don't use external, use alias instead | 不使用 external,使用 alias
|
external,
|
||||||
alias,
|
|
||||||
projectRoot: options.projectPath
|
projectRoot: options.projectPath
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,6 +269,14 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
* Load compiled user code module.
|
* Load compiled user code module.
|
||||||
* 加载编译后的用户代码模块。
|
* 加载编译后的用户代码模块。
|
||||||
*
|
*
|
||||||
|
* Uses Blob URL for ESM dynamic import in Tauri environment.
|
||||||
|
* 在 Tauri 环境中使用 Blob URL 进行 ESM 动态导入。
|
||||||
|
*
|
||||||
|
* Note: Browser's import() only supports http://, https://, and blob:// protocols.
|
||||||
|
* Custom protocols like project:// are not supported for ESM imports.
|
||||||
|
* 注意:浏览器的 import() 只支持 http://、https:// 和 blob:// 协议。
|
||||||
|
* 自定义协议如 project:// 不支持 ESM 导入。
|
||||||
|
*
|
||||||
* @param modulePath - Path to compiled JS file | 编译后的 JS 文件路径
|
* @param modulePath - Path to compiled JS file | 编译后的 JS 文件路径
|
||||||
* @param target - Target environment | 目标环境
|
* @param target - Target environment | 目标环境
|
||||||
* @returns Loaded module | 加载的模块
|
* @returns Loaded module | 加载的模块
|
||||||
@@ -268,20 +286,23 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
let moduleExports: Record<string, any>;
|
let moduleExports: Record<string, any>;
|
||||||
|
|
||||||
if (PlatformDetector.isTauriEnvironment()) {
|
if (PlatformDetector.isTauriEnvironment()) {
|
||||||
// In Tauri, read file content and execute via script tag
|
// Read file content via Tauri and load via Blob URL
|
||||||
// 在 Tauri 中,读取文件内容并通过 script 标签执行
|
// 通过 Tauri 读取文件内容并通过 Blob URL 加载
|
||||||
// This avoids CORS and module resolution issues
|
// Browser's import() doesn't support custom protocols like project://
|
||||||
// 这避免了 CORS 和模块解析问题
|
// 浏览器的 import() 不支持自定义协议如 project://
|
||||||
const { invoke } = await import('@tauri-apps/api/core');
|
const { invoke } = await import('@tauri-apps/api/core');
|
||||||
|
|
||||||
const content = await invoke<string>('read_file_content', {
|
const content = await invoke<string>('read_file_content', {
|
||||||
path: modulePath
|
path: modulePath
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`Loading module via script injection`, { originalPath: modulePath });
|
logger.debug(`Loading ESM module via Blob URL`, {
|
||||||
|
path: modulePath,
|
||||||
|
contentLength: content.length
|
||||||
|
});
|
||||||
|
|
||||||
// Execute module code and capture exports | 执行模块代码并捕获导出
|
// Load ESM via Blob URL | 通过 Blob URL 加载 ESM
|
||||||
moduleExports = await this._executeModuleCode(content, target);
|
moduleExports = await this._loadESMFromContent(content);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to file:// for non-Tauri environments
|
// Fallback to file:// for non-Tauri environments
|
||||||
// 非 Tauri 环境使用 file://
|
// 非 Tauri 环境使用 file://
|
||||||
@@ -924,6 +945,35 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
return this._watching;
|
return this._watching;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for user code to be ready (compiled and loaded).
|
||||||
|
* 等待用户代码准备就绪(已编译并加载)。
|
||||||
|
*
|
||||||
|
* @returns Promise that resolves when user code is ready | 当用户代码就绪时解决的 Promise
|
||||||
|
*/
|
||||||
|
waitForReady(): Promise<void> {
|
||||||
|
return this._readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that user code is ready.
|
||||||
|
* 发出用户代码就绪信号。
|
||||||
|
*/
|
||||||
|
signalReady(): void {
|
||||||
|
if (this._readyResolve) {
|
||||||
|
this._readyResolve();
|
||||||
|
this._readyResolve = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the ready state (for project switching).
|
||||||
|
* 重置就绪状态(用于项目切换)。
|
||||||
|
*/
|
||||||
|
resetReady(): void {
|
||||||
|
this._readyPromise = this._createReadyPromise();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose service resources.
|
* Dispose service resources.
|
||||||
* 释放服务资源。
|
* 释放服务资源。
|
||||||
@@ -1058,44 +1108,6 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create shim file that maps SDK global variable to module import.
|
|
||||||
* 创建将 SDK 全局变量映射到模块导入的 shim 文件。
|
|
||||||
*
|
|
||||||
* This is used for IIFE format to resolve external dependencies.
|
|
||||||
* Creates a single shim for @esengine/sdk.
|
|
||||||
* 这用于 IIFE 格式解析外部依赖。
|
|
||||||
* 只创建一个 @esengine/sdk 的 shim。
|
|
||||||
*
|
|
||||||
* @param outputDir - Output directory | 输出目录
|
|
||||||
* @param _sdkModules - Deprecated, not used | 已废弃,不再使用
|
|
||||||
* @returns Mapping from package name to shim path | 包名到 shim 路径的映射
|
|
||||||
*/
|
|
||||||
private async _createDependencyShims(
|
|
||||||
outputDir: string,
|
|
||||||
_sdkModules?: SDKModuleInfo[]
|
|
||||||
): Promise<Record<string, string>> {
|
|
||||||
const sep = outputDir.includes('\\') ? '\\' : '/';
|
|
||||||
const sdkGlobalName = EditorConfig.globals.sdk;
|
|
||||||
|
|
||||||
// Create single SDK shim
|
|
||||||
// 创建单一 SDK shim
|
|
||||||
const shimPath = `${outputDir}${sep}_shim_sdk.js`;
|
|
||||||
const shimContent = `// Shim for @esengine/sdk
|
|
||||||
// Maps to window.${sdkGlobalName}
|
|
||||||
// User code imports from '@esengine/sdk' will use this shim
|
|
||||||
module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName}) || {};
|
|
||||||
`;
|
|
||||||
await this._fileSystem.writeFile(shimPath, shimContent);
|
|
||||||
const normalizedPath = shimPath.replace(/\\/g, '/');
|
|
||||||
|
|
||||||
logger.info('Created SDK shim', { path: normalizedPath });
|
|
||||||
|
|
||||||
return {
|
|
||||||
'@esengine/sdk': normalizedPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get external dependencies that should not be bundled.
|
* Get external dependencies that should not be bundled.
|
||||||
* 获取不应打包的外部依赖。
|
* 获取不应打包的外部依赖。
|
||||||
@@ -1122,16 +1134,24 @@ module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName}) || {
|
|||||||
*
|
*
|
||||||
* Uses Tauri command to invoke esbuild CLI.
|
* Uses Tauri command to invoke esbuild CLI.
|
||||||
* 使用 Tauri 命令调用 esbuild CLI。
|
* 使用 Tauri 命令调用 esbuild CLI。
|
||||||
|
*
|
||||||
|
* @param options - Compilation options | 编译选项
|
||||||
|
* @returns Compilation result | 编译结果
|
||||||
*/
|
*/
|
||||||
private async _runEsbuild(options: {
|
private async _runEsbuild(options: {
|
||||||
|
/** Entry file path | 入口文件路径 */
|
||||||
entryPath: string;
|
entryPath: string;
|
||||||
|
/** Output file path | 输出文件路径 */
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
|
/** Output format (ESM for dynamic import) | 输出格式(ESM 用于动态导入)*/
|
||||||
format: 'esm' | 'iife';
|
format: 'esm' | 'iife';
|
||||||
globalName?: string;
|
/** Generate source maps | 生成源码映射 */
|
||||||
sourceMap: boolean;
|
sourceMap: boolean;
|
||||||
|
/** Minify output | 压缩输出 */
|
||||||
minify: boolean;
|
minify: boolean;
|
||||||
|
/** External dependencies (not bundled) | 外部依赖(不打包)*/
|
||||||
external: string[];
|
external: string[];
|
||||||
alias?: Record<string, string>;
|
/** Project root for resolving paths | 项目根路径用于解析路径 */
|
||||||
projectRoot: string;
|
projectRoot: string;
|
||||||
}): Promise<{ success: boolean; errors: CompileError[] }> {
|
}): Promise<{ success: boolean; errors: CompileError[] }> {
|
||||||
try {
|
try {
|
||||||
@@ -1143,13 +1163,9 @@ module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName}) || {
|
|||||||
entry: options.entryPath,
|
entry: options.entryPath,
|
||||||
output: options.outputPath,
|
output: options.outputPath,
|
||||||
format: options.format,
|
format: options.format,
|
||||||
aliasCount: options.alias ? Object.keys(options.alias).length : 0
|
external: options.external
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.alias) {
|
|
||||||
logger.debug('esbuild alias mappings:', options.alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Tauri command | 使用 Tauri 命令
|
// Use Tauri command | 使用 Tauri 命令
|
||||||
const { invoke } = await import('@tauri-apps/api/core');
|
const { invoke } = await import('@tauri-apps/api/core');
|
||||||
|
|
||||||
@@ -1167,11 +1183,9 @@ module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName}) || {
|
|||||||
entryPath: options.entryPath,
|
entryPath: options.entryPath,
|
||||||
outputPath: options.outputPath,
|
outputPath: options.outputPath,
|
||||||
format: options.format,
|
format: options.format,
|
||||||
globalName: options.globalName,
|
|
||||||
sourceMap: options.sourceMap,
|
sourceMap: options.sourceMap,
|
||||||
minify: options.minify,
|
minify: options.minify,
|
||||||
external: options.external,
|
external: options.external,
|
||||||
alias: options.alias,
|
|
||||||
projectRoot: options.projectRoot
|
projectRoot: options.projectRoot
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1206,52 +1220,30 @@ module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName}) || {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute compiled module code and return exports.
|
* Load ESM module from JavaScript content string.
|
||||||
* 执行编译后的模块代码并返回导出。
|
* 从 JavaScript 内容字符串加载 ESM 模块。
|
||||||
*
|
*
|
||||||
* The code should be in IIFE format that sets a global variable.
|
* Uses Blob URL to enable dynamic import() of ESM content.
|
||||||
* 代码应该是设置全局变量的 IIFE 格式。
|
* 使用 Blob URL 实现 ESM 内容的动态 import()。
|
||||||
*
|
*
|
||||||
* @param code - Compiled JavaScript code | 编译后的 JavaScript 代码
|
* @param content - JavaScript module content (ESM format) | JavaScript 模块内容(ESM 格式)
|
||||||
* @param target - Target environment | 目标环境
|
|
||||||
* @returns Module exports | 模块导出
|
* @returns Module exports | 模块导出
|
||||||
*/
|
*/
|
||||||
private async _executeModuleCode(
|
private async _loadESMFromContent(content: string): Promise<Record<string, any>> {
|
||||||
code: string,
|
// Create Blob URL for ESM module | 为 ESM 模块创建 Blob URL
|
||||||
target: UserCodeTarget
|
const blob = new Blob([content], { type: 'application/javascript' });
|
||||||
): Promise<Record<string, any>> {
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
// Determine global name based on target | 根据目标确定全局名称
|
|
||||||
const globalName = target === UserCodeTarget.Runtime
|
|
||||||
? EditorConfig.globals.userRuntimeExports
|
|
||||||
: EditorConfig.globals.userEditorExports;
|
|
||||||
|
|
||||||
// Clear any previous exports | 清除之前的导出
|
|
||||||
(window as any)[globalName] = undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// esbuild generates: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
// Dynamic import the ESM module | 动态导入 ESM 模块
|
||||||
// When executed via new Function(), var declarations stay in function scope
|
const moduleExports = await import(/* @vite-ignore */ blobUrl);
|
||||||
// We need to replace "var globalName" with "window.globalName" to expose it
|
|
||||||
// esbuild 生成: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
|
||||||
// 通过 new Function() 执行时,var 声明在函数作用域内
|
|
||||||
// 需要替换 "var globalName" 为 "window.globalName" 以暴露到全局
|
|
||||||
const modifiedCode = code.replace(
|
|
||||||
new RegExp(`^"use strict";\\s*var ${globalName}`, 'm'),
|
|
||||||
`"use strict";\nwindow.${globalName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the IIFE code | 执行 IIFE 代码
|
// Return all exports | 返回所有导出
|
||||||
// eslint-disable-next-line no-new-func
|
return { ...moduleExports };
|
||||||
const executeScript = new Function(modifiedCode);
|
} finally {
|
||||||
executeScript();
|
// Always revoke Blob URL to prevent memory leaks
|
||||||
|
// 始终撤销 Blob URL 以防止内存泄漏
|
||||||
// Get exports from global | 从全局获取导出
|
URL.revokeObjectURL(blobUrl);
|
||||||
const exports = (window as any)[globalName] || {};
|
|
||||||
|
|
||||||
return exports;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to execute user code | 执行用户代码失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,10 @@
|
|||||||
* ↓
|
* ↓
|
||||||
* [UserCodeService.scan()] - Discovers all scripts
|
* [UserCodeService.scan()] - Discovers all scripts
|
||||||
* ↓
|
* ↓
|
||||||
* [UserCodeService.compile()] - Compiles to JS using esbuild
|
* [UserCodeService.compile()] - Compiles to ESM using esbuild
|
||||||
|
* (@esengine/sdk marked as external)
|
||||||
* ↓
|
* ↓
|
||||||
* [UserCodeService.load()] - Loads compiled module
|
* [UserCodeService.load()] - Loads via project:// protocol + import()
|
||||||
* ↓
|
* ↓
|
||||||
* [registerComponents()] - Registers with ECS runtime
|
* [registerComponents()] - Registers with ECS runtime
|
||||||
* [registerEditorExtensions()] - Registers inspectors/gizmos
|
* [registerEditorExtensions()] - Registers inspectors/gizmos
|
||||||
@@ -53,6 +54,16 @@
|
|||||||
* [UserCodeService.watch()] - Hot reload on file changes
|
* [UserCodeService.watch()] - Hot reload on file changes
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
* # Architecture | 架构
|
||||||
|
*
|
||||||
|
* - **Compilation**: ESM format with `external: ['@esengine/sdk']`
|
||||||
|
* - **Loading**: Reads file via Tauri, loads via Blob URL + import()
|
||||||
|
* - **Runtime**: SDK accessed via `window.__ESENGINE_SDK__` global
|
||||||
|
* - **Hot Reload**: File watching via Rust backend + Tauri events
|
||||||
|
*
|
||||||
|
* Note: Browser's import() only supports http/https/blob protocols.
|
||||||
|
* Custom protocols like project:// are not supported for ESM imports.
|
||||||
|
*
|
||||||
* # Example User Component | 用户组件示例
|
* # Example User Component | 用户组件示例
|
||||||
*
|
*
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
|||||||
Reference in New Issue
Block a user