Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ All notable changes to evjs are documented here. Releases follow [Semantic Versi

## [Unreleased]

### Features

- Let plugins declare browser module references through `emit.moduleReference()`.
Core validates and watches the references, and Utoopack replaces the original
browser modules with runtime factory results while retaining server source.
- Expose the generated application's frozen `apiRuntimeScope` so integration
runtimes can use the same application and build identity as the API client.

### Dependencies

- Require `@utoo/pack` 1.6.0 or newer in the default bundler adapter and use its
published module-reference configuration types. Browser module references
continue to validate the capability of the loaded native binding.

### Bug Fixes

- Refresh Utoopack development HTML and asset metadata after client rebuilds
Expand Down
57 changes: 57 additions & 0 deletions docs/docs/generated-contributions.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,63 @@ Contribution ids are local to the plugin. In `emitPageIR()`, ids are also local
to the current page, so the same id can be reused safely for every enabled
page. The `@evjs/` prefix is reserved by the framework.

## Replace a browser module with runtime references

`emit.moduleReference()` declares a browser-only replacement for one exact source
file. It returns nothing and does not attach to a slot. The plugin supplies the
export mapping and browser-safe data; the runtime factory decides what each
reference does. evjs does not parse decorators or interpret protocols.

```ts
import { definePlugin } from "@evjs/ev/plugin";

export const references = definePlugin({
id: "greeting-references",
emitIR({ emit }) {
emit.moduleReference({
id: "greeting",
resource: "./src/greeting.ts",
references: [{
id: "greeting",
factory: {
module: "./src/greeting-client.ts",
export: "createGreetingReference",
},
data: { endpoint: "/greetings" },
}],
exports: [
{ name: "Greeting", reference: "greeting" },
{ name: "default", reference: "greeting" },
],
});
},
});
```

The browser receives the result of `createGreetingReference(data)` for both
exports. If the factory returns a constructor, application code can keep
`import { Greeting } from "./greeting"; new Greeting()`. Original module
initialization and dependencies are excluded from this browser replacement.
Server compilation continues to use the original module. The factory and its
dependencies must themselves be browser-safe; static types are not rewritten.

Source files must exist inside the application root. Relative source and runtime
filenames resolve from that root; runtime package specifiers are also supported.
Export names and factory exports currently use ASCII JavaScript identifiers,
including `default`. Several exports can share one reference identity. A source
file may have only one contribution, including across symlink aliases and
plugins. Invalid or conflicting contributions fail during planning.

Payloads must be strict JSON and should contain only information meant for the
browser. The complete build plan stays in `.ev`; it is not a public deployment
manifest. Source changes are watched automatically. Plugins reading additional
metadata files must register those files with `ctx.addWatchFile()`.

This capability requires `@utoo/pack` 1.6.0 or newer with a matching native binding
reporting `moduleReferencesVersion = 1`. Older Utoopack builds and the Webpack
adapter fail explicitly instead of compiling the original module into the browser.
Bundler hooks cannot replace the framework-owned reference list.

## Add code to the client entry

Generate an installer and import it after the main application entry:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,56 @@ scope: { kind: "server" }

生成项的 id 只需在插件内部唯一。`emitPageIR()` 中的 id 还限定在当前页面,因此每个启用页面都可以安全复用相同 id。`@evjs/` 前缀由框架保留。

## 将浏览器模块替换为运行时引用

`emit.moduleReference()` 为一个精确的源码文件声明仅用于浏览器的替换。它没有返回值,
也不需要挂到槽位。插件提供导出映射和允许进入浏览器的数据,运行时工厂决定每个引用的行为。
evjs 不解析装饰器,也不解释协议。

```ts
import { definePlugin } from "@evjs/ev/plugin";

export const references = definePlugin({
id: "greeting-references",
emitIR({ emit }) {
emit.moduleReference({
id: "greeting",
resource: "./src/greeting.ts",
references: [{
id: "greeting",
factory: {
module: "./src/greeting-client.ts",
export: "createGreetingReference",
},
data: { endpoint: "/greetings" },
}],
exports: [
{ name: "Greeting", reference: "greeting" },
{ name: "default", reference: "greeting" },
],
});
},
});
```

浏览器中的两个导出共享 `createGreetingReference(data)` 的结果。如果工厂返回构造函数,
业务可以继续写 `import { Greeting } from "./greeting"; new Greeting()`。
该浏览器替换不包含原模块的初始化逻辑和依赖,服务端编译继续使用原模块。
工厂及其依赖必须适用于浏览器;这项能力不会改写静态类型。

源码必须是应用根目录内已存在的文件。相对源码和运行时路径均从应用根目录解析,
运行时也可以使用包说明符。导出名和工厂导出当前支持 ASCII JavaScript 标识符,
包括 `default`。多个导出可以共享同一引用。一个源码文件只能对应一项贡献,
该限制也覆盖符号链接别名和不同插件。无效或冲突的贡献会在规划阶段报错。

数据必须是严格 JSON,只能包含准备向浏览器公开的信息。完整构建计划保留在 `.ev`,
不是公开部署 manifest。源码变化会自动被监听;插件读取其他元数据文件时,
必须通过 `ctx.addWatchFile()` 注册这些文件。

该能力需要 `@utoo/pack` 1.6.0 或更新版本,并加载报告 `moduleReferencesVersion = 1`
的配套原生绑定。旧版 Utoopack 和 Webpack 适配器
会明确报错,避免将原模块编译进浏览器。bundler 钩子不能替换框架拥有的引用列表。

## 向客户端入口添加代码

生成安装器,并在应用主入口后导入:
Expand Down
72 changes: 36 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/bundler-utoopack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The default utoopack (`@utoo/pack`) bundler adapter for the evjs framework.
This package exports `utoopackAdapter`, which implements the `BundlerAdapter`
interface for `ev build` and `ev dev`.

The adapter requires `@utoo/pack` 1.6.0 or newer. Plugin-contributed browser
module references also check the loaded native binding's capability at runtime.

The adapter integrates with Utoopack's programmatic API, compiles the concrete
entries in `BuildPlan`, and returns build facts for framework output linking.
Its declared capabilities are the authoritative boundary: conventional
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler-utoopack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@evjs/ev": "*",
"@evjs/shared": "*",
"@logtape/logtape": "^2.0.4",
"@utoo/pack": "^1.5.36",
"@utoo/pack": "^1.6.0",
"fast-glob": "^3.3.3",
"less": "4.1.3",
"less-loader": "12.3.2"
Expand Down
13 changes: 12 additions & 1 deletion packages/bundler-utoopack/src/adapter/config/create-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import {
assertSafeUtoopackCleanOutput,
assertUtoopackOutputPathsMatchPlan,
} from "../output/output-paths.js";
import {
assertUtoopackModuleReferencesMatchPlan,
createUtoopackModuleReferences,
type UtoopackModuleReferenceConfig,
} from "./module-references.js";

const logger = getLogger(["evjs", "bundler-utoopack", "config"]);
const lessImplementation = require.resolve("less");
Expand Down Expand Up @@ -152,8 +157,12 @@ export async function createUtoopackConfig(
: {}),
};

const utoopackConfig: ConfigComplete = {
const moduleReferences = createUtoopackModuleReferences(plan);
const utoopackConfig: UtoopackModuleReferenceConfig = {
mode,
...(moduleReferences
? { module: { references: structuredClone(moduleReferences) } }
: {}),
// Keep concurrent dev and production Projects on separate databases and locks.
cacheDirectory: `.turbopack/evjs/${mode}`,
...(isProduction && config.target !== undefined && clientEntries.length > 0
Expand Down Expand Up @@ -271,6 +280,7 @@ export async function createUtoopackConfig(
for (const h of hooks) {
if (h.configureBundler) {
await h.configureBundler(utoopackConfig, ctx);
assertUtoopackModuleReferencesMatchPlan(utoopackConfig, moduleReferences);
assertUtoopackServerEntryMatchesPlan(utoopackConfig, expectedServerEntry);
assertUtoopackOutputPathsMatchPlan(cwd, utoopackConfig, outputPaths, {
requireServerOutput: finalServerEntry !== undefined,
Expand All @@ -282,6 +292,7 @@ export async function createUtoopackConfig(
}

assertUtoopackServerEntryMatchesPlan(utoopackConfig, expectedServerEntry);
assertUtoopackModuleReferencesMatchPlan(utoopackConfig, moduleReferences);
assertUtoopackOutputPathsMatchPlan(cwd, utoopackConfig, outputPaths, {
requireServerOutput: finalServerEntry !== undefined,
});
Expand Down
Loading
Loading