@ecopages/core 0.2.0-beta.38 → 0.2.0-beta.39
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/README.md +1 -0
- package/package.json +27 -3
- package/src/adapters/bun/server-adapter.js +1 -0
- package/src/adapters/node/server-adapter.js +1 -0
- package/src/adapters/shared/http/explicit-static-render-preparation.js +1 -1
- package/src/adapters/shared/http/explicit-static-route-matcher.js +1 -1
- package/src/adapters/shared/http/fs-server-response-matcher.js +15 -7
- package/src/adapters/shared/runtime/collect-dev-prewarm-plan.d.ts +11 -0
- package/src/adapters/shared/runtime/collect-dev-prewarm-plan.js +29 -0
- package/src/adapters/shared/runtime/dev-static-route-prewarm.d.ts +16 -0
- package/src/adapters/shared/runtime/dev-static-route-prewarm.js +66 -0
- package/src/adapters/shared/runtime/render-context.js +1 -1
- package/src/adapters/shared/runtime/server-adapter.d.ts +7 -1
- package/src/adapters/shared/runtime/server-adapter.js +64 -5
- package/src/build/README.md +1 -1
- package/src/build/browser/browser-runtime-plugin.js +2 -5
- package/src/build/cache/production-build-cache.d.ts +3 -2
- package/src/build/cache/production-build-cache.js +2 -2
- package/src/build/contracts/content-virtual-modules.d.ts +12 -0
- package/src/build/contracts/content-virtual-modules.js +29 -0
- package/src/build/rolldown/rolldown-adapter-helpers.js +1 -1
- package/src/build/runtime/build-request-policy.d.ts +3 -4
- package/src/build/runtime/build-request-policy.js +4 -4
- package/src/cache/index.d.ts +2 -1
- package/src/cache/index.js +2 -1
- package/src/cache/module-parse-cache.d.ts +5 -0
- package/src/cache/module-parse-cache.js +27 -3
- package/src/cache/module-transform-profiler.d.ts +8 -0
- package/src/cache/module-transform-profiler.js +8 -0
- package/src/client/view-transitions.d.ts +17 -1
- package/src/client/view-transitions.js +45 -1
- package/src/client/view-transitions.test.browser.d.ts +1 -0
- package/src/client/view-transitions.test.browser.js +56 -0
- package/src/config/config-builder.d.ts +16 -9
- package/src/config/config-builder.js +24 -14
- package/src/dev/transform-server/dev-transform-vendor-registry.d.ts +1 -0
- package/src/dev/transform-server/dev-transform-vendor-registry.js +15 -2
- package/src/diagnostics/request-pipeline-metrics.d.ts +38 -0
- package/src/diagnostics/request-pipeline-metrics.js +121 -0
- package/src/eco/component-identity.d.ts +13 -0
- package/src/eco/component-identity.js +18 -0
- package/src/eco/eco-declared-component.d.ts +3 -2
- package/src/eco/eco-declared-component.js +6 -4
- package/src/eco/eco.browser.js +2 -2
- package/src/eco/eco.js +7 -6
- package/src/eco/eco.types.d.ts +6 -5
- package/src/errors/http-error.d.ts +8 -0
- package/src/errors/http-error.js +16 -0
- package/src/errors/undeclared-component-dependency-error.d.ts +1 -1
- package/src/errors/undeclared-component-dependency-error.js +2 -2
- package/src/index.browser.d.ts +1 -0
- package/src/index.browser.js +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/plugins/README.md +2 -1
- package/src/plugins/eco-component-meta-plugin.d.ts +6 -102
- package/src/plugins/eco-component-meta-plugin.js +110 -391
- package/src/plugins/processor.d.ts +10 -0
- package/src/plugins/processor.js +9 -0
- package/src/route-renderer/README.md +10 -8
- package/src/route-renderer/orchestration/document-shell/document-shell-render.service.js +1 -1
- package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +1 -1
- package/src/route-renderer/orchestration/integration-renderer.d.ts +0 -8
- package/src/route-renderer/orchestration/integration-renderer.js +6 -19
- package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +5 -4
- package/src/route-renderer/orchestration/ownership-graph/component-graph.js +6 -4
- package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.d.ts +0 -1
- package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.js +10 -19
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.d.ts +1 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +12 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +9 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +7 -0
- package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +65 -33
- package/src/route-renderer/page-loading/component-dependency-collection.js +3 -2
- package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +0 -5
- package/src/route-renderer/page-loading/ecopages-virtual-imports.js +4 -10
- package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +2 -2
- package/src/route-renderer/page-loading/file-scoped-dependency-components.js +12 -11
- package/src/route-renderer/page-loading/lazy-trigger-planning.js +2 -1
- package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -1
- package/src/route-renderer/page-loading/page-module-loader.js +8 -3
- package/src/router/README.md +8 -0
- package/src/services/README.md +5 -0
- package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +1 -0
- package/src/services/assets/asset-processing-service/asset-processing.service.js +22 -5
- package/src/services/cache/cache.types.d.ts +4 -2
- package/src/services/cache/html-page-cache-dependency-index.d.ts +34 -0
- package/src/services/cache/html-page-cache-dependency-index.js +88 -0
- package/src/services/cache/index.d.ts +2 -0
- package/src/services/cache/index.js +2 -0
- package/src/services/cache/page-cache-service.d.ts +31 -1
- package/src/services/cache/page-cache-service.js +79 -3
- package/src/services/cache/page-request-cache-coordinator.service.js +4 -0
- package/src/services/module-loading/README.md +9 -6
- package/src/services/module-loading/app-server-module-transpiler.service.js +0 -3
- package/src/services/module-loading/collection-server-module-build.service.d.ts +19 -0
- package/src/services/module-loading/collection-server-module-build.service.js +83 -0
- package/src/services/module-loading/page-module-import.service.d.ts +5 -11
- package/src/services/module-loading/page-module-import.service.js +56 -73
- package/src/services/module-loading/route-module-build-cache.store.js +6 -6
- package/src/services/module-loading/route-module-build-manifest.d.ts +8 -5
- package/src/services/module-loading/route-module-build-manifest.js +20 -32
- package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -2
- package/src/services/module-loading/server-module-transpiler.service.js +0 -3
- package/src/static-site-generator/static-site-generator.js +2 -2
- package/src/types/internal-types.d.ts +4 -0
- package/src/types/public-types.d.ts +18 -18
- package/src/watchers/project-watcher.js +14 -0
|
@@ -1,414 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bun plugin that auto-injects `__eco` metadata into EcoComponent config objects.
|
|
3
|
-
*
|
|
4
|
-
* This plugin uses AST parsing (via oxc-parser) to reliably inject the `__eco` property
|
|
5
|
-
* into EcoComponent config objects at import time. The injected metadata contains:
|
|
6
|
-
* - `dir`: The directory path of the component file (used for dependency resolution)
|
|
7
|
-
* - `integration`: The integration type (e.g., 'react', 'kitajs', 'ghtml', 'lit')
|
|
8
|
-
*
|
|
9
|
-
* The plugin intercepts file loading for all configured integration extensions and
|
|
10
|
-
* transforms component configs before they are executed.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* // Before transformation:
|
|
15
|
-
* export default eco.page({
|
|
16
|
-
* render: () => '<div>Hello</div>',
|
|
17
|
-
* });
|
|
18
|
-
*
|
|
19
|
-
* // After transformation:
|
|
20
|
-
* export default eco.page({
|
|
21
|
-
* __eco: { id: "<hash>", file: "/path/to/pages/index.tsx", integration: "react" },
|
|
22
|
-
* render: () => '<div>Hello</div>',
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @module eco-component-meta-plugin
|
|
27
|
-
*/
|
|
28
|
-
import { cachedParseSync } from '../cache/module-parse-cache.js';
|
|
29
1
|
import { prependJsxImportSourceIfMissing } from './jsx-import-source.utils.js';
|
|
2
|
+
import { createEcoBuildPluginFromSourceTransform, createVitePluginFromSourceTransform } from './source-transform.js';
|
|
3
|
+
import { cachedParseSync } from '../cache/module-parse-cache.js';
|
|
30
4
|
import { rapidhash } from '../utils/hash.js';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const validExt of VALID_LOADER_EXTENSIONS) {
|
|
53
|
-
if (ext.endsWith(validExt)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
5
|
+
function integrationForFile(filePath, config) {
|
|
6
|
+
const candidates = config.integrations
|
|
7
|
+
.flatMap((integration) => integration.extensions.map((extension) => [extension, integration]))
|
|
8
|
+
.sort(([left], [right]) => right.length - left.length);
|
|
9
|
+
const match = candidates.find(([extension]) => filePath.endsWith(extension));
|
|
10
|
+
return match ? { name: match[1].name, jsxImportSource: match[1].jsxImportSource } : { name: 'ghtml' };
|
|
11
|
+
}
|
|
12
|
+
function isAstNode(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null;
|
|
14
|
+
}
|
|
15
|
+
function isEcoFactoryCall(node) {
|
|
16
|
+
if (node.type !== 'CallExpression' || !isAstNode(node.callee))
|
|
17
|
+
return false;
|
|
18
|
+
const callee = node.callee;
|
|
19
|
+
if (callee.type !== 'MemberExpression' && callee.type !== 'StaticMemberExpression')
|
|
20
|
+
return false;
|
|
21
|
+
if (!isAstNode(callee.object) || callee.object.type !== 'Identifier' || callee.object.name !== 'eco')
|
|
22
|
+
return false;
|
|
23
|
+
if (!isAstNode(callee.property) || callee.property.type !== 'Identifier')
|
|
24
|
+
return false;
|
|
25
|
+
return ['page', 'component', 'layout', 'html'].includes(String(callee.property.name));
|
|
58
26
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]);
|
|
70
|
-
}
|
|
27
|
+
function isIdentityBinding(node) {
|
|
28
|
+
if (!isAstNode(node) || node.type !== 'CallExpression' || !isAstNode(node.callee))
|
|
29
|
+
return false;
|
|
30
|
+
return node.callee.type === 'Identifier' && node.callee.name === 'bindComponentIdentity';
|
|
31
|
+
}
|
|
32
|
+
function walkAst(node, visit) {
|
|
33
|
+
if (Array.isArray(node)) {
|
|
34
|
+
for (const child of node)
|
|
35
|
+
walkAst(child, visit);
|
|
36
|
+
return;
|
|
71
37
|
}
|
|
72
|
-
|
|
73
|
-
|
|
38
|
+
if (!isAstNode(node))
|
|
39
|
+
return;
|
|
40
|
+
visit(node);
|
|
41
|
+
for (const value of Object.values(node))
|
|
42
|
+
walkAst(value, visit);
|
|
74
43
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
44
|
+
function addIdentityBindingImport(contents, program) {
|
|
45
|
+
const imports = program.body.filter((node) => isAstNode(node) &&
|
|
46
|
+
node.type === 'ImportDeclaration' &&
|
|
47
|
+
isAstNode(node.source) &&
|
|
48
|
+
node.source.value === '@ecopages/core');
|
|
49
|
+
const valueImport = imports.find((node) => node.importKind !== 'type');
|
|
50
|
+
if (!valueImport)
|
|
51
|
+
return `import { bindComponentIdentity } from '@ecopages/core';\n${contents}`;
|
|
52
|
+
const specifiers = Array.isArray(valueImport.specifiers) ? valueImport.specifiers.filter(isAstNode) : [];
|
|
53
|
+
if (specifiers.some((specifier) => specifier.type === 'ImportSpecifier' &&
|
|
54
|
+
isAstNode(specifier.imported) &&
|
|
55
|
+
specifier.imported.name === 'bindComponentIdentity')) {
|
|
56
|
+
return contents;
|
|
57
|
+
}
|
|
58
|
+
const namedSpecifiers = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
|
|
59
|
+
if (namedSpecifiers.length > 0) {
|
|
60
|
+
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
61
|
+
return `${contents.slice(0, lastSpecifier.end)}, bindComponentIdentity${contents.slice(lastSpecifier.end)}`;
|
|
90
62
|
}
|
|
91
|
-
|
|
63
|
+
if (specifiers.some((specifier) => specifier.type === 'ImportNamespaceSpecifier')) {
|
|
64
|
+
return `import { bindComponentIdentity } from '@ecopages/core';\n${contents}`;
|
|
65
|
+
}
|
|
66
|
+
const defaultSpecifier = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
|
|
67
|
+
if (defaultSpecifier) {
|
|
68
|
+
return `${contents.slice(0, defaultSpecifier.end)}, { bindComponentIdentity }${contents.slice(defaultSpecifier.end)}`;
|
|
69
|
+
}
|
|
70
|
+
return `import { bindComponentIdentity } from '@ecopages/core';\n${contents}`;
|
|
92
71
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* @example
|
|
104
|
-
* ```typescript
|
|
105
|
-
* const pattern = createExtensionPattern(['.tsx', '.kita.tsx']);
|
|
106
|
-
* pattern.test('component.tsx'); // true
|
|
107
|
-
* pattern.test('component.tsx?v=123'); // true
|
|
108
|
-
* pattern.test('component.ts'); // false
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
function createExtensionPattern(extensions) {
|
|
112
|
-
if (extensions.length === 0) {
|
|
113
|
-
throw new Error('[eco-component-meta-plugin] No extensions configured. At least one integration is required.');
|
|
72
|
+
/** Attributes real `eco.*()` factory calls with canonical component identity. */
|
|
73
|
+
export function attributeComponentIdentity(contents, filePath, integration) {
|
|
74
|
+
if (!contents.includes('eco.'))
|
|
75
|
+
return contents;
|
|
76
|
+
let program;
|
|
77
|
+
try {
|
|
78
|
+
program = cachedParseSync(filePath, contents, { sourceType: 'module' }).program;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return contents;
|
|
114
82
|
}
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
83
|
+
const identityLiteral = `{ id: ${JSON.stringify(rapidhash(filePath).toString(36))}, file: ${JSON.stringify(filePath)}, integration: ${JSON.stringify(integration)} }`;
|
|
84
|
+
const edits = [];
|
|
85
|
+
walkAst(program, (node) => {
|
|
86
|
+
if (!isEcoFactoryCall(node) || !Array.isArray(node.arguments))
|
|
87
|
+
return;
|
|
88
|
+
const firstArgument = node.arguments[0];
|
|
89
|
+
if (!isAstNode(firstArgument) || isIdentityBinding(firstArgument))
|
|
90
|
+
return;
|
|
91
|
+
if (typeof firstArgument.start !== 'number' || typeof firstArgument.end !== 'number')
|
|
92
|
+
return;
|
|
93
|
+
edits.push({
|
|
94
|
+
start: firstArgument.start,
|
|
95
|
+
end: firstArgument.end,
|
|
96
|
+
replacement: `bindComponentIdentity(${identityLiteral}, ${contents.slice(firstArgument.start, firstArgument.end)})`,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
if (edits.length === 0)
|
|
100
|
+
return contents;
|
|
101
|
+
let transformed = contents;
|
|
102
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
103
|
+
transformed = `${transformed.slice(0, edit.start)}${edit.replacement}${transformed.slice(edit.end)}`;
|
|
104
|
+
}
|
|
105
|
+
return addIdentityBindingImport(transformed, program);
|
|
118
106
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Creates the bundler-neutral metadata transform used by Ecopages loaders and
|
|
121
|
-
* Vite-compatible adapters.
|
|
122
|
-
*/
|
|
123
107
|
export function createEcoComponentMetaTransform(options) {
|
|
124
|
-
const
|
|
108
|
+
const extensions = options.config.integrations
|
|
125
109
|
.flatMap((integration) => integration.extensions)
|
|
126
|
-
.filter(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
const extensionPattern = createExtensionPattern(allExtensions);
|
|
131
|
-
const extensionToIntegration = buildExtensionToIntegrationMap(options.config.integrations);
|
|
110
|
+
.filter((extension) => ['.ts', '.tsx', '.js', '.jsx'].some((suffix) => extension.endsWith(suffix)))
|
|
111
|
+
.map((extension) => extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
112
|
+
const filter = new RegExp(`(${extensions.join('|')})(\\?.*)?$`);
|
|
132
113
|
return {
|
|
133
|
-
name: 'eco-component-
|
|
114
|
+
name: 'eco-component-identity-attribution',
|
|
134
115
|
enforce: 'pre',
|
|
135
|
-
filter
|
|
116
|
+
filter,
|
|
136
117
|
transform(code, id) {
|
|
137
|
-
|
|
118
|
+
if (id.endsWith('.mdx'))
|
|
119
|
+
return { code };
|
|
120
|
+
const integration = integrationForFile(id, options.config);
|
|
138
121
|
return {
|
|
139
|
-
code: prependJsxImportSourceIfMissing(
|
|
122
|
+
code: prependJsxImportSourceIfMissing(attributeComponentIdentity(code, id, integration.name), integration.jsxImportSource),
|
|
140
123
|
};
|
|
141
124
|
},
|
|
142
125
|
};
|
|
143
126
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Creates a build plugin that auto-injects `__eco` metadata into EcoComponent config objects.
|
|
146
|
-
*
|
|
147
|
-
* This plugin intercepts file loading for all integration-compatible files and:
|
|
148
|
-
* 1. Strips any query string from the file path (for dev mode cache-busting)
|
|
149
|
-
* 2. Reads the file contents
|
|
150
|
-
* 3. Parses the AST using oxc-parser to find injection points
|
|
151
|
-
* 4. Injects `__eco: { id: "...", file: "...", integration: "..." }` into config objects
|
|
152
|
-
* 5. Returns the transformed content with the appropriate loader
|
|
153
|
-
*
|
|
154
|
-
* Supported patterns:
|
|
155
|
-
* - `eco.page({ ... })` - Page component declarations
|
|
156
|
-
* - `eco.component({ ... })` - Reusable component declarations
|
|
157
|
-
* - `eco.html({ ... })` - HTML shell declarations
|
|
158
|
-
* - `eco.layout({ ... })` - Layout declarations
|
|
159
|
-
* - `Component.config = { ... }` - Config assignment pattern
|
|
160
|
-
* - `config: { ... }` - Config property in object literals
|
|
161
|
-
* - `export const config = { ... }` - Exported config declarations
|
|
162
|
-
*
|
|
163
|
-
* @param options - Plugin options containing the EcoPages config
|
|
164
|
-
* @returns A build plugin instance ready for registration
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* ```typescript
|
|
168
|
-
* import { createEcoComponentMetaPlugin } from '@ecopages/core';
|
|
169
|
-
*
|
|
170
|
-
* const plugin = createEcoComponentMetaPlugin({ config: appConfig });
|
|
171
|
-
* appConfig.loaders.set(plugin.name, plugin);
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
export function createEcoComponentMetaPlugin(options) {
|
|
175
|
-
return createEcoBuildPluginFromSourceTransform(createEcoComponentMetaTransform(options));
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Creates a Vite-compatible metadata injection plugin from the shared
|
|
179
|
-
* Ecopages source-transform primitive.
|
|
180
|
-
*/
|
|
181
127
|
export function createEcoComponentMetaVitePlugin(options) {
|
|
182
128
|
return createVitePluginFromSourceTransform(createEcoComponentMetaTransform(options));
|
|
183
129
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
*
|
|
187
|
-
* ## What is an AST?
|
|
188
|
-
*
|
|
189
|
-
* An AST is a tree representation of source code. Instead of treating code as text,
|
|
190
|
-
* a parser breaks it down into a structured tree where each node represents a
|
|
191
|
-
* syntactic construct (variable, function call, object, etc.).
|
|
192
|
-
*
|
|
193
|
-
* For example, this code:
|
|
194
|
-
* ```typescript
|
|
195
|
-
* eco.page({ render: () => 'hi' })
|
|
196
|
-
* ```
|
|
197
|
-
*
|
|
198
|
-
* Becomes an AST like:
|
|
199
|
-
* ```
|
|
200
|
-
* CallExpression
|
|
201
|
-
* ├── callee: MemberExpression
|
|
202
|
-
* │ ├── object: Identifier (name: "eco")
|
|
203
|
-
* │ └── property: Identifier (name: "page")
|
|
204
|
-
* └── arguments: [
|
|
205
|
-
* └── ObjectExpression (start: 9) <-- We inject here at position 10 (after "{")
|
|
206
|
-
* └── properties: [...]
|
|
207
|
-
* ]
|
|
208
|
-
* ```
|
|
209
|
-
*
|
|
210
|
-
* ## How this function works
|
|
211
|
-
*
|
|
212
|
-
* 1. **Recursive traversal**: Visits every node in the tree, checking each one
|
|
213
|
-
* 2. **Pattern matching**: Checks if the current node matches one of our target patterns
|
|
214
|
-
* 3. **Position tracking**: When a match is found, records the `start` position of the
|
|
215
|
-
* config object (the character index in the original source where `{` appears)
|
|
216
|
-
* 4. **Insertion offset**: Adds +1 to insert right after the opening `{`
|
|
217
|
-
*
|
|
218
|
-
* ## Supported patterns
|
|
219
|
-
*
|
|
220
|
-
* | Pattern | AST Node Type | Example | File Types |
|
|
221
|
-
* |---------|---------------|---------|------------|
|
|
222
|
-
* | `eco.page({...})` | CallExpression | `export default eco.page({ render: () => 'hi' })` | All |
|
|
223
|
-
* | `eco.component({...})` | CallExpression | `export const Btn = eco.component({ render: () => '<button/>' })` | All |
|
|
224
|
-
* | `eco.html({...})` | CallExpression | `export default eco.html({ render: () => '<html />' })` | All |
|
|
225
|
-
* | `eco.layout({...})` | CallExpression | `export const MainLayout = eco.layout({ render: () => '<main />' })` | All |
|
|
226
|
-
* | `X.config = {...}` | AssignmentExpression | `MyComponent.config = { dependencies: [] }` | All |
|
|
227
|
-
* | `config: {...}` | ObjectProperty | `const X: EcoComponent = { config: {...} }` | EcoComponent-typed only |
|
|
228
|
-
*
|
|
229
|
-
* ## Why AST over regex?
|
|
230
|
-
*
|
|
231
|
-
* Regex would fail on edge cases like:
|
|
232
|
-
* - `eco.page<ComplexType<(arg: string) => void>>({...})` - generics with arrows
|
|
233
|
-
* - `// eco.page({ commented out })` - comments
|
|
234
|
-
* - `const str = "eco.page({ in a string })"` - string literals
|
|
235
|
-
* - Nested objects that look like config patterns
|
|
236
|
-
*
|
|
237
|
-
* AST parsing understands the actual code structure, not just text patterns.
|
|
238
|
-
*
|
|
239
|
-
* @param node - Current AST node being visited (starts with the root Program node)
|
|
240
|
-
* @param insertions - Array to collect insertion points (mutated by this function)
|
|
241
|
-
* @param injection - The injection text to insert at each point (e.g., ` __eco: {...},`)
|
|
242
|
-
* @param isInsideEcoComponent - Whether we're inside an EcoComponent-typed declaration
|
|
243
|
-
*/
|
|
244
|
-
function findInjectionPoints(node, insertions, injection, isInsideEcoComponent = false) {
|
|
245
|
-
if (!node || typeof node !== 'object')
|
|
246
|
-
return;
|
|
247
|
-
const n = node;
|
|
248
|
-
/**
|
|
249
|
-
* Pattern 1: eco.page({...}), eco.component({...}), eco.html({...}), or eco.layout({...})
|
|
250
|
-
* AST structure: CallExpression with MemberExpression callee where object is "eco"
|
|
251
|
-
*/
|
|
252
|
-
if (n.type === 'CallExpression') {
|
|
253
|
-
const callee = n.callee;
|
|
254
|
-
/**
|
|
255
|
-
* MemberExpression represents "something.property" syntax.
|
|
256
|
-
* StaticMemberExpression is oxc's variant for computed vs non-computed access.
|
|
257
|
-
*/
|
|
258
|
-
if (callee?.type === 'MemberExpression' || callee?.type === 'StaticMemberExpression') {
|
|
259
|
-
const obj = callee.object;
|
|
260
|
-
const prop = callee.property;
|
|
261
|
-
/** Check: is this `eco.page(...)`, `eco.component(...)`, `eco.html(...)`, or `eco.layout(...)`? */
|
|
262
|
-
if (obj?.type === 'Identifier' &&
|
|
263
|
-
obj?.name === 'eco' &&
|
|
264
|
-
(prop?.name === 'page' ||
|
|
265
|
-
prop?.name === 'component' ||
|
|
266
|
-
prop?.name === 'html' ||
|
|
267
|
-
prop?.name === 'layout')) {
|
|
268
|
-
/** Get the first argument - should be an object literal {...} */
|
|
269
|
-
const args = n.arguments;
|
|
270
|
-
const firstArg = args?.[0];
|
|
271
|
-
if (firstArg?.type === 'ObjectExpression') {
|
|
272
|
-
/**
|
|
273
|
-
* `start` is the character index where this object begins (the "{").
|
|
274
|
-
* Insert at position+1 to place content right after "{".
|
|
275
|
-
*/
|
|
276
|
-
const start = firstArg.start;
|
|
277
|
-
if (typeof start === 'number') {
|
|
278
|
-
insertions.push({ position: start + 1, text: injection });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Pattern 2: Something.config = {...}
|
|
286
|
-
* AST structure: AssignmentExpression with MemberExpression left side ending in "config"
|
|
287
|
-
* This pattern is safe for all files because it requires a qualifier (e.g., MyComponent.config).
|
|
288
|
-
*/
|
|
289
|
-
if (n.type === 'AssignmentExpression') {
|
|
290
|
-
const left = n.left;
|
|
291
|
-
const right = n.right;
|
|
292
|
-
/** Case: MyComponent.config = {...} */
|
|
293
|
-
if (left?.type === 'MemberExpression' || left?.type === 'StaticMemberExpression') {
|
|
294
|
-
const prop = left.property;
|
|
295
|
-
if (prop?.name === 'config' && right?.type === 'ObjectExpression') {
|
|
296
|
-
const start = right.start;
|
|
297
|
-
if (typeof start === 'number') {
|
|
298
|
-
insertions.push({ position: start + 1, text: injection });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Pattern 3: { config: {...} } - config as an object property inside EcoComponent
|
|
305
|
-
* AST structure: ObjectProperty/Property with key "config" and value as ObjectExpression
|
|
306
|
-
*
|
|
307
|
-
* This pattern is matched when the parent VariableDeclarator has a type annotation
|
|
308
|
-
* containing "EcoComponent", e.g., `const X: EcoComponent = { config: {...} }`
|
|
309
|
-
*
|
|
310
|
-
* We track whether we're inside an EcoComponent-typed object via the `isInsideEcoComponent` flag.
|
|
311
|
-
*/
|
|
312
|
-
if (n.type === 'ObjectProperty' || n.type === 'Property') {
|
|
313
|
-
const key = n.key;
|
|
314
|
-
const value = n.value;
|
|
315
|
-
if ((key?.type === 'Identifier' || key?.type === 'IdentifierName') &&
|
|
316
|
-
key?.name === 'config' &&
|
|
317
|
-
value?.type === 'ObjectExpression' &&
|
|
318
|
-
isInsideEcoComponent) {
|
|
319
|
-
const start = value.start;
|
|
320
|
-
if (typeof start === 'number') {
|
|
321
|
-
insertions.push({ position: start + 1, text: injection });
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Check if we're entering an EcoComponent-typed variable declaration.
|
|
327
|
-
* This sets a flag for child nodes to know they're inside an EcoComponent.
|
|
328
|
-
*
|
|
329
|
-
* Type annotation is on the `id` (Identifier), not the VariableDeclarator directly.
|
|
330
|
-
* e.g., `const X: EcoComponent = {...}` has the annotation on the "X" Identifier.
|
|
331
|
-
*/
|
|
332
|
-
let childIsInsideEcoComponent = isInsideEcoComponent;
|
|
333
|
-
if (n.type === 'VariableDeclarator') {
|
|
334
|
-
const id = n.id;
|
|
335
|
-
const typeAnnotation = id?.typeAnnotation;
|
|
336
|
-
if (typeAnnotation) {
|
|
337
|
-
const typeStr = JSON.stringify(typeAnnotation);
|
|
338
|
-
if (typeStr.includes('EcoComponent')) {
|
|
339
|
-
childIsInsideEcoComponent = true;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Recursive traversal: Visit all child nodes in the AST.
|
|
345
|
-
*
|
|
346
|
-
* This is how we "walk" the tree - for each property of the current node,
|
|
347
|
-
* if it's an array (like `body` containing statements) or an object (like `callee`),
|
|
348
|
-
* we recursively call findInjectionPoints on it.
|
|
349
|
-
* We skip metadata properties (start, end, type) that don't contain child nodes.
|
|
350
|
-
*/
|
|
351
|
-
for (const key in n) {
|
|
352
|
-
if (key === 'start' || key === 'end' || key === 'type')
|
|
353
|
-
continue;
|
|
354
|
-
const value = n[key];
|
|
355
|
-
if (Array.isArray(value)) {
|
|
356
|
-
for (const child of value) {
|
|
357
|
-
findInjectionPoints(child, insertions, injection, childIsInsideEcoComponent);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
else if (typeof value === 'object' && value !== null) {
|
|
361
|
-
findInjectionPoints(value, insertions, injection, childIsInsideEcoComponent);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Injects `__eco` metadata into EcoComponent config objects in file content.
|
|
367
|
-
*
|
|
368
|
-
* Uses oxc-parser for robust AST-based code analysis, which handles edge cases
|
|
369
|
-
* that regex-based approaches would miss (e.g., complex generics, nested objects,
|
|
370
|
-
* comments, string literals containing similar patterns).
|
|
371
|
-
*
|
|
372
|
-
* The injection is performed by:
|
|
373
|
-
* 1. Parsing the source code into an AST
|
|
374
|
-
* 2. Walking the AST to find all config object patterns
|
|
375
|
-
* 3. Collecting insertion points (sorted in reverse order to preserve positions)
|
|
376
|
-
* 4. Inserting the `__eco` property at each point
|
|
377
|
-
*
|
|
378
|
-
* @param contents - The source code content to transform
|
|
379
|
-
* @param filePath - Absolute path to the file (used to derive the directory)
|
|
380
|
-
* @param integration - The integration identifier for this file type
|
|
381
|
-
* @returns Transformed source code with `__eco` injected, or original if no patterns found
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* ```typescript
|
|
385
|
-
* const result = injectEcoMeta(
|
|
386
|
-
* 'export default eco.page({ render: () => "<div>Hi</div>" });',
|
|
387
|
-
* '/app/src/pages/index.tsx',
|
|
388
|
-
* 'react'
|
|
389
|
-
* );
|
|
390
|
-
* // Result: 'export default eco.page({ __eco: { id: "<hash>", file: "/app/src/pages/index.tsx", integration: "react" }, render: () => "<div>Hi</div>" });'
|
|
391
|
-
* ```
|
|
392
|
-
*/
|
|
393
|
-
export function injectEcoMeta(contents, filePath, integration) {
|
|
394
|
-
const result = cachedParseSync(filePath, contents);
|
|
395
|
-
if (result.errors.length > 0) {
|
|
396
|
-
console.warn(`[eco-component-meta-plugin] Parse errors in ${filePath}:`, result.errors);
|
|
397
|
-
return contents;
|
|
398
|
-
}
|
|
399
|
-
const ast = result.program;
|
|
400
|
-
const id = rapidhash(filePath).toString(36);
|
|
401
|
-
const injection = ` __eco: { id: "${id}", file: "${filePath}", integration: "${integration}" },`;
|
|
402
|
-
const insertions = [];
|
|
403
|
-
findInjectionPoints(ast, insertions, injection);
|
|
404
|
-
if (insertions.length === 0) {
|
|
405
|
-
return contents;
|
|
406
|
-
}
|
|
407
|
-
insertions.sort((a, b) => b.position - a.position);
|
|
408
|
-
let transformed = contents;
|
|
409
|
-
for (const { position, text } of insertions) {
|
|
410
|
-
transformed = transformed.slice(0, position) + text + transformed.slice(position);
|
|
411
|
-
}
|
|
412
|
-
return transformed;
|
|
130
|
+
export function createEcoComponentMetaPlugin(options) {
|
|
131
|
+
return createEcoBuildPluginFromSourceTransform(createEcoComponentMetaTransform(options));
|
|
413
132
|
}
|
|
414
|
-
export default
|
|
133
|
+
export default createEcoComponentMetaTransform;
|
|
@@ -112,6 +112,16 @@ export declare abstract class Processor<TOptions = Record<string, unknown>> {
|
|
|
112
112
|
* warming or watcher registration should stay in `setup()`.
|
|
113
113
|
*/
|
|
114
114
|
prepareBuildContributions(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Declares watch-mode SSR prewarm pathnames and readiness for core.
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* Processors return paths and readiness only; core owns parallel rendering and page cache population.
|
|
120
|
+
*/
|
|
121
|
+
collectDevPrewarmPlan(): Promise<{
|
|
122
|
+
pathnames: readonly string[];
|
|
123
|
+
readiness: 'background' | 'beforeReady';
|
|
124
|
+
}>;
|
|
115
125
|
/**
|
|
116
126
|
* Reports whether this processor's build inputs changed since the last
|
|
117
127
|
* incremental static build.
|
package/src/plugins/processor.js
CHANGED
|
@@ -66,6 +66,15 @@ export class Processor {
|
|
|
66
66
|
* warming or watcher registration should stay in `setup()`.
|
|
67
67
|
*/
|
|
68
68
|
async prepareBuildContributions() { }
|
|
69
|
+
/**
|
|
70
|
+
* Declares watch-mode SSR prewarm pathnames and readiness for core.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Processors return paths and readiness only; core owns parallel rendering and page cache population.
|
|
74
|
+
*/
|
|
75
|
+
collectDevPrewarmPlan() {
|
|
76
|
+
return Promise.resolve({ pathnames: [], readiness: 'background' });
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* Reports whether this processor's build inputs changed since the last
|
|
71
80
|
* incremental static build.
|
|
@@ -33,11 +33,11 @@ These concepts intentionally live in different places:
|
|
|
33
33
|
|
|
34
34
|
Mixed-integration apps use two separate mechanisms:
|
|
35
35
|
|
|
36
|
-
| Layer | Mechanism | Purpose
|
|
37
|
-
| ------- | ----------------------------------------------------------------- |
|
|
38
|
-
| Build | `getJsxOwnershipPlugins()` / `getHostScopedJsxOwnershipPlugins()` | Prepend `@jsxImportSource` so bundled `.tsx` files compile with the correct JSX runtime
|
|
39
|
-
| Build |
|
|
40
|
-
| Runtime | `eco.component()` / `eco.embed()` + foreign-child runtime | Hand off cross-integration children during SSR
|
|
36
|
+
| Layer | Mechanism | Purpose |
|
|
37
|
+
| ------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| Build | `getJsxOwnershipPlugins()` / `getHostScopedJsxOwnershipPlugins()` | Prepend `@jsxImportSource` so bundled `.tsx` files compile with the correct JSX runtime |
|
|
39
|
+
| Build | component identity source transform | After a lexical `eco.` gate, uses Oxc to wrap native factory options with `bindComponentIdentity()` and prepends the owning integration pragma |
|
|
40
|
+
| Runtime | `eco.component()` / `eco.embed()` + foreign-child runtime | Hand off cross-integration children during SSR |
|
|
41
41
|
|
|
42
42
|
See [`../build/README.md`](../build/README.md) for the JSX ownership helper split.
|
|
43
43
|
|
|
@@ -69,9 +69,11 @@ Domain folders:
|
|
|
69
69
|
|
|
70
70
|
**Page Browser Graph session behavior:**
|
|
71
71
|
|
|
72
|
-
- In development, each Page Browser Graph is built on first request, cached in `page-browser-graph-session` with generation-safe commits, and invalidated when a tracked dependency changes. Hosts call `prepareHmrFileChange()` before HMR dispatch and defer client broadcasts when no browser subscribers are connected.
|
|
72
|
+
- In development, each Page Browser Graph is built on first request, cached in `page-browser-graph-session` with generation-safe commits, and invalidated when a tracked dependency changes. The per-route fast path is disabled while HMR is enabled so prop-dependent `dependencies(props)` cannot reuse the wrong graph. Hosts call `prepareHmrFileChange()` before HMR dispatch and defer client broadcasts when no browser subscribers are connected.
|
|
73
|
+
- Dev SSR prewarm schedules processor-declared pathnames for background rendering. Static pages cache in watch mode when their Cache Strategy allows it; dynamic pages are never retained. The watcher invalidates HTML through `invalidateAppPageCacheBySourcePaths` when a registered source dependency changes, and falls back to global cache clear for categories whose route impact cannot be narrowed.
|
|
74
|
+
- **Measuring dev page load:** set `ECOPAGES_REQUEST_PIPELINE_METRICS=1` and run `pnpm run test:bench:docs`. Compare cold vs warm navigations and phase totals from the benchmark output.
|
|
73
75
|
- Production static export prebuilds browser graphs from the finalized route list into the in-memory `page-browser-graph-session` via `production-page-browser-graph-prebuild.ts`. Failed exports clear staged production session records so retries cannot reuse partial graph output.
|
|
74
|
-
- Integrations activate lazily on first render or graph prebuild via `ensureIntegrationRuntimeReady()`. Processors
|
|
76
|
+
- Integrations activate lazily on first render or graph prebuild via `ensureIntegrationRuntimeReady()`. Processors initialize their small virtual modules during `setupAppRuntimePlugins()`; content collection server artifacts are built lazily when a server build first resolves the collection.
|
|
75
77
|
|
|
76
78
|
### `page-loading/`
|
|
77
79
|
|
|
@@ -92,7 +94,7 @@ The route-render contract is:
|
|
|
92
94
|
Important:
|
|
93
95
|
|
|
94
96
|
- route-level fallback resolution is gone; unresolved artifacts are now a hard failure
|
|
95
|
-
- ownership is declared from component
|
|
97
|
+
- ownership is declared from canonical component identity, not inferred from final HTML
|
|
96
98
|
- declared page dependencies are resolved from final render inputs and carried to the owning integration; integrations may use them for renderer-specific lifecycle work such as HMR ownership
|
|
97
99
|
- same-integration children stay renderer-local and do not need to pass through a universal transport
|
|
98
100
|
|
|
@@ -6,7 +6,7 @@ function resolveDocumentShellLayouts(input) {
|
|
|
6
6
|
return input.layout ? [input.layout] : [];
|
|
7
7
|
}
|
|
8
8
|
function resolveComponentIntegrationName(component) {
|
|
9
|
-
return component.config?.integration ?? component.config?.
|
|
9
|
+
return component.config?.integration ?? component.config?.identity?.integration ?? 'ecopages';
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Default sequential string-child composition for document shell layers.
|
|
@@ -271,7 +271,7 @@ export class ForeignSubtreeExecutionService {
|
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
273
|
getForeignOwnerIntegrationName(component, currentIntegrationName) {
|
|
274
|
-
const integrationName = component.config?.integration ?? component.config?.
|
|
274
|
+
const integrationName = component.config?.integration ?? component.config?.identity?.integration;
|
|
275
275
|
if (!integrationName || integrationName === 'html' || integrationName === currentIntegrationName) {
|
|
276
276
|
return undefined;
|
|
277
277
|
}
|
|
@@ -18,14 +18,9 @@ import type { ForeignChildRuntime } from './foreign-child/component-render-conte
|
|
|
18
18
|
import { ForeignSubtreeExecutionService } from './foreign-child/foreign-subtree-execution.service.js';
|
|
19
19
|
/**
|
|
20
20
|
* Controls how one route module is loaded outside the normal render path.
|
|
21
|
-
*
|
|
22
|
-
* Request-time metadata inspection and static-generation probes use these
|
|
23
|
-
* options to isolate their module identity from the main render cache while
|
|
24
|
-
* still going through the owning integration's import setup.
|
|
25
21
|
*/
|
|
26
22
|
export type RouteModuleLoadOptions = {
|
|
27
23
|
bypassCache?: boolean;
|
|
28
|
-
cacheScope?: string;
|
|
29
24
|
};
|
|
30
25
|
/**
|
|
31
26
|
* Context for renderToResponse method.
|
|
@@ -239,12 +234,9 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
|
|
|
239
234
|
* @returns The HTML template component.
|
|
240
235
|
*/
|
|
241
236
|
protected getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
|
|
242
|
-
protected usesIntegrationPageImporter(_file: string): boolean;
|
|
243
|
-
protected importIntegrationPageFile(_file: string, _options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
|
|
244
237
|
protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(_file: string, pageModule: TPageModule): TPageModule;
|
|
245
238
|
/**
|
|
246
239
|
* Imports the page file from the specified path.
|
|
247
|
-
* It uses dynamic import to load the file and returns the imported module.
|
|
248
240
|
*
|
|
249
241
|
* @param file - The file path to import.
|
|
250
242
|
* @returns The imported module.
|