@ecopages/core 0.2.0-beta.13 → 0.2.0-beta.14

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 (54) hide show
  1. package/README.md +1 -1
  2. package/package.json +26 -2
  3. package/src/adapters/abstract/application-adapter.d.ts +27 -3
  4. package/src/adapters/abstract/application-adapter.js +28 -0
  5. package/src/adapters/abstract/server-adapter.d.ts +3 -2
  6. package/src/adapters/bun/create-app.d.ts +1 -1
  7. package/src/adapters/bun/create-app.js +23 -6
  8. package/src/adapters/bun/server-adapter.d.ts +9 -2
  9. package/src/adapters/bun/server-adapter.js +28 -4
  10. package/src/adapters/create-app.d.ts +1 -12
  11. package/src/adapters/create-app.js +0 -58
  12. package/src/adapters/index.d.ts +1 -1
  13. package/src/adapters/index.js +1 -2
  14. package/src/adapters/node/create-app.d.ts +1 -1
  15. package/src/adapters/node/create-app.js +14 -4
  16. package/src/adapters/node/server-adapter.d.ts +7 -1
  17. package/src/adapters/node/server-adapter.js +25 -3
  18. package/src/adapters/shared/fs-server-response-matcher.d.ts +2 -0
  19. package/src/adapters/shared/fs-server-response-matcher.js +39 -17
  20. package/src/dev/server-ready-message.d.ts +6 -0
  21. package/src/dev/server-ready-message.js +8 -0
  22. package/src/eco/README.md +11 -0
  23. package/src/eco/eco-declared-component.d.ts +14 -0
  24. package/src/eco/eco-declared-component.js +16 -0
  25. package/src/eco/eco.browser.js +5 -6
  26. package/src/eco/eco.js +5 -6
  27. package/src/eco/eco.types.d.ts +2 -2
  28. package/src/eco/page-layout-normalization.d.ts +21 -0
  29. package/src/eco/page-layout-normalization.js +59 -0
  30. package/src/errors/index.d.ts +1 -0
  31. package/src/errors/index.js +7 -1
  32. package/src/errors/undeclared-component-dependency-error.d.ts +11 -0
  33. package/src/errors/undeclared-component-dependency-error.js +17 -0
  34. package/src/route-renderer/orchestration/component-graph.js +12 -4
  35. package/src/route-renderer/orchestration/document-shell-render.service.d.ts +37 -1
  36. package/src/route-renderer/orchestration/document-shell-render.service.js +79 -13
  37. package/src/route-renderer/orchestration/integration-renderer.js +5 -1
  38. package/src/route-renderer/orchestration/layout-shell-props.service.d.ts +20 -0
  39. package/src/route-renderer/orchestration/layout-shell-props.service.js +31 -0
  40. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +1 -0
  41. package/src/route-renderer/orchestration/ownership-validation.service.js +18 -0
  42. package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +3 -1
  43. package/src/route-renderer/orchestration/route-prepared-options.builder.js +3 -1
  44. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +3 -1
  45. package/src/route-renderer/orchestration/route-render-orchestrator.js +3 -3
  46. package/src/route-renderer/orchestration/string-markup-renderer.js +2 -1
  47. package/src/route-renderer/page-loading/component-dependency-collection.js +5 -2
  48. package/src/types/public-types.d.ts +51 -4
  49. package/src/utils/parse-cli-args.d.ts +1 -0
  50. package/src/utils/parse-cli-args.js +3 -0
  51. package/src/utils/server-utils.module.js +1 -0
  52. package/src/watchers/project-watcher-ignore.d.ts +11 -0
  53. package/src/watchers/project-watcher-ignore.js +16 -0
  54. package/src/watchers/project-watcher.js +2 -6
@@ -24,6 +24,7 @@ export declare class OwnershipValidationService {
24
24
  * Validates foreign ownership edges reachable from the supplied route roots.
25
25
  */
26
26
  validate(input: OwnershipValidationInput): OwnershipValidationError[];
27
+ private assertDeclaredComponentDependencies;
27
28
  private isRegisteredIntegration;
28
29
  }
29
30
  /**
@@ -1,4 +1,6 @@
1
+ import { assertEcoDeclaredComponent } from "../../eco/eco-declared-component.js";
1
2
  import { mapDeclaredOwnershipGraph } from "./declared-ownership-graph.js";
3
+ import { walkComponentGraph } from "./component-graph.js";
2
4
  class OwnershipValidationService {
3
5
  appConfig;
4
6
  /**
@@ -11,6 +13,7 @@ class OwnershipValidationService {
11
13
  * Validates foreign ownership edges reachable from the supplied route roots.
12
14
  */
13
15
  validate(input) {
16
+ this.assertDeclaredComponentDependencies(input);
14
17
  return mapDeclaredOwnershipGraph({
15
18
  roots: input.roots,
16
19
  currentIntegrationName: input.currentIntegrationName,
@@ -44,6 +47,21 @@ class OwnershipValidationService {
44
47
  }
45
48
  }).flat();
46
49
  }
50
+ assertDeclaredComponentDependencies(input) {
51
+ walkComponentGraph({
52
+ roots: input.roots,
53
+ currentIntegrationName: input.currentIntegrationName,
54
+ onComponent: ({ component }) => {
55
+ const parentFile = component.config?.__eco?.file;
56
+ for (const child of component.config?.dependencies?.components ?? []) {
57
+ if (!child) {
58
+ continue;
59
+ }
60
+ assertEcoDeclaredComponent(child, { parentComponentFile: parentFile });
61
+ }
62
+ }
63
+ });
64
+ }
47
65
  isRegisteredIntegration(integrationName, currentIntegrationName) {
48
66
  if (integrationName === currentIntegrationName) {
49
67
  return true;
@@ -1,10 +1,12 @@
1
1
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
- import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphResult, PageMetadataProps, RouteRendererOptions } from '../../types/public-types.js';
2
+ import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphResult, PageMetadataProps, EcoPageLayoutEntry, RouteRendererOptions } from '../../types/public-types.js';
3
3
  import { type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
4
4
  type PreparedRenderInputs = {
5
5
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
6
6
  HtmlTemplate: EcoComponent<HtmlTemplateProps>;
7
+ Layouts: EcoComponent[];
7
8
  Layout?: EcoComponent;
9
+ layoutEntries?: EcoPageLayoutEntry[];
8
10
  props: Record<string, unknown>;
9
11
  metadata: PageMetadataProps;
10
12
  integrationSpecificProps: Record<string, unknown>;
@@ -11,7 +11,7 @@ function buildPreparedRenderOptions(input) {
11
11
  componentRender,
12
12
  appConfig
13
13
  } = input;
14
- const { Page, HtmlTemplate, Layout, props, metadata, integrationSpecificProps } = resolvedInputs;
14
+ const { Page, HtmlTemplate, Layouts, Layout, layoutEntries, props, metadata, integrationSpecificProps } = resolvedInputs;
15
15
  const dedupedDependencies = dedupeProcessedAssets(allDependencies);
16
16
  const pagePackage = createPagePackage(dedupedDependencies, { pageBrowserGraph });
17
17
  const pageProps = {
@@ -31,7 +31,9 @@ function buildPreparedRenderOptions(input) {
31
31
  pagePackage,
32
32
  componentRender,
33
33
  HtmlTemplate,
34
+ Layouts,
34
35
  Layout,
36
+ layoutEntries,
35
37
  props,
36
38
  Page,
37
39
  metadata,
@@ -1,5 +1,5 @@
1
1
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
- import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
2
+ import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult, EcoPageLayoutEntry } from '../../types/public-types.js';
3
3
  import type { AssetProcessingService, ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
4
4
  import type { HtmlDocumentContribution } from '../../services/html/html-transformer.service.js';
5
5
  import { OwnershipValidationService } from './ownership-validation.service.js';
@@ -7,7 +7,9 @@ import { PageBrowserGraphService } from './page-browser-graph.service.js';
7
7
  export type RouteRenderOrchestratorResolvedInputs = {
8
8
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
9
9
  HtmlTemplate: EcoComponent<HtmlTemplateProps>;
10
+ Layouts: EcoComponent[];
10
11
  Layout?: EcoComponent;
12
+ layoutEntries?: EcoPageLayoutEntry[];
11
13
  props: Record<string, unknown>;
12
14
  metadata: PageMetadataProps;
13
15
  integrationSpecificProps: Record<string, unknown>;
@@ -28,17 +28,17 @@ class RouteRenderOrchestrator {
28
28
  */
29
29
  async prepareRenderOptions(routeOptions, adapter) {
30
30
  const resolvedInputs = await adapter.resolveRouteRenderInputs(routeOptions);
31
- const { Page, HtmlTemplate, Layout } = resolvedInputs;
31
+ const { Page, HtmlTemplate, Layouts, Layout } = resolvedInputs;
32
32
  const validationErrors = this.ownershipValidationService.validate({
33
33
  currentIntegrationName: adapter.name,
34
34
  roots: [
35
35
  { component: HtmlTemplate, source: "html-template" },
36
- ...Layout ? [{ component: Layout, source: "layout" }] : [],
36
+ ...Layouts.map((layout) => ({ component: layout, source: "layout" })),
37
37
  { component: Page, source: "page" }
38
38
  ]
39
39
  });
40
40
  throwIfOwnershipInvalid(validationErrors);
41
- const componentsToResolve = Layout ? [HtmlTemplate, Layout, Page] : [HtmlTemplate, Page];
41
+ const componentsToResolve = [HtmlTemplate, ...Layouts, Page];
42
42
  const [{ resolvedDependencies }, pageBrowserGraph, componentRender] = await Promise.all([
43
43
  adapter.resolveRouteDependencies({
44
44
  components: componentsToResolve
@@ -1,4 +1,5 @@
1
1
  import { IntegrationRenderer } from "./integration-renderer.js";
2
+ import { resolveInnermostPageLayout } from "./layout-shell-props.service.js";
2
3
  class StringMarkupRenderer extends IntegrationRenderer {
3
4
  async renderComponent(input) {
4
5
  if (typeof input.component !== "function") {
@@ -42,7 +43,7 @@ class StringMarkupRenderer extends IntegrationRenderer {
42
43
  view,
43
44
  props,
44
45
  ctx,
45
- layout: view.config?.layout
46
+ layout: resolveInnermostPageLayout(view.config?.layouts)
46
47
  });
47
48
  } catch (error) {
48
49
  throw this.createRenderError("Error rendering view", error);
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { assertEcoDeclaredComponent } from "../../eco/eco-declared-component.js";
2
3
  import { AssetFactory } from "../../services/assets/asset-processing-service/index.js";
3
4
  import { extractEcopagesVirtualImports } from "./ecopages-virtual-imports.js";
4
5
  import { collectDeclaredAssetEntries } from "./declared-asset-collection.js";
@@ -103,9 +104,11 @@ function collectComponentDependencies(options) {
103
104
  });
104
105
  if (dependenciesConfig?.components) {
105
106
  for (const nestedComponent of dependenciesConfig.components) {
106
- if (nestedComponent?.config) {
107
- collect(nestedComponent.config);
107
+ if (!nestedComponent) {
108
+ continue;
108
109
  }
110
+ assertEcoDeclaredComponent(nestedComponent, { parentComponentFile: file });
111
+ collect(nestedComponent.config);
109
112
  }
110
113
  }
111
114
  };
@@ -334,8 +334,21 @@ export type EcoComponentDependencies = {
334
334
  * to express explicit module imports for client bundles.
335
335
  */
336
336
  modules?: string[];
337
- components?: EcoComponent[];
337
+ /**
338
+ * Child components whose assets and foreign-child graph are collected transitively.
339
+ * Each entry must be an `eco.component()`, `eco.layout()`, or `eco.html()` result
340
+ * with plugin-injected `config.__eco` metadata.
341
+ */
342
+ components?: EcoDeclaredComponent[];
338
343
  };
344
+ /**
345
+ * Component returned from `eco.component()`, `eco.layout()`, or `eco.html()`.
346
+ *
347
+ * @remarks
348
+ * Used for `dependencies.components` and eco factory return types. Plugin-injected
349
+ * `config.__eco` is enforced at runtime via `isEcoDeclaredComponent()`, not by this alias.
350
+ */
351
+ export type EcoDeclaredComponent<P = any, R = any> = EcoComponent<P, R>;
339
352
  export type EcoPagesElement = string | Promise<string>;
340
353
  /**
341
354
  * Serializable child payloads accepted by cross-integration deferred rendering.
@@ -401,11 +414,13 @@ export type EcoComponentConfig = {
401
414
  * );
402
415
  *
403
416
  * // Page using the layout
404
- * const MyPage = () => <h1>Hello</h1>;
405
- * MyPage.config = { layout: Layout };
417
+ * const MyPage = eco.page({ layout: Layout, render: () => <h1>Hello</h1> });
406
418
  * ```
407
419
  */
408
- layout?: EcoPageLayoutComponent<any>;
420
+ /** Normalized outer→inner layout stack from `eco.page({ layout: [...] })`. */
421
+ layouts?: EcoDeclaredComponent[];
422
+ /** Layout entries retained for per-tier prop factories. */
423
+ layoutEntries?: EcoPageLayoutEntry[];
409
424
  dependencies?: EcoComponentDependencies;
410
425
  /**
411
426
  * Internal: Resolved lazy scripts grouped by trigger.
@@ -529,6 +544,36 @@ export interface HtmlTemplateProps<T = EcoPagesElement> extends PageHeadProps<T>
529
544
  headContent?: T;
530
545
  pageProps: Record<string, unknown>;
531
546
  }
547
+ /**
548
+ * Request-scoped context available to layout prop factories on `eco.page`.
549
+ */
550
+ export type LayoutPropsContext = {
551
+ params?: Record<string, string>;
552
+ query?: Record<string, string>;
553
+ locals?: RequestLocals;
554
+ };
555
+ /**
556
+ * One layout tier in a page layout stack (outer→inner).
557
+ */
558
+ export type EcoPageLayoutEntry<E = EcoPagesElement> = {
559
+ component: EcoDeclaredComponent<any, E>;
560
+ props?: (context: LayoutPropsContext) => Record<string, unknown>;
561
+ };
562
+ /**
563
+ * Layout declaration accepted by `eco.page()`.
564
+ *
565
+ * @remarks
566
+ * Array order is outer→inner, matching Next.js App Router segment nesting.
567
+ */
568
+ export type EcoPageLayoutSpec<E = EcoPagesElement> = EcoDeclaredComponent<any, E> | EcoPageLayoutEntry<E>;
569
+ /**
570
+ * One or more layout tiers for `eco.page({ layout })`.
571
+ *
572
+ * @remarks
573
+ * When an array, order is **outer → inner** (outermost layout wraps all inner tiers).
574
+ * Normalized at factory time to `config.layouts` and `config.layoutEntries`.
575
+ */
576
+ export type EcoPageLayouts<E = EcoPagesElement> = EcoPageLayoutSpec<E> | EcoPageLayoutSpec<E>[];
532
577
  /**
533
578
  * Layout components accepted by pages.
534
579
  *
@@ -746,7 +791,9 @@ export type IntegrationRendererRenderOptions<C = EcoPagesElement> = RouteRendere
746
791
  metadata: PageMetadataProps;
747
792
  HtmlTemplate: EcoHtmlComponent<C>;
748
793
  Page: EcoComponent<PageProps, C>;
794
+ Layouts?: EcoDeclaredComponent[];
749
795
  Layout?: EcoPageLayoutComponent<any>;
796
+ layoutEntries?: EcoPageLayoutEntry[];
750
797
  dependencies?: EcoComponentDependencies;
751
798
  resolvedDependencies: ProcessedAsset[];
752
799
  pagePackage?: PagePackageResult;
@@ -14,6 +14,7 @@ export type ReturnParseCliArgs = {
14
14
  start: boolean;
15
15
  dev: boolean;
16
16
  force: boolean;
17
+ serveOnly: boolean;
17
18
  port?: number;
18
19
  hostname?: string;
19
20
  reactFastRefresh?: boolean;
@@ -8,6 +8,7 @@ function getEmbeddedRuntimeCommandOptions() {
8
8
  start: !isDevelopment,
9
9
  dev: isDevelopment,
10
10
  force: false,
11
+ serveOnly: false,
11
12
  port: void 0,
12
13
  hostname: void 0,
13
14
  reactFastRefresh: void 0
@@ -27,6 +28,7 @@ function parseCliArgs(options = {}) {
27
28
  preview: { type: "boolean" },
28
29
  build: { type: "boolean" },
29
30
  force: { type: "boolean" },
31
+ "serve-only": { type: "boolean" },
30
32
  port: { type: "string" },
31
33
  hostname: { type: "string" },
32
34
  "react-fast-refresh": { type: "boolean" }
@@ -51,6 +53,7 @@ function parseCliArgs(options = {}) {
51
53
  start: isStartCommand,
52
54
  dev: isDevCommand,
53
55
  force: !!values.force,
56
+ serveOnly: !!values["serve-only"] || process.env.ECOPAGES_PREVIEW_SERVE_ONLY === "true",
54
57
  port: values.port ? Number(values.port) : void 0,
55
58
  hostname: values.hostname,
56
59
  reactFastRefresh: values["react-fast-refresh"]
@@ -17,6 +17,7 @@ const ContentTypeMap = /* @__PURE__ */ new Map([
17
17
  ["ogv", "video/ogg"],
18
18
  ["mov", "video/quicktime"],
19
19
  ["txt", "text/plain"],
20
+ ["md", "text/markdown"],
20
21
  ["html", "text/html"],
21
22
  ["css", "text/css"],
22
23
  ["js", "text/javascript"],
@@ -0,0 +1,11 @@
1
+ export type ProjectWatcherIgnorePaths = {
2
+ workDir: string;
3
+ distDir: string;
4
+ };
5
+ /**
6
+ * @remarks
7
+ * chokidar v4+ no longer treats glob strings in `ignored` as patterns. A path
8
+ * predicate keeps node_modules, VCS metadata, and scoped artifact dirs out of
9
+ * the watch tree without descending into symlinked monorepo node_modules.
10
+ */
11
+ export declare function createProjectWatcherIgnorePredicate(absolutePaths: ProjectWatcherIgnorePaths): (watchedPath: string) => boolean;
@@ -0,0 +1,16 @@
1
+ import path from "node:path";
2
+ function createProjectWatcherIgnorePredicate(absolutePaths) {
3
+ const ignoredPrefixes = [absolutePaths.workDir, absolutePaths.distDir];
4
+ return (watchedPath) => {
5
+ const segments = watchedPath.split(path.sep);
6
+ if (segments.includes("node_modules") || segments.includes(".git")) {
7
+ return true;
8
+ }
9
+ return ignoredPrefixes.some(
10
+ (prefix) => watchedPath === prefix || watchedPath.startsWith(`${prefix}${path.sep}`)
11
+ );
12
+ };
13
+ }
14
+ export {
15
+ createProjectWatcherIgnorePredicate
16
+ };
@@ -3,6 +3,7 @@ import chokidar, {} from "chokidar";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
4
  import { appLogger } from "../global/app-logger.js";
5
5
  import { DevelopmentInvalidationService } from "../services/invalidation/development-invalidation.service.js";
6
+ import { createProjectWatcherIgnorePredicate } from "./project-watcher-ignore.js";
6
7
  class ProjectWatcher {
7
8
  /**
8
9
  * Duplicate identical watcher events within this window are ignored.
@@ -261,12 +262,7 @@ class ProjectWatcher {
261
262
  for (const watchPath of this.appConfig.additionalWatchPaths) {
262
263
  processorPaths.add(watchPath);
263
264
  }
264
- const ignored = [
265
- "**/node_modules/**",
266
- "**/.git/**",
267
- path.join(this.appConfig.absolutePaths.workDir, "**"),
268
- path.join(this.appConfig.absolutePaths.distDir, "**")
269
- ];
265
+ const ignored = createProjectWatcherIgnorePredicate(this.appConfig.absolutePaths);
270
266
  this.watcher = chokidar.watch(Array.from(processorPaths), {
271
267
  ignoreInitial: true,
272
268
  ignorePermissionErrors: true,