@ecopages/core 0.2.0-beta.23 → 0.2.0-beta.25

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 (49) hide show
  1. package/package.json +27 -2
  2. package/src/adapters/abstract/application-adapter.js +3 -0
  3. package/src/adapters/bun/create-app.js +2 -0
  4. package/src/adapters/node/create-app.js +2 -0
  5. package/src/adapters/shared/server-adapter.js +17 -9
  6. package/src/build/app-build-manifest-runtime.js +12 -1
  7. package/src/build/build-adapter.js +4 -4
  8. package/src/build/dev-browser-script-cache.d.ts +34 -0
  9. package/src/build/dev-browser-script-cache.js +90 -0
  10. package/src/build/lit-static-render-worker-context.d.ts +2 -0
  11. package/src/build/lit-static-render-worker-context.js +6 -0
  12. package/src/build/rolldown-adapter-helpers.js +4 -3
  13. package/src/build/runtime-build-output-normalizer.js +2 -4
  14. package/src/diagnostics/request-build-dedupe.d.ts +18 -0
  15. package/src/diagnostics/request-build-dedupe.js +33 -0
  16. package/src/diagnostics/startup-trace.d.ts +25 -0
  17. package/src/diagnostics/startup-trace.js +121 -0
  18. package/src/eco/eco.types.d.ts +12 -1
  19. package/src/env.d.ts +2 -0
  20. package/src/plugins/alias-resolver-cache.d.ts +6 -9
  21. package/src/plugins/alias-resolver-plugin.d.ts +2 -2
  22. package/src/plugins/alias-resolver-plugin.js +38 -51
  23. package/src/plugins/integration-plugin.d.ts +8 -0
  24. package/src/plugins/tsconfig-import-resolver.d.ts +18 -0
  25. package/src/plugins/tsconfig-import-resolver.js +159 -0
  26. package/src/route-renderer/orchestration/page-browser-graph.service.js +15 -9
  27. package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +6 -0
  28. package/src/route-renderer/page-loading/ecopages-virtual-imports.js +7 -2
  29. package/src/route-renderer/page-loading/module-declaration-aggregation.js +4 -0
  30. package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +3 -0
  31. package/src/services/assets/asset-processing-service/asset-processing.service.js +41 -4
  32. package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.d.ts +52 -0
  33. package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.js +78 -0
  34. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +13 -18
  35. package/src/services/assets/asset-processing-service/finalize-processed-asset.d.ts +3 -0
  36. package/src/services/assets/asset-processing-service/finalize-processed-asset.js +7 -0
  37. package/src/services/assets/asset-processing-service/grouped-content-bundles.d.ts +2 -1
  38. package/src/services/assets/asset-processing-service/grouped-content-bundles.js +20 -10
  39. package/src/services/assets/asset-processing-service/index.d.ts +2 -0
  40. package/src/services/assets/asset-processing-service/index.js +2 -0
  41. package/src/services/assets/asset-processing-service/materialize-content-script-asset.d.ts +11 -0
  42. package/src/services/assets/asset-processing-service/materialize-content-script-asset.js +17 -0
  43. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.d.ts +7 -0
  44. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js +105 -79
  45. package/src/services/assets/asset-processing-service/resolve-integration-plugin.d.ts +4 -0
  46. package/src/services/assets/asset-processing-service/resolve-integration-plugin.js +17 -0
  47. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.d.ts +0 -1
  48. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.js +5 -10
  49. package/src/services/assets/browser-bundle.service.js +10 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-beta.23",
3
+ "version": "0.2.0-beta.25",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-beta.23",
20
+ "@ecopages/file-system": "0.2.0-beta.25",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.134.0",
@@ -26,6 +26,7 @@
26
26
  "esbuild": "^0.28.0",
27
27
  "ghtml": "^4.0.2",
28
28
  "oxc-parser": "^0.124.0",
29
+ "oxc-resolver": "^11.24.2",
29
30
  "rolldown": "^1.1.0",
30
31
  "ws": "^8.20.1",
31
32
  "@standard-schema/spec": "^1.1.0",
@@ -207,6 +208,10 @@
207
208
  "types": "./src/build/jsx-ownership-plugins.d.ts",
208
209
  "default": "./src/build/jsx-ownership-plugins.js"
209
210
  },
211
+ "./build/lit-static-render-worker-context": {
212
+ "types": "./src/build/lit-static-render-worker-context.d.ts",
213
+ "default": "./src/build/lit-static-render-worker-context.js"
214
+ },
210
215
  "./cache": {
211
216
  "types": "./src/cache/index.d.ts",
212
217
  "default": "./src/cache/index.js"
@@ -215,6 +220,14 @@
215
220
  "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
216
221
  "default": "./src/plugins/foreign-jsx-override-plugin.js"
217
222
  },
223
+ "./plugins/alias-resolver-plugin": {
224
+ "types": "./src/plugins/alias-resolver-plugin.d.ts",
225
+ "default": "./src/plugins/alias-resolver-plugin.js"
226
+ },
227
+ "./plugins/tsconfig-import-resolver": {
228
+ "types": "./src/plugins/tsconfig-import-resolver.d.ts",
229
+ "default": "./src/plugins/tsconfig-import-resolver.js"
230
+ },
218
231
  "./plugins/jsx-import-source.utils": {
219
232
  "types": "./src/plugins/jsx-import-source.utils.d.ts",
220
233
  "default": "./src/plugins/jsx-import-source.utils.js"
@@ -416,6 +429,10 @@
416
429
  "types": "./src/build/jsx-ownership-plugins.d.ts",
417
430
  "default": "./src/build/jsx-ownership-plugins.js"
418
431
  },
432
+ "./build/lit-static-render-worker-context.ts": {
433
+ "types": "./src/build/lit-static-render-worker-context.d.ts",
434
+ "default": "./src/build/lit-static-render-worker-context.js"
435
+ },
419
436
  "./cache.ts": {
420
437
  "types": "./src/cache/index.d.ts",
421
438
  "default": "./src/cache/index.js"
@@ -424,6 +441,14 @@
424
441
  "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
425
442
  "default": "./src/plugins/foreign-jsx-override-plugin.js"
426
443
  },
444
+ "./plugins/alias-resolver-plugin.ts": {
445
+ "types": "./src/plugins/alias-resolver-plugin.d.ts",
446
+ "default": "./src/plugins/alias-resolver-plugin.js"
447
+ },
448
+ "./plugins/tsconfig-import-resolver.ts": {
449
+ "types": "./src/plugins/tsconfig-import-resolver.d.ts",
450
+ "default": "./src/plugins/tsconfig-import-resolver.js"
451
+ },
427
452
  "./plugins/jsx-import-source.utils.ts": {
428
453
  "types": "./src/plugins/jsx-import-source.utils.d.ts",
429
454
  "default": "./src/plugins/jsx-import-source.utils.js"
@@ -10,6 +10,7 @@ import {
10
10
  formatRuntimeServerStartedMessage
11
11
  } from "../../dev/runtime-server-started-message.js";
12
12
  import { parseCliArgs } from "../../utils/parse-cli-args.js";
13
+ import { startupTrace } from "../../diagnostics/startup-trace.js";
13
14
  class AbstractApplicationAdapter {
14
15
  appConfig;
15
16
  serverOptions;
@@ -42,6 +43,7 @@ class AbstractApplicationAdapter {
42
43
  setAppHostModuleLoader(this.appConfig, hostModuleLoader);
43
44
  }
44
45
  getAppModuleLoader(this.appConfig);
46
+ startupTrace.markConfigReady();
45
47
  if (options.clearOutput) {
46
48
  this.clearDistFolder().catch((error) => {
47
49
  appLogger.error("Error clearing dist folder", error);
@@ -221,6 +223,7 @@ class AbstractApplicationAdapter {
221
223
  }
222
224
  notifyListening(origin) {
223
225
  const normalizedOrigin = origin.replace(/\/$/, "");
226
+ startupTrace.markServerListening();
224
227
  if (!this.onAppStartCallback) {
225
228
  this.logServerStarted(normalizedOrigin);
226
229
  }
@@ -1,6 +1,7 @@
1
1
  import { appLogger } from "../../global/app-logger.js";
2
2
  import { SharedApplicationAdapter } from "../shared/application-adapter.js";
3
3
  import { resolveRuntimeBinding, resolveStaticRuntimeMode } from "../shared/runtime-app-bootstrap.js";
4
+ import { startupTrace } from "../../diagnostics/startup-trace.js";
4
5
  import { createBunServerAdapter } from "./server-adapter.js";
5
6
  import { BunRuntimeHost } from "./runtime-host.js";
6
7
  import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
@@ -101,6 +102,7 @@ class BunEcopagesApp extends SharedApplicationAdapter {
101
102
  const enableHmr = dev || !preview && !build;
102
103
  const serverOptions = this.serverAdapter.getServerOptions({ enableHmr });
103
104
  const runtimeServerOptions = serverOptions;
105
+ startupTrace.beginServerListen();
104
106
  this.server = await this.runtimeHost.start({
105
107
  serveOptions: runtimeServerOptions,
106
108
  handleRequest: async () => new Response(null, { status: 500 }),
@@ -5,6 +5,7 @@ import { createNodeServerAdapter } from "./server-adapter.js";
5
5
  import { NodeHttpRequestBridge } from "./http-request-bridge.js";
6
6
  import { NodeRuntimeHost } from "./runtime-host.js";
7
7
  import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
8
+ import { startupTrace } from "../../diagnostics/startup-trace.js";
8
9
  class NodeEcopagesApp extends SharedApplicationAdapter {
9
10
  serverAdapter;
10
11
  server = null;
@@ -87,6 +88,7 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
87
88
  return;
88
89
  }
89
90
  const serveOptions = this.serverAdapter.getServerOptions();
91
+ startupTrace.beginServerListen();
90
92
  this.server = await this.runtimeHost.start({
91
93
  serveOptions,
92
94
  handleRequest: async (request) => await this.serverAdapter.handleRequest(request),
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
  import { AbstractServerAdapter } from "../abstract/server-adapter.js";
3
3
  import { RouteRendererFactory } from "../../route-renderer/route-renderer.js";
4
4
  import { RouteRegistry } from "../../router/server/route-registry.js";
5
+ import { startupTrace } from "../../diagnostics/startup-trace.js";
6
+ import { requestBuildDedupe } from "../../diagnostics/request-build-dedupe.js";
5
7
  import { MemoryCacheStore } from "../../services/cache/memory-cache-store.js";
6
8
  import { PageCacheService } from "../../services/cache/page-cache-service.js";
7
9
  import { SchemaValidationService } from "../../services/validation/schema-validation-service.js";
@@ -71,6 +73,7 @@ class SharedServerAdapter extends AbstractServerAdapter {
71
73
  * Web Requests (`Request`) to their corresponding internal execution paths.
72
74
  */
73
75
  async initSharedRouter() {
76
+ startupTrace.markPhaseStart("route-registry");
74
77
  this.router = new RouteRegistry({
75
78
  pagesDir: path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.pagesDir),
76
79
  appConfig: this.appConfig,
@@ -80,6 +83,7 @@ class SharedServerAdapter extends AbstractServerAdapter {
80
83
  pageModuleAdapter: this.createRouteRegistryPageModuleAdapter()
81
84
  });
82
85
  await this.router.init();
86
+ startupTrace.markPhaseEnd("route-registry");
83
87
  }
84
88
  createRouteRegistryPageModuleAdapter() {
85
89
  this.ensureRouteRendererFactory();
@@ -428,15 +432,19 @@ class SharedServerAdapter extends AbstractServerAdapter {
428
432
  * native HTTP objects into Web Standard Requests.
429
433
  */
430
434
  async handleSharedRequest(request, context) {
431
- const hmrResponse = this.tryHandleSharedHmrRequest(request, context);
432
- if (hmrResponse) {
433
- return hmrResponse;
434
- }
435
- const apiResponse = await this.tryHandleSharedApiRequest(request, context);
436
- if (apiResponse) {
437
- return apiResponse;
438
- }
439
- return this.routeHandler.handleResponse(request);
435
+ return requestBuildDedupe.run(
436
+ () => startupTrace.traceFirstRequest(request, async () => {
437
+ const hmrResponse = this.tryHandleSharedHmrRequest(request, context);
438
+ if (hmrResponse) {
439
+ return hmrResponse;
440
+ }
441
+ const apiResponse = await this.tryHandleSharedApiRequest(request, context);
442
+ if (apiResponse) {
443
+ return apiResponse;
444
+ }
445
+ return this.routeHandler.handleResponse(request);
446
+ })
447
+ );
440
448
  }
441
449
  }
442
450
  export {
@@ -1,5 +1,7 @@
1
1
  import { mergeBrowserRuntimeManifests } from "./browser-runtime-manifest.js";
2
2
  import { appLogger } from "../global/app-logger.js";
3
+ import { startupTrace } from "../diagnostics/startup-trace.js";
4
+ import { isLitStaticRenderWorkerThread } from "./lit-static-render-worker-context.js";
3
5
  function patchAppRuntime(appConfig, patch) {
4
6
  appConfig.runtime = {
5
7
  ...appConfig.runtime ?? {},
@@ -50,18 +52,26 @@ async function collectConfiguredAppBuildManifestContributions(appConfig) {
50
52
  };
51
53
  }
52
54
  async function setupAppRuntimePlugins(options) {
55
+ startupTrace.markPhaseStart("setupAppRuntimePlugins");
53
56
  if (options.appConfig.runtime?.runtimeAssetsPrepared) {
54
57
  appLogger.debug("Skipped setupAppRuntimePlugins: runtime assets already prepared");
55
58
  registerRuntimePlugins(options.appConfig, options.onRuntimePlugin);
59
+ startupTrace.markPhaseEnd("setupAppRuntimePlugins");
56
60
  return;
57
61
  }
62
+ if (isLitStaticRenderWorkerThread()) {
63
+ appLogger.debug("Lit static-render worker: skipping processor setup (main thread already prepared artifacts)");
64
+ }
58
65
  appLogger.debugTime("setupAppRuntimePlugins");
59
66
  try {
60
67
  for (const loader of options.appConfig.loaders.values()) {
61
68
  options.onRuntimePlugin?.(loader);
62
69
  }
70
+ const skipProcessorSetup = isLitStaticRenderWorkerThread();
63
71
  for (const processor of options.appConfig.processors.values()) {
64
- await processor.setup();
72
+ if (!skipProcessorSetup) {
73
+ await processor.setup();
74
+ }
65
75
  if (processor.plugins) {
66
76
  for (const plugin of processor.plugins) {
67
77
  options.onRuntimePlugin?.(plugin);
@@ -79,6 +89,7 @@ async function setupAppRuntimePlugins(options) {
79
89
  patchAppRuntime(options.appConfig, { runtimeAssetsPrepared: true });
80
90
  } finally {
81
91
  appLogger.debugTimeEnd("setupAppRuntimePlugins");
92
+ startupTrace.markPhaseEnd("setupAppRuntimePlugins");
82
93
  }
83
94
  }
84
95
  export {
@@ -120,16 +120,16 @@ import {
120
120
  setupAppRuntimePlugins
121
121
  } from "./app-build-manifest-runtime.js";
122
122
  function getAppServerBuildPlugins(appConfig) {
123
- const srcDir = appConfig.absolutePaths?.srcDir;
124
- const aliasPlugin = srcDir ? [createAliasResolverPlugin(srcDir)] : [];
123
+ const projectDir = appConfig.absolutePaths?.projectDir;
124
+ const aliasPlugin = projectDir ? [createAliasResolverPlugin(projectDir)] : [];
125
125
  return [...aliasPlugin, ...getServerBuildPlugins(getAppBuildManifest(appConfig))];
126
126
  }
127
127
  function getAppBrowserBuildPlugins(appConfig) {
128
128
  const manifest = getAppBuildManifest(appConfig);
129
129
  const sourceTransformNames = new Set(getAppSourceTransforms(appConfig).map((transform) => transform.name));
130
130
  const browserPlugins = getBrowserBuildPlugins(manifest).filter((plugin) => !sourceTransformNames.has(plugin.name));
131
- const srcDir = appConfig.absolutePaths?.srcDir;
132
- const aliasPlugin = srcDir ? [createAliasResolverPlugin(srcDir)] : [];
131
+ const projectDir = appConfig.absolutePaths?.projectDir;
132
+ const aliasPlugin = projectDir ? [createAliasResolverPlugin(projectDir)] : [];
133
133
  return [...aliasPlugin, ...browserPlugins, ...getJsxOwnershipPlugins(appConfig)];
134
134
  }
135
135
  function build(options, executor = defaultRolldownBuildAdapter) {
@@ -0,0 +1,34 @@
1
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
+ import type { ProcessedAsset } from '../services/assets/asset-processing-service/assets.types.js';
3
+ export declare const DEV_BROWSER_SCRIPT_CACHE_DIR = ".browser-script-bundles";
4
+ export declare const DEV_BROWSER_SCRIPT_CACHE_FILENAME = ".build-cache.json";
5
+ /** Output path restored from the dev disk cache before dependency metadata is merged. */
6
+ export type DevBrowserScriptCacheHit = {
7
+ filepath: string;
8
+ };
9
+ /**
10
+ * @remarks
11
+ * Dev asset caching spans three scopes that should not be conflated:
12
+ * - request dedupe (`request-build-dedupe.ts`) — one in-flight build per request key
13
+ * - in-memory service cache (`AssetProcessingService`) — per-process reuse within one dev server
14
+ * - this disk manifest (`.eco/.browser-script-bundles`) — cross-request reuse of content-script bundles
15
+ *
16
+ * Disk entries intentionally store only output file paths. Declarative fields such as
17
+ * `groupedBundle`, `attributes`, and `packageRole` live on the originating
18
+ * `ContentScriptAsset` and are merged via `materializeContentScriptAsset()` on lookup.
19
+ */
20
+ export interface DevBrowserScriptCacheEntry {
21
+ filepath: string;
22
+ builtAt: number;
23
+ }
24
+ export interface DevBrowserScriptCacheManifest {
25
+ invalidationVersion: string;
26
+ buildInputsFingerprint: string;
27
+ entries: Record<string, DevBrowserScriptCacheEntry>;
28
+ }
29
+ /** Returns whether dev-only cross-request content-script bundle cache is active. */
30
+ export declare function shouldUseDevBrowserScriptCache(): boolean;
31
+ /** Looks up one persisted dev content-script output path by dependency cache key. */
32
+ export declare function getDevBrowserScriptCacheEntry(appConfig: EcoPagesAppConfig, depKey: string): DevBrowserScriptCacheHit | null;
33
+ /** Persists one dev content-script bundle output for cross-request reuse. */
34
+ export declare function setDevBrowserScriptCacheEntry(appConfig: EcoPagesAppConfig, depKey: string, asset: ProcessedAsset): void;
@@ -0,0 +1,90 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { getAppBrowserBuildPlugins } from "./build-adapter.js";
4
+ import { createBuildInputsFingerprint, hashAppConfigFile } from "./build-input-fingerprint.js";
5
+ import {
6
+ readProductionCacheManifest,
7
+ writeProductionCacheManifest,
8
+ isProductionCacheManifestCurrent,
9
+ matchesProductionCacheFingerprint
10
+ } from "./production-build-cache.js";
11
+ import { getCorePackageVersion } from "../services/module-loading/route-module-build-manifest.js";
12
+ import { resolveInternalExecutionDir } from "../utils/resolve-work-dir.js";
13
+ import { isDevelopmentRuntime } from "../utils/runtime.js";
14
+ const DEV_BROWSER_SCRIPT_CACHE_DIR = ".browser-script-bundles";
15
+ const DEV_BROWSER_SCRIPT_CACHE_FILENAME = ".build-cache.json";
16
+ function getDevBrowserScriptCacheDir(appConfig) {
17
+ return path.join(resolveInternalExecutionDir(appConfig), DEV_BROWSER_SCRIPT_CACHE_DIR);
18
+ }
19
+ function getDevBrowserScriptCacheManifestPath(appConfig) {
20
+ return path.join(getDevBrowserScriptCacheDir(appConfig), DEV_BROWSER_SCRIPT_CACHE_FILENAME);
21
+ }
22
+ function createBrowserBuildKey(appConfig) {
23
+ try {
24
+ const pluginNames = getAppBrowserBuildPlugins(appConfig).map((plugin) => plugin.name).sort().join("|");
25
+ return pluginNames || "default";
26
+ } catch {
27
+ return "default";
28
+ }
29
+ }
30
+ function createDevBrowserScriptCacheInvalidationVersion(appConfig) {
31
+ return [getCorePackageVersion(), hashAppConfigFile(appConfig), createBrowserBuildKey(appConfig)].join("::");
32
+ }
33
+ function readManifest(appConfig) {
34
+ const manifest = readProductionCacheManifest(
35
+ getDevBrowserScriptCacheManifestPath(appConfig)
36
+ );
37
+ if (!manifest) {
38
+ return void 0;
39
+ }
40
+ const invalidationVersion = createDevBrowserScriptCacheInvalidationVersion(appConfig);
41
+ const buildInputsFingerprint = createBuildInputsFingerprint(appConfig);
42
+ if (!isProductionCacheManifestCurrent(manifest, invalidationVersion) || !matchesProductionCacheFingerprint(manifest, buildInputsFingerprint)) {
43
+ return void 0;
44
+ }
45
+ return manifest;
46
+ }
47
+ function writeManifest(appConfig, manifest) {
48
+ writeProductionCacheManifest(getDevBrowserScriptCacheManifestPath(appConfig), manifest);
49
+ }
50
+ function shouldUseDevBrowserScriptCache() {
51
+ return isDevelopmentRuntime();
52
+ }
53
+ function getDevBrowserScriptCacheEntry(appConfig, depKey) {
54
+ if (!shouldUseDevBrowserScriptCache()) {
55
+ return null;
56
+ }
57
+ const manifest = readManifest(appConfig);
58
+ const entry = manifest?.entries[depKey];
59
+ if (!entry?.filepath || !fileSystem.exists(entry.filepath)) {
60
+ return null;
61
+ }
62
+ return { filepath: entry.filepath };
63
+ }
64
+ function setDevBrowserScriptCacheEntry(appConfig, depKey, asset) {
65
+ if (!shouldUseDevBrowserScriptCache() || !asset.filepath) {
66
+ return;
67
+ }
68
+ const invalidationVersion = createDevBrowserScriptCacheInvalidationVersion(appConfig);
69
+ const buildInputsFingerprint = createBuildInputsFingerprint(appConfig);
70
+ const existing = readManifest(appConfig);
71
+ const manifest = existing ?? {
72
+ invalidationVersion,
73
+ buildInputsFingerprint,
74
+ entries: {}
75
+ };
76
+ manifest.invalidationVersion = invalidationVersion;
77
+ manifest.buildInputsFingerprint = buildInputsFingerprint;
78
+ manifest.entries[depKey] = {
79
+ filepath: asset.filepath,
80
+ builtAt: Date.now()
81
+ };
82
+ writeManifest(appConfig, manifest);
83
+ }
84
+ export {
85
+ DEV_BROWSER_SCRIPT_CACHE_DIR,
86
+ DEV_BROWSER_SCRIPT_CACHE_FILENAME,
87
+ getDevBrowserScriptCacheEntry,
88
+ setDevBrowserScriptCacheEntry,
89
+ shouldUseDevBrowserScriptCache
90
+ };
@@ -0,0 +1,2 @@
1
+ /** Returns whether the current thread is the Lit static-render worker. */
2
+ export declare function isLitStaticRenderWorkerThread(): boolean;
@@ -0,0 +1,6 @@
1
+ function isLitStaticRenderWorkerThread() {
2
+ return process.env.ECOPAGES_LIT_STATIC_RENDER_WORKER === "true";
3
+ }
4
+ export {
5
+ isLitStaticRenderWorkerThread
6
+ };
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { isBarePackageImportSpecifier } from "../plugins/tsconfig-import-resolver.js";
4
5
  import { collectBrowserRuntimeImportRewriteMap, rewriteBrowserRuntimeImports } from "./browser-runtime-plugin.js";
5
6
  import { createServerSideCssShimPlugin } from "./server-side-css-shim-plugin.js";
6
7
  import { createRolldownPluginBridge } from "./rolldown-plugin-bridge.js";
@@ -48,8 +49,8 @@ function transpileProfileToOptions(profile) {
48
49
  };
49
50
  }
50
51
  }
51
- function isPackageImport(id) {
52
- return !id.startsWith(".") && !path.isAbsolute(id) && !id.startsWith("/") && !id.startsWith("node:") && !id.startsWith("@/") && !id.startsWith("~/") && !id.startsWith("#") && !id.includes(":");
52
+ function isPackageImport(id, contextRoot) {
53
+ return isBarePackageImportSpecifier(id, contextRoot);
53
54
  }
54
55
  function tryResolveModule(id, resolver) {
55
56
  try {
@@ -92,7 +93,7 @@ function createExternalMatcher(options, appRootRequireCache) {
92
93
  if (explicitExternals.has(id)) {
93
94
  return true;
94
95
  }
95
- if (!externalPackages || !isPackageImport(id)) {
96
+ if (!externalPackages || !isPackageImport(id, contextRoot)) {
96
97
  return false;
97
98
  }
98
99
  return !shouldBundlePackageImport(id, contextRoot, appRootRequireCache);
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { isBarePackageImportSpecifier } from "../plugins/tsconfig-import-resolver.js";
5
6
  const corePackageRequire = createRequire(new URL("../../package.json", import.meta.url));
6
7
  const appDeclaredPackageCache = /* @__PURE__ */ new Map();
7
8
  const appWorkspacePackageCache = /* @__PURE__ */ new Map();
@@ -13,9 +14,6 @@ function tryResolveRuntimeImport(specifier, resolver) {
13
14
  return void 0;
14
15
  }
15
16
  }
16
- function isBareRuntimeImport(specifier) {
17
- return !specifier.startsWith(".") && !path.isAbsolute(specifier) && !specifier.startsWith("/") && !specifier.startsWith("node:") && !specifier.startsWith("@/") && !specifier.startsWith("~/") && !specifier.startsWith("#") && !specifier.includes(":");
18
- }
19
17
  function extractPackageNameFromFileUrl(specifier) {
20
18
  if (!specifier.startsWith("file://")) {
21
19
  return void 0;
@@ -108,7 +106,7 @@ function isDeclaredInResolutionChain(specifier, rootDir) {
108
106
  return isDeclaredAppPackageImport(specifier, rootDir);
109
107
  }
110
108
  function rewriteRuntimeImportSpecifier(specifier, quote, rootDir) {
111
- if (!isBareRuntimeImport(specifier)) {
109
+ if (!isBarePackageImportSpecifier(specifier, rootDir)) {
112
110
  return void 0;
113
111
  }
114
112
  if (isDeclaredInResolutionChain(specifier, rootDir)) {
@@ -0,0 +1,18 @@
1
+ import type { BuildResult } from '../build/build-adapter.js';
2
+ /**
3
+ * Per-request browser build dedupe cache.
4
+ *
5
+ * @remarks
6
+ * `DedupingBuildExecutor` only coalesces concurrent identical builds. Asset
7
+ * processing can call the same logical bundle several times in one request
8
+ * (page graph, HMR, content scripts) after the first build settles. This cache
9
+ * reuses the settled result until the request scope ends.
10
+ */
11
+ declare class RequestBuildDedupe {
12
+ private readonly storage;
13
+ run<T>(fn: () => Promise<T>): Promise<T>;
14
+ dedupeBuild(key: string, build: () => Promise<BuildResult>): Promise<BuildResult>;
15
+ getCachedCountForTests(): number;
16
+ }
17
+ export declare const requestBuildDedupe: RequestBuildDedupe;
18
+ export {};
@@ -0,0 +1,33 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ class RequestBuildDedupe {
3
+ storage = new AsyncLocalStorage();
4
+ run(fn) {
5
+ if (this.storage.getStore()) {
6
+ return fn();
7
+ }
8
+ return this.storage.run(/* @__PURE__ */ new Map(), fn);
9
+ }
10
+ dedupeBuild(key, build) {
11
+ const store = this.storage.getStore();
12
+ if (!store) {
13
+ return build();
14
+ }
15
+ const existing = store.get(key);
16
+ if (existing) {
17
+ return existing;
18
+ }
19
+ const buildPromise = build().catch((error) => {
20
+ store.delete(key);
21
+ throw error;
22
+ });
23
+ store.set(key, buildPromise);
24
+ return buildPromise;
25
+ }
26
+ getCachedCountForTests() {
27
+ return this.storage.getStore()?.size ?? 0;
28
+ }
29
+ }
30
+ const requestBuildDedupe = new RequestBuildDedupe();
31
+ export {
32
+ requestBuildDedupe
33
+ };
@@ -0,0 +1,25 @@
1
+ export type StartupTracePhase = 'config-ready' | 'setupAppRuntimePlugins' | 'route-registry' | 'server-listen' | 'first-request-ssr';
2
+ declare class StartupTrace {
3
+ private readonly phaseStarts;
4
+ private readonly phases;
5
+ private firstRequestPending;
6
+ private firstRequestPath;
7
+ private firstRequestBundleCount;
8
+ private firstRequestClientBytes;
9
+ private summaryEmitted;
10
+ isEnabled(): boolean;
11
+ markPhaseStart(phase: StartupTracePhase): void;
12
+ markPhaseEnd(phase: StartupTracePhase): void;
13
+ markConfigReady(): void;
14
+ markServerListening(): void;
15
+ beginServerListen(): void;
16
+ recordBrowserBundle(outputs: Array<{
17
+ path: string;
18
+ }>): void;
19
+ traceFirstRequest<T>(request: Request, handler: () => Promise<T>): Promise<T>;
20
+ private emitFirstRequestSummary;
21
+ /** Resets mutable trace state for unit tests. */
22
+ resetForTests(): void;
23
+ }
24
+ export declare const startupTrace: StartupTrace;
25
+ export {};
@@ -0,0 +1,121 @@
1
+ import { statSync } from "node:fs";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ const LOG_PREFIX = "[ecopages:startup-trace]";
4
+ function isStartupTraceEnabled() {
5
+ return process.env.ECOPAGES_STARTUP_TRACE === "true" || process.env.ECOPAGES_LOGGER_DEBUG === "true";
6
+ }
7
+ function wallMsSinceProcessStart() {
8
+ return Math.round(process.uptime() * 1e3);
9
+ }
10
+ function writeTraceLine(message) {
11
+ process.stderr.write(`${LOG_PREFIX} ${message}
12
+ `);
13
+ }
14
+ class StartupTrace {
15
+ phaseStarts = /* @__PURE__ */ new Map();
16
+ phases = /* @__PURE__ */ new Map();
17
+ firstRequestPending = true;
18
+ firstRequestPath;
19
+ firstRequestBundleCount = 0;
20
+ firstRequestClientBytes = 0;
21
+ summaryEmitted = false;
22
+ isEnabled() {
23
+ return isStartupTraceEnabled();
24
+ }
25
+ markPhaseStart(phase) {
26
+ if (!isStartupTraceEnabled() || this.phases.has(phase)) {
27
+ return;
28
+ }
29
+ this.phaseStarts.set(phase, performance.now());
30
+ }
31
+ markPhaseEnd(phase) {
32
+ if (!isStartupTraceEnabled() || this.phases.has(phase)) {
33
+ return;
34
+ }
35
+ const startedAt = this.phaseStarts.get(phase);
36
+ if (startedAt === void 0) {
37
+ return;
38
+ }
39
+ const durationMs = Math.round(performance.now() - startedAt);
40
+ const record = {
41
+ durationMs,
42
+ wallMs: wallMsSinceProcessStart()
43
+ };
44
+ this.phases.set(phase, record);
45
+ writeTraceLine(`phase=${phase} durationMs=${durationMs} wallMs=${record.wallMs}`);
46
+ }
47
+ markConfigReady() {
48
+ if (!isStartupTraceEnabled() || this.phases.has("config-ready")) {
49
+ return;
50
+ }
51
+ const record = {
52
+ durationMs: 0,
53
+ wallMs: wallMsSinceProcessStart()
54
+ };
55
+ this.phases.set("config-ready", record);
56
+ writeTraceLine(`phase=config-ready wallMs=${record.wallMs}`);
57
+ }
58
+ markServerListening() {
59
+ this.markPhaseEnd("server-listen");
60
+ }
61
+ beginServerListen() {
62
+ this.markPhaseStart("server-listen");
63
+ }
64
+ recordBrowserBundle(outputs) {
65
+ if (!isStartupTraceEnabled() || !this.firstRequestPending) {
66
+ return;
67
+ }
68
+ this.firstRequestBundleCount += 1;
69
+ for (const output of outputs) {
70
+ try {
71
+ if (fileSystem.exists(output.path)) {
72
+ this.firstRequestClientBytes += statSync(output.path).size;
73
+ }
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ async traceFirstRequest(request, handler) {
79
+ if (!isStartupTraceEnabled() || !this.firstRequestPending) {
80
+ return handler();
81
+ }
82
+ this.firstRequestPath = new URL(request.url).pathname;
83
+ this.markPhaseStart("first-request-ssr");
84
+ try {
85
+ return await handler();
86
+ } finally {
87
+ this.markPhaseEnd("first-request-ssr");
88
+ this.emitFirstRequestSummary();
89
+ this.firstRequestPending = false;
90
+ }
91
+ }
92
+ emitFirstRequestSummary() {
93
+ if (!isStartupTraceEnabled() || this.summaryEmitted) {
94
+ return;
95
+ }
96
+ this.summaryEmitted = true;
97
+ writeTraceLine(
98
+ [
99
+ "summary",
100
+ `path=${this.firstRequestPath ?? "unknown"}`,
101
+ `bundleCount=${this.firstRequestBundleCount}`,
102
+ `clientBundleBytes=${this.firstRequestClientBytes}`,
103
+ `wallMs=${wallMsSinceProcessStart()}`
104
+ ].join(" ")
105
+ );
106
+ }
107
+ /** Resets mutable trace state for unit tests. */
108
+ resetForTests() {
109
+ this.phaseStarts.clear();
110
+ this.phases.clear();
111
+ this.firstRequestPending = true;
112
+ this.firstRequestPath = void 0;
113
+ this.firstRequestBundleCount = 0;
114
+ this.firstRequestClientBytes = 0;
115
+ this.summaryEmitted = false;
116
+ }
117
+ }
118
+ const startupTrace = new StartupTrace();
119
+ export {
120
+ startupTrace
121
+ };
@@ -72,7 +72,18 @@ export interface ComponentOptions<P, E = EcoPagesElement> {
72
72
  render: (props: P) => E | Promise<E>;
73
73
  }
74
74
  export type HtmlOptions<E = EcoPagesElement> = ComponentOptions<HtmlTemplateProps, E>;
75
- export type LayoutOptions<E = EcoPagesElement> = ComponentOptions<LayoutProps<E>, E>;
75
+ export type LayoutOptions<E = EcoPagesElement> = ComponentOptions<LayoutProps<E>, E> & {
76
+ /**
77
+ * Marks this layout as mounting shared client runtime state such as React context
78
+ * providers or query clients.
79
+ *
80
+ * @remarks
81
+ * When any layout in the app sets this flag, only flagged layouts participate in
82
+ * React auto-vendoring discovery. This keeps shell-only layouts out of provider
83
+ * scans while persisted SPA navigation stays on one shared vendor instance.
84
+ */
85
+ runtimeProvider?: boolean;
86
+ };
76
87
  /**
77
88
  * Base options shared by all page variants
78
89
  */