@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
@@ -0,0 +1,64 @@
1
+ import type { BuildExecutor, BuildOptions, BuildResult } from './build-adapter.js';
2
+ /**
3
+ * FIFO-serialized build executor wrapper.
4
+ *
5
+ * @remarks
6
+ * Wraps any `BuildExecutor` and ensures that no two builds run
7
+ * concurrently. The next build waits for the previous one to finish
8
+ * (success or failure) before starting.
9
+ *
10
+ * This is a bundler-agnostic primitive. It is unaware of bundler
11
+ * lifecycle events and exists purely to enforce one build at a time.
12
+ * Use it from the dev watch pipeline, the static preview path, or any
13
+ * caller that issues builds concurrently.
14
+ */
15
+ /**
16
+ * FIFO-serialized wrapper around a {@link BuildExecutor}.
17
+ *
18
+ * Every `build()` call enqueues onto the wrapper's internal queue. A
19
+ * build only starts after the previous build's promise has settled.
20
+ */
21
+ export declare class SerializedBuildExecutor implements BuildExecutor {
22
+ private readonly inner;
23
+ private tail;
24
+ constructor(inner: BuildExecutor);
25
+ /**
26
+ * Run a build through the serialized queue. The returned promise
27
+ * resolves with the inner executor's `BuildResult`. A rejected
28
+ * inner build does not block the queue — the next caller can
29
+ * proceed.
30
+ */
31
+ build(options: BuildOptions): Promise<BuildResult>;
32
+ /**
33
+ * Run an arbitrary async operation through the serialized queue.
34
+ *
35
+ * Use this when the work is not a `build()` call against a wrapped
36
+ * executor (e.g. when composing with a coordinator that needs to
37
+ * inject recovery around the inner call). The returned promise
38
+ * resolves with the operation's result. A rejected operation does
39
+ * not block the queue.
40
+ */
41
+ run<T>(operation: () => Promise<T>): Promise<T>;
42
+ /**
43
+ * Returns the wrapped executor. Tests use this to assert the
44
+ * delegation chain.
45
+ */
46
+ unwrap(): BuildExecutor;
47
+ /**
48
+ * Reset the queue to an empty state. Tests use this to recover
49
+ * from a poisoned tail.
50
+ */
51
+ resetForTests(): void;
52
+ /**
53
+ * Overrides the internal queue tail for fault-recovery tests. The
54
+ * supplied promise is awaited before the next enqueued operation
55
+ * starts.
56
+ */
57
+ setBuildQueueForTests(queue: Promise<unknown>): void;
58
+ /**
59
+ * Returns the current internal queue tail for fault-recovery
60
+ * tests. Tests await this to assert that recovery has cleared a
61
+ * wedged queue.
62
+ */
63
+ getBuildQueueForTests(): Promise<unknown>;
64
+ }
@@ -0,0 +1,63 @@
1
+ class SerializedBuildExecutor {
2
+ inner;
3
+ tail = Promise.resolve();
4
+ constructor(inner) {
5
+ this.inner = inner;
6
+ }
7
+ /**
8
+ * Run a build through the serialized queue. The returned promise
9
+ * resolves with the inner executor's `BuildResult`. A rejected
10
+ * inner build does not block the queue — the next caller can
11
+ * proceed.
12
+ */
13
+ build(options) {
14
+ return this.run(() => this.inner.build(options));
15
+ }
16
+ /**
17
+ * Run an arbitrary async operation through the serialized queue.
18
+ *
19
+ * Use this when the work is not a `build()` call against a wrapped
20
+ * executor (e.g. when composing with a coordinator that needs to
21
+ * inject recovery around the inner call). The returned promise
22
+ * resolves with the operation's result. A rejected operation does
23
+ * not block the queue.
24
+ */
25
+ run(operation) {
26
+ const next = this.tail.catch(() => void 0).then(() => operation());
27
+ this.tail = next.catch(() => void 0);
28
+ return next;
29
+ }
30
+ /**
31
+ * Returns the wrapped executor. Tests use this to assert the
32
+ * delegation chain.
33
+ */
34
+ unwrap() {
35
+ return this.inner;
36
+ }
37
+ /**
38
+ * Reset the queue to an empty state. Tests use this to recover
39
+ * from a poisoned tail.
40
+ */
41
+ resetForTests() {
42
+ this.tail = Promise.resolve();
43
+ }
44
+ /**
45
+ * Overrides the internal queue tail for fault-recovery tests. The
46
+ * supplied promise is awaited before the next enqueued operation
47
+ * starts.
48
+ */
49
+ setBuildQueueForTests(queue) {
50
+ this.tail = queue;
51
+ }
52
+ /**
53
+ * Returns the current internal queue tail for fault-recovery
54
+ * tests. Tests await this to assert that recovery has cleared a
55
+ * wedged queue.
56
+ */
57
+ getBuildQueueForTests() {
58
+ return this.tail;
59
+ }
60
+ }
61
+ export {
62
+ SerializedBuildExecutor
63
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Server-side CSS shim plugin for the bundler adapter.
3
+ *
4
+ * @remarks
5
+ * Page modules imported by the server (so the framework can read
6
+ * `getStaticProps` / `staticPaths` / page metadata) commonly have
7
+ * `import './style.css'` at the top. The bundler does not bundle
8
+ * CSS, and the server only needs the JS exports — the CSS itself
9
+ * is delivered to the browser via the page's `dependencies.stylesheets`
10
+ * declarations. This plugin:
11
+ *
12
+ * - Resolves `.css` imports to the absolute on-disk path.
13
+ * - Loads them and returns an empty ESM module with
14
+ * `moduleSideEffects: true` so the bundler keeps the import in
15
+ * the dependency graph without trying to bundle the bytes.
16
+ *
17
+ * The plugin is added by the adapter on every server-side build.
18
+ * Browser-side builds that want real CSS bundling should keep using
19
+ * a dedicated CSS pipeline (e.g. the postcss processor's output)
20
+ * and continue to declare stylesheets in `dependencies.stylesheets`
21
+ * — the shim is intentionally inert for those flows.
22
+ */
23
+ type RolldownPluginLike = {
24
+ name: string;
25
+ resolveId: (source: string, importer: string | undefined) => Promise<string | null> | string | null;
26
+ load: (id: string) => Promise<{
27
+ code: string;
28
+ moduleType: 'js';
29
+ moduleSideEffects: boolean;
30
+ } | null> | {
31
+ code: string;
32
+ moduleType: 'js';
33
+ moduleSideEffects: boolean;
34
+ } | null;
35
+ };
36
+ /**
37
+ * Builds a Rolldown plugin that turns `.css` imports into no-op ESM
38
+ * modules while preserving the dependency graph.
39
+ */
40
+ export declare function createServerSideCssShimPlugin(): RolldownPluginLike;
41
+ export {};
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const CSS_PATH = /\.css$/u;
4
+ const CSS_QUERY = "?ecopages-css-shim";
5
+ function stripShimQuery(id) {
6
+ const queryIndex = id.indexOf("?");
7
+ return queryIndex === -1 ? id : id.slice(0, queryIndex);
8
+ }
9
+ function createServerSideCssShimPlugin() {
10
+ return {
11
+ name: "ecopages:server-side-css-shim",
12
+ async resolveId(source, importer) {
13
+ if (!CSS_PATH.test(source)) {
14
+ return null;
15
+ }
16
+ const resolved = importer ? path.resolve(path.dirname(importer), source) : path.resolve(source);
17
+ return existsSync(resolved) ? `${resolved}${CSS_QUERY}` : null;
18
+ },
19
+ async load(id) {
20
+ if (!id.endsWith(CSS_QUERY)) {
21
+ return null;
22
+ }
23
+ const realPath = stripShimQuery(id);
24
+ readFileSync(realPath, "utf-8");
25
+ return {
26
+ code: 'export default "";',
27
+ moduleType: "js",
28
+ moduleSideEffects: true
29
+ };
30
+ }
31
+ };
32
+ }
33
+ export {
34
+ createServerSideCssShimPlugin
35
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public cache exports for the bundler plugin chain.
3
+ *
4
+ * @module @ecopages/core/cache
5
+ */
6
+ export { ModuleParseCache, moduleParseCache, cachedParseSync, type ModuleParseOptions } from './module-parse-cache.js';
@@ -0,0 +1,6 @@
1
+ import { ModuleParseCache, moduleParseCache, cachedParseSync } from "./module-parse-cache.js";
2
+ export {
3
+ ModuleParseCache,
4
+ cachedParseSync,
5
+ moduleParseCache
6
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Process-wide parse cache for `oxc-parser.parseSync`.
3
+ *
4
+ * @remarks
5
+ * Several build plugins (eco-component-meta, client-graph-boundary,
6
+ * browser-runtime) each call `parseSync` on the same source file with
7
+ * overlapping options. This cache memoizes the parse result keyed by
8
+ * `(absolute path, source, options)`. When the source is unchanged,
9
+ * subsequent calls return the cached result without re-parsing.
10
+ *
11
+ * The cache is LRU-bounded (10 000 entries) and key-stable across a
12
+ * single HMR session. It is **content-hashed** rather than
13
+ * mtime-hashed so that `touch`/`utimes` does not invalidate a
14
+ * still-valid parse.
15
+ */
16
+ import { type ParseResult, type ParserOptions } from 'oxc-parser';
17
+ export type ModuleParseOptions = ParserOptions & {
18
+ /**
19
+ * Optional parser language override. If omitted, derived from the file
20
+ * extension at lookup time. Set this explicitly if your caller already
21
+ * computed the language (avoids re-deriving inside the cache).
22
+ */
23
+ lang?: ParserOptions['lang'];
24
+ };
25
+ /**
26
+ * LRU-bounded module parse cache.
27
+ *
28
+ * Single instance shared across the process. Constructed lazily; use
29
+ * {@link moduleParseCache} for the default shared instance.
30
+ */
31
+ export declare class ModuleParseCache {
32
+ private readonly entries;
33
+ private readonly maxEntries;
34
+ private hits;
35
+ private misses;
36
+ constructor(maxEntries?: number);
37
+ /**
38
+ * Parse `source` for `filePath`, memoizing by (filePath, source, options).
39
+ *
40
+ * @returns the {@link ParseResult} from `oxc-parser.parseSync`.
41
+ */
42
+ getOrParse(filePath: string, source: string, options?: ModuleParseOptions): ParseResult;
43
+ /** Clear all cached entries. Useful in tests and on full-rebuild signals. */
44
+ clear(): void;
45
+ /** Current cache size (for observability). */
46
+ get size(): number;
47
+ /** Cumulative hit/miss counters (for observability). */
48
+ stats(): {
49
+ hits: number;
50
+ misses: number;
51
+ size: number;
52
+ hitRate: number;
53
+ };
54
+ }
55
+ /**
56
+ * Default shared cache. Use this from plugin code so the cache is
57
+ * amortized across plugins and build invocations.
58
+ */
59
+ export declare const moduleParseCache: ModuleParseCache;
60
+ /**
61
+ * Drop-in replacement for `oxc-parser.parseSync` that uses the shared
62
+ * {@link moduleParseCache}. Use everywhere we currently call `parseSync`
63
+ * on user/source files during a build.
64
+ */
65
+ export declare function cachedParseSync(filePath: string, source: string, options?: ModuleParseOptions): ParseResult;
@@ -0,0 +1,75 @@
1
+ import { parseSync } from "oxc-parser";
2
+ import { rapidhash } from "../utils/hash.js";
3
+ const DEFAULT_MAX_ENTRIES = 1e4;
4
+ class ModuleParseCache {
5
+ entries = /* @__PURE__ */ new Map();
6
+ maxEntries;
7
+ hits = 0;
8
+ misses = 0;
9
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
10
+ if (maxEntries <= 0) {
11
+ throw new Error(`ModuleParseCache: maxEntries must be > 0, got ${maxEntries}`);
12
+ }
13
+ this.maxEntries = maxEntries;
14
+ }
15
+ /**
16
+ * Parse `source` for `filePath`, memoizing by (filePath, source, options).
17
+ *
18
+ * @returns the {@link ParseResult} from `oxc-parser.parseSync`.
19
+ */
20
+ getOrParse(filePath, source, options = {}) {
21
+ const hash = rapidhash(source);
22
+ const key = makeKey(filePath, hash, options);
23
+ const existing = this.entries.get(key);
24
+ if (existing && existing.hash === hash) {
25
+ this.hits += 1;
26
+ this.entries.delete(key);
27
+ this.entries.set(key, existing);
28
+ return existing.result;
29
+ }
30
+ this.misses += 1;
31
+ const result = parseSync(filePath, source, options);
32
+ this.entries.set(key, { hash, result });
33
+ if (this.entries.size > this.maxEntries) {
34
+ const oldestKey = this.entries.keys().next().value;
35
+ if (oldestKey !== void 0) {
36
+ this.entries.delete(oldestKey);
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ /** Clear all cached entries. Useful in tests and on full-rebuild signals. */
42
+ clear() {
43
+ this.entries.clear();
44
+ this.hits = 0;
45
+ this.misses = 0;
46
+ }
47
+ /** Current cache size (for observability). */
48
+ get size() {
49
+ return this.entries.size;
50
+ }
51
+ /** Cumulative hit/miss counters (for observability). */
52
+ stats() {
53
+ const total = this.hits + this.misses;
54
+ return {
55
+ hits: this.hits,
56
+ misses: this.misses,
57
+ size: this.entries.size,
58
+ hitRate: total === 0 ? 0 : this.hits / total
59
+ };
60
+ }
61
+ }
62
+ const moduleParseCache = new ModuleParseCache();
63
+ function cachedParseSync(filePath, source, options = {}) {
64
+ return moduleParseCache.getOrParse(filePath, source, options);
65
+ }
66
+ function makeKey(filePath, sourceHash, options) {
67
+ const lang = options.lang ?? "";
68
+ const sourceType = options.sourceType ?? "";
69
+ return `${filePath} ${sourceHash.toString(36)} ${lang} ${sourceType}`;
70
+ }
71
+ export {
72
+ ModuleParseCache,
73
+ cachedParseSync,
74
+ moduleParseCache
75
+ };
@@ -27,7 +27,7 @@ It is responsible for:
27
27
  - `ConfigBuilder.build()` decides ordering, validates compatibility, and seals build ownership for the finalized app config.
28
28
  - Runtime startup reuses finalized config/build state; it should not recompute manifest ownership.
29
29
 
30
- Bun-native is the default ownership path. Vite-host ownership is explicit and should be selected during config construction when a host-driven compatibility flow must avoid silently falling back to Bun build execution.
30
+ App-owned is the default ownership path. Host-owned is explicit and should be selected during config construction when a host-driven compatibility flow must avoid silently falling back to app build execution.
31
31
 
32
32
  ## Output
33
33
 
@@ -66,9 +66,9 @@ export declare class ConfigBuilder {
66
66
  * Sets which runtime path owns build execution for the finalized app config.
67
67
  *
68
68
  * @remarks
69
- * Bun-native remains the default. Vite-host ownership should be selected only
70
- * for host-driven compatibility flows where core must not silently fall back to
71
- * Bun build execution.
69
+ * The app-owned build path is the default. The host-owned path should be
70
+ * selected only for host-driven compatibility flows where core must not
71
+ * silently fall back to app build execution.
72
72
  */
73
73
  setBuildOwnership(buildOwnership: BuildOwnership): this;
74
74
  /**
@@ -8,12 +8,10 @@ import {
8
8
  import {
9
9
  collectConfiguredAppBuildManifestContributions,
10
10
  createBuildAdapter,
11
- getAppServerBuildPlugins,
12
11
  setAppBuildAdapter,
13
12
  setAppBuildExecutor,
14
13
  updateAppBuildManifest
15
14
  } from "../build/build-adapter.js";
16
- import { createAppBuildExecutor } from "../build/dev-build-coordinator.js";
17
15
  import { GHTML_PLUGIN_NAME } from "../integrations/ghtml/ghtml.constants.js";
18
16
  import { ghtmlPlugin } from "../integrations/ghtml/ghtml.plugin.js";
19
17
  import { createEcoComponentMetaPlugin } from "../plugins/eco-component-meta-plugin.js";
@@ -41,7 +39,7 @@ const CONFIG_BUILDER_ERRORS = {
41
39
  invalidRuntimeVersion: (kind, name, version) => `Cannot validate ${kind} "${name}" runtimeCapability.minRuntimeVersion "${version}" because it is not a dot-separated numeric version`
42
40
  };
43
41
  class ConfigBuilder {
44
- buildOwnership = "bun-native";
42
+ buildOwnership = "rolldown";
45
43
  config = {
46
44
  baseUrl: "",
47
45
  rootDir: ".",
@@ -110,9 +108,9 @@ class ConfigBuilder {
110
108
  * Sets which runtime path owns build execution for the finalized app config.
111
109
  *
112
110
  * @remarks
113
- * Bun-native remains the default. Vite-host ownership should be selected only
114
- * for host-driven compatibility flows where core must not silently fall back to
115
- * Bun build execution.
111
+ * The app-owned build path is the default. The host-owned path should be
112
+ * selected only for host-driven compatibility flows where core must not
113
+ * silently fall back to app build execution.
116
114
  */
117
115
  setBuildOwnership(buildOwnership) {
118
116
  this.buildOwnership = buildOwnership;
@@ -581,14 +579,7 @@ class ConfigBuilder {
581
579
  updateAppBuildManifest(this.config, await collectConfiguredAppBuildManifestContributions(this.config));
582
580
  setAppServerInvalidationState(this.config, new CounterServerInvalidationState());
583
581
  setAppEntrypointDependencyGraph(this.config, new NoopEntrypointDependencyGraph());
584
- setAppBuildExecutor(
585
- this.config,
586
- createAppBuildExecutor({
587
- development: false,
588
- adapter: buildAdapter,
589
- getPlugins: () => getAppServerBuildPlugins(this.config)
590
- })
591
- );
582
+ setAppBuildExecutor(this.config, buildAdapter);
592
583
  return this.config;
593
584
  }
594
585
  }
@@ -136,11 +136,8 @@ interface PageOptionsWithMiddleware<T, E = EcoPagesElement> extends PageOptionsB
136
136
  /**
137
137
  * Options for creating a page with eco.page()
138
138
  *
139
- * Supports two patterns:
140
- * 1. **Consolidated API** (recommended): Define staticPaths, staticProps, and metadata inline
141
- * 2. **Separate exports** (legacy): Export getStaticPaths, getStaticProps, getMetadata separately
142
- *
143
- * When using `middleware`, `cache` must be set to `'dynamic'` because middleware
139
+ * Define staticPaths, staticProps, and metadata inline. When using
140
+ * `middleware`, `cache` must be set to `'dynamic'` because middleware
144
141
  * runs on every request and caching would bypass middleware effects.
145
142
  *
146
143
  * @template T - The props type for the page
@@ -111,13 +111,13 @@ export declare class JsHmrStrategy extends HmrStrategy {
111
111
  * @remarks
112
112
  * If runtime-specific dependency graph hooks are unavailable, this strategy
113
113
  * falls back to rebuilding all watched entrypoints.
114
- * When multiple entrypoints are impacted they are bundled in a single esbuild
114
+ * When multiple entrypoints are impacted they are bundled in a single
115
115
  * invocation to share AST parsing and chunk deduplication.
116
116
  * @returns Action to broadcast update events
117
117
  */
118
118
  process(filePath: string): Promise<HmrAction>;
119
119
  /**
120
- * Bundles one or more entrypoints in a single esbuild invocation.
120
+ * Bundles one or more entrypoints in a single build invocation.
121
121
  * Uses the source directory as the output base so that the directory structure
122
122
  * is preserved under the HMR dist folder.
123
123
  */
@@ -46,7 +46,7 @@ class JsHmrStrategy extends HmrStrategy {
46
46
  * @remarks
47
47
  * If runtime-specific dependency graph hooks are unavailable, this strategy
48
48
  * falls back to rebuilding all watched entrypoints.
49
- * When multiple entrypoints are impacted they are bundled in a single esbuild
49
+ * When multiple entrypoints are impacted they are bundled in a single
50
50
  * invocation to share AST parsing and chunk deduplication.
51
51
  * @returns Action to broadcast update events
52
52
  */
@@ -114,7 +114,7 @@ class JsHmrStrategy extends HmrStrategy {
114
114
  return { type: "none" };
115
115
  }
116
116
  /**
117
- * Bundles one or more entrypoints in a single esbuild invocation.
117
+ * Bundles one or more entrypoints in a single build invocation.
118
118
  * Uses the source directory as the output base so that the directory structure
119
119
  * is preserved under the HMR dist folder.
120
120
  */
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Per-plugin cache for `@/...` alias resolution.
3
+ *
4
+ * @remarks
5
+ * The `ecopages-alias-resolver` plugin resolves project aliases like
6
+ * `@/components/Button` to concrete file paths under the app's `srcDir`.
7
+ * Each resolution can trigger up to 22 `existsSync` calls and (when
8
+ * the resolved path is a barrel) a `readFileSync` to detect
9
+ * `export * from './...'` forwarding.
10
+ *
11
+ * This cache memoizes the result keyed by `(srcDir, specifier)`. It is
12
+ * process-local and not currently invalidated by the file watcher —
13
+ * `srcDir` contents are assumed to be stable for the lifetime of the
14
+ * process.
15
+ */
16
+ export declare class AliasResolverCache {
17
+ private readonly entries;
18
+ private readonly maxEntries;
19
+ private hits;
20
+ private misses;
21
+ constructor(maxEntries?: number);
22
+ /**
23
+ * Look up a previously-computed resolution for `(srcDir, specifier)`.
24
+ *
25
+ * Returns `{ hit: true, resolved }` on cache hit (resolved may be
26
+ * `undefined` for an unresolvable specifier), or `{ hit: false }` on
27
+ * miss. The caller is responsible for re-running the resolver and
28
+ * calling `set` to store the new entry.
29
+ */
30
+ get(srcDir: string, specifier: string): {
31
+ hit: true;
32
+ resolved: string | undefined;
33
+ } | {
34
+ hit: false;
35
+ };
36
+ /**
37
+ * Store a resolution result.
38
+ *
39
+ * Pass `undefined` for `resolved` to memoize the negative case
40
+ * (specifier did not resolve) and avoid repeating the FS walk on
41
+ * every `@/...` import.
42
+ */
43
+ set(srcDir: string, specifier: string, resolved: string | undefined): void;
44
+ /**
45
+ * Drop all entries whose `srcDir` is under `rootDir`. Use this from
46
+ * the file watcher when files are added or removed under the app's
47
+ * source tree.
48
+ *
49
+ * Path comparison is path-aware (handles both POSIX `/` and
50
+ * Windows `\` separators) via `path.relative` so cached entries
51
+ * normalized on one platform still match the watcher's `rootDir`
52
+ * on another. Without this, a Windows build that sees
53
+ * `entry.srcDir === 'C:\app\src'` would miss
54
+ * `entry.srcDir.startsWith('C:/app/src/')` and leave stale entries.
55
+ */
56
+ invalidateUnder(rootDir: string): number;
57
+ /** Clear all entries. */
58
+ clear(): void;
59
+ /** Current cache size. */
60
+ get size(): number;
61
+ /** Hit/miss counters for observability. */
62
+ stats(): {
63
+ hits: number;
64
+ misses: number;
65
+ size: number;
66
+ hitRate: number;
67
+ };
68
+ }
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ const DEFAULT_MAX_ENTRIES = 2e3;
3
+ class AliasResolverCache {
4
+ entries = /* @__PURE__ */ new Map();
5
+ maxEntries;
6
+ hits = 0;
7
+ misses = 0;
8
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
9
+ if (maxEntries <= 0) {
10
+ throw new Error(`AliasResolverCache: maxEntries must be > 0, got ${maxEntries}`);
11
+ }
12
+ this.maxEntries = maxEntries;
13
+ }
14
+ /**
15
+ * Look up a previously-computed resolution for `(srcDir, specifier)`.
16
+ *
17
+ * Returns `{ hit: true, resolved }` on cache hit (resolved may be
18
+ * `undefined` for an unresolvable specifier), or `{ hit: false }` on
19
+ * miss. The caller is responsible for re-running the resolver and
20
+ * calling `set` to store the new entry.
21
+ */
22
+ get(srcDir, specifier) {
23
+ const key = makeKey(srcDir, specifier);
24
+ const existing = this.entries.get(key);
25
+ if (existing) {
26
+ this.hits += 1;
27
+ this.entries.delete(key);
28
+ this.entries.set(key, existing);
29
+ return { hit: true, resolved: existing.resolved };
30
+ }
31
+ this.misses += 1;
32
+ return { hit: false };
33
+ }
34
+ /**
35
+ * Store a resolution result.
36
+ *
37
+ * Pass `undefined` for `resolved` to memoize the negative case
38
+ * (specifier did not resolve) and avoid repeating the FS walk on
39
+ * every `@/...` import.
40
+ */
41
+ set(srcDir, specifier, resolved) {
42
+ const key = makeKey(srcDir, specifier);
43
+ this.entries.set(key, { srcDir, resolved });
44
+ if (this.entries.size > this.maxEntries) {
45
+ const oldestKey = this.entries.keys().next().value;
46
+ if (oldestKey !== void 0) {
47
+ this.entries.delete(oldestKey);
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Drop all entries whose `srcDir` is under `rootDir`. Use this from
53
+ * the file watcher when files are added or removed under the app's
54
+ * source tree.
55
+ *
56
+ * Path comparison is path-aware (handles both POSIX `/` and
57
+ * Windows `\` separators) via `path.relative` so cached entries
58
+ * normalized on one platform still match the watcher's `rootDir`
59
+ * on another. Without this, a Windows build that sees
60
+ * `entry.srcDir === 'C:\app\src'` would miss
61
+ * `entry.srcDir.startsWith('C:/app/src/')` and leave stale entries.
62
+ */
63
+ invalidateUnder(rootDir) {
64
+ const normalizedRoot = path.resolve(rootDir);
65
+ let removed = 0;
66
+ for (const [key, entry] of this.entries) {
67
+ if (isUnderDirectory(path.resolve(entry.srcDir), normalizedRoot)) {
68
+ this.entries.delete(key);
69
+ removed += 1;
70
+ }
71
+ }
72
+ return removed;
73
+ }
74
+ /** Clear all entries. */
75
+ clear() {
76
+ this.entries.clear();
77
+ this.hits = 0;
78
+ this.misses = 0;
79
+ }
80
+ /** Current cache size. */
81
+ get size() {
82
+ return this.entries.size;
83
+ }
84
+ /** Hit/miss counters for observability. */
85
+ stats() {
86
+ const total = this.hits + this.misses;
87
+ return {
88
+ hits: this.hits,
89
+ misses: this.misses,
90
+ size: this.entries.size,
91
+ hitRate: total === 0 ? 0 : this.hits / total
92
+ };
93
+ }
94
+ }
95
+ function makeKey(srcDir, specifier) {
96
+ return `${srcDir} ${specifier}`;
97
+ }
98
+ function isUnderDirectory(candidate, parent) {
99
+ if (candidate === parent) return true;
100
+ const rel = path.relative(parent, candidate);
101
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return false;
102
+ return true;
103
+ }
104
+ export {
105
+ AliasResolverCache
106
+ };
@@ -1,3 +1,6 @@
1
1
  import type { EcoBuildPlugin } from '../build/build-types.js';
2
+ import { AliasResolverCache } from './alias-resolver-cache.js';
2
3
  export declare function resolveAppSourceAliasPath(srcDir: string, specifier: string): string | undefined;
3
- export declare function createAliasResolverPlugin(srcDir: string): EcoBuildPlugin;
4
+ export declare function createAliasResolverPlugin(srcDir: string, options?: {
5
+ cache?: AliasResolverCache;
6
+ }): EcoBuildPlugin;