@ecopages/ecopages-jsx 0.2.0-beta.30 → 0.2.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-beta.30",
3
+ "version": "0.2.0-beta.32",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -31,12 +31,12 @@
31
31
  "directory": "packages/integrations/ecopages-jsx"
32
32
  },
33
33
  "dependencies": {
34
- "@ecopages/mdx": "0.2.0-beta.30",
34
+ "@ecopages/mdx": "0.2.0-beta.32",
35
35
  "@mdx-js/mdx": "^3.1.1",
36
36
  "vfile": "^6.0.3"
37
37
  },
38
38
  "peerDependencies": {
39
- "@ecopages/core": "0.2.0-beta.30",
39
+ "@ecopages/core": "0.2.0-beta.32",
40
40
  "@ecopages/jsx": "0.3.0-beta.3",
41
41
  "@ecopages/radiant": "0.3.0-beta.3"
42
42
  }
@@ -0,0 +1,17 @@
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
+ import type { DevTransformBundleContributor } from '@ecopages/core/dev/transform-server';
3
+ import type { EcopagesJsxHmrStrategy } from '../ecopages-jsx-hmr-strategy.js';
4
+ export type EcopagesJsxDevTransformContributorOptions = {
5
+ strategy: EcopagesJsxHmrStrategy;
6
+ getMdxLoaderPlugin: () => EcoBuildPlugin | undefined;
7
+ };
8
+ /**
9
+ * Supplies Ecopages JSX module plugins for the core dev transform bundler.
10
+ */
11
+ export declare class EcopagesJsxDevTransformContributor implements DevTransformBundleContributor {
12
+ private readonly strategy;
13
+ private readonly getMdxLoaderPlugin;
14
+ constructor(options: EcopagesJsxDevTransformContributorOptions);
15
+ ownsModule(sourcePath: string): boolean;
16
+ getModulePlugins(sourcePath: string): Promise<readonly EcoBuildPlugin[]>;
17
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Supplies Ecopages JSX module plugins for the core dev transform bundler.
3
+ */
4
+ export class EcopagesJsxDevTransformContributor {
5
+ strategy;
6
+ getMdxLoaderPlugin;
7
+ constructor(options) {
8
+ this.strategy = options.strategy;
9
+ this.getMdxLoaderPlugin = options.getMdxLoaderPlugin;
10
+ }
11
+ ownsModule(sourcePath) {
12
+ return this.strategy.ownsDevTransformEntrypoint(sourcePath);
13
+ }
14
+ async getModulePlugins(sourcePath) {
15
+ if (!sourcePath.endsWith('.mdx')) {
16
+ return [];
17
+ }
18
+ const mdxLoader = this.getMdxLoaderPlugin();
19
+ return mdxLoader ? [mdxLoader] : [];
20
+ }
21
+ }
package/src/eco-embed.js CHANGED
@@ -1,11 +1,9 @@
1
- import { eco } from "@ecopages/core";
2
- function EcoEmbed({
3
- component,
4
- props,
5
- children
6
- }) {
7
- return eco.embed(component, props, children);
1
+ /** @jsxImportSource @ecopages/jsx */
2
+ import { eco } from '@ecopages/core';
3
+ /**
4
+ * Renders a foreign or same-integration eco component from an Ecopages JSX
5
+ * file without forcing inline mixed-JSX authoring.
6
+ */
7
+ export function EcoEmbed({ component, props, children, }) {
8
+ return eco.embed(component, props, children);
8
9
  }
9
- export {
10
- EcoEmbed
11
- };
@@ -1,63 +1,87 @@
1
- import path from "node:path";
2
- import { rapidhash } from "@ecopages/core/hash";
1
+ /**
2
+ * Tracks the source files that participate in the active SSR render tree.
3
+ *
4
+ * @remarks
5
+ * The JSX integration renderer calls {@link updateEjsxHmrOwnership} after each
6
+ * page, component, or view render so the HMR strategy can answer
7
+ * "does this changed file affect SSR HTML?" without re-walking the tree on
8
+ * every watcher event.
9
+ *
10
+ * The state is module-scoped because the HMR strategy and renderer live in
11
+ * different call sites (server strategy vs SSR renderer) and share no other
12
+ * lifecycle anchor. A fresh state is installed only when the source-file set
13
+ * changes, so the renderer's per-render hook stays O(1) in the steady state.
14
+ */
15
+ import path from 'node:path';
16
+ import { rapidhash } from '@ecopages/core/hash';
3
17
  const EMPTY_STATE = {
4
- fileOwners: /* @__PURE__ */ new Set(),
5
- hash: ""
18
+ fileOwners: new Set(),
19
+ hash: '',
6
20
  };
7
21
  let currentState = EMPTY_STATE;
8
- function getEjsxHmrOwnership() {
9
- return currentState;
22
+ /**
23
+ * Returns the most recently recorded active render tree.
24
+ */
25
+ export function getEjsxHmrOwnership() {
26
+ return currentState;
10
27
  }
11
- function updateEjsxHmrOwnership(components) {
12
- const files = collectFileOwners(components);
13
- const hash = hashFileSet(files);
14
- if (hash === currentState.hash) {
15
- return;
16
- }
17
- currentState = {
18
- fileOwners: files,
19
- hash
20
- };
28
+ /**
29
+ * Updates the active render tree from one or more root components.
30
+ *
31
+ * @remarks
32
+ * Walks each root's `config.__eco.file`, `config.dependencies.components[*].config`,
33
+ * and `config.layouts[*].config` recursively, collecting every file path. The
34
+ * state is replaced only when the resulting file set has a different hash, so
35
+ * the renderer's per-render hook is free in the steady state.
36
+ */
37
+ export function updateEjsxHmrOwnership(components) {
38
+ const files = collectFileOwners(components);
39
+ const hash = hashFileSet(files);
40
+ if (hash === currentState.hash) {
41
+ return;
42
+ }
43
+ currentState = {
44
+ fileOwners: files,
45
+ hash,
46
+ };
21
47
  }
22
- function resetEjsxHmrOwnership() {
23
- currentState = EMPTY_STATE;
48
+ /**
49
+ * Resets the state to empty. Used by tests and by HMR manager teardown.
50
+ */
51
+ export function resetEjsxHmrOwnership() {
52
+ currentState = EMPTY_STATE;
24
53
  }
25
54
  function collectFileOwners(components) {
26
- const files = /* @__PURE__ */ new Set();
27
- const visited = /* @__PURE__ */ new Set();
28
- const visit = (config) => {
29
- const file = config?.__eco?.file;
30
- if (!file) {
31
- return;
32
- }
33
- const resolved = path.resolve(file);
34
- if (visited.has(resolved)) {
35
- return;
36
- }
37
- visited.add(resolved);
38
- files.add(resolved);
39
- for (const dependency of config?.dependencies?.components ?? []) {
40
- visit(dependency?.config);
55
+ const files = new Set();
56
+ const visited = new Set();
57
+ const visit = (config) => {
58
+ const file = config?.__eco?.file;
59
+ if (!file) {
60
+ return;
61
+ }
62
+ const resolved = path.resolve(file);
63
+ if (visited.has(resolved)) {
64
+ return;
65
+ }
66
+ visited.add(resolved);
67
+ files.add(resolved);
68
+ for (const dependency of config?.dependencies?.components ?? []) {
69
+ visit(dependency?.config);
70
+ }
71
+ for (const layout of config?.layouts ?? []) {
72
+ visit(layout?.config);
73
+ }
74
+ };
75
+ for (const component of components) {
76
+ visit(component?.config);
41
77
  }
42
- for (const layout of config?.layouts ?? []) {
43
- visit(layout?.config);
44
- }
45
- };
46
- for (const component of components) {
47
- visit(component?.config);
48
- }
49
- return files;
78
+ return files;
50
79
  }
51
80
  function hashFileSet(files) {
52
- if (files.size === 0) {
53
- return "";
54
- }
55
- const sorted = Array.from(files).sort();
56
- const joined = sorted.join("\n");
57
- return rapidhash(joined).toString(36);
81
+ if (files.size === 0) {
82
+ return '';
83
+ }
84
+ const sorted = Array.from(files).sort();
85
+ const joined = sorted.join('\n');
86
+ return rapidhash(joined).toString(36);
58
87
  }
59
- export {
60
- getEjsxHmrOwnership,
61
- resetEjsxHmrOwnership,
62
- updateEjsxHmrOwnership
63
- };
@@ -1,35 +1,18 @@
1
- /**
2
- * HMR strategy for JSX integration-owned SSR source modules.
3
- *
4
- * @remarks
5
- * Edits to files that produce SSR HTML (page sources, layout sources, components
6
- * that the active render tree depends on) need a soft page refetch so the
7
- * browser picks up the new markup. Plain `update` events re-import modules on
8
- * the client but leave the server-rendered DOM stale.
9
- *
10
- * The strategy matches a file when any of these hold:
11
- * - the file lives under `pagesDir` or `layoutsDir` and has a JSX `templatesExt`
12
- * - the file appears in the active render tree's component dependency graph
13
- *
14
- * It defers registered HMR entrypoints to {@link JsHmrStrategy} so script-only
15
- * edits keep their existing `update`-then-hot-accept path. It also defers
16
- * include templates and explicit server views to {@link ServerRenderedTemplateHmrStrategy}
17
- * by returning `false` for those paths.
18
- */
19
1
  import { HmrStrategy, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
20
- export interface EcopagesJsxHmrStrategyContext {
21
- getWatchedFiles(): Map<string, string>;
2
+ import type { HmrRegisteredEntrypointsContext } from '@ecopages/core/hmr/hmr-registered-entrypoints-context';
3
+ export type EcopagesJsxHmrStrategyContext = HmrRegisteredEntrypointsContext & {
22
4
  getSrcDir(): string;
23
5
  getPagesDir(): string;
24
6
  getLayoutsDir(): string;
25
7
  getIncludesDir(): string;
26
8
  getTemplateExtensions(): ReadonlyArray<string>;
27
- }
9
+ };
28
10
  export declare class EcopagesJsxHmrStrategy extends HmrStrategy {
29
11
  readonly type: 100;
30
12
  private readonly context;
31
13
  constructor(context: EcopagesJsxHmrStrategyContext);
32
14
  matches(filePath: string): boolean;
15
+ ownsDevTransformEntrypoint(filePath: string): boolean;
33
16
  process(filePath: string): Promise<HmrAction>;
34
17
  private isPageOrLayoutSource;
35
18
  private hasJsxTemplateExtension;
@@ -1,63 +1,67 @@
1
- import path from "node:path";
2
- import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
3
- import { getEjsxHmrOwnership } from "./ecopages-jsx-hmr-ownership.js";
4
- class EcopagesJsxHmrStrategy extends HmrStrategy {
5
- type = HmrStrategyType.INTEGRATION;
6
- context;
7
- constructor(context) {
8
- super();
9
- this.context = context;
10
- }
11
- matches(filePath) {
12
- const resolvedPath = path.resolve(filePath);
13
- const watchedFiles = this.context.getWatchedFiles();
14
- if (watchedFiles.has(resolvedPath) || [...watchedFiles.keys()].some((key) => path.resolve(key) === resolvedPath)) {
15
- return false;
1
+ import path from 'node:path';
2
+ import { HmrStrategy, HmrStrategyType } from '@ecopages/core/hmr/hmr-strategy';
3
+ import { isRegisteredDevTransformEntrypoint } from '@ecopages/core/hmr/hmr-entrypoint-output';
4
+ import { getEjsxHmrOwnership } from './ecopages-jsx-hmr-ownership.js';
5
+ export class EcopagesJsxHmrStrategy extends HmrStrategy {
6
+ type = HmrStrategyType.INTEGRATION;
7
+ context;
8
+ constructor(context) {
9
+ super();
10
+ this.context = context;
16
11
  }
17
- const srcDir = path.resolve(this.context.getSrcDir());
18
- if (!resolvedPath.startsWith(`${srcDir}${path.sep}`) && resolvedPath !== srcDir) {
19
- return false;
20
- }
21
- const includesDir = path.resolve(this.context.getIncludesDir());
22
- if (includesDir && (resolvedPath === includesDir || resolvedPath.startsWith(`${includesDir}${path.sep}`))) {
23
- return false;
12
+ matches(filePath) {
13
+ const resolvedPath = path.resolve(filePath);
14
+ if (isRegisteredDevTransformEntrypoint(this.context.getRegisteredEntrypoints(), resolvedPath)) {
15
+ return false;
16
+ }
17
+ const srcDir = path.resolve(this.context.getSrcDir());
18
+ if (!resolvedPath.startsWith(`${srcDir}${path.sep}`) && resolvedPath !== srcDir) {
19
+ return false;
20
+ }
21
+ const includesDir = path.resolve(this.context.getIncludesDir());
22
+ if (includesDir && (resolvedPath === includesDir || resolvedPath.startsWith(`${includesDir}${path.sep}`))) {
23
+ return false;
24
+ }
25
+ if (this.isPageOrLayoutSource(resolvedPath)) {
26
+ return this.hasJsxTemplateExtension(resolvedPath);
27
+ }
28
+ const ownership = getEjsxHmrOwnership();
29
+ if (ownership.fileOwners.has(resolvedPath)) {
30
+ return true;
31
+ }
32
+ return false;
24
33
  }
25
- if (this.isPageOrLayoutSource(resolvedPath)) {
26
- return this.hasJsxTemplateExtension(resolvedPath);
34
+ ownsDevTransformEntrypoint(filePath) {
35
+ const resolvedPath = path.resolve(filePath);
36
+ if (!isRegisteredDevTransformEntrypoint(this.context.getRegisteredEntrypoints(), resolvedPath)) {
37
+ return false;
38
+ }
39
+ return this.isPageOrLayoutSource(resolvedPath) && this.hasJsxTemplateExtension(resolvedPath);
27
40
  }
28
- const ownership = getEjsxHmrOwnership();
29
- if (ownership.fileOwners.has(resolvedPath)) {
30
- return true;
41
+ async process(filePath) {
42
+ return {
43
+ type: 'broadcast',
44
+ events: [
45
+ {
46
+ type: 'layout-update',
47
+ path: filePath,
48
+ timestamp: Date.now(),
49
+ },
50
+ ],
51
+ };
31
52
  }
32
- return false;
33
- }
34
- async process(filePath) {
35
- return {
36
- type: "broadcast",
37
- events: [
38
- {
39
- type: "layout-update",
40
- path: filePath,
41
- timestamp: Date.now()
42
- }
43
- ]
44
- };
45
- }
46
- isPageOrLayoutSource(resolvedPath) {
47
- const pagesDir = path.resolve(this.context.getPagesDir());
48
- const layoutsDir = path.resolve(this.context.getLayoutsDir());
49
- if (pagesDir && (resolvedPath === pagesDir || resolvedPath.startsWith(`${pagesDir}${path.sep}`))) {
50
- return true;
53
+ isPageOrLayoutSource(resolvedPath) {
54
+ const pagesDir = path.resolve(this.context.getPagesDir());
55
+ const layoutsDir = path.resolve(this.context.getLayoutsDir());
56
+ if (pagesDir && (resolvedPath === pagesDir || resolvedPath.startsWith(`${pagesDir}${path.sep}`))) {
57
+ return true;
58
+ }
59
+ if (layoutsDir && (resolvedPath === layoutsDir || resolvedPath.startsWith(`${layoutsDir}${path.sep}`))) {
60
+ return true;
61
+ }
62
+ return false;
51
63
  }
52
- if (layoutsDir && (resolvedPath === layoutsDir || resolvedPath.startsWith(`${layoutsDir}${path.sep}`))) {
53
- return true;
64
+ hasJsxTemplateExtension(resolvedPath) {
65
+ return this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension));
54
66
  }
55
- return false;
56
- }
57
- hasJsxTemplateExtension(resolvedPath) {
58
- return this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension));
59
- }
60
67
  }
61
- export {
62
- EcopagesJsxHmrStrategy
63
- };
@@ -1,82 +1,70 @@
1
- import { readFile } from "node:fs/promises";
2
- import { ensurePageConfigLayouts } from "@ecopages/core/eco/page-layout-normalization";
3
- import { rapidhash } from "@ecopages/core/hash";
4
- import {
5
- appendMdxExtensions,
6
- createMdxExtensionFilter,
7
- createMdxLoaderPlugin as createMdxLoaderPluginCore,
8
- resolveMdxCompilerOptions as resolveMdxCompilerOptionsCore
9
- } from "@ecopages/mdx/core";
10
- import { VFile } from "vfile";
11
- import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
12
- const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptionsCore(mdxOptions, {
13
- jsxImportSource: "@ecopages/jsx",
14
- defaults: {
15
- format: "detect",
16
- outputFormat: "program"
17
- }
1
+ import { readFile } from 'node:fs/promises';
2
+ import { ensurePageConfigLayouts } from '@ecopages/core/eco/page-layout-normalization';
3
+ import { rapidhash } from '@ecopages/core/hash';
4
+ import { appendMdxExtensions, createMdxExtensionFilter, createMdxLoaderPlugin as createMdxLoaderPluginCore, resolveMdxCompilerOptions as resolveMdxCompilerOptionsCore, } from '@ecopages/mdx/core';
5
+ import { VFile } from 'vfile';
6
+ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
7
+ export { appendMdxExtensions, createMdxExtensionFilter };
8
+ export const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptionsCore(mdxOptions, {
9
+ jsxImportSource: '@ecopages/jsx',
10
+ defaults: {
11
+ format: 'detect',
12
+ outputFormat: 'program',
13
+ },
18
14
  });
19
- const createMdxLoaderPlugin = (compilerOptions, extensions) => createMdxLoaderPluginCore({
20
- name: "ecopages-jsx-mdx-loader",
21
- compilerOptions,
22
- extensions,
23
- includeSourceMap: false,
24
- loader: "js"
15
+ export const createMdxLoaderPlugin = (compilerOptions, extensions) => createMdxLoaderPluginCore({
16
+ name: 'ecopages-jsx-mdx-loader',
17
+ compilerOptions,
18
+ extensions,
19
+ includeSourceMap: false,
20
+ loader: 'js',
25
21
  });
26
- const registerBunMdxPlugin = async (compilerOptions, extensions) => {
27
- if (typeof Bun === "undefined") {
28
- return;
29
- }
30
- const filter = createMdxExtensionFilter(extensions);
31
- Bun.plugin({
32
- name: "ecopages-jsx-mdx",
33
- setup(build) {
34
- build.onLoad({ filter }, async (args) => {
35
- const { compile } = await import("@mdx-js/mdx");
36
- const source = await readFile(args.path, "utf-8");
37
- const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
38
- return { contents: String(compiled.value), loader: "js" };
39
- });
22
+ export const registerBunMdxPlugin = async (compilerOptions, extensions) => {
23
+ if (typeof Bun === 'undefined') {
24
+ return;
40
25
  }
41
- });
26
+ const filter = createMdxExtensionFilter(extensions);
27
+ Bun.plugin({
28
+ name: 'ecopages-jsx-mdx',
29
+ setup(build) {
30
+ build.onLoad({ filter }, async (args) => {
31
+ const { compile } = await import('@mdx-js/mdx');
32
+ const source = await readFile(args.path, 'utf-8');
33
+ const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
34
+ return { contents: String(compiled.value), loader: 'js' };
35
+ });
36
+ },
37
+ });
42
38
  };
43
- const isMdxFile = (filePath, extensions) => {
44
- return extensions.some((ext) => filePath.endsWith(ext));
39
+ export const isMdxFile = (filePath, extensions) => {
40
+ return extensions.some((ext) => filePath.endsWith(ext));
45
41
  };
46
- const normalizeMdxPageModule = (file, module) => {
47
- if (typeof module.default !== "function") {
48
- throw new TypeError("MDX file must export a callable default component.");
49
- }
50
- const Page = module.default;
51
- const normalizedConfig = {
52
- ...module.config ?? Page.config ?? {},
53
- __eco: module.config?.__eco ?? Page.config?.__eco ?? {
54
- id: String(rapidhash(file)),
55
- file,
56
- integration: ECOPAGES_JSX_PLUGIN_NAME
42
+ export const normalizeMdxPageModule = (file, module) => {
43
+ if (typeof module.default !== 'function') {
44
+ throw new TypeError('MDX file must export a callable default component.');
57
45
  }
58
- };
59
- if (module.layout && !(normalizedConfig.layouts && normalizedConfig.layouts.length > 0)) {
60
- normalizedConfig.layout = module.layout;
61
- }
62
- ensurePageConfigLayouts(normalizedConfig);
63
- const wrappedPage = async (props) => await Page(props);
64
- wrappedPage.config = normalizedConfig;
65
- if (module.getMetadata ?? Page.metadata) {
66
- wrappedPage.metadata = module.getMetadata ?? Page.metadata;
67
- }
68
- return {
69
- ...module,
70
- default: wrappedPage,
71
- config: normalizedConfig
72
- };
73
- };
74
- export {
75
- appendMdxExtensions,
76
- createMdxExtensionFilter,
77
- createMdxLoaderPlugin,
78
- isMdxFile,
79
- normalizeMdxPageModule,
80
- registerBunMdxPlugin,
81
- resolveMdxCompilerOptions
46
+ const Page = module.default;
47
+ const normalizedConfig = {
48
+ ...(module.config ?? Page.config ?? {}),
49
+ __eco: module.config?.__eco ??
50
+ Page.config?.__eco ?? {
51
+ id: String(rapidhash(file)),
52
+ file,
53
+ integration: ECOPAGES_JSX_PLUGIN_NAME,
54
+ },
55
+ };
56
+ if (module.layout && !(normalizedConfig.layouts && normalizedConfig.layouts.length > 0)) {
57
+ normalizedConfig.layout = module.layout;
58
+ }
59
+ ensurePageConfigLayouts(normalizedConfig);
60
+ const wrappedPage = async (props) => await Page(props);
61
+ wrappedPage.config = normalizedConfig;
62
+ if (module.getMetadata ?? Page.metadata) {
63
+ wrappedPage.metadata = module.getMetadata ?? Page.metadata;
64
+ }
65
+ return {
66
+ ...module,
67
+ default: wrappedPage,
68
+ config: normalizedConfig,
69
+ };
82
70
  };