@ecopages/core 0.2.0-rc.4 → 0.2.0-rc.6

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 (55) hide show
  1. package/README.md +9 -3
  2. package/package.json +7 -4
  3. package/src/build/contracts/content-virtual-modules.d.ts +3 -1
  4. package/src/build/contracts/content-virtual-modules.js +8 -0
  5. package/src/css-imports.d.ts +4 -0
  6. package/src/declarations.d.ts +2 -5
  7. package/src/dev/README.md +2 -0
  8. package/src/dev/transform-server/dev-transform-server.d.ts +6 -0
  9. package/src/dev/transform-server/dev-transform-server.js +16 -5
  10. package/src/eco/README.md +71 -5
  11. package/src/eco/component-identity.d.ts +3 -2
  12. package/src/eco/component-identity.js +5 -2
  13. package/src/eco/discovered-dependencies.d.ts +45 -0
  14. package/src/eco/discovered-dependencies.js +87 -0
  15. package/src/eco/eco.js +2 -0
  16. package/src/eco/eco.types.d.ts +18 -2
  17. package/src/eco/page-dependency-contributions.d.ts +19 -0
  18. package/src/eco/page-dependency-contributions.js +52 -0
  19. package/src/index.browser.d.ts +3 -0
  20. package/src/index.browser.js +3 -0
  21. package/src/index.d.ts +4 -1
  22. package/src/index.js +4 -1
  23. package/src/plugins/README.md +8 -1
  24. package/src/plugins/component-import-discovery.d.ts +19 -0
  25. package/src/plugins/component-import-discovery.js +203 -0
  26. package/src/plugins/eco-component-meta-plugin.d.ts +3 -1
  27. package/src/plugins/eco-component-meta-plugin.js +101 -16
  28. package/src/plugins/processor.d.ts +4 -0
  29. package/src/plugins/processor.js +4 -0
  30. package/src/plugins/tsconfig-import-resolver.d.ts +3 -1
  31. package/src/plugins/tsconfig-import-resolver.js +4 -2
  32. package/src/route-renderer/GRAPH.md +1 -1
  33. package/src/route-renderer/README.md +8 -0
  34. package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.d.ts +8 -0
  35. package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +9 -1
  36. package/src/route-renderer/orchestration/integration-renderer.d.ts +2 -2
  37. package/src/route-renderer/orchestration/integration-renderer.js +8 -9
  38. package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.d.ts +1 -4
  39. package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +0 -66
  40. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +5 -8
  41. package/src/route-renderer/page-loading/component-dependency-collection.d.ts +6 -2
  42. package/src/route-renderer/page-loading/component-dependency-collection.js +29 -5
  43. package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +11 -1
  44. package/src/route-renderer/page-loading/file-scoped-dependency-components.js +40 -6
  45. package/src/route-renderer/page-loading/resolved-page-dependencies.d.ts +1 -2
  46. package/src/route-renderer/page-loading/resolved-page-dependencies.js +8 -7
  47. package/src/services/invalidation/development-invalidation.service.js +10 -3
  48. package/src/services/module-loading/README.md +2 -0
  49. package/src/services/module-loading/collection-server-module-build.service.d.ts +1 -0
  50. package/src/services/module-loading/collection-server-module-build.service.js +5 -2
  51. package/src/services/module-loading/page-module-import.service.d.ts +2 -0
  52. package/src/services/module-loading/page-module-import.service.js +2 -1
  53. package/src/static-site-generator/README.md +3 -1
  54. package/src/types/public-types.d.ts +1 -1
  55. package/src/watchers/project-watcher.js +5 -1
package/README.md CHANGED
@@ -13,13 +13,16 @@ Ecopages is an extensible static site generator (SSG) built around a Bun-first c
13
13
 
14
14
  ## Current Architecture
15
15
 
16
+ Projects load shared ambient types through `@ecopages/core/declarations`. This entry references `src/css-imports.d.ts`, which keeps the wildcard CSS declaration in a standalone ambient file so side-effect CSS imports typecheck without per-project declarations.
17
+
16
18
  The current core package is organized around app-owned runtime state and explicit service boundaries.
17
19
 
18
20
  The important ownership rules are:
19
21
 
20
22
  - `ConfigBuilder.build()` finalizes app-owned build and runtime services.
21
23
  - Component identity attribution is one shared source transform for server, browser, and HMR compilation paths: after a lexical `eco.` gate, it uses Oxc to wrap supported factory options with `bindComponentIdentity()`, which factories retain as `config.identity`.
22
- - browser bundling and server module loading are explicit, separate paths.
24
+ - Direct local Eco Component imports (including named `export { X } from` barrels) and relative side-effect CSS imports supply Dependencies by default through the shared transform and existing collector; explicit asset declarations override inference, and inferred styles stay keyed by identity until collection. See [eco](src/eco/README.md) and [Plugin Contracts](src/plugins/README.md).
25
+ - Browser bundling and server module loading are separate paths. SSR-enabled lazy scripts execute on the server before rendering and in the browser only on their configured trigger.
23
26
  - runtime hosts stay thin and delegate framework work into core services.
24
27
  - HMR and invalidation use shared graph-aware services instead of runtime-specific ad hoc wiring.
25
28
 
@@ -50,7 +53,10 @@ flowchart TD
50
53
  B --> C[DevelopmentInvalidationService]
51
54
  C --> D{Change kind}
52
55
  D -->|Route or server source| E[Invalidate server modules]
53
- D -->|Public or include| F[Reload browser]
56
+ D -->|Additional watch| E
57
+ E --> N[Notify processors]
58
+ N --> F[Reload browser]
59
+ D -->|Public asset| F
54
60
  D -->|Processor-owned asset| G[Notify processor only]
55
61
  D -->|HMR-eligible source| H[Core HMR manager]
56
62
  H --> I[Strategy selection]
@@ -83,7 +89,7 @@ Use this package README as the top-level map, then drill into the focused subsys
83
89
  - `src/build/README.md`: build adapter, executor, development build coordination, and standalone Node server packaging
84
90
  - `src/services/README.md`: cross-cutting runtime services and orchestration helpers
85
91
  - `src/adapters/README.md`: Bun, Node, and shared adapter boundaries
86
- - `src/dev/README.md`: dev transform server and on-demand client delivery
92
+ - `src/dev/README.md`: dev transform server, on-demand client delivery, and invalidation during compilation
87
93
  - `src/hmr/README.md`: HMR strategy and update-layer ownership
88
94
  - `src/router/README.md`: route discovery, matching, and browser navigation coordination
89
95
  - `src/route-renderer/README.md`: rendering orchestration and dependency resolution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-rc.4",
3
+ "version": "0.2.0-rc.6",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -10,6 +10,9 @@
10
10
  "license": "MIT",
11
11
  "main": "./src/index.js",
12
12
  "type": "module",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
13
16
  "sideEffects": false,
14
17
  "repository": {
15
18
  "type": "git",
@@ -17,7 +20,7 @@
17
20
  "directory": "packages/core"
18
21
  },
19
22
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-rc.4",
23
+ "@ecopages/file-system": "0.2.0-rc.6",
21
24
  "@ecopages/logger": "^0.2.3",
22
25
  "@ecopages/scripts-injector": "^0.1.5",
23
26
  "@oxc-project/runtime": "0.141.0",
@@ -31,7 +34,7 @@
31
34
  "@standard-schema/utils": "^0.3.0"
32
35
  },
33
36
  "peerDependencies": {
34
- "@ecopages/dev-toolbar": "0.2.0-rc.4"
37
+ "@ecopages/dev-toolbar": "0.2.0-rc.6"
35
38
  },
36
39
  "peerDependenciesMeta": {
37
40
  "@ecopages/dev-toolbar": {
@@ -631,4 +634,4 @@
631
634
  }
632
635
  },
633
636
  "types": "./src/index.d.ts"
634
- }
637
+ }
@@ -4,9 +4,11 @@ export declare const CONTENT_VIRTUAL_MODULE_PREFIX = "ecopages:content";
4
4
  export declare const CONTENT_VIRTUAL_MODULE_PATTERN: RegExp;
5
5
  /** Matches `ecopages:content/<collection>/server` component resolver specifiers. */
6
6
  export declare const CONTENT_SERVER_VIRTUAL_MODULE_PATTERN: RegExp;
7
+ /** Matches browser-only `ecopages:content/<collection>/browser` component loader specifiers. */
8
+ export declare const CONTENT_BROWSER_VIRTUAL_MODULE_PATTERN: RegExp;
7
9
  export type ParsedCollectionSpecifier = {
8
10
  collectionName: string;
9
- variant: 'entries' | 'server';
11
+ variant: 'entries' | 'server' | 'browser';
10
12
  };
11
13
  export declare function parseCollectionSpecifier(specifier: string): ParsedCollectionSpecifier | null;
12
14
  export declare function isContentServerVirtualModule(specifier: string): boolean;
@@ -4,6 +4,8 @@ export const CONTENT_VIRTUAL_MODULE_PREFIX = 'ecopages:content';
4
4
  export const CONTENT_VIRTUAL_MODULE_PATTERN = /^ecopages:content\/[a-z][a-z0-9-]*$/;
5
5
  /** Matches `ecopages:content/<collection>/server` component resolver specifiers. */
6
6
  export const CONTENT_SERVER_VIRTUAL_MODULE_PATTERN = /^ecopages:content\/[a-z][a-z0-9-]+\/server$/;
7
+ /** Matches browser-only `ecopages:content/<collection>/browser` component loader specifiers. */
8
+ export const CONTENT_BROWSER_VIRTUAL_MODULE_PATTERN = /^ecopages:content\/[a-z][a-z0-9-]+\/browser$/;
7
9
  export function parseCollectionSpecifier(specifier) {
8
10
  const normalized = specifier.startsWith(`${CONTENT_VIRTUAL_MODULE_PREFIX}/`)
9
11
  ? specifier.slice(`${CONTENT_VIRTUAL_MODULE_PREFIX}/`.length)
@@ -19,6 +21,12 @@ export function parseCollectionSpecifier(specifier) {
19
21
  variant: 'server',
20
22
  };
21
23
  }
24
+ if (normalized.endsWith('/browser')) {
25
+ return {
26
+ collectionName: normalized.slice(0, -'/browser'.length),
27
+ variant: 'browser',
28
+ };
29
+ }
22
30
  return {
23
31
  collectionName: normalized,
24
32
  variant: 'entries',
@@ -0,0 +1,4 @@
1
+ declare module '*.css' {
2
+ const styles: string;
3
+ export default styles;
4
+ }
@@ -1,3 +1,5 @@
1
+ /// <reference path="./css-imports.d.js" />
2
+
1
3
  import type { EcoPagesAppConfig } from './types/internal-types';
2
4
  import type { EcoNavigationRuntime } from './router/client/navigation-coordinator';
3
5
 
@@ -25,8 +27,3 @@ declare global {
25
27
  __ECO_PAGES__?: EcoPagesWindowRuntime;
26
28
  }
27
29
  }
28
-
29
- declare module '*.css' {
30
- const styles: string;
31
- export default styles;
32
- }
package/src/dev/README.md CHANGED
@@ -22,3 +22,5 @@ Install the client package in the app project. Core resolves it from the applica
22
22
  Disable per project with `devToolbar: { enabled: false }`, or per process with `ECOPAGES_DEV_TOOLBAR=false`.
23
23
 
24
24
  Server wiring: [dev-toolbar/README.md](../dev-toolbar/README.md) (`DevToolbarHost`, manifest, runtime bundling, HTML injection). Bring-your-own toolbars export `src/bootstrap.ts` and use the same `devToolbar.package` config key.
25
+
26
+ Dev transform requests retry when their source changes or is invalidated during compilation. Concurrent requests share one in-flight compile per source, including across source and global invalidation; stale attempts release their slot before retrying. Only results for the current source snapshot enter the module cache, so rapid edits cannot label stale browser code with a newer source hash.
@@ -24,6 +24,7 @@ export declare class DevTransformServer {
24
24
  private readonly cache;
25
25
  private readonly inFlight;
26
26
  private cacheGeneration;
27
+ private readonly sourceGenerations;
27
28
  constructor(options: DevTransformServerOptions);
28
29
  registerModule(sourcePath: string): string;
29
30
  getRegisteredSourcePath(moduleUrl: string): string | undefined;
@@ -31,6 +32,11 @@ export declare class DevTransformServer {
31
32
  private rebuildRuntimeSpecifierMap;
32
33
  private resolveVendorBundlePlugins;
33
34
  getWatchedModules(): ReadonlyMap<string, string>;
35
+ /**
36
+ * @remarks
37
+ * Marks pending work stale while preserving one in-flight compile per source.
38
+ * Requests awaiting that work retry against the current source when it completes.
39
+ */
34
40
  invalidateSource(sourcePath: string): void;
35
41
  invalidateAll(): void;
36
42
  reset(): void;
@@ -26,6 +26,7 @@ export class DevTransformServer {
26
26
  cache = new Map();
27
27
  inFlight = new Map();
28
28
  cacheGeneration = 0;
29
+ sourceGenerations = new Map();
29
30
  constructor(options) {
30
31
  this.appConfig = options.appConfig;
31
32
  this.onModuleDependencies = options.onModuleDependencies;
@@ -81,14 +82,20 @@ export class DevTransformServer {
81
82
  getWatchedModules() {
82
83
  return this.urlToSource;
83
84
  }
85
+ /**
86
+ * @remarks
87
+ * Marks pending work stale while preserving one in-flight compile per source.
88
+ * Requests awaiting that work retry against the current source when it completes.
89
+ */
84
90
  invalidateSource(sourcePath) {
85
91
  const normalized = path.resolve(sourcePath);
86
92
  this.cache.delete(normalized);
93
+ this.sourceGenerations.set(normalized, (this.sourceGenerations.get(normalized) ?? 0) + 1);
87
94
  }
88
95
  invalidateAll() {
89
96
  this.cacheGeneration += 1;
90
97
  this.cache.clear();
91
- this.inFlight.clear();
98
+ this.sourceGenerations.clear();
92
99
  this.vendorRegistry.invalidateAll();
93
100
  }
94
101
  reset() {
@@ -169,22 +176,26 @@ export class DevTransformServer {
169
176
  return entry;
170
177
  }
171
178
  const generation = this.cacheGeneration;
179
+ const sourceGeneration = this.sourceGenerations.get(normalized) ?? 0;
172
180
  const promise = this.bundler
173
181
  .transpileModule(normalized)
174
182
  .then((result) => {
175
- if (generation !== this.cacheGeneration) {
176
- throw new Error(`[dev-transform] Stale transpile result for ${normalized}`);
183
+ if (generation !== this.cacheGeneration ||
184
+ sourceGeneration !== (this.sourceGenerations.get(normalized) ?? 0) ||
185
+ fileSystem.hash(normalized) !== sourceHash) {
186
+ return undefined;
177
187
  }
178
188
  if (result.dependencies) {
179
189
  this.onModuleDependencies?.(normalized, result.dependencies);
180
190
  }
181
- const entry = { code: result.code, sourceHash: fileSystem.hash(normalized) };
191
+ const entry = { code: result.code, sourceHash };
182
192
  this.cache.set(normalized, entry);
183
193
  return entry;
184
194
  })
185
195
  .finally(() => {
186
196
  this.inFlight.delete(normalized);
187
- });
197
+ })
198
+ .then((entry) => entry ?? this.materialize(normalized));
188
199
  this.inFlight.set(normalized, promise);
189
200
  return promise;
190
201
  }
package/src/eco/README.md CHANGED
@@ -20,6 +20,57 @@ Layouts are assigned explicitly on each `eco.page({ layout })` call — either o
20
20
 
21
21
  EcoPages does **not** infer layouts from `src/layouts/` file paths or route segment directories. A file under `src/layouts/` is only used when a page imports it and passes it to `layout`.
22
22
 
23
+ ## Dependency discovery
24
+
25
+ Discovery is enabled by default for modules declaring `eco.component()`, `eco.layout()`, `eco.html()`, or `eco.page()`, as well as MDX documents compiled through `@ecopages/mdx/core`.
26
+
27
+ ```tsx
28
+ import { eco } from '@ecopages/core';
29
+ import { Counter } from './counter';
30
+ import './page.css';
31
+
32
+ export default eco.page({
33
+ render: () => <Counter />,
34
+ });
35
+ ```
36
+
37
+ Direct static named/default imports of local Eco Components contribute their Dependencies transitively. Configured TypeScript path aliases and relative imports are supported for both Components and CSS stylesheets (e.g., `import '@/components/button'` or `import '@/styles/main.css'`). Side-effect CSS imports become stylesheet Dependencies; the shared transform removes the imports so the existing asset pipeline owns delivery in server and browser builds. In MDX documents, top-level component and CSS imports are discovered while markdown code blocks and dynamic imports within functions are safely ignored.
38
+
39
+ Discovery is conservative and module-scoped: every Eco declaration in a file shares its imported Components and styles, even when a render condition omits a Component. It does not infer which Components actually render. **Best practice:** author **one `eco.component()` per file**. Co-locating multiple components in a single file will cause all declared components in that file to share discovered dependencies.
40
+
41
+ Imported Layouts contribute assets; only the explicit `layout` option controls Layout composition.
42
+
43
+ Explicit `dependencies` remain supported. Explicit Components come first, followed by discovered Components in import order, with duplicate Components removed. Explicit stylesheet declarations take precedence over discovered references to the same resolved file, preserving their attributes and order. Discovered stylesheets are **not** written into `config.dependencies.stylesheets`; they stay on Component identity until collection. Inferred styles follow explicit styles and are emitted once per collection. Circular dependency traversal is guarded by Component config identity, so two Components in one file remain distinct.
44
+
45
+ When a Page combines its own relative assets with a content entry, use `mergePageDependencies()`. Do not object-spread the two bags: spread copies `ownerFile` onto the other side's relative paths.
46
+
47
+ ```ts
48
+ import { eco, mergePageDependencies } from '@ecopages/core';
49
+ import { getEntryDependencies } from 'ecopages:content/posts/server';
50
+
51
+ export default eco.page({
52
+ dependencies: async ({ props }) =>
53
+ mergePageDependencies({ stylesheets: ['./post.css'] }, await getEntryDependencies(props.slug)),
54
+ render: () => <article />,
55
+ });
56
+ ```
57
+
58
+ Browser scripts remain explicit:
59
+
60
+ ```ts
61
+ scripts: [
62
+ {
63
+ src: './counter.script.ts',
64
+ ssr: true,
65
+ lazy: { 'on:visible': true },
66
+ },
67
+ ];
68
+ ```
69
+
70
+ For Lit, this registers the custom element before server rendering while delaying browser execution until visibility. `ssr: true` does not force eager browser loading. To load eagerly, omit `lazy`. This corrects the previous behavior that emitted an extra eager script for SSR-enabled lazy entries.
71
+
72
+ Named barrel re-exports (`export { Counter } from './counter'`) are followed for the imported binding only. Successful named re-export hops are watch paths, so retargeting a barrel updates page assets without editing the importing Page. Discovery does not follow `export * from './components'`, dynamic imports, namespace imports (`import * as`), package Components/CSS, CSS Modules, or custom import attributes. `export *` would pull an entire kit into the page asset graph; keep that explicit with `dependencies.components`. Plain-function modules retain their existing behavior. Ordinary utility imports do not become browser script entries. Missing supported imports report the owner file and import specifier.
73
+
23
74
  ## Component Patterns
24
75
 
25
76
  EcoPages supports two approaches for creating components, each suited for different use cases:
@@ -546,9 +597,6 @@ import { Counter } from '@/components/counter';
546
597
 
547
598
  export default eco.page({
548
599
  layout: BaseLayout,
549
- dependencies: {
550
- components: [Counter],
551
- },
552
600
  metadata: () => ({
553
601
  title: 'Home',
554
602
  description: 'Welcome to EcoPages',
@@ -562,6 +610,8 @@ export default eco.page({
562
610
  });
563
611
  ```
564
612
 
613
+ The static `Counter` import is discovered automatically. You do not list it again in `dependencies.components`. Lazy scripts stay on the Component.
614
+
565
615
  ## Type Definitions
566
616
 
567
617
  ```typescript
@@ -578,9 +628,25 @@ type DependencyEntry = {
578
628
  interface EcoComponentDependencies {
579
629
  scripts?: Array<string | DependencyEntry>;
580
630
  stylesheets?: Array<string | DependencyEntry>;
581
- components?: EcoComponent[];
631
+ modules?: string[];
632
+ /** Declared eco components only — see `EcoDeclaredComponent`. */
633
+ components?: EcoDeclaredComponent[];
582
634
  }
583
635
 
636
+ type FileOwnedDependencyContribution = EcoComponentDependencies & {
637
+ ownerFile?: string;
638
+ };
639
+
640
+ type PageDependenciesResult = FileOwnedDependencyContribution & {
641
+ contributions?: FileOwnedDependencyContribution[];
642
+ };
643
+
644
+ type GetPageDependencies<T> = (context: {
645
+ props: PagePropsFor<T>;
646
+ params?: Record<string, string>;
647
+ query?: Record<string, string>;
648
+ }) => PageDependenciesResult | undefined | Promise<PageDependenciesResult | undefined>;
649
+
584
650
  // Shared base option shape used by component(), html(), and layout()
585
651
  interface ComponentOptions<P, E = EcoPagesElement> {
586
652
  componentDir?: string;
@@ -595,7 +661,7 @@ type LayoutOptions<E = EcoPagesElement> = ComponentOptions<{ children: E }, E>;
595
661
 
596
662
  interface PageOptions<T, E = EcoPagesElement> {
597
663
  componentDir?: string;
598
- dependencies?: EcoComponentDependencies;
664
+ dependencies?: EcoComponentDependencies | GetPageDependencies<T>;
599
665
  layout?: EcoComponent<{ children: E }>;
600
666
  staticPaths?: GetStaticPaths;
601
667
  staticProps?: GetStaticProps<T>;
@@ -1,4 +1,5 @@
1
1
  import type { EcoComponent, EcoComponentConfig } from '../types/public-types.js';
2
+ import { type DiscoveredDependencies } from './discovered-dependencies.js';
2
3
  /** Stable attribution for a Component created by an Integration-owned module. */
3
4
  export type ComponentIdentity = {
4
5
  id: string;
@@ -7,7 +8,7 @@ export type ComponentIdentity = {
7
8
  };
8
9
  /** Gets canonical component attribution. */
9
10
  export declare function getComponentIdentity(value: EcoComponent | EcoComponentConfig | undefined): ComponentIdentity | undefined;
10
- /** Merges attribution into factory options. */
11
- export declare function bindComponentIdentity<T extends object>(identity: ComponentIdentity, options: T): T & {
11
+ /** Merges attribution into factory options. Optional discovered stylesheets stay keyed by identity. */
12
+ export declare function bindComponentIdentity<T extends object>(identity: ComponentIdentity, options: T, discovered?: DiscoveredDependencies): T & {
12
13
  identity: ComponentIdentity;
13
14
  };
@@ -1,3 +1,4 @@
1
+ import { registerDiscoveredDependencies } from './discovered-dependencies.js';
1
2
  function asConfig(value) {
2
3
  if (!value)
3
4
  return undefined;
@@ -12,7 +13,9 @@ export function getComponentIdentity(value) {
12
13
  const config = asConfig(value);
13
14
  return config?.identity;
14
15
  }
15
- /** Merges attribution into factory options. */
16
- export function bindComponentIdentity(identity, options) {
16
+ /** Merges attribution into factory options. Optional discovered stylesheets stay keyed by identity. */
17
+ export function bindComponentIdentity(identity, options, discovered) {
18
+ if (discovered)
19
+ registerDiscoveredDependencies(identity, discovered);
17
20
  return { ...options, identity };
18
21
  }
@@ -0,0 +1,45 @@
1
+ import type { EcoComponentConfig } from '../types/public-types.js';
2
+ import type { ComponentIdentity } from './component-identity.js';
3
+ export type DiscoveredDependencies = {
4
+ components: () => unknown[];
5
+ stylesheets: string[];
6
+ watchFiles?: string[];
7
+ };
8
+ /**
9
+ * Records discovered Components, inferred styles, and barrel watch hops on identity.
10
+ *
11
+ * @remarks
12
+ * Styles and watch hops are stored on the identity object through interned symbols so
13
+ * a bundled copy of this module still sees them during collection. The component getter
14
+ * stays on a same-isolate WeakMap and is attached to `config.dependencies` at factory time.
15
+ */
16
+ export declare function registerDiscoveredDependencies(identity: ComponentIdentity, discovered: DiscoveredDependencies): void;
17
+ /**
18
+ * Inferred stylesheet paths registered for this component identity.
19
+ *
20
+ * @remarks
21
+ * Provenance stays on identity so copying or merging `config.dependencies` cannot
22
+ * turn inferred styles into explicit ones. Paths are stored on the identity object
23
+ * so collection still sees them when server modules bundle a separate copy of `eco`.
24
+ * The collector prefers explicit entries across the whole graph by resolved path.
25
+ */
26
+ export declare function getInferredStylesheets(config: EcoComponentConfig | undefined): readonly string[];
27
+ /**
28
+ * Named re-export hops recorded for this component identity.
29
+ *
30
+ * @remarks
31
+ * Barrel files are not Component identity files. Collecting them as watch paths
32
+ * lets a barrel retarget invalidate HTML and Page Browser Graph caches without
33
+ * editing the importing Page.
34
+ */
35
+ export declare function getDiscoveredWatchFiles(config: EcoComponentConfig | undefined): readonly string[];
36
+ /**
37
+ * Attaches deferred discovered Component references to `config.dependencies`.
38
+ *
39
+ * @remarks
40
+ * The component getter reads live import bindings only during graph traversal,
41
+ * after module initialization. Inferred stylesheets stay keyed by identity and are
42
+ * not copied into the public bag, so rendering, SSR preload, ownership, and watch
43
+ * collectors share the same graph without object-identity bookkeeping.
44
+ */
45
+ export declare function attachDiscoveredDependencies(config: EcoComponentConfig): void;
@@ -0,0 +1,87 @@
1
+ const discoveries = new WeakMap();
2
+ const INFERRED_STYLESHEETS = Symbol.for('ecopages.identity.inferredStylesheets');
3
+ const DISCOVERED_WATCH_FILES = Symbol.for('ecopages.identity.watchFiles');
4
+ function discoveryRecord(identity) {
5
+ return identity;
6
+ }
7
+ /**
8
+ * Records discovered Components, inferred styles, and barrel watch hops on identity.
9
+ *
10
+ * @remarks
11
+ * Styles and watch hops are stored on the identity object through interned symbols so
12
+ * a bundled copy of this module still sees them during collection. The component getter
13
+ * stays on a same-isolate WeakMap and is attached to `config.dependencies` at factory time.
14
+ */
15
+ export function registerDiscoveredDependencies(identity, discovered) {
16
+ discoveries.set(identity, discovered);
17
+ const record = discoveryRecord(identity);
18
+ record[INFERRED_STYLESHEETS] = discovered.stylesheets;
19
+ record[DISCOVERED_WATCH_FILES] = discovered.watchFiles;
20
+ }
21
+ /**
22
+ * Inferred stylesheet paths registered for this component identity.
23
+ *
24
+ * @remarks
25
+ * Provenance stays on identity so copying or merging `config.dependencies` cannot
26
+ * turn inferred styles into explicit ones. Paths are stored on the identity object
27
+ * so collection still sees them when server modules bundle a separate copy of `eco`.
28
+ * The collector prefers explicit entries across the whole graph by resolved path.
29
+ */
30
+ export function getInferredStylesheets(config) {
31
+ const identity = config?.identity;
32
+ if (!identity)
33
+ return [];
34
+ return discoveryRecord(identity)[INFERRED_STYLESHEETS] ?? discoveries.get(identity)?.stylesheets ?? [];
35
+ }
36
+ /**
37
+ * Named re-export hops recorded for this component identity.
38
+ *
39
+ * @remarks
40
+ * Barrel files are not Component identity files. Collecting them as watch paths
41
+ * lets a barrel retarget invalidate HTML and Page Browser Graph caches without
42
+ * editing the importing Page.
43
+ */
44
+ export function getDiscoveredWatchFiles(config) {
45
+ const identity = config?.identity;
46
+ if (!identity)
47
+ return [];
48
+ return discoveryRecord(identity)[DISCOVERED_WATCH_FILES] ?? discoveries.get(identity)?.watchFiles ?? [];
49
+ }
50
+ function isDeclared(value) {
51
+ return typeof value === 'function' && Boolean(value.config?.identity);
52
+ }
53
+ /**
54
+ * Attaches deferred discovered Component references to `config.dependencies`.
55
+ *
56
+ * @remarks
57
+ * The component getter reads live import bindings only during graph traversal,
58
+ * after module initialization. Inferred stylesheets stay keyed by identity and are
59
+ * not copied into the public bag, so rendering, SSR preload, ownership, and watch
60
+ * collectors share the same graph without object-identity bookkeeping.
61
+ */
62
+ export function attachDiscoveredDependencies(config) {
63
+ const discovery = config.identity && discoveries.get(config.identity);
64
+ if (!discovery)
65
+ return;
66
+ let explicit = config.dependencies;
67
+ let merged;
68
+ const merge = () => {
69
+ const current = explicit;
70
+ merged = {
71
+ ...current,
72
+ get components() {
73
+ return [...new Set([...(current?.components ?? []), ...discovery.components().filter(isDeclared)])];
74
+ },
75
+ };
76
+ };
77
+ merge();
78
+ Object.defineProperty(config, 'dependencies', {
79
+ enumerable: true,
80
+ configurable: true,
81
+ get: () => merged,
82
+ set: (value) => {
83
+ explicit = value;
84
+ merge();
85
+ },
86
+ });
87
+ }
package/src/eco/eco.js CHANGED
@@ -6,6 +6,7 @@ import { finalizeComponentRender, getComponentRenderContext, interceptForeignChi
6
6
  import { isThenable } from '../route-renderer/orchestration/foreign-child/foreign-child-output.utils.js';
7
7
  import { applyPageLayoutConfig, mergeLayoutDependencies, normalizePageLayouts } from './page-layout-normalization.js';
8
8
  import { getComponentIdentity } from './component-identity.js';
9
+ import { attachDiscoveredDependencies } from './discovered-dependencies.js';
9
10
  /**
10
11
  * Creates a component factory with lazy-trigger support and foreign-child-runtime
11
12
  * interception.
@@ -58,6 +59,7 @@ function createComponentFactory(options) {
58
59
  integration: options.integration,
59
60
  dependencies: options.dependencies,
60
61
  };
62
+ attachDiscoveredDependencies(comp.config);
61
63
  return comp;
62
64
  }
63
65
  /**
@@ -64,11 +64,27 @@ export type GetPageDependenciesContext<T = Record<string, unknown>> = {
64
64
  query?: PageQuery;
65
65
  };
66
66
  /**
67
- * Page dependency bag optionally scoped to a declaring file.
67
+ * Explicit dependency declarations owned by one file.
68
+ *
69
+ * @remarks
70
+ * Relative `scripts`, `stylesheets`, and `modules` resolve against `ownerFile`
71
+ * when present. Component entries keep their own identity files. Inferred
72
+ * stylesheets are not stored here; the collector reads them from identity.
68
73
  */
69
- export type PageDependenciesResult = EcoComponentDependencies & {
74
+ export type FileOwnedDependencyContribution = EcoComponentDependencies & {
70
75
  ownerFile?: string;
71
76
  };
77
+ /**
78
+ * Page dependency bag made of one or more file-owned contributions.
79
+ *
80
+ * @remarks
81
+ * A single bag with `ownerFile` is one contribution. `contributions` keeps
82
+ * assets from different files separate until collection, so merging a Page and
83
+ * a content entry cannot re-home relative paths.
84
+ */
85
+ export type PageDependenciesResult = FileOwnedDependencyContribution & {
86
+ contributions?: FileOwnedDependencyContribution[];
87
+ };
72
88
  /**
73
89
  * Resolves request-specific page dependencies for one route render.
74
90
  */
@@ -0,0 +1,19 @@
1
+ import type { EcoComponentDependencies, FileOwnedDependencyContribution, PageDependenciesResult } from '../types/public-types.js';
2
+ /**
3
+ * Expands a page dependency result into file-owned contributions.
4
+ *
5
+ * @remarks
6
+ * Nested `contributions` are expanded so collection can resolve each bag against
7
+ * its own owner. Empty bags are omitted.
8
+ */
9
+ export declare function listFileOwnedDependencyContributions(value: PageDependenciesResult | EcoComponentDependencies | undefined): FileOwnedDependencyContribution[];
10
+ /**
11
+ * Merges base page dependencies with additional dependencies (such as those returned by a content entry).
12
+ *
13
+ * @remarks
14
+ * Keeps each file-owned contribution intact, including `modules`. Relative assets
15
+ * are not flattened under a single `ownerFile`, so a Page's `./page.css` cannot
16
+ * resolve against a content entry directory. Object-spread of two bags copies
17
+ * `ownerFile` onto the other side's relative paths; use this helper instead.
18
+ */
19
+ export declare function mergePageDependencies(base: EcoComponentDependencies | undefined, additional: PageDependenciesResult | EcoComponentDependencies | undefined): PageDependenciesResult | undefined;
@@ -0,0 +1,52 @@
1
+ function hasCollectableDependencyFields(dependencies) {
2
+ return Boolean(dependencies.stylesheets?.length ||
3
+ dependencies.scripts?.length ||
4
+ dependencies.modules?.length ||
5
+ dependencies.components?.length);
6
+ }
7
+ /**
8
+ * Expands a page dependency result into file-owned contributions.
9
+ *
10
+ * @remarks
11
+ * Nested `contributions` are expanded so collection can resolve each bag against
12
+ * its own owner. Empty bags are omitted.
13
+ */
14
+ export function listFileOwnedDependencyContributions(value) {
15
+ if (!value)
16
+ return [];
17
+ const result = value;
18
+ const nested = result.contributions ?? [];
19
+ const { contributions: _contributions, ownerFile, ...dependencies } = result;
20
+ const listed = [];
21
+ if (hasCollectableDependencyFields(dependencies)) {
22
+ listed.push(ownerFile ? { ...dependencies, ownerFile } : dependencies);
23
+ }
24
+ for (const contribution of nested) {
25
+ listed.push(...listFileOwnedDependencyContributions(contribution));
26
+ }
27
+ return listed;
28
+ }
29
+ /**
30
+ * Merges base page dependencies with additional dependencies (such as those returned by a content entry).
31
+ *
32
+ * @remarks
33
+ * Keeps each file-owned contribution intact, including `modules`. Relative assets
34
+ * are not flattened under a single `ownerFile`, so a Page's `./page.css` cannot
35
+ * resolve against a content entry directory. Object-spread of two bags copies
36
+ * `ownerFile` onto the other side's relative paths; use this helper instead.
37
+ */
38
+ export function mergePageDependencies(base, additional) {
39
+ if (!base)
40
+ return additional;
41
+ if (!additional)
42
+ return base;
43
+ const contributions = [
44
+ ...listFileOwnedDependencyContributions(base),
45
+ ...listFileOwnedDependencyContributions(additional),
46
+ ];
47
+ if (contributions.length === 0)
48
+ return undefined;
49
+ if (contributions.length === 1)
50
+ return contributions[0];
51
+ return { contributions };
52
+ }
@@ -2,3 +2,6 @@ export type * from './types/public-types.js';
2
2
  export type * from './eco/eco.types.js';
3
3
  export { eco } from './eco/eco.browser.js';
4
4
  export { bindComponentIdentity, getComponentIdentity, type ComponentIdentity } from './eco/component-identity.js';
5
+ export { attachDiscoveredDependencies } from './eco/discovered-dependencies.js';
6
+ export { listFileOwnedDependencyContributions, mergePageDependencies } from './eco/page-dependency-contributions.js';
7
+ export { mergeLayoutDependencies } from './eco/page-layout-normalization.js';
@@ -1,2 +1,5 @@
1
1
  export { eco } from './eco/eco.browser.js';
2
2
  export { bindComponentIdentity, getComponentIdentity } from './eco/component-identity.js';
3
+ export { attachDiscoveredDependencies } from './eco/discovered-dependencies.js';
4
+ export { listFileOwnedDependencyContributions, mergePageDependencies } from './eco/page-dependency-contributions.js';
5
+ export { mergeLayoutDependencies } from './eco/page-layout-normalization.js';