@ecopages/core 0.2.0-alpha.53 → 0.2.0-alpha.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +2 -3
  2. package/package.json +34 -14
  3. package/src/adapters/bun/server-adapter.d.ts +7 -0
  4. package/src/adapters/bun/server-adapter.js +8 -3
  5. package/src/adapters/node/server-adapter.d.ts +1 -1
  6. package/src/adapters/node/server-adapter.js +2 -4
  7. package/src/build/README.md +57 -73
  8. package/src/build/browser-runtime-plugin-helpers.d.ts +26 -0
  9. package/src/build/browser-runtime-plugin-helpers.js +14 -0
  10. package/src/build/browser-runtime-plugin.d.ts +78 -0
  11. package/src/build/{browser-runtime-import-rewrite-plugin.js → browser-runtime-plugin.js} +49 -42
  12. package/src/build/build-adapter.d.ts +350 -93
  13. package/src/build/build-adapter.js +61 -492
  14. package/src/build/build-manifest.js +3 -6
  15. package/src/build/build-types.d.ts +2 -2
  16. package/src/build/rolldown-build-adapter.d.ts +32 -0
  17. package/src/build/rolldown-build-adapter.js +260 -0
  18. package/src/build/rolldown-plugin-bridge.d.ts +50 -0
  19. package/src/build/rolldown-plugin-bridge.js +194 -0
  20. package/src/build/runtime-build-executor.d.ts +14 -7
  21. package/src/build/runtime-build-executor.js +8 -11
  22. package/src/build/runtime-build-output-normalizer.d.ts +3 -0
  23. package/src/build/runtime-build-output-normalizer.js +111 -0
  24. package/src/build/serialized-build-executor.d.ts +64 -0
  25. package/src/build/serialized-build-executor.js +63 -0
  26. package/src/build/server-side-css-shim-plugin.d.ts +41 -0
  27. package/src/build/server-side-css-shim-plugin.js +35 -0
  28. package/src/cache/index.d.ts +6 -0
  29. package/src/cache/index.js +6 -0
  30. package/src/cache/module-parse-cache.d.ts +65 -0
  31. package/src/cache/module-parse-cache.js +75 -0
  32. package/src/config/README.md +1 -1
  33. package/src/config/config-builder.d.ts +3 -3
  34. package/src/config/config-builder.js +5 -14
  35. package/src/eco/eco.types.d.ts +2 -5
  36. package/src/hmr/strategies/js-hmr-strategy.d.ts +2 -2
  37. package/src/hmr/strategies/js-hmr-strategy.js +2 -2
  38. package/src/plugins/alias-resolver-cache.d.ts +68 -0
  39. package/src/plugins/alias-resolver-cache.js +106 -0
  40. package/src/plugins/alias-resolver-plugin.d.ts +4 -1
  41. package/src/plugins/alias-resolver-plugin.js +9 -5
  42. package/src/plugins/eco-component-meta-plugin.js +2 -2
  43. package/src/plugins/foreign-jsx-override-plugin.d.ts +1 -1
  44. package/src/route-renderer/orchestration/render-output.utils.d.ts +1 -1
  45. package/src/services/assets/browser-bundle.service.d.ts +1 -1
  46. package/src/services/module-loading/app-module-loader.service.d.ts +1 -1
  47. package/src/services/module-loading/app-server-module-transpiler.service.js +8 -29
  48. package/src/services/module-loading/page-module-import.service.js +2 -0
  49. package/src/types/internal-types.d.ts +1 -1
  50. package/src/build/browser-runtime-import-rewrite-plugin.d.ts +0 -26
  51. package/src/build/dev-build-coordinator.d.ts +0 -72
  52. package/src/build/dev-build-coordinator.js +0 -154
  53. package/src/build/esbuild-build-adapter.d.ts +0 -79
  54. package/src/build/esbuild-build-adapter.js +0 -521
  55. package/src/build/runtime-specifier-alias-plugin.d.ts +0 -15
  56. package/src/build/runtime-specifier-alias-plugin.js +0 -31
  57. package/src/services/module-loading/node-bootstrap-plugin.d.ts +0 -38
  58. package/src/services/module-loading/node-bootstrap-plugin.js +0 -215
package/README.md CHANGED
@@ -67,11 +67,11 @@ The manager/orchestration layer is core-owned, but framework-specific strategies
67
67
 
68
68
  ### Practical Summary
69
69
 
70
- - `ConfigBuilder` now seeds one app-owned build ownership path, adapter, manifest, executor, dev graph, and runtime registry.
70
+ - `ConfigBuilder` seeds one app-owned build ownership path, adapter, manifest, executor, dev graph, and runtime registry.
71
71
  - `BrowserBundleService` is the shared browser build seam used by HMR and asset-oriented browser output paths.
72
72
  - `ServerModuleTranspiler` is the shared server-side source loading seam used by runtime bootstrap and HMR metadata loading.
73
73
  - `createApp()` stays the universal runtime entrypoint, while Vite and Nitro hosts own their advanced dev and build workflows.
74
- - esbuild remains only as a temporary Bun-path implementation detail and is not a strategic core dependency.
74
+ - One bundled adapter is the default bundler. Vite-based apps route through the `ViteHostBuildAdapter` boundary marker instead.
75
75
 
76
76
  ## Documentation Map
77
77
 
@@ -279,7 +279,6 @@ Use these entrypoints only when implementing host adapters or framework-owned bu
279
279
  - `@ecopages/core/dev/host-runtime`
280
280
  - `@ecopages/core/build/build-adapter`
281
281
  - `@ecopages/core/build/build-types`
282
- - `@ecopages/core/build/runtime-specifier-alias-plugin`
283
282
  - `@ecopages/core/plugins/foreign-jsx-override-plugin`
284
283
 
285
284
  These host-facing entrypoints are narrower compatibility seams. App code and most extensions should prefer the app-authoring or extension-authoring surfaces.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.53",
3
+ "version": "0.2.0-alpha.55",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,14 +17,16 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-alpha.53",
20
+ "@ecopages/file-system": "0.2.0-alpha.55",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
+ "@oxc-project/runtime": "0.134.0",
23
24
  "@worker-tools/html-rewriter": "0.1.0-pre.19",
24
25
  "chokidar": "^5.0.0",
25
26
  "esbuild": "^0.28.0",
26
27
  "ghtml": "^4.0.2",
27
28
  "oxc-parser": "^0.124.0",
29
+ "rolldown": "^1.1.0",
28
30
  "ws": "^8.20.1"
29
31
  },
30
32
  "exports": {
@@ -115,17 +117,21 @@
115
117
  "types": "./src/build/build-adapter.d.ts",
116
118
  "default": "./src/build/build-adapter.js"
117
119
  },
120
+ "./build/runtime-build-output-normalizer": {
121
+ "types": "./src/build/runtime-build-output-normalizer.d.ts",
122
+ "default": "./src/build/runtime-build-output-normalizer.js"
123
+ },
118
124
  "./build/browser-runtime-manifest": {
119
125
  "types": "./src/build/browser-runtime-manifest.d.ts",
120
126
  "default": "./src/build/browser-runtime-manifest.js"
121
127
  },
122
- "./build/browser-runtime-import-rewrite-plugin": {
123
- "types": "./src/build/browser-runtime-import-rewrite-plugin.d.ts",
124
- "default": "./src/build/browser-runtime-import-rewrite-plugin.js"
128
+ "./build/browser-runtime-plugin": {
129
+ "types": "./src/build/browser-runtime-plugin.d.ts",
130
+ "default": "./src/build/browser-runtime-plugin.js"
125
131
  },
126
- "./build/runtime-specifier-alias-plugin": {
127
- "types": "./src/build/runtime-specifier-alias-plugin.d.ts",
128
- "default": "./src/build/runtime-specifier-alias-plugin.js"
132
+ "./cache": {
133
+ "types": "./src/cache/index.d.ts",
134
+ "default": "./src/cache/index.js"
129
135
  },
130
136
  "./plugins/foreign-jsx-override-plugin": {
131
137
  "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
@@ -141,6 +147,11 @@
141
147
  "default": "./src/utils/hash.js",
142
148
  "types": "./src/utils/hash.d.ts"
143
149
  },
150
+ "./utils/hash": {
151
+ "import": "./src/utils/hash.js",
152
+ "default": "./src/utils/hash.js",
153
+ "types": "./src/utils/hash.d.ts"
154
+ },
144
155
  "./errors": {
145
156
  "types": "./src/errors/index.d.ts",
146
157
  "default": "./src/errors/index.js"
@@ -230,17 +241,21 @@
230
241
  "types": "./src/build/build-adapter.d.ts",
231
242
  "default": "./src/build/build-adapter.js"
232
243
  },
244
+ "./build/runtime-build-output-normalizer.ts": {
245
+ "types": "./src/build/runtime-build-output-normalizer.d.ts",
246
+ "default": "./src/build/runtime-build-output-normalizer.js"
247
+ },
233
248
  "./build/browser-runtime-manifest.ts": {
234
249
  "types": "./src/build/browser-runtime-manifest.d.ts",
235
250
  "default": "./src/build/browser-runtime-manifest.js"
236
251
  },
237
- "./build/browser-runtime-import-rewrite-plugin.ts": {
238
- "types": "./src/build/browser-runtime-import-rewrite-plugin.d.ts",
239
- "default": "./src/build/browser-runtime-import-rewrite-plugin.js"
252
+ "./build/browser-runtime-plugin.ts": {
253
+ "types": "./src/build/browser-runtime-plugin.d.ts",
254
+ "default": "./src/build/browser-runtime-plugin.js"
240
255
  },
241
- "./build/runtime-specifier-alias-plugin.ts": {
242
- "types": "./src/build/runtime-specifier-alias-plugin.d.ts",
243
- "default": "./src/build/runtime-specifier-alias-plugin.js"
256
+ "./cache.ts": {
257
+ "types": "./src/cache/index.d.ts",
258
+ "default": "./src/cache/index.js"
244
259
  },
245
260
  "./plugins/foreign-jsx-override-plugin.ts": {
246
261
  "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
@@ -256,6 +271,11 @@
256
271
  "default": "./src/utils/hash.js",
257
272
  "types": "./src/utils/hash.d.ts"
258
273
  },
274
+ "./utils/hash.ts": {
275
+ "import": "./src/utils/hash.js",
276
+ "default": "./src/utils/hash.js",
277
+ "types": "./src/utils/hash.d.ts"
278
+ },
259
279
  "./errors.ts": {
260
280
  "types": "./src/errors/index.d.ts",
261
281
  "default": "./src/errors/index.js"
@@ -186,6 +186,13 @@ export declare class BunServerAdapter extends SharedServerAdapter<BunServerAdapt
186
186
  createAdapter(): Promise<BunServerAdapterResult>;
187
187
  /**
188
188
  * Handles HTTP requests by passing them securely to the shared core router adapter.
189
+ *
190
+ * @remarks
191
+ * Filesystem page responses are wrapped by `ServerRouteHandler`. This
192
+ * adapter-level pass only covers HTML returned by explicit API handlers,
193
+ * which bypass that route-layer wrapper and would otherwise miss the
194
+ * dev HMR runtime — so the HMR script injection happens here, after
195
+ * the shared handler runs.
189
196
  */
190
197
  handleRequest(request: Request): Promise<Response>;
191
198
  /**
@@ -91,9 +91,7 @@ class BunServerAdapter extends SharedServerAdapter {
91
91
  * Initializes the server adapter's core runtime components.
92
92
  */
93
93
  async initialize() {
94
- installAppRuntimeBuildExecutor(this.appConfig, {
95
- development: this.options?.watch === true
96
- });
94
+ installAppRuntimeBuildExecutor(this.appConfig);
97
95
  this.staticSiteGenerator = new StaticSiteGenerator({ appConfig: this.appConfig });
98
96
  await this.hmrManager.buildRuntime();
99
97
  this.prepareRuntimePublicDir();
@@ -387,6 +385,13 @@ class BunServerAdapter extends SharedServerAdapter {
387
385
  }
388
386
  /**
389
387
  * Handles HTTP requests by passing them securely to the shared core router adapter.
388
+ *
389
+ * @remarks
390
+ * Filesystem page responses are wrapped by `ServerRouteHandler`. This
391
+ * adapter-level pass only covers HTML returned by explicit API handlers,
392
+ * which bypass that route-layer wrapper and would otherwise miss the
393
+ * dev HMR runtime — so the HMR script injection happens here, after
394
+ * the shared handler runs.
390
395
  */
391
396
  async handleRequest(request) {
392
397
  const response = await this.handleSharedRequest(request, {
@@ -118,7 +118,7 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
118
118
  * `upgrade` event (no separate port needed).
119
119
  * - `NodeClientBridge` tracks active WebSocket connections and handles
120
120
  * broadcast + heartbeat cleanup.
121
- * - `NodeHmrManager` watches the filesystem and triggers incremental esbuild
121
+ * - `NodeHmrManager` watches the filesystem and triggers incremental
122
122
  * rebuilds, notifying connected clients via the bridge.
123
123
  * - Shared watcher bootstrapping listens for route-level file changes and
124
124
  * refreshes the router and response handlers when pages are added or removed.
@@ -67,9 +67,7 @@ class NodeServerAdapter extends SharedServerAdapter {
67
67
  * processors during their `setup()` calls.
68
68
  */
69
69
  async initialize() {
70
- installAppRuntimeBuildExecutor(this.appConfig, {
71
- development: this.options?.watch === true
72
- });
70
+ installAppRuntimeBuildExecutor(this.appConfig);
73
71
  this.prepareRuntimePublicDir();
74
72
  await setupAppRuntimePlugins({
75
73
  appConfig: this.appConfig,
@@ -241,7 +239,7 @@ class NodeServerAdapter extends SharedServerAdapter {
241
239
  * `upgrade` event (no separate port needed).
242
240
  * - `NodeClientBridge` tracks active WebSocket connections and handles
243
241
  * broadcast + heartbeat cleanup.
244
- * - `NodeHmrManager` watches the filesystem and triggers incremental esbuild
242
+ * - `NodeHmrManager` watches the filesystem and triggers incremental
245
243
  * rebuilds, notifying connected clients via the bridge.
246
244
  * - Shared watcher bootstrapping listens for route-level file changes and
247
245
  * refreshes the router and response handlers when pages are added or removed.
@@ -1,107 +1,91 @@
1
1
  # Build Layer
2
2
 
3
- This directory contains the runtime-neutral build contract used across Ecopages, the Bun-native adapter that currently uses esbuild under the hood, and the explicit host-owned Vite compatibility boundary.
3
+ The build layer is the bundler contract for Ecopages. One bundled adapter is the default; one host-owned boundary marker covers the Vite-host path.
4
4
 
5
- ## Files
6
-
7
- - `build-adapter.ts`: shared build interfaces, explicit Bun-native versus Vite-host ownership helpers, app-owned adapter/executor helpers, and compatibility fallback helpers for older Bun-native call paths.
8
- - `build-types.ts`: plugin bridge types used by integrations and processors.
9
- - `esbuild-build-adapter.ts`: the concrete Bun-native adapter implementation. It is compatibility infrastructure, not strategic architecture.
10
- - `dev-build-coordinator.ts`: development-only orchestration around the temporary Bun-native esbuild backend.
11
- - `*.test.ts`: focused regression coverage for plain builds and development serialization and recovery.
12
-
13
- ## Responsibilities
5
+ ## Mental Model
14
6
 
15
- The build layer is intentionally split into two parts.
7
+ Three concentric shapes, plus a serializer and a plugin injector:
16
8
 
17
- `BuildExecutor` is the runtime-facing contract.
9
+ | Shape | Lives in | Purpose |
10
+ | -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `BuildAdapter` | `build-adapter.ts` | Low-level backend. Two implementations: the bundled adapter (the real bundler) and `ViteHostBuildAdapter` (a host-owned boundary marker that throws on direct use). |
12
+ | `BuildExecutor` | `build-adapter.ts` | Narrower runtime facade. Only `build` is exposed. Stored on `appConfig.runtime.buildExecutor`. |
13
+ | `SerializedBuildExecutor` | `serialized-build-executor.ts` | FIFO queue around any `BuildExecutor`. Used by the dev watch pipeline. |
14
+ | `withBuildExecutorPlugins` | `build-adapter.ts` | Merges app-owned plugins into every `build` call. The single point of plugin injection. |
18
15
 
19
- - It is the narrow facade stored on `appConfig.runtime.buildExecutor`.
20
- - It answers only how a given app instance should execute builds right now.
21
- - the Bun-native adapter satisfies this contract directly in plain flows.
22
- - `DevBuildCoordinator` also satisfies this contract by wrapping the temporary Bun-native esbuild adapter with development-only serialization and recovery policy.
16
+ Plus one translation bridge:
23
17
 
24
- `EsbuildBuildAdapter` is the current Bun-native backend. It knows how to:
18
+ - `rolldown-plugin-bridge.ts` converts the runtime-agnostic `EcoBuildPlugin[]` array (the contract integrations and processors register) into the bundler's native `Plugin` array. Each `EcoBuildPlugin` becomes its own plugin entry to preserve plugin-priority order.
25
19
 
26
- - load the esbuild module
27
- - translate Ecopages `BuildOptions` into esbuild options
28
- - bridge Ecopages build plugins into esbuild hooks
29
- - normalize build output, logs, and dependency graph metadata
30
- - detect the subset of runtime faults that mean the esbuild worker protocol is corrupted
31
-
32
- `ViteHostBuildAdapter` is a boundary marker, not a real backend. It exists so app/runtime state can represent that Vite owns host-side build execution instead of silently falling back to a framework-owned esbuild path.
20
+ ## Files
33
21
 
34
- `DevBuildCoordinator` is the development policy layer. It exists because one app/runtime can have many build callers during dev mode, including:
22
+ - `build-adapter.ts`: types, factories, app-owned helpers, `withBuildExecutorPlugins`.
23
+ - `build-types.ts`: the `EcoBuildPlugin` contract used by integrations and processors.
24
+ - `rolldown-build-adapter.ts`: the production `BuildAdapter`. Wraps the bundler and exposes a normalized `BuildResult` (outputs, dependency graph, logs).
25
+ - `rolldown-plugin-bridge.ts`: `EcoBuildPlugin[]` → bundler-plugin translation.
26
+ - `serialized-build-executor.ts`: FIFO queue primitive.
27
+ - `runtime-build-executor.ts`: dev-watch entrypoint that wraps the app-owned adapter in a `SerializedBuildExecutor + withBuildExecutorPlugins` chain.
28
+ - `*.test.ts`: regression coverage.
35
29
 
36
- - page module imports
37
- - HMR entrypoint builds
38
- - script and asset processors
39
- - React integration build paths
30
+ ## Default Flow
40
31
 
41
- Those callers must not race each other against one long-lived esbuild worker. The coordinator therefore owns temporary compatibility policy for the Bun-native path:
32
+ `ConfigBuilder.build()` creates one app-owned adapter, manifest, executor, dev graph, and runtime registry. The executor it stores is the raw `BuildAdapter`; the plugin-wrap step lives in the runtime path.
42
33
 
43
- - serialized access to the shared adapter in development
44
- - recycling warm Node-target esbuild sessions between builds
45
- - recovery from known esbuild worker protocol faults
34
+ When a server adapter initializes, it calls `installAppRuntimeBuildExecutor(appConfig)`. That function reads the existing executor (or falls back to the app-owned adapter) and stores a fresh wrapper:
46
35
 
47
- ## Default Flow
36
+ ```
37
+ BuildExecutor
38
+ └─ SerializedBuildExecutor // FIFO queue
39
+ └─ withBuildExecutorPlugins // injects app plugins
40
+ └─ BuildAdapter (bundled adapter or Vite-host)
41
+ ```
48
42
 
49
- Each `EcoPagesAppConfig` owns explicit build ownership, a build adapter, a build manifest, and a `buildExecutor` in `appConfig.runtime`. `ConfigBuilder.build()` now creates that app-owned build state up front so later runtime startup can reuse it rather than mutating a shared adapter.
43
+ Every dev-watch caller reads `appConfig.runtime.buildExecutor` and gets the merged plugin set without further ceremony.
50
44
 
51
- When a Bun server adapter starts in watch mode, it replaces that executor with a per-app `DevBuildCoordinator`. Vite-hosted flows should not use that coordinator; the host owns watch, graph, and HMR policy there. Build consumers then either call the executor directly or pass it explicitly to the top-level `build()` helper.
45
+ The exported `defaultBuildAdapter` and the top-level `build()` / `getTranspileOptions()` helpers are non-app-aware escape hatches. New runtime code should prefer `getAppBuildAdapter()`, `getAppBuildExecutor()`, and `getAppTranspileOptions()`.
52
46
 
53
- The exported `defaultBuildAdapter` and top-level `getTranspileOptions()` helper are compatibility fallbacks only. New runtime code should prefer app-owned access through `getAppBuildAdapter()`, `getAppBuildExecutor()`, and `getAppTranspileOptions()`.
47
+ ## Vite-Host Boundary
54
48
 
55
- The same rule applies to source-module loading: host-owned import behavior must be injected through abstract runtime state rather than imported directly into core services.
49
+ `ViteHostBuildAdapter` is not a real backend. It exists so `appConfig.runtime.buildAdapter` can carry the `'vite-host'` ownership without falling back to a framework-owned bundler path. Every method throws a clear `Vite-hosted builds are owned by the host runtime. Core cannot …` error so misrouted calls fail loudly.
56
50
 
57
- Plugins are part of app-owned manifest or per-build input now. The source build contract no longer exposes adapter-level plugin registration, which keeps build composition scoped to an app/runtime instance instead of leaking across instances.
51
+ Vite-based apps (or any future host runtime) should:
58
52
 
59
- HMR callers follow the same ownership model. Integration-specific runtime aliasing stays with the integration that owns those specifiers, rather than in generic core HMR bundling.
53
+ 1. Construct a `ViteHostBuildAdapter` via `createViteHostBuildAdapter()` and install it on the app config with `setAppBuildAdapter`.
54
+ 2. Run their own build pipeline outside the core.
55
+ 3. Reuse the core's `BuildExecutor`-shaped surface where possible so call-sites stay backend-neutral.
60
56
 
61
- ## Orchestration Diagram
57
+ ## Plugin Authoring
62
58
 
63
- ```mermaid
64
- flowchart TD
65
- Config["ConfigBuilder.build()"] --> DefaultExec["appConfig.runtime.buildExecutor = createAppBuildExecutor(app adapter, manifest)"]
66
- Adapter["Server adapter initialize() in watch mode"] --> DevExec["appConfig.runtime.buildExecutor = DevBuildCoordinator"]
67
- Caller["Any build caller with app/runtime context"] --> Build["executor.build(options) or build(options, executor)"]
68
- Build --> Executor["BuildExecutor"]
69
- Executor --> Coordinator["DevBuildCoordinator.build()"]
70
- Coordinator --> Backend["EsbuildBuildAdapter.buildOrThrow()"]
71
- Executor -->|plain flow| Direct["EsbuildBuildAdapter.build()"]
72
- Backend --> Result["BuildResult"]
73
- Direct --> Result["BuildResult"]
74
- Result --> Browser["Browser consumes emitted bundle directly"]
75
- ```
59
+ `EcoBuildPlugin` is the runtime-agnostic contract integrations and processors register. The shape:
76
60
 
77
- ## Recovery Model
61
+ - `name: string`
62
+ - `setup(build: EcoBuildPluginBuilder): void | Promise<void>`
78
63
 
79
- The recovery path is narrow on purpose. The coordinator only treats an error as recoverable when `EsbuildBuildAdapter.isEsbuildProtocolError()` matches one of the known worker-protocol failure signatures.
64
+ `EcoBuildPluginBuilder` exposes three hooks:
80
65
 
81
- When that happens, recovery does three things in order:
66
+ - `onResolve({ filter, namespace? }, callback)` the bundler's `resolveId` mapped to the shared plugin shape.
67
+ - `onLoad({ filter, namespace? }, callback)` — the bundler's `load` mapped the same way.
68
+ - `module(specifier, callback)` — declares a virtual module by name, with bundler-side namespace encoding.
82
69
 
83
- 1. Reset the serialized queue so future builds are not stuck behind a wedged promise.
84
- 2. Stop the current esbuild service instance.
85
- 3. Increment the esbuild module generation so the next import gets a fresh worker instance.
70
+ Namespace handling: the shared plugin contract scopes handlers with a `namespace` string; the bundler encodes namespaces into the module id. The bridge prepends `<namespace>:` to resolved ids and matches filters against that prefix, then strips it before forwarding back to callbacks. Plugin code keeps the same `path` shape it had on the shared contract.
86
71
 
87
- After that reset, the coordinator retries the failed build once.
72
+ Plugin ordering: the bundler's `resolveId` and `load` are "first" hooks. The bridge translates each `EcoBuildPlugin` into its own plugin and preserves the array order, so position determines priority: index 0 wins first, the last index is the lowest priority. Security-critical plugins (e.g. `ecopages-client-graph-boundary`) should be placed before general-purpose loaders in the array.
88
73
 
89
- ## Why Explicit App Ownership
74
+ ## BuildOptions Caveats
90
75
 
91
- There are many build callsites across core and integrations. The coordinator still needs to stay centralized for the remaining Bun-native compatibility path, but process-global installation hid the real dependency and tied behavior to startup order.
76
+ `BuildOptions` is modeled on the bundler's options shape. Most fields map cleanly. The exceptions:
92
77
 
93
- The explicit app-owned executor model keeps the design honest:
78
+ - `splitting` — accepted but currently ignored. The bundler splits by default. The per-chunk naming is fixed to `[name]-[hash]`. If you need to disable splitting or rename chunks, the adapter will need a new option.
79
+ - `bundle` — accepted but ignored. The bundler always bundles.
80
+ - `outbase` — accepted but ignored. The adapter derives the base from `options.root` directly.
94
81
 
95
- - each app/runtime owns its own build executor
96
- - development policy stays in one place (`DevBuildCoordinator`) for the remaining Bun-native path only
97
- - callers with app context use that executor explicitly instead of consulting global state
98
- - tests can still instantiate `EsbuildBuildAdapter`, `ViteHostBuildAdapter`, or `DevBuildCoordinator` directly when they want the raw ownership boundary or compatibility backend only
82
+ These fields are kept in the type so existing call-sites compile. The proper fix is a more focused `BuildOptions` schema in a follow-up.
99
83
 
100
84
  ## Testing Strategy
101
85
 
102
- The build tests are split by concern.
103
-
104
- - `build-adapter.test.ts` verifies plain backend behavior and plugin bridging.
105
- - `build-adapter-serialization.test.ts` verifies development orchestration behavior such as serialization, warm-session recycling, and protocol-fault recovery.
86
+ - `rolldown-build-adapter.test.ts` covers the adapter's `build`, `resolve`, `getTranspileOptions`, and dependency-graph extraction end-to-end.
87
+ - `rolldown-plugin-bridge.test.ts` covers the `EcoBuildPlugin[]` → plugin translation in isolation.
88
+ - `build-adapter.test.ts` covers the app-owned helpers, the `BuildOwnership` routing, the `withBuildExecutorPlugins` injection, and the default-fallback behaviour.
89
+ - `runtime-build-executor.test.ts` covers the dev-watch wrapper: serialization, plugin injection, and the Vite-host rejection path.
106
90
 
107
- If you change the build orchestration rules, update the coordinator tests first. If you change esbuild option mapping or plugin behavior, update the backend tests first.
91
+ If you change option mapping or plugin-bridge semantics, update the adapter and bridge tests first. If you change the app-owned helper contracts, update `build-adapter.test.ts` first.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared helpers for the browser-runtime plugin.
3
+ *
4
+ * @remarks
5
+ * `createBrowserRuntimePlugin` needs to:
6
+ *
7
+ * 1. Escape each specifier for inclusion in a regex filter.
8
+ * 2. Build a `RegExp` whose alternation matches any of those specifiers.
9
+ *
10
+ * These utilities are extracted here so the plugin does not duplicate
11
+ * the same code path.
12
+ */
13
+ /**
14
+ * Escapes a literal specifier for inclusion in a regular expression.
15
+ *
16
+ * Only the regex metacharacters are escaped: `. * + ? ^ $ { } ( ) | [ ] \`
17
+ */
18
+ export declare function escapeRegExp(value: string): string;
19
+ /**
20
+ * Builds a `RegExp` whose alternation matches any of the keys in
21
+ * `specifierMap`.
22
+ *
23
+ * Returns `null` for an empty map so callers can short-circuit and
24
+ * avoid registering a no-op `onResolve` / `onLoad` filter.
25
+ */
26
+ export declare function buildSpecifierFilter(specifierMap: ReadonlyMap<string, string>): RegExp | null;
@@ -0,0 +1,14 @@
1
+ function escapeRegExp(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
+ }
4
+ function buildSpecifierFilter(specifierMap) {
5
+ if (specifierMap.size === 0) {
6
+ return null;
7
+ }
8
+ const alternation = Array.from(specifierMap.keys()).map(escapeRegExp).join("|");
9
+ return new RegExp(`^(${alternation})$`);
10
+ }
11
+ export {
12
+ buildSpecifierFilter,
13
+ escapeRegExp
14
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Unified browser-runtime plugin factory.
3
+ *
4
+ * @remarks
5
+ * Single factory that exposes the union of behaviors for the browser
6
+ * runtime:
7
+ *
8
+ * - `onResolve` for manifest specifiers → mapped public URL, marked external
9
+ * - `onResolve` for paths starting with `/` whose target is in the manifest's
10
+ * public path set → marked external
11
+ * - `onLoad` for JS/TS files → AST-walking import/export rewrite against the
12
+ * manifest specifier set, with a `code.includes(specifier)` fast path
13
+ *
14
+ * The plugin object carries the manifest's `specifier → publicPath` map
15
+ * under `BROWSER_RUNTIME_IMPORT_REWRITE_MAP` so
16
+ * `collectBrowserRuntimeImportRewriteMap` and the post-build rewriter can
17
+ * read it without re-walking the manifest.
18
+ *
19
+ * This is the single source of truth for browser-runtime plugin behavior.
20
+ * Per-bundler bridges can treat it as a single plugin.
21
+ */
22
+ import type { EcoBuildPlugin } from './build-types.js';
23
+ import { type BrowserRuntimeManifest } from './browser-runtime-manifest.js';
24
+ /**
25
+ * Symbol used to attach the manifest's `specifier → publicPath` map to a
26
+ * plugin instance. Consumers read it via `getBrowserRuntimeImportRewriteMap`
27
+ * or `collectBrowserRuntimeImportRewriteMap`.
28
+ */
29
+ export declare const BROWSER_RUNTIME_IMPORT_REWRITE_MAP: unique symbol;
30
+ /**
31
+ * Default name used by `createBrowserRuntimePlugin` when the caller
32
+ * does not provide one.
33
+ */
34
+ export declare const DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME = "browser-runtime-plugin";
35
+ export type CreateBrowserRuntimePluginOptions = {
36
+ /** Manifest whose specifier → publicPath entries drive the plugin. */
37
+ manifest: BrowserRuntimeManifest;
38
+ /** Stable build plugin name used for deduplication and selective exclusion. */
39
+ name?: string;
40
+ /**
41
+ * Whether alias `onResolve` results should be marked `external`.
42
+ * Default `true`. Pass `false` to let the bundler try to bundle the
43
+ * mapped URL.
44
+ */
45
+ external?: boolean;
46
+ /**
47
+ * Enable source-level AST import/export rewrite via the `onLoad`
48
+ * hook. Default `true`. Set `false` for alias-only consumers
49
+ * (e.g. `react`/`react-dom` externals in vendor assets).
50
+ */
51
+ rewriteImports?: boolean;
52
+ /**
53
+ * Register an `onResolve` for paths starting with `/` whose target
54
+ * is in the manifest's public path set. Marks them external so the
55
+ * bundler does not try to resolve them as source modules. Default
56
+ * `true`.
57
+ */
58
+ matchPublicPaths?: boolean;
59
+ };
60
+ /**
61
+ * Rewrites static ESM import/export specifiers and string-literal dynamic imports
62
+ * from manifest-owned runtime specifiers to concrete browser public URLs.
63
+ *
64
+ * Exposed for the post-build rewriter and for tests.
65
+ */
66
+ export declare function rewriteBrowserRuntimeImports(code: string, specifierMap: ReadonlyMap<string, string>, filePath?: string): string;
67
+ export declare function getBrowserRuntimeImportRewriteMap(plugin: EcoBuildPlugin): ReadonlyMap<string, string> | undefined;
68
+ export declare function collectBrowserRuntimeImportRewriteMap(plugins: EcoBuildPlugin[] | undefined): ReadonlyMap<string, string>;
69
+ /**
70
+ * Creates a build plugin that exposes the union of browser-runtime
71
+ * manifest behaviors (alias resolution, public-path matching, and source
72
+ * import/export rewrite). See {@link CreateBrowserRuntimePluginOptions}
73
+ * for per-behavior toggles.
74
+ *
75
+ * Returns `null` for an empty manifest so callers can short-circuit
76
+ * registration without sprinkling `if (plugin)` everywhere.
77
+ */
78
+ export declare function createBrowserRuntimePlugin(options: CreateBrowserRuntimePluginOptions): EcoBuildPlugin | null;
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { parseSync } from "oxc-parser";
3
+ import { cachedParseSync } from "../cache/module-parse-cache.js";
4
4
  import { getBrowserRuntimeSpecifierMap } from "./browser-runtime-manifest.js";
5
+ import { buildSpecifierFilter } from "./browser-runtime-plugin-helpers.js";
5
6
  const BROWSER_RUNTIME_IMPORT_REWRITE_MAP = /* @__PURE__ */ Symbol.for("ecopages.browserRuntimeImportRewriteMap");
6
- const DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME = "browser-runtime-import-rewrite";
7
+ const DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME = "browser-runtime-plugin";
7
8
  function isRecord(value) {
8
9
  return Boolean(value) && typeof value === "object";
9
10
  }
@@ -22,9 +23,6 @@ function inferLoaderFromPath(filePath) {
22
23
  return "js";
23
24
  }
24
25
  }
25
- function escapeRegExp(value) {
26
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
- }
28
26
  function queueReplacement(options) {
29
27
  if (!isRecord(options.source) || typeof options.source.value !== "string") {
30
28
  return;
@@ -46,7 +44,7 @@ function rewriteBrowserRuntimeImports(code, specifierMap, filePath = "browser-ru
46
44
  }
47
45
  const edits = [];
48
46
  try {
49
- const result = parseSync(filePath, code, {
47
+ const result = cachedParseSync(filePath, code, {
50
48
  sourceType: "module",
51
49
  lang: path.extname(filePath).endsWith("x") ? "tsx" : "ts"
52
50
  });
@@ -104,15 +102,19 @@ function collectBrowserRuntimeImportRewriteMap(plugins) {
104
102
  }
105
103
  return merged;
106
104
  }
107
- function createBrowserRuntimeImportRewritePlugin(options) {
108
- const specifierMap = getBrowserRuntimeSpecifierMap(options.manifest);
109
- const publicPathSet = new Set(specifierMap.values());
110
- const specifierFilter = new RegExp(`^(${Array.from(specifierMap.keys()).map(escapeRegExp).join("|")})$`);
111
- if (specifierMap.size === 0) {
105
+ function createBrowserRuntimePlugin(options) {
106
+ const { manifest } = options;
107
+ const specifierMap = getBrowserRuntimeSpecifierMap(manifest);
108
+ const specifierFilter = buildSpecifierFilter(specifierMap);
109
+ if (!specifierFilter) {
112
110
  return null;
113
111
  }
112
+ const external = options.external ?? true;
113
+ const rewriteImports = options.rewriteImports ?? true;
114
+ const matchPublicPaths = options.matchPublicPaths ?? true;
115
+ const publicPathSet = new Set(specifierMap.values());
114
116
  const plugin = {
115
- name: options.name ?? DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME,
117
+ name: options.name ?? DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME,
116
118
  setup(build) {
117
119
  build.onResolve({ filter: specifierFilter }, (args) => {
118
120
  const mappedPath = specifierMap.get(args.path);
@@ -121,45 +123,50 @@ function createBrowserRuntimeImportRewritePlugin(options) {
121
123
  }
122
124
  return {
123
125
  path: mappedPath,
124
- external: true
125
- };
126
- });
127
- build.onResolve({ filter: /^\// }, (args) => {
128
- if (!publicPathSet.has(args.path)) {
129
- return void 0;
130
- }
131
- return {
132
- path: args.path,
133
- external: true
134
- };
135
- });
136
- build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
137
- if (!path.isAbsolute(args.path) || !existsSync(args.path)) {
138
- return void 0;
139
- }
140
- const code = readFileSync(args.path, "utf-8");
141
- if (!Array.from(specifierMap.keys()).some((specifier) => code.includes(specifier))) {
142
- return void 0;
143
- }
144
- const rewritten = rewriteBrowserRuntimeImports(code, specifierMap, args.path);
145
- if (rewritten === code) {
146
- return void 0;
147
- }
148
- return {
149
- contents: rewritten,
150
- loader: inferLoaderFromPath(args.path),
151
- resolveDir: path.dirname(args.path)
126
+ external
152
127
  };
153
128
  });
129
+ if (matchPublicPaths) {
130
+ build.onResolve({ filter: /^\// }, (args) => {
131
+ if (!publicPathSet.has(args.path)) {
132
+ return void 0;
133
+ }
134
+ return {
135
+ path: args.path,
136
+ external: true
137
+ };
138
+ });
139
+ }
140
+ if (rewriteImports) {
141
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
142
+ if (!path.isAbsolute(args.path) || !existsSync(args.path)) {
143
+ return void 0;
144
+ }
145
+ const code = readFileSync(args.path, "utf-8");
146
+ if (!Array.from(specifierMap.keys()).some((specifier) => code.includes(specifier))) {
147
+ return void 0;
148
+ }
149
+ const rewritten = rewriteBrowserRuntimeImports(code, specifierMap, args.path);
150
+ if (rewritten === code) {
151
+ return void 0;
152
+ }
153
+ return {
154
+ contents: rewritten,
155
+ loader: inferLoaderFromPath(args.path),
156
+ resolveDir: path.dirname(args.path)
157
+ };
158
+ });
159
+ }
154
160
  }
155
161
  };
156
162
  plugin[BROWSER_RUNTIME_IMPORT_REWRITE_MAP] = specifierMap;
157
163
  return plugin;
158
164
  }
159
165
  export {
160
- DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME,
166
+ BROWSER_RUNTIME_IMPORT_REWRITE_MAP,
167
+ DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME,
161
168
  collectBrowserRuntimeImportRewriteMap,
162
- createBrowserRuntimeImportRewritePlugin,
169
+ createBrowserRuntimePlugin,
163
170
  getBrowserRuntimeImportRewriteMap,
164
171
  rewriteBrowserRuntimeImports
165
172
  };