@ecopages/core 0.2.0-beta.8 → 0.2.1-alpha.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-beta.8",
3
+ "version": "0.2.1-alpha.0",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -13,11 +13,11 @@
13
13
  "sideEffects": false,
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/ecopages/ecopages.git",
16
+ "url": "git+https://github.com/ecopages/ecopages.git",
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-beta.8",
20
+ "@ecopages/file-system": "0.2.1-alpha.0",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.134.0",
@@ -16,6 +16,7 @@
16
16
  * the `set*` counterparts) — the supported way for runtime code to read
17
17
  * and mutate the active adapter per `EcoPagesAppConfig`.
18
18
  */
19
+ import type { EcoSourceTransform } from '../plugins/source-transform.js';
19
20
  import type { EcoBuildPlugin } from './build-types.js';
20
21
  import { type AppBuildManifest } from './build-manifest.js';
21
22
  import type { EcoPagesAppConfig } from '../types/internal-types.js';
@@ -191,6 +192,20 @@ export interface BuildOptions {
191
192
  * bridge.
192
193
  */
193
194
  plugins?: EcoBuildPlugin[];
195
+ /**
196
+ * App-owned source transforms for browser-targeted Rolldown builds.
197
+ *
198
+ * @remarks
199
+ * Applied by the Rolldown plugin bridge after first-wins `onLoad` plugins
200
+ * produce module contents. This is the canonical browser/HMR path for
201
+ * `eco-component-meta` and other transforms registered in
202
+ * `appConfig.sourceTransforms`. Server builds continue to use loader plugins
203
+ * instead; this field is ignored unless `target` is `'browser'`.
204
+ *
205
+ * {@link BrowserBundleService} forwards {@link getAppSourceTransforms} here
206
+ * automatically.
207
+ */
208
+ sourceTransforms?: EcoSourceTransform[];
194
209
  /**
195
210
  * Escape hatch for backends that need to forward unknown options
196
211
  * to their underlying driver. Consumers should prefer the typed
@@ -468,6 +483,10 @@ export declare function getAppServerBuildPlugins(appConfig: EcoPagesAppConfig):
468
483
  * Reads from the app's sealed build manifest. The browser-bundle
469
484
  * manifest is the source of truth for which plugins participate in the
470
485
  * browser bundle.
486
+ *
487
+ * Plugins whose `name` matches a registered {@link EcoSourceTransform} are
488
+ * excluded here because browser builds run those transforms via the Rolldown
489
+ * bridge post-load pass instead of as competing `onLoad` handlers.
471
490
  */
472
491
  export declare function getAppBrowserBuildPlugins(appConfig: EcoPagesAppConfig): EcoBuildPlugin[];
473
492
  /**
@@ -5,6 +5,7 @@ import {
5
5
  getBrowserBuildPlugins,
6
6
  getServerBuildPlugins
7
7
  } from "./build-manifest.js";
8
+ import { getAppSourceTransforms } from "../plugins/source-transform.js";
8
9
  import { getJsxOwnershipPlugins } from "./jsx-ownership-plugins.js";
9
10
  import { createRolldownBuildAdapter } from "./rolldown-build-adapter.js";
10
11
  import { createRolldownDevBuildAdapter } from "./rolldown-dev-build-adapter.js";
@@ -200,7 +201,11 @@ function getAppServerBuildPlugins(appConfig) {
200
201
  }
201
202
  function getAppBrowserBuildPlugins(appConfig) {
202
203
  const manifest = getAppBuildManifest(appConfig);
203
- return [...getBrowserBuildPlugins(manifest), ...getJsxOwnershipPlugins(appConfig)];
204
+ const sourceTransformNames = new Set(getAppSourceTransforms(appConfig).map((transform) => transform.name));
205
+ const browserPlugins = getBrowserBuildPlugins(manifest).filter(
206
+ (plugin) => !sourceTransformNames.has(plugin.name)
207
+ );
208
+ return [...browserPlugins, ...getJsxOwnershipPlugins(appConfig)];
204
209
  }
205
210
  function getAppBuildExecutor(appConfig) {
206
211
  return appConfig.runtime?.routeModuleBuildExecutor ?? appConfig.runtime?.buildExecutor ?? getAppBuildAdapter(appConfig);
@@ -201,7 +201,8 @@ function resolveRolldownOptions(options, contextRoot, outdir, appRootRequireCach
201
201
  transformOptions.target = options.target;
202
202
  }
203
203
  const bundlePlugins = options.plugins ?? [];
204
- const appPlugins = createRolldownPluginBridge(bundlePlugins, contextRoot);
204
+ const sourceTransforms = options.target === "browser" ? options.sourceTransforms ?? [] : [];
205
+ const appPlugins = createRolldownPluginBridge(bundlePlugins, contextRoot, sourceTransforms);
205
206
  const allPlugins = [...options.target !== "browser" ? [createServerSideCssShimPlugin()] : [], ...appPlugins];
206
207
  const inputOptions = {
207
208
  input: options.entrypoints,
@@ -40,6 +40,7 @@
40
40
  * on every source file.
41
41
  */
42
42
  import type { Plugin } from 'rolldown';
43
+ import type { EcoSourceTransform } from '../plugins/source-transform.js';
43
44
  import type { EcoBuildPlugin } from './build-types.js';
44
45
  /**
45
46
  * Creates a Rolldown `Plugin` array that drives the supplied
@@ -55,5 +56,13 @@ import type { EcoBuildPlugin } from './build-types.js';
55
56
  * Plugin ordering is preserved: registrations from earlier eco plugins
56
57
  * are checked before registrations from later ones, matching the
57
58
  * original per-plugin priority semantics.
59
+ *
60
+ * @param plugins - `EcoBuildPlugin` instances registered for this build.
61
+ * @param contextRoot - Project root used to resolve relative load paths.
62
+ * @param sourceTransforms - Optional app-owned transforms applied after a matching
63
+ * `onLoad` handler returns module contents. Browser builds pass
64
+ * {@link getAppSourceTransforms | app source transforms} here so metadata injection
65
+ * still runs on output rewritten by boundary/runtime plugins. Virtual modules,
66
+ * CSS, and asset loads are skipped.
58
67
  */
59
- export declare function createRolldownPluginBridge(plugins: EcoBuildPlugin[], contextRoot: string): Plugin[];
68
+ export declare function createRolldownPluginBridge(plugins: EcoBuildPlugin[], contextRoot: string, sourceTransforms?: readonly EcoSourceTransform[]): Plugin[];
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { finalizeLoadResultWithSourceTransforms } from "./rolldown-source-transform-pass.js";
2
3
  import { escapeRegExp } from "./browser-runtime-plugin-helpers.js";
3
4
  const NAMESPACE_SEPARATOR = ":";
4
5
  function joinNamespace(namespace, value) {
@@ -124,7 +125,7 @@ function convertPluginOnResolveResult(result, importer, contextRoot) {
124
125
  }
125
126
  return partial;
126
127
  }
127
- function createRolldownPluginBridge(plugins, contextRoot) {
128
+ function createRolldownPluginBridge(plugins, contextRoot, sourceTransforms = []) {
128
129
  if (plugins.length === 0) {
129
130
  return [];
130
131
  }
@@ -152,18 +153,29 @@ function createRolldownPluginBridge(plugins, contextRoot) {
152
153
  return void 0;
153
154
  };
154
155
  const loadHandler = async (id) => {
156
+ let loadResult;
155
157
  for (const { filter, callback } of loadRegistrations) {
156
158
  if (!filter.test(id)) {
157
159
  continue;
158
160
  }
159
- const { namespace, path: sourcePath } = splitNamespace(id);
160
- const result = await callback({ path: sourcePath, namespace });
161
+ const { namespace: namespace2, path: sourcePath2 } = splitNamespace(id);
162
+ const result = await callback({ path: sourcePath2, namespace: namespace2 });
161
163
  const converted = convertPluginOnLoadResult({ id }, result);
162
164
  if (converted !== void 0) {
163
- return converted;
165
+ loadResult = converted;
166
+ break;
164
167
  }
165
168
  }
166
- return void 0;
169
+ const { namespace, path: sourcePath } = splitNamespace(id);
170
+ return finalizeLoadResultWithSourceTransforms({
171
+ id,
172
+ namespace,
173
+ sourcePath,
174
+ loadResult,
175
+ sourceTransforms,
176
+ contextRoot,
177
+ inferModuleTypeFromPath: (filePath) => inferRolldownModuleTypeFromPath(filePath)
178
+ });
167
179
  };
168
180
  const plugin = {
169
181
  name: "ecopages-plugin-bridge",
@@ -0,0 +1,15 @@
1
+ import type { LoadResult, SourceDescription } from 'rolldown';
2
+ import { type EcoSourceTransform } from '../plugins/source-transform.js';
3
+ /**
4
+ * Runs app-owned source transforms after first-wins `onLoad` plugins produce
5
+ * module contents for one Rolldown load request.
6
+ */
7
+ export declare function finalizeLoadResultWithSourceTransforms(options: {
8
+ id: string;
9
+ namespace: string | undefined;
10
+ sourcePath: string;
11
+ loadResult: LoadResult | undefined;
12
+ sourceTransforms: readonly EcoSourceTransform[];
13
+ contextRoot: string;
14
+ inferModuleTypeFromPath: (filePath: string) => SourceDescription['moduleType'];
15
+ }): Promise<LoadResult | undefined>;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { applySourceTransforms, normalizeTransformId } from "../plugins/source-transform.js";
4
+ const TRANSFORMABLE_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mdx"]);
5
+ function shouldApplySourceTransforms(namespace, sourcePath) {
6
+ if (namespace !== void 0) {
7
+ return false;
8
+ }
9
+ const normalizedPath = normalizeTransformId(sourcePath);
10
+ return TRANSFORMABLE_SOURCE_EXTENSIONS.has(path.extname(normalizedPath).toLowerCase());
11
+ }
12
+ function shouldTransformLoadedModule(moduleType) {
13
+ return moduleType !== "css" && moduleType !== "asset";
14
+ }
15
+ function applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, code) {
16
+ if (sourceTransforms.length === 0) {
17
+ return code;
18
+ }
19
+ return applySourceTransforms(sourceTransforms, code, sourcePath);
20
+ }
21
+ async function finalizeLoadResultWithSourceTransforms(options) {
22
+ const { id, namespace, sourcePath, loadResult, sourceTransforms, contextRoot, inferModuleTypeFromPath } = options;
23
+ if (!shouldApplySourceTransforms(namespace, sourcePath)) {
24
+ return loadResult;
25
+ }
26
+ if (typeof loadResult === "object" && loadResult !== null && "code" in loadResult && typeof loadResult.code === "string") {
27
+ if (!shouldTransformLoadedModule(loadResult.moduleType)) {
28
+ return loadResult;
29
+ }
30
+ const transformedCode2 = applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, loadResult.code);
31
+ if (transformedCode2 === loadResult.code) {
32
+ return loadResult;
33
+ }
34
+ return {
35
+ code: transformedCode2,
36
+ moduleType: loadResult.moduleType
37
+ };
38
+ }
39
+ if (loadResult !== void 0 || sourceTransforms.length === 0) {
40
+ return loadResult;
41
+ }
42
+ const normalizedPath = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(contextRoot, sourcePath);
43
+ if (!fileSystem.exists(normalizedPath)) {
44
+ return loadResult;
45
+ }
46
+ const originalCode = fileSystem.readFileSync(normalizedPath);
47
+ const transformedCode = applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, originalCode);
48
+ if (transformedCode === originalCode) {
49
+ return loadResult;
50
+ }
51
+ return {
52
+ code: transformedCode,
53
+ moduleType: inferModuleTypeFromPath(normalizedPath)
54
+ };
55
+ }
56
+ export {
57
+ finalizeLoadResultWithSourceTransforms
58
+ };
@@ -250,7 +250,17 @@ export declare class ConfigBuilder {
250
250
  private compareVersions;
251
251
  /**
252
252
  * Initializes default loaders that are required for EcoPages to function.
253
- * This includes the eco-component-meta-plugin which auto-injects __eco metadata into component configs.
253
+ *
254
+ * @remarks
255
+ * `eco-component-meta` is registered twice on purpose:
256
+ *
257
+ * - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
258
+ * runs these after first-wins `onLoad` plugins rewrite module source.
259
+ * - `loaders` keeps the same transform available to server-oriented builds
260
+ * that still use competing `onLoad` handlers directly.
261
+ *
262
+ * Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
263
+ * when the transform name is already present in `sourceTransforms`.
254
264
  */
255
265
  private initializeDefaultLoaders;
256
266
  private reviewBaseUrl;
@@ -547,7 +547,17 @@ class ConfigBuilder {
547
547
  }
548
548
  /**
549
549
  * Initializes default loaders that are required for EcoPages to function.
550
- * This includes the eco-component-meta-plugin which auto-injects __eco metadata into component configs.
550
+ *
551
+ * @remarks
552
+ * `eco-component-meta` is registered twice on purpose:
553
+ *
554
+ * - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
555
+ * runs these after first-wins `onLoad` plugins rewrite module source.
556
+ * - `loaders` keeps the same transform available to server-oriented builds
557
+ * that still use competing `onLoad` handlers directly.
558
+ *
559
+ * Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
560
+ * when the transform name is already present in `sourceTransforms`.
551
561
  */
552
562
  async initializeDefaultLoaders() {
553
563
  const componentMetaTransform = createEcoComponentMetaTransform({ config: this.config });
@@ -4,9 +4,21 @@ export interface EcoSourceTransformResult {
4
4
  code: string;
5
5
  map?: unknown;
6
6
  }
7
+ /**
8
+ * Bundler-neutral source transform registered on {@link EcoPagesAppConfig.sourceTransforms}.
9
+ *
10
+ * @remarks
11
+ * Prefer this shape over a competing {@link EcoBuildPlugin} `onLoad` handler when
12
+ * the transform only rewrites module source. Browser/HMR builds run source
13
+ * transforms after first-wins `onLoad` plugins, so metadata injection and similar
14
+ * passes still run on rewritten output from boundary/runtime plugins.
15
+ */
7
16
  export interface EcoSourceTransform {
17
+ /** Stable transform name. Also used to dedupe loader plugins in browser builds. */
8
18
  name: string;
19
+ /** File-path filter tested against {@link normalizeTransformId | normalized ids}. */
9
20
  filter: RegExp;
21
+ /** Runs before default transforms, or after them when set to `post`. */
10
22
  enforce?: 'pre' | 'post';
11
23
  transform(code: string, id: string): EcoSourceTransformResult | string | undefined;
12
24
  }
@@ -24,8 +36,27 @@ export declare function normalizeTransformId(id: string): string;
24
36
  * Applies one source transform if the normalized id matches its filter.
25
37
  */
26
38
  export declare function applySourceTransform(transform: EcoSourceTransform, code: string, id: string): EcoSourceTransformResult | string | undefined;
39
+ /**
40
+ * Applies app-owned source transforms in deterministic `pre` → default → `post` order.
41
+ *
42
+ * @remarks
43
+ * Used by the Rolldown plugin bridge after `onLoad` plugins produce final module
44
+ * contents. Transforms that do not match `filter` are skipped; matching transforms
45
+ * are chained left-to-right on the current source string.
46
+ *
47
+ * @param transforms - App-owned transforms, usually from {@link getAppSourceTransforms}.
48
+ * @param code - Current module source.
49
+ * @param id - Module id forwarded to each transform after query/hash normalization.
50
+ * @returns The transformed source, or the original `code` when no transform matches.
51
+ */
52
+ export declare function applySourceTransforms(transforms: readonly EcoSourceTransform[], code: string, id: string): string;
27
53
  /**
28
54
  * Adapts a source transform into the existing Ecopages build-plugin contract.
55
+ *
56
+ * @remarks
57
+ * Server-oriented builds and loader registration still use this adapter.
58
+ * Browser/HMR builds should register the transform in `appConfig.sourceTransforms`
59
+ * instead so the Rolldown bridge can run it after competing `onLoad` plugins.
29
60
  */
30
61
  export declare function createEcoBuildPluginFromSourceTransform(transform: EcoSourceTransform): EcoBuildPlugin;
31
62
  /**
@@ -13,6 +13,27 @@ function applySourceTransform(transform, code, id) {
13
13
  }
14
14
  return transform.transform(code, normalizedId);
15
15
  }
16
+ const SOURCE_TRANSFORM_ENFORCE_ORDER = {
17
+ pre: 0,
18
+ default: 1,
19
+ post: 2
20
+ };
21
+ function getSourceTransformEnforceOrder(transform) {
22
+ return SOURCE_TRANSFORM_ENFORCE_ORDER[transform.enforce ?? "default"];
23
+ }
24
+ function applySourceTransforms(transforms, code, id) {
25
+ let current = code;
26
+ for (const transform of [...transforms].sort(
27
+ (left, right) => getSourceTransformEnforceOrder(left) - getSourceTransformEnforceOrder(right)
28
+ )) {
29
+ const result = applySourceTransform(transform, current, id);
30
+ if (!result) {
31
+ continue;
32
+ }
33
+ current = typeof result === "string" ? result : result.code;
34
+ }
35
+ return current;
36
+ }
16
37
  function inferLoaderFromPath(filePath) {
17
38
  const extension = path.extname(filePath).toLowerCase();
18
39
  switch (extension) {
@@ -56,13 +77,14 @@ function createVitePluginFromSourceTransform(transform) {
56
77
  };
57
78
  }
58
79
  function getAppSourceTransforms(appConfig) {
59
- return Array.from(appConfig.sourceTransforms.values());
80
+ return appConfig.sourceTransforms ? Array.from(appConfig.sourceTransforms.values()) : [];
60
81
  }
61
82
  function createVitePluginsFromAppSourceTransforms(appConfig) {
62
83
  return getAppSourceTransforms(appConfig).map((transform) => createVitePluginFromSourceTransform(transform));
63
84
  }
64
85
  export {
65
86
  applySourceTransform,
87
+ applySourceTransforms,
66
88
  createEcoBuildPluginFromSourceTransform,
67
89
  createVitePluginFromSourceTransform,
68
90
  createVitePluginsFromAppSourceTransforms,
@@ -66,7 +66,8 @@ export declare class BrowserBundleService implements BrowserBundleExecutor {
66
66
  * @remarks
67
67
  * Browser defaults and app-owned browser build plugins are applied here so HMR
68
68
  * and runtime asset generation do not have to recreate that policy at each call
69
- * site.
69
+ * site. Also forwards {@link getAppSourceTransforms | app source transforms} so
70
+ * metadata injection runs after boundary/runtime `onLoad` rewrites.
70
71
  */
71
72
  bundle(options: BrowserBundleOptions): Promise<BuildResult>;
72
73
  bundleGroupedEntries(entries: BrowserBundleGroupedEntry[], options: BrowserBundleGroupedOptions): Promise<BuildResult>;
@@ -5,6 +5,7 @@ import {
5
5
  getAppTranspileOptions
6
6
  } from "../../build/build-adapter.js";
7
7
  import { mergeEcoBuildPlugins } from "../../build/build-manifest.js";
8
+ import { getAppSourceTransforms } from "../../plugins/source-transform.js";
8
9
  class BrowserBundleService {
9
10
  appConfig;
10
11
  /**
@@ -19,7 +20,8 @@ class BrowserBundleService {
19
20
  * @remarks
20
21
  * Browser defaults and app-owned browser build plugins are applied here so HMR
21
22
  * and runtime asset generation do not have to recreate that policy at each call
22
- * site.
23
+ * site. Also forwards {@link getAppSourceTransforms | app source transforms} so
24
+ * metadata injection runs after boundary/runtime `onLoad` rewrites.
23
25
  */
24
26
  async bundle(options) {
25
27
  const { profile, excludeAppBuildPlugins, plugins, executor = "hmr", ...rawBuildOptions } = options;
@@ -29,7 +31,8 @@ class BrowserBundleService {
29
31
  ...rawBuildOptions,
30
32
  entrypoints: options.entrypoints,
31
33
  ...getAppTranspileOptions(this.appConfig, profile),
32
- plugins: mergeEcoBuildPlugins(plugins, filteredAppBrowserPlugins)
34
+ plugins: mergeEcoBuildPlugins(plugins, filteredAppBrowserPlugins),
35
+ sourceTransforms: getAppSourceTransforms(this.appConfig)
33
36
  };
34
37
  const buildExecutor = executor === "build" ? getAppBuildExecutor(this.appConfig) : getAppHmrBuildExecutor(this.appConfig);
35
38
  return await buildExecutor.build(request);