@ecopages/core 0.2.0-alpha.33 → 0.2.0-alpha.35
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 +14 -4
- package/src/adapters/create-app.js +2 -2
- package/src/adapters/node/node-hmr-manager.js +5 -0
- package/src/adapters/node/server-adapter.d.ts +3 -0
- package/src/adapters/node/server-adapter.js +21 -3
- package/src/adapters/shared/hmr-html-response.js +4 -3
- package/src/adapters/shared/render-context.d.ts +1 -0
- package/src/adapters/shared/render-context.js +21 -0
- package/src/adapters/shared/server-adapter.js +8 -1
- package/src/build/build-adapter.d.ts +8 -0
- package/src/build/build-adapter.js +4 -0
- package/src/config/config-builder.js +1 -1
- package/src/eco/eco.js +19 -6
- package/src/hmr/strategies/js-hmr-strategy.js +1 -2
- package/src/plugins/alias-resolver-plugin.d.ts +1 -0
- package/src/plugins/alias-resolver-plugin.js +12 -4
- package/src/plugins/eco-component-meta-plugin.js +16 -3
- package/src/plugins/foreign-jsx-override-plugin.d.ts +2 -0
- package/src/plugins/foreign-jsx-override-plugin.js +6 -0
- package/src/route-renderer/orchestration/component-render-context.d.ts +10 -2
- package/src/route-renderer/orchestration/component-render-context.js +2 -2
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +2 -0
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +31 -4
- package/src/route-renderer/orchestration/integration-renderer.js +12 -1
- package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +1 -0
- package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.js +18 -6
- package/src/services/module-loading/app-module-loader.service.d.ts +2 -2
- package/src/services/module-loading/app-server-module-transpiler.service.js +38 -2
- package/src/services/module-loading/node-bootstrap-plugin.js +82 -1
- package/src/services/module-loading/page-module-import.service.d.ts +29 -10
- package/src/services/module-loading/page-module-import.service.js +39 -18
- package/src/services/module-loading/server-module-transpiler.service.d.ts +2 -2
- package/src/types/public-types.d.ts +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/core",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.35",
|
|
4
4
|
"description": "Core package for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"directory": "packages/core"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@ecopages/file-system": "0.2.0-alpha.
|
|
20
|
+
"@ecopages/file-system": "0.2.0-alpha.35",
|
|
21
21
|
"@ecopages/logger": "^0.2.3",
|
|
22
|
-
"@ecopages/scripts-injector": "^0.1.
|
|
22
|
+
"@ecopages/scripts-injector": "^0.1.5",
|
|
23
23
|
"@worker-tools/html-rewriter": "0.1.0-pre.19",
|
|
24
24
|
"chokidar": "^5.0.0",
|
|
25
25
|
"esbuild": "^0.28.0",
|
|
26
26
|
"ghtml": "^4.0.2",
|
|
27
27
|
"oxc-parser": "^0.124.0",
|
|
28
|
-
"ws": "^8.
|
|
28
|
+
"ws": "^8.20.1"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
31
|
".": {
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"types": "./src/route-renderer/orchestration/integration-renderer.d.ts",
|
|
44
44
|
"default": "./src/route-renderer/orchestration/integration-renderer.js"
|
|
45
45
|
},
|
|
46
|
+
"./route-renderer/component-render-context": {
|
|
47
|
+
"import": "./src/route-renderer/orchestration/component-render-context.js",
|
|
48
|
+
"types": "./src/route-renderer/orchestration/component-render-context.d.ts",
|
|
49
|
+
"default": "./src/route-renderer/orchestration/component-render-context.js"
|
|
50
|
+
},
|
|
46
51
|
"./router/navigation-coordinator": {
|
|
47
52
|
"types": "./src/router/client/navigation-coordinator.d.ts",
|
|
48
53
|
"default": "./src/router/client/navigation-coordinator.js"
|
|
@@ -145,6 +150,11 @@
|
|
|
145
150
|
"types": "./src/route-renderer/orchestration/integration-renderer.d.ts",
|
|
146
151
|
"default": "./src/route-renderer/orchestration/integration-renderer.js"
|
|
147
152
|
},
|
|
153
|
+
"./route-renderer/component-render-context.ts": {
|
|
154
|
+
"import": "./src/route-renderer/orchestration/component-render-context.js",
|
|
155
|
+
"types": "./src/route-renderer/orchestration/component-render-context.d.ts",
|
|
156
|
+
"default": "./src/route-renderer/orchestration/component-render-context.js"
|
|
157
|
+
},
|
|
148
158
|
"./router/navigation-coordinator.ts": {
|
|
149
159
|
"types": "./src/router/client/navigation-coordinator.d.ts",
|
|
150
160
|
"default": "./src/router/client/navigation-coordinator.js"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { AbstractApplicationAdapter } from "./abstract/application-adapter.js";
|
|
2
2
|
import { SharedApplicationAdapter } from "./shared/application-adapter.js";
|
|
3
|
+
import { createApp as createBunApp } from "./bun/create-app.js";
|
|
4
|
+
import { createNodeApp } from "./node/create-app.js";
|
|
3
5
|
async function createRuntimeApp(options) {
|
|
4
6
|
const bun = globalThis.Bun;
|
|
5
7
|
if (bun) {
|
|
6
|
-
const { createApp: createBunApp } = await import("./bun/create-app.js");
|
|
7
8
|
return await createBunApp(options);
|
|
8
9
|
}
|
|
9
|
-
const { createApp: createNodeApp } = await import("./node/create-app.js");
|
|
10
10
|
return await createNodeApp(options);
|
|
11
11
|
}
|
|
12
12
|
async function createApp(options) {
|
|
@@ -138,6 +138,11 @@ class NodeHmrManager {
|
|
|
138
138
|
this.bridge.broadcast(event);
|
|
139
139
|
}
|
|
140
140
|
async handleFileChange(filePath, options = {}) {
|
|
141
|
+
if (!fileSystem.exists(filePath)) {
|
|
142
|
+
appLogger.debug(`[NodeHmrManager] Skipping missing file change: ${filePath}`);
|
|
143
|
+
this.clearFailedEntrypointRegistration(filePath);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
141
146
|
const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
|
|
142
147
|
const strategy = sorted.find((s) => {
|
|
143
148
|
try {
|
|
@@ -53,6 +53,9 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
|
|
|
53
53
|
private previewServer;
|
|
54
54
|
private bridge;
|
|
55
55
|
private hmrManager;
|
|
56
|
+
private shouldInjectHmrScript;
|
|
57
|
+
private isHtmlResponse;
|
|
58
|
+
private maybeInjectHmrScript;
|
|
56
59
|
constructor(options: NodeServerAdapterParams);
|
|
57
60
|
/**
|
|
58
61
|
* Prepares the adapter for use.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
2
3
|
import { appLogger } from "../../global/app-logger.js";
|
|
3
4
|
import { NodeClientBridge } from "./node-client-bridge.js";
|
|
5
|
+
import { NodeHmrManager } from "./node-hmr-manager.js";
|
|
4
6
|
import { StaticSiteGenerator } from "../../static-site-generator/static-site-generator.js";
|
|
5
7
|
import { SharedServerAdapter } from "../shared/server-adapter.js";
|
|
6
8
|
import { ServerStaticBuilder } from "../shared/server-static-builder.js";
|
|
@@ -13,6 +15,11 @@ import {
|
|
|
13
15
|
} from "../shared/runtime-bootstrap.js";
|
|
14
16
|
import { NodeStaticContentServer } from "./static-content-server.js";
|
|
15
17
|
import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
|
|
18
|
+
import {
|
|
19
|
+
injectHmrRuntimeIntoHtmlResponse,
|
|
20
|
+
isHtmlResponse,
|
|
21
|
+
shouldInjectHmrHtmlResponse
|
|
22
|
+
} from "../shared/hmr-html-response.js";
|
|
16
23
|
class ClientAbortError extends Error {
|
|
17
24
|
constructor() {
|
|
18
25
|
super("Client closed the request");
|
|
@@ -28,6 +35,18 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
28
35
|
previewServer = null;
|
|
29
36
|
bridge = null;
|
|
30
37
|
hmrManager = null;
|
|
38
|
+
shouldInjectHmrScript() {
|
|
39
|
+
return shouldInjectHmrHtmlResponse(this.options?.watch === true, this.hmrManager ?? void 0);
|
|
40
|
+
}
|
|
41
|
+
isHtmlResponse(response) {
|
|
42
|
+
return isHtmlResponse(response);
|
|
43
|
+
}
|
|
44
|
+
async maybeInjectHmrScript(response) {
|
|
45
|
+
if (this.shouldInjectHmrScript() && this.isHtmlResponse(response)) {
|
|
46
|
+
return injectHmrRuntimeIntoHtmlResponse(response);
|
|
47
|
+
}
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
31
50
|
constructor(options) {
|
|
32
51
|
super(options);
|
|
33
52
|
this.apiHandlers = options.apiHandlers || [];
|
|
@@ -283,12 +302,13 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
283
302
|
throw new Error("Node server adapter is not initialized. Call createAdapter() first.");
|
|
284
303
|
}
|
|
285
304
|
try {
|
|
286
|
-
|
|
305
|
+
const response = await this.handleSharedRequest(request, {
|
|
287
306
|
apiHandlers: this.apiHandlers,
|
|
288
307
|
errorHandler: this.errorHandler,
|
|
289
308
|
serverInstance: this.serverInstance,
|
|
290
309
|
hmrManager: this.hmrManager
|
|
291
310
|
});
|
|
311
|
+
return await this.maybeInjectHmrScript(response);
|
|
292
312
|
} catch (error) {
|
|
293
313
|
if (error instanceof ClientAbortError) {
|
|
294
314
|
return new Response(null, { status: 499 });
|
|
@@ -315,8 +335,6 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
315
335
|
async completeInitialization(server) {
|
|
316
336
|
this.serverInstance = server;
|
|
317
337
|
if (this.options?.watch) {
|
|
318
|
-
const { NodeHmrManager } = await import("./node-hmr-manager.js");
|
|
319
|
-
const { WebSocketServer } = await import("ws");
|
|
320
338
|
const wss = new WebSocketServer({ noServer: true });
|
|
321
339
|
this.bridge = new NodeClientBridge();
|
|
322
340
|
this.hmrManager = new NodeHmrManager({ appConfig: this.appConfig, bridge: this.bridge });
|
|
@@ -9,16 +9,17 @@ function shouldInjectHmrHtmlResponse(watch, hmrManager) {
|
|
|
9
9
|
}
|
|
10
10
|
async function injectHmrRuntimeIntoHtmlResponse(response) {
|
|
11
11
|
const html = await response.text();
|
|
12
|
+
const headers = new Headers(response.headers);
|
|
13
|
+
headers.set("Cache-Control", "no-store, must-revalidate");
|
|
14
|
+
headers.delete("Content-Length");
|
|
12
15
|
if (html.includes(HMR_RUNTIME_IMPORT)) {
|
|
13
16
|
return new Response(html, {
|
|
14
17
|
status: response.status,
|
|
15
18
|
statusText: response.statusText,
|
|
16
|
-
headers
|
|
19
|
+
headers
|
|
17
20
|
});
|
|
18
21
|
}
|
|
19
22
|
const updatedHtml = html.replace(/<\/html>/i, `${HMR_RUNTIME_SCRIPT}</html>`);
|
|
20
|
-
const headers = new Headers(response.headers);
|
|
21
|
-
headers.delete("Content-Length");
|
|
22
23
|
return new Response(updatedHtml, {
|
|
23
24
|
status: response.status,
|
|
24
25
|
statusText: response.statusText,
|
|
@@ -3,6 +3,7 @@ import type { AnyIntegrationPlugin } from '../../plugins/integration-plugin.js';
|
|
|
3
3
|
export interface CreateRenderContextOptions {
|
|
4
4
|
integrations: AnyIntegrationPlugin[];
|
|
5
5
|
rendererModules?: unknown;
|
|
6
|
+
importServerModule?: (filePath: string) => Promise<unknown>;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* Creates a render context for route handlers.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
1
2
|
import { invariant } from "../../utils/invariant.js";
|
|
3
|
+
function resolveServerModulePath(filePath) {
|
|
4
|
+
return filePath instanceof URL ? fileURLToPath(filePath) : filePath;
|
|
5
|
+
}
|
|
2
6
|
function mergePropsWithLocals(props, locals) {
|
|
3
7
|
if (!locals || typeof props !== "object" || props === null) {
|
|
4
8
|
return props;
|
|
@@ -20,6 +24,23 @@ function createRenderContext(options) {
|
|
|
20
24
|
});
|
|
21
25
|
};
|
|
22
26
|
const renderContext = {
|
|
27
|
+
importServerModule: async (filePath) => {
|
|
28
|
+
invariant(!!options.importServerModule, "Server module importing is not available in this render context.");
|
|
29
|
+
return await options.importServerModule(resolveServerModulePath(filePath));
|
|
30
|
+
},
|
|
31
|
+
async renderServerModule(filePath, props, renderOptions) {
|
|
32
|
+
const module = await renderContext.importServerModule(filePath);
|
|
33
|
+
const view = module.default;
|
|
34
|
+
const locals = this?.locals;
|
|
35
|
+
const mergedProps = mergePropsWithLocals(props ?? {}, locals);
|
|
36
|
+
const renderer = getRendererForView(view);
|
|
37
|
+
const ctx = {
|
|
38
|
+
partial: false,
|
|
39
|
+
status: renderOptions?.status,
|
|
40
|
+
headers: renderOptions?.headers
|
|
41
|
+
};
|
|
42
|
+
return await renderer.renderToResponse(view, mergedProps, ctx);
|
|
43
|
+
},
|
|
23
44
|
async render(view, props, renderOptions) {
|
|
24
45
|
const locals = this?.locals;
|
|
25
46
|
const mergedProps = mergePropsWithLocals(props ?? {}, locals);
|
|
@@ -157,9 +157,16 @@ class SharedServerAdapter extends AbstractServerAdapter {
|
|
|
157
157
|
return this.fileSystemResponseMatcher?.getCacheService() ?? null;
|
|
158
158
|
}
|
|
159
159
|
getRenderContext() {
|
|
160
|
+
const serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
|
|
160
161
|
return createRenderContext({
|
|
161
162
|
integrations: this.appConfig.integrations,
|
|
162
|
-
rendererModules: this.appConfig.runtime?.rendererModuleContext
|
|
163
|
+
rendererModules: this.appConfig.runtime?.rendererModuleContext,
|
|
164
|
+
importServerModule: async (filePath) => await serverModuleTranspiler.importModule({
|
|
165
|
+
filePath,
|
|
166
|
+
outdir: path.join(resolveInternalExecutionDir(this.appConfig), ".server-modules"),
|
|
167
|
+
externalPackages: true,
|
|
168
|
+
bypassCache: this.options?.watch === true
|
|
169
|
+
})
|
|
163
170
|
});
|
|
164
171
|
}
|
|
165
172
|
/**
|
|
@@ -54,6 +54,14 @@ export interface BuildOptions {
|
|
|
54
54
|
bundle?: boolean;
|
|
55
55
|
externalPackages?: boolean;
|
|
56
56
|
external?: string[];
|
|
57
|
+
jsx?: {
|
|
58
|
+
development?: boolean;
|
|
59
|
+
factory?: string;
|
|
60
|
+
fragment?: string;
|
|
61
|
+
importSource?: string;
|
|
62
|
+
runtime?: 'classic' | 'automatic';
|
|
63
|
+
sideEffects?: boolean;
|
|
64
|
+
};
|
|
57
65
|
plugins?: EcoBuildPlugin[];
|
|
58
66
|
[key: string]: unknown;
|
|
59
67
|
}
|
|
@@ -415,6 +415,8 @@ class BunBuildAdapter {
|
|
|
415
415
|
try {
|
|
416
416
|
const contextRoot = options.root ? path.resolve(options.root) : process.cwd();
|
|
417
417
|
const outdir = path.resolve(options.outdir ?? "dist/assets");
|
|
418
|
+
const tsconfigPath = path.join(contextRoot, "tsconfig.json");
|
|
419
|
+
const tsconfigExists = fs.existsSync(tsconfigPath);
|
|
418
420
|
const plugins = this.getPluginsForBuild(options.plugins);
|
|
419
421
|
const result = await bun.build({
|
|
420
422
|
entrypoints: options.entrypoints,
|
|
@@ -429,6 +431,8 @@ class BunBuildAdapter {
|
|
|
429
431
|
splitting: !!options.splitting,
|
|
430
432
|
minify: !!options.minify,
|
|
431
433
|
packages: options.target !== "browser" && options.externalPackages !== false ? "external" : void 0,
|
|
434
|
+
tsconfig: tsconfigExists ? tsconfigPath : void 0,
|
|
435
|
+
jsx: options.jsx,
|
|
432
436
|
plugins: plugins.length > 0 ? [this.createEcoPluginBridge(plugins, contextRoot)] : void 0
|
|
433
437
|
});
|
|
434
438
|
return this.rewriteAliasedRuntimeSpecifiers(
|
|
@@ -395,7 +395,7 @@ class ConfigBuilder {
|
|
|
395
395
|
const hasKitaJs = uniqueName.has("kitajs");
|
|
396
396
|
const hasReact = uniqueName.has("react");
|
|
397
397
|
if (hasKitaJs && hasReact) {
|
|
398
|
-
appLogger.
|
|
398
|
+
appLogger.debug(CONFIG_BUILDER_ERRORS.MIXED_JSX_ENGINES);
|
|
399
399
|
}
|
|
400
400
|
const integrationsExtensions = integrations.flatMap((integration) => integration.extensions);
|
|
401
401
|
const uniqueExtensions = new Set(integrationsExtensions);
|
package/src/eco/eco.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
finalizeComponentRender,
|
|
3
|
+
getComponentRenderContext,
|
|
3
4
|
interceptForeignChild
|
|
4
5
|
} from "../route-renderer/orchestration/component-render-context.js";
|
|
5
6
|
import { isThenable } from "../route-renderer/orchestration/render-output.utils.js";
|
|
@@ -7,19 +8,31 @@ function createComponentFactory(options) {
|
|
|
7
8
|
const integrationName = options.integration ?? options.__eco?.integration;
|
|
8
9
|
const comp = ((props) => {
|
|
9
10
|
const componentProps = props ?? {};
|
|
10
|
-
const renderInline = () => finalizeComponentRender(comp, options.render(
|
|
11
|
+
const renderInline = (nextProps = props) => finalizeComponentRender(comp, options.render(nextProps));
|
|
12
|
+
const activeRenderContext = getComponentRenderContext();
|
|
11
13
|
const foreignChildRender = interceptForeignChild({
|
|
12
14
|
component: comp,
|
|
13
15
|
props: componentProps,
|
|
14
16
|
targetIntegration: integrationName
|
|
15
17
|
});
|
|
16
18
|
if (isThenable(foreignChildRender)) {
|
|
17
|
-
return foreignChildRender.then(
|
|
18
|
-
(resolvedForeignChildRender
|
|
19
|
-
|
|
19
|
+
return foreignChildRender.then((resolvedForeignChildRender) => {
|
|
20
|
+
if (resolvedForeignChildRender?.kind === "resolved") {
|
|
21
|
+
return resolvedForeignChildRender.value;
|
|
22
|
+
}
|
|
23
|
+
return renderInline(resolvedForeignChildRender?.props ?? props);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (foreignChildRender?.kind === "resolved") {
|
|
27
|
+
return foreignChildRender.value;
|
|
20
28
|
}
|
|
21
|
-
if (foreignChildRender
|
|
22
|
-
return foreignChildRender;
|
|
29
|
+
if (foreignChildRender?.kind === "inline") {
|
|
30
|
+
return renderInline(foreignChildRender.props ?? props);
|
|
31
|
+
}
|
|
32
|
+
if (activeRenderContext && activeRenderContext.foreignChildRuntime && integrationName && integrationName !== activeRenderContext.currentIntegration) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`[ecopages] Missing foreign-child interception from ${activeRenderContext.currentIntegration} to ${integrationName} for ${options.__eco?.file ?? "unknown component"}.`
|
|
35
|
+
);
|
|
23
36
|
}
|
|
24
37
|
return renderInline();
|
|
25
38
|
});
|
|
@@ -23,8 +23,7 @@ class JsHmrStrategy extends HmrStrategy {
|
|
|
23
23
|
const watchedFiles = this.context.getWatchedFiles();
|
|
24
24
|
const isJsTs = /\.(ts|tsx|js|jsx)$/.test(filePath);
|
|
25
25
|
const isInSrc = filePath.startsWith(this.context.getSrcDir());
|
|
26
|
-
const
|
|
27
|
-
const isIntegrationTemplate = isRouteTemplate && this.context.getTemplateExtensions().some((extension) => filePath.endsWith(extension));
|
|
26
|
+
const isIntegrationTemplate = this.context.getTemplateExtensions().some((extension) => filePath.endsWith(extension));
|
|
28
27
|
if (watchedFiles.size === 0) {
|
|
29
28
|
return false;
|
|
30
29
|
}
|
|
@@ -33,15 +33,22 @@ function resolveAliasedBarrelTarget(resolvedPath) {
|
|
|
33
33
|
const target = findResolvablePath(path.resolve(path.dirname(resolvedPath), match[1]));
|
|
34
34
|
return target ?? resolvedPath;
|
|
35
35
|
}
|
|
36
|
+
function resolveAppSourceAliasPath(srcDir, specifier) {
|
|
37
|
+
if (!specifier.startsWith("@/")) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
const candidate = path.join(srcDir, specifier.slice(2));
|
|
41
|
+
const resolved = findResolvablePath(candidate);
|
|
42
|
+
return resolved ? resolveAliasedBarrelTarget(resolved) : void 0;
|
|
43
|
+
}
|
|
36
44
|
function createAliasResolverPlugin(srcDir) {
|
|
37
45
|
return {
|
|
38
46
|
name: "ecopages-alias-resolver",
|
|
39
47
|
setup(build) {
|
|
40
48
|
build.onResolve({ filter: /^@\// }, (args) => {
|
|
41
|
-
const
|
|
42
|
-
const resolved = findResolvablePath(candidate);
|
|
49
|
+
const resolved = resolveAppSourceAliasPath(srcDir, args.path);
|
|
43
50
|
if (resolved) {
|
|
44
|
-
return { path:
|
|
51
|
+
return { path: resolved };
|
|
45
52
|
}
|
|
46
53
|
return {};
|
|
47
54
|
});
|
|
@@ -49,5 +56,6 @@ function createAliasResolverPlugin(srcDir) {
|
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
58
|
export {
|
|
52
|
-
createAliasResolverPlugin
|
|
59
|
+
createAliasResolverPlugin,
|
|
60
|
+
resolveAppSourceAliasPath
|
|
53
61
|
};
|
|
@@ -18,7 +18,13 @@ function buildExtensionToIntegrationMap(integrations) {
|
|
|
18
18
|
const mapping = [];
|
|
19
19
|
for (const integration of integrations) {
|
|
20
20
|
for (const ext of integration.extensions) {
|
|
21
|
-
mapping.push([
|
|
21
|
+
mapping.push([
|
|
22
|
+
ext,
|
|
23
|
+
{
|
|
24
|
+
name: integration.name,
|
|
25
|
+
jsxImportSource: integration.jsxImportSource
|
|
26
|
+
}
|
|
27
|
+
]);
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
mapping.sort((a, b) => b[0].length - a[0].length);
|
|
@@ -30,7 +36,14 @@ function detectIntegration(filePath, extensionToIntegration) {
|
|
|
30
36
|
return integration;
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
|
-
return "ghtml";
|
|
39
|
+
return { name: "ghtml" };
|
|
40
|
+
}
|
|
41
|
+
function prependJsxImportSource(code, jsxImportSource) {
|
|
42
|
+
if (!jsxImportSource || code.includes("@jsxImportSource")) {
|
|
43
|
+
return code;
|
|
44
|
+
}
|
|
45
|
+
return `/** @jsxImportSource ${jsxImportSource} */
|
|
46
|
+
${code}`;
|
|
34
47
|
}
|
|
35
48
|
function createExtensionPattern(extensions) {
|
|
36
49
|
if (extensions.length === 0) {
|
|
@@ -54,7 +67,7 @@ function createEcoComponentMetaTransform(options) {
|
|
|
54
67
|
transform(code, id) {
|
|
55
68
|
const integration = detectIntegration(id, extensionToIntegration);
|
|
56
69
|
return {
|
|
57
|
-
code: injectEcoMeta(code, id, integration)
|
|
70
|
+
code: prependJsxImportSource(injectEcoMeta(code, id, integration.name), integration.jsxImportSource)
|
|
58
71
|
};
|
|
59
72
|
}
|
|
60
73
|
};
|
|
@@ -7,6 +7,8 @@ export interface ForeignJsxOverrideOptions {
|
|
|
7
7
|
hostJsxImportSource: string;
|
|
8
8
|
/** Extensions claimed by other JSX integrations that may appear in the host graph. */
|
|
9
9
|
foreignExtensions: string[];
|
|
10
|
+
/** More specific owned suffixes that must not be claimed by this override. */
|
|
11
|
+
excludeExtensions?: string[];
|
|
10
12
|
/** Optional plugin name override for debug output. */
|
|
11
13
|
name?: string;
|
|
12
14
|
}
|
|
@@ -2,6 +2,9 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
function createForeignJsxOverridePlugin(options) {
|
|
4
4
|
const extensions = options.foreignExtensions.filter((ext) => ext.endsWith(".tsx") || ext.endsWith(".jsx"));
|
|
5
|
+
const excludedExtensions = (options.excludeExtensions ?? []).filter(
|
|
6
|
+
(ext) => ext.endsWith(".tsx") || ext.endsWith(".jsx")
|
|
7
|
+
);
|
|
5
8
|
if (extensions.length === 0) {
|
|
6
9
|
return {
|
|
7
10
|
name: options.name ?? "foreign-jsx-override",
|
|
@@ -16,6 +19,9 @@ function createForeignJsxOverridePlugin(options) {
|
|
|
16
19
|
name: options.name ?? "foreign-jsx-override",
|
|
17
20
|
setup(build) {
|
|
18
21
|
build.onLoad({ filter }, (args) => {
|
|
22
|
+
if (excludedExtensions.some((extension) => args.path.endsWith(extension))) {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
19
25
|
const source = readFileSync(args.path, "utf-8");
|
|
20
26
|
const loader = args.path.endsWith(".jsx") ? "jsx" : "tsx";
|
|
21
27
|
if (source.includes("@jsxImportSource")) {
|
|
@@ -8,10 +8,18 @@ import type { EcoComponent } from '../../types/public-types.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export type ForeignChildInterceptionResult = {
|
|
10
10
|
kind: 'inline';
|
|
11
|
+
props?: Record<string, unknown>;
|
|
11
12
|
} | {
|
|
12
13
|
kind: 'resolved';
|
|
13
14
|
value: unknown;
|
|
14
15
|
};
|
|
16
|
+
export type ForeignChildRenderInterception = {
|
|
17
|
+
kind: 'inline';
|
|
18
|
+
props?: Record<string, unknown>;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'resolved';
|
|
21
|
+
value: unknown;
|
|
22
|
+
} | undefined;
|
|
15
23
|
/**
|
|
16
24
|
* Foreign-child metadata passed into the active renderer-owned runtime.
|
|
17
25
|
*/
|
|
@@ -50,7 +58,7 @@ type ForeignChildRenderInput = {
|
|
|
50
58
|
export type ComponentRenderContext = {
|
|
51
59
|
currentIntegration: string;
|
|
52
60
|
foreignChildRuntime?: ForeignChildRuntime;
|
|
53
|
-
interceptForeignChild(input: ForeignChildRenderInput): Promise<
|
|
61
|
+
interceptForeignChild(input: ForeignChildRenderInput): Promise<ForeignChildRenderInterception> | ForeignChildRenderInterception;
|
|
54
62
|
finalizeComponentRender<T>(component: EcoComponent, content: T): T;
|
|
55
63
|
};
|
|
56
64
|
/**
|
|
@@ -64,7 +72,7 @@ export declare function getComponentRenderContext(): ComponentRenderContext | un
|
|
|
64
72
|
*
|
|
65
73
|
* The active runtime may resolve the foreign child immediately or keep it inline.
|
|
66
74
|
*/
|
|
67
|
-
export declare function interceptForeignChild(input: ForeignChildRenderInput): Promise<
|
|
75
|
+
export declare function interceptForeignChild(input: ForeignChildRenderInput): Promise<ForeignChildRenderInterception> | ForeignChildRenderInterception;
|
|
68
76
|
/**
|
|
69
77
|
* Applies lazy trigger or injector wrapping to completed component output.
|
|
70
78
|
*
|
|
@@ -32,9 +32,9 @@ class ContextualComponentRenderRuntime extends ComponentRenderOutputRuntime {
|
|
|
32
32
|
}
|
|
33
33
|
applyForeignChildInterceptionResult(result) {
|
|
34
34
|
if (result.kind === "resolved") {
|
|
35
|
-
return result
|
|
35
|
+
return result;
|
|
36
36
|
}
|
|
37
|
-
return void 0;
|
|
37
|
+
return result.props ? result : void 0;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Resolves one foreign-child interception through the active runtime.
|
|
@@ -47,6 +47,7 @@ export interface ForeignSubtreeQueuedHtmlOptions<TContext extends QueuedForeignS
|
|
|
47
47
|
queueLabel: string;
|
|
48
48
|
renderQueuedChildren(children: unknown, runtimeContext: TContext, queuedResolutionsByToken: Map<string, QueuedForeignSubtreeResolution>, resolveToken: (token: string) => Promise<string>): Promise<{
|
|
49
49
|
assets: ProcessedAsset[];
|
|
50
|
+
children?: unknown;
|
|
50
51
|
html?: string;
|
|
51
52
|
}>;
|
|
52
53
|
getOwningRenderer(integrationName: string, rendererCache: Map<string, ForeignSubtreeExecutionOwningRenderer>): ForeignSubtreeExecutionOwningRenderer;
|
|
@@ -64,6 +65,7 @@ export interface ForeignSubtreeQueuedHtmlOptions<TContext extends QueuedForeignS
|
|
|
64
65
|
export declare class ForeignSubtreeExecutionService {
|
|
65
66
|
private readonly queuedForeignSubtreeResolutionService;
|
|
66
67
|
constructor(queuedForeignSubtreeResolutionService?: QueuedForeignSubtreeResolutionService);
|
|
68
|
+
private requiresForeignChildRuntime;
|
|
67
69
|
/**
|
|
68
70
|
* Returns whether the current render pass must hand the child off to a foreign owner.
|
|
69
71
|
*/
|
|
@@ -5,11 +5,27 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
QueuedForeignSubtreeResolutionService
|
|
7
7
|
} from "./queued-foreign-subtree-resolution.service.js";
|
|
8
|
+
function isMarkupNodeLike(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && "nodeType" in value && typeof value.nodeType === "number" && (!("outerHTML" in value) || typeof value.outerHTML === "string");
|
|
10
|
+
}
|
|
8
11
|
class ForeignSubtreeExecutionService {
|
|
9
12
|
queuedForeignSubtreeResolutionService;
|
|
10
13
|
constructor(queuedForeignSubtreeResolutionService = new QueuedForeignSubtreeResolutionService()) {
|
|
11
14
|
this.queuedForeignSubtreeResolutionService = queuedForeignSubtreeResolutionService;
|
|
12
15
|
}
|
|
16
|
+
requiresForeignChildRuntime(input) {
|
|
17
|
+
const children = input.children ?? input.props.children;
|
|
18
|
+
if (children === void 0 || children === null) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (typeof children === "string") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (typeof children !== "object") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return !isMarkupNodeLike(children);
|
|
28
|
+
}
|
|
13
29
|
/**
|
|
14
30
|
* Returns whether the current render pass must hand the child off to a foreign owner.
|
|
15
31
|
*/
|
|
@@ -25,7 +41,10 @@ class ForeignSubtreeExecutionService {
|
|
|
25
41
|
createFailFastRuntime(rendererName) {
|
|
26
42
|
const interceptForeignChild = (input) => {
|
|
27
43
|
if (!this.shouldDelegateForeignChild(input)) {
|
|
28
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
kind: "inline",
|
|
46
|
+
props: "props" in input && input.props ? { ...input.props } : void 0
|
|
47
|
+
};
|
|
29
48
|
}
|
|
30
49
|
throw new Error(
|
|
31
50
|
`[ecopages] ${rendererName} renderer crossed into ${input.targetIntegration} without a renderer-owned foreign-child runtime. Override createForeignChildRuntime() to resolve foreign children inside the owning renderer.`
|
|
@@ -63,8 +82,11 @@ class ForeignSubtreeExecutionService {
|
|
|
63
82
|
if (children === void 0) {
|
|
64
83
|
return { assets: [], html: void 0 };
|
|
65
84
|
}
|
|
85
|
+
if (typeof children !== "string" && !isMarkupNodeLike(children)) {
|
|
86
|
+
return { assets: [], children };
|
|
87
|
+
}
|
|
66
88
|
const html = await this.resolveQueuedTokens(
|
|
67
|
-
typeof children === "string" ? children : String(children ?? ""),
|
|
89
|
+
typeof children === "string" ? children : children.outerHTML ?? String(children ?? ""),
|
|
68
90
|
queuedResolutionsByToken,
|
|
69
91
|
resolveToken
|
|
70
92
|
);
|
|
@@ -105,7 +127,7 @@ class ForeignSubtreeExecutionService {
|
|
|
105
127
|
if (delegatedForeignChildRender) {
|
|
106
128
|
return delegatedForeignChildRender;
|
|
107
129
|
}
|
|
108
|
-
const hasForeignChildren = options.hasForeignChildDescendants(options.input.component);
|
|
130
|
+
const hasForeignChildren = options.hasForeignChildDescendants(options.input.component) || this.requiresForeignChildRuntime(options.input);
|
|
109
131
|
const activeRenderContext = getComponentRenderContext();
|
|
110
132
|
if (!hasForeignChildren) {
|
|
111
133
|
if (!activeRenderContext || activeRenderContext.currentIntegration === options.currentIntegrationName) {
|
|
@@ -188,7 +210,12 @@ class ForeignSubtreeExecutionService {
|
|
|
188
210
|
if (owningRenderer.name === options.currentIntegrationName) {
|
|
189
211
|
return void 0;
|
|
190
212
|
}
|
|
191
|
-
|
|
213
|
+
const delegatedInputWithCache = this.withRendererCache(options.input, options.rendererCache);
|
|
214
|
+
const delegatedChildren = delegatedInputWithCache.children ?? delegatedInputWithCache.props.children;
|
|
215
|
+
return await options.run(owningRenderer, {
|
|
216
|
+
...delegatedInputWithCache,
|
|
217
|
+
children: delegatedChildren
|
|
218
|
+
});
|
|
192
219
|
}
|
|
193
220
|
async resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken) {
|
|
194
221
|
let resolvedHtml = html;
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
ForeignSubtreeExecutionService
|
|
16
16
|
} from "./foreign-subtree-execution.service.js";
|
|
17
17
|
import {} from "./queued-foreign-subtree-resolution.service.js";
|
|
18
|
+
function isMarkupNodeLike(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && "nodeType" in value && typeof value.nodeType === "number" && "outerHTML" in value && typeof value.outerHTML === "string";
|
|
20
|
+
}
|
|
18
21
|
class IntegrationRenderer {
|
|
19
22
|
appConfig;
|
|
20
23
|
assetProcessingService;
|
|
@@ -348,7 +351,15 @@ class IntegrationRenderer {
|
|
|
348
351
|
* @returns Structured component render result for orchestration paths.
|
|
349
352
|
*/
|
|
350
353
|
async renderStringComponentWithSerializedChildren(input, component) {
|
|
351
|
-
const
|
|
354
|
+
const serializedChildren = input.children === void 0 ? void 0 : typeof input.children === "string" ? input.children : isMarkupNodeLike(input.children) ? input.children.outerHTML : void 0;
|
|
355
|
+
if (input.children !== void 0 && serializedChildren === void 0) {
|
|
356
|
+
const componentFile = input.component.config?.__eco?.file ?? "unknown component";
|
|
357
|
+
const childTag = Object.prototype.toString.call(input.children);
|
|
358
|
+
throw new TypeError(
|
|
359
|
+
`[ecopages] ${this.name} renderer expected serialized children for ${componentFile}, received ${childTag}.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const props = serializedChildren === void 0 ? input.props : { ...input.props, children: serializedChildren };
|
|
352
363
|
const content = await component(props);
|
|
353
364
|
const html = String(content);
|
|
354
365
|
const assets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : void 0;
|
|
@@ -30,6 +30,7 @@ type QueuedForeignSubtreeIntegrationContext = BaseIntegrationContext & Record<st
|
|
|
30
30
|
type QueuedForeignSubtreeChildRenderResult = {
|
|
31
31
|
assets: ProcessedAsset[];
|
|
32
32
|
html?: string;
|
|
33
|
+
children?: unknown;
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
35
36
|
* Lower-level queue orchestration for renderer-owned foreign-child runtimes that emit
|
|
@@ -21,7 +21,10 @@ class QueuedForeignSubtreeResolutionService {
|
|
|
21
21
|
const runtimeContext = this.ensureRuntimeContext(options);
|
|
22
22
|
const interceptForeignChild = (input) => {
|
|
23
23
|
if (!options.shouldQueueForeignChild(input)) {
|
|
24
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
kind: "inline",
|
|
26
|
+
props: { ...input.props }
|
|
27
|
+
};
|
|
25
28
|
}
|
|
26
29
|
runtimeContext.nextForeignSubtreeId += 1;
|
|
27
30
|
const foreignSubtreeId = runtimeContext.nextForeignSubtreeId;
|
|
@@ -54,13 +57,19 @@ class QueuedForeignSubtreeResolutionService {
|
|
|
54
57
|
return { assets: [], html: options.html };
|
|
55
58
|
}
|
|
56
59
|
const runtimeContext = options.runtimeContext;
|
|
57
|
-
const queuedResolutionsByToken = new Map(
|
|
58
|
-
runtimeContext.queuedResolutions.map((resolution) => [resolution.token, resolution])
|
|
59
|
-
);
|
|
60
|
+
const queuedResolutionsByToken = /* @__PURE__ */ new Map();
|
|
60
61
|
const resolvedHtmlByToken = /* @__PURE__ */ new Map();
|
|
61
62
|
const resolvingTokens = /* @__PURE__ */ new Set();
|
|
62
63
|
const collectedAssets = [];
|
|
64
|
+
const syncQueuedResolutions = () => {
|
|
65
|
+
for (const resolution of runtimeContext.queuedResolutions) {
|
|
66
|
+
if (!queuedResolutionsByToken.has(resolution.token)) {
|
|
67
|
+
queuedResolutionsByToken.set(resolution.token, resolution);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
63
71
|
const resolveToken = async (token) => {
|
|
72
|
+
syncQueuedResolutions();
|
|
64
73
|
const cachedHtml = resolvedHtmlByToken.get(token);
|
|
65
74
|
if (cachedHtml) {
|
|
66
75
|
return cachedHtml;
|
|
@@ -82,6 +91,7 @@ class QueuedForeignSubtreeResolutionService {
|
|
|
82
91
|
queuedResolutionsByToken,
|
|
83
92
|
resolveToken
|
|
84
93
|
);
|
|
94
|
+
syncQueuedResolutions();
|
|
85
95
|
if (renderedChildren.assets.length > 0) {
|
|
86
96
|
collectedAssets.push(...renderedChildren.assets);
|
|
87
97
|
}
|
|
@@ -89,7 +99,7 @@ class QueuedForeignSubtreeResolutionService {
|
|
|
89
99
|
{
|
|
90
100
|
component: resolution.component,
|
|
91
101
|
props: { ...resolution.props },
|
|
92
|
-
children: renderedChildren.html,
|
|
102
|
+
children: renderedChildren.html ?? renderedChildren.children,
|
|
93
103
|
integrationContext: {
|
|
94
104
|
rendererCache: runtimeContext.rendererCache,
|
|
95
105
|
componentInstanceId: resolution.componentInstanceId
|
|
@@ -116,7 +126,9 @@ class QueuedForeignSubtreeResolutionService {
|
|
|
116
126
|
}
|
|
117
127
|
};
|
|
118
128
|
let resolvedHtml = options.html;
|
|
119
|
-
for (
|
|
129
|
+
for (let index = 0; index < runtimeContext.queuedResolutions.length; index += 1) {
|
|
130
|
+
syncQueuedResolutions();
|
|
131
|
+
const resolution = runtimeContext.queuedResolutions[index];
|
|
120
132
|
if (!resolvedHtml.includes(resolution.token)) {
|
|
121
133
|
continue;
|
|
122
134
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PageModuleBuildImportOptions } from './page-module-import.service.js';
|
|
2
2
|
export type AppModuleLoaderOwner = 'bun' | 'host';
|
|
3
3
|
export interface AppModuleLoader {
|
|
4
4
|
readonly owner: AppModuleLoaderOwner;
|
|
5
|
-
importModule<T = unknown>(options:
|
|
5
|
+
importModule<T = unknown>(options: PageModuleBuildImportOptions): Promise<T>;
|
|
6
6
|
invalidateDevelopmentGraph(): void;
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getAppBuildExecutor } from "../../build/build-adapter.js";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createForeignJsxOverridePlugin } from "../../plugins/foreign-jsx-override-plugin.js";
|
|
3
4
|
import { DevelopmentInvalidationService } from "../invalidation/development-invalidation.service.js";
|
|
4
5
|
import {} from "./app-module-loader.service.js";
|
|
5
6
|
import { createAppNodeBootstrapPlugin } from "./node-bootstrap-plugin.js";
|
|
@@ -37,6 +38,24 @@ function setAppHostModuleLoader(appConfig, hostModuleLoader) {
|
|
|
37
38
|
hostModuleLoader
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function getOwningIntegration(appConfig, filePath) {
|
|
42
|
+
return appConfig.integrations?.flatMap(
|
|
43
|
+
(integration) => integration.extensions.filter((extension) => filePath.endsWith(extension)).map((extension) => ({ integration, extension }))
|
|
44
|
+
).sort((left, right) => right.extension.length - left.extension.length)[0]?.integration;
|
|
45
|
+
}
|
|
46
|
+
function getBunOwnedJsxPlugins(appConfig) {
|
|
47
|
+
const jsxExtensions = (appConfig.integrations ?? []).filter((integration) => integration.jsxImportSource).flatMap(
|
|
48
|
+
(integration) => integration.extensions.filter((extension) => extension.endsWith(".tsx") || extension.endsWith(".jsx")).map((extension) => ({ integration, extension }))
|
|
49
|
+
).sort((left, right) => right.extension.length - left.extension.length);
|
|
50
|
+
return jsxExtensions.map(
|
|
51
|
+
({ integration, extension }) => createForeignJsxOverridePlugin({
|
|
52
|
+
hostJsxImportSource: integration.jsxImportSource,
|
|
53
|
+
foreignExtensions: [extension],
|
|
54
|
+
excludeExtensions: jsxExtensions.filter((candidate) => candidate.extension.length > extension.length).filter((candidate) => candidate.extension.endsWith(extension)).map((candidate) => candidate.extension),
|
|
55
|
+
name: `ecopages-bun-jsx-ownership-${integration.name}-${extension.replace(/[^a-zA-Z0-9]+/g, "-")}`
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
40
59
|
function createAppModuleLoader(appConfig) {
|
|
41
60
|
const invalidationService = new DevelopmentInvalidationService(appConfig);
|
|
42
61
|
const pageModuleImportService = new PageModuleImportService({
|
|
@@ -44,18 +63,35 @@ function createAppModuleLoader(appConfig) {
|
|
|
44
63
|
getHostModuleLoader: () => getAppHostModuleLoader(appConfig)
|
|
45
64
|
});
|
|
46
65
|
const getDefaultPlugins = typeof Bun === "undefined" && appConfig.rootDir ? () => [createAppNodeBootstrapPlugin(appConfig)] : () => [];
|
|
66
|
+
const appLoaderPlugins = Array.from(appConfig.loaders?.values() ?? []);
|
|
67
|
+
const bunOwnedJsxPlugins = typeof Bun !== "undefined" ? getBunOwnedJsxPlugins(appConfig) : [];
|
|
47
68
|
const appModuleLoader = {
|
|
48
69
|
get owner() {
|
|
49
70
|
return getAppModuleLoaderOwner(appConfig);
|
|
50
71
|
},
|
|
51
72
|
pageModuleImportService,
|
|
52
73
|
async importModule(options) {
|
|
53
|
-
const
|
|
74
|
+
const invalidationVersion = options.invalidationVersion ?? invalidationService.getServerModuleInvalidationVersion();
|
|
75
|
+
const owningIntegration = getOwningIntegration(appConfig, options.filePath);
|
|
76
|
+
const owningJsxImportSource = owningIntegration?.jsxImportSource;
|
|
77
|
+
const mergedPlugins = [
|
|
78
|
+
...getDefaultPlugins(),
|
|
79
|
+
...appLoaderPlugins,
|
|
80
|
+
...bunOwnedJsxPlugins,
|
|
81
|
+
...options.plugins ?? []
|
|
82
|
+
];
|
|
54
83
|
return await pageModuleImportService.importModule({
|
|
55
84
|
...options,
|
|
56
85
|
...mergedPlugins.length > 0 ? { plugins: mergedPlugins } : {},
|
|
86
|
+
...typeof Bun !== "undefined" && owningJsxImportSource ? {
|
|
87
|
+
jsx: {
|
|
88
|
+
development: process.env.NODE_ENV === "development",
|
|
89
|
+
importSource: owningJsxImportSource,
|
|
90
|
+
runtime: "automatic"
|
|
91
|
+
}
|
|
92
|
+
} : {},
|
|
57
93
|
buildExecutor: options.buildExecutor ?? getAppBuildExecutor(appConfig),
|
|
58
|
-
invalidationVersion
|
|
94
|
+
invalidationVersion
|
|
59
95
|
});
|
|
60
96
|
},
|
|
61
97
|
invalidateDevelopmentGraph() {
|
|
@@ -91,6 +91,78 @@ function resolveSpecifier(specifier, parentPath) {
|
|
|
91
91
|
function resolveFromCore(specifier) {
|
|
92
92
|
return createRequire(import.meta.url).resolve(specifier);
|
|
93
93
|
}
|
|
94
|
+
function readPackageManifest(packageDir) {
|
|
95
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
96
|
+
if (!existsSync(packageJsonPath)) {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function findInstalledPackageDir(packageName, parentPath) {
|
|
106
|
+
let currentPath = path.dirname(parentPath);
|
|
107
|
+
const packageSegments = packageName.split("/");
|
|
108
|
+
while (true) {
|
|
109
|
+
const candidateDir = path.join(currentPath, "node_modules", ...packageSegments);
|
|
110
|
+
if (existsSync(path.join(candidateDir, "package.json"))) {
|
|
111
|
+
return candidateDir;
|
|
112
|
+
}
|
|
113
|
+
const nextPath = path.dirname(currentPath);
|
|
114
|
+
if (nextPath === currentPath) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
currentPath = nextPath;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function resolvePackageExportTarget(packageDir, target) {
|
|
121
|
+
if (typeof target === "string") {
|
|
122
|
+
return path.resolve(packageDir, target);
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(target)) {
|
|
125
|
+
for (const candidate of target) {
|
|
126
|
+
const resolvedTarget = resolvePackageExportTarget(packageDir, candidate);
|
|
127
|
+
if (resolvedTarget) {
|
|
128
|
+
return resolvedTarget;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
if (!target || typeof target !== "object") {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
const record = target;
|
|
137
|
+
return resolvePackageExportTarget(packageDir, record.import) ?? resolvePackageExportTarget(packageDir, record.default) ?? resolvePackageExportTarget(packageDir, record.require);
|
|
138
|
+
}
|
|
139
|
+
function resolveInstalledPackageTarget(specifier, parentPath) {
|
|
140
|
+
const packageName = getPackageNameFromSpecifier(specifier);
|
|
141
|
+
const packageDir = findInstalledPackageDir(packageName, parentPath);
|
|
142
|
+
if (!packageDir) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
const manifest = readPackageManifest(packageDir);
|
|
146
|
+
if (!manifest) {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
const subpath = specifier === packageName ? "." : `./${specifier.slice(packageName.length + 1)}`;
|
|
150
|
+
const exportsField = manifest.exports;
|
|
151
|
+
if (exportsField !== void 0) {
|
|
152
|
+
const exportsRecord = exportsField;
|
|
153
|
+
const hasSubpathKeys = typeof exportsField === "object" && exportsField !== null && Object.keys(exportsRecord).some((key) => key.startsWith("."));
|
|
154
|
+
const exportTarget = hasSubpathKeys ? resolvePackageExportTarget(packageDir, exportsRecord[subpath]) : subpath === "." ? resolvePackageExportTarget(packageDir, exportsField) : void 0;
|
|
155
|
+
if (exportTarget && existsSync(exportTarget)) {
|
|
156
|
+
return exportTarget;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (subpath !== ".") {
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
const mainTarget = manifest.module ?? manifest.main ?? "index.js";
|
|
163
|
+
const resolvedMainTarget = path.resolve(packageDir, mainTarget);
|
|
164
|
+
return existsSync(resolvedMainTarget) ? resolvedMainTarget : void 0;
|
|
165
|
+
}
|
|
94
166
|
function findResolutionParent(importer, projectDir) {
|
|
95
167
|
if (!importer || !path.isAbsolute(importer)) {
|
|
96
168
|
return path.join(projectDir, "package.json");
|
|
@@ -135,6 +207,16 @@ function resolveNodeBootstrapDependency(args, options) {
|
|
|
135
207
|
}
|
|
136
208
|
const resolveParent = findResolutionParent(args.importer, options.projectDir);
|
|
137
209
|
if (args.path.startsWith("@ecopages/")) {
|
|
210
|
+
const packageName = getPackageNameFromSpecifier(args.path);
|
|
211
|
+
const isBareWorkspacePackage = args.path === packageName;
|
|
212
|
+
const installedResolvedPath = resolveInstalledPackageTarget(args.path, resolveParent);
|
|
213
|
+
if (installedResolvedPath) {
|
|
214
|
+
return { path: installedResolvedPath };
|
|
215
|
+
}
|
|
216
|
+
if (!isBareWorkspacePackage) {
|
|
217
|
+
const resolvedSubpath = resolveSpecifier(args.path, resolveParent);
|
|
218
|
+
return { path: resolvedSubpath };
|
|
219
|
+
}
|
|
138
220
|
let resolvedPath2;
|
|
139
221
|
try {
|
|
140
222
|
resolvedPath2 = resolveFromCore(args.path);
|
|
@@ -142,7 +224,6 @@ function resolveNodeBootstrapDependency(args, options) {
|
|
|
142
224
|
try {
|
|
143
225
|
resolvedPath2 = resolveSpecifier(args.path, resolveParent);
|
|
144
226
|
} catch {
|
|
145
|
-
const packageName = getPackageNameFromSpecifier(args.path);
|
|
146
227
|
const candidatePath = path.join(options.projectDir, "node_modules", packageName);
|
|
147
228
|
const candidatePackageJson = path.join(candidatePath, "package.json");
|
|
148
229
|
if (existsSync(candidatePackageJson)) {
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { build, type BuildExecutor, type BuildResult } from '../../build/build-adapter.js';
|
|
2
2
|
import type { EcoBuildPlugin } from '../../build/build-types.js';
|
|
3
3
|
import type { SourceModuleLoaderFactory } from './module-loading-types.js';
|
|
4
|
-
|
|
4
|
+
interface PageModuleImportBaseOptions {
|
|
5
5
|
filePath: string;
|
|
6
|
-
rootDir: string;
|
|
7
|
-
outdir: string;
|
|
8
6
|
bypassCache?: boolean;
|
|
9
7
|
cacheScope?: string;
|
|
10
|
-
buildExecutor?: BuildExecutor;
|
|
11
8
|
invalidationVersion?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for imports that must pass through the Ecopages build pipeline.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Callers should use build mode for framework-owned page modules and any other
|
|
15
|
+
* source that relies on Ecopages build resolution before runtime execution.
|
|
16
|
+
*/
|
|
17
|
+
export interface PageModuleBuildImportOptions extends PageModuleImportBaseOptions {
|
|
18
|
+
rootDir: string;
|
|
19
|
+
outdir: string;
|
|
20
|
+
buildExecutor?: BuildExecutor;
|
|
12
21
|
splitting?: boolean;
|
|
13
22
|
externalPackages?: boolean;
|
|
23
|
+
jsx?: {
|
|
24
|
+
development?: boolean;
|
|
25
|
+
factory?: string;
|
|
26
|
+
fragment?: string;
|
|
27
|
+
importSource?: string;
|
|
28
|
+
runtime?: 'classic' | 'automatic';
|
|
29
|
+
sideEffects?: boolean;
|
|
30
|
+
};
|
|
14
31
|
plugins?: EcoBuildPlugin[];
|
|
15
32
|
transpileErrorMessage?: (details: string) => string;
|
|
16
33
|
noOutputMessage?: (filePath: string) => string;
|
|
@@ -30,12 +47,13 @@ export interface PageModuleImportDependencies {
|
|
|
30
47
|
getHostModuleLoader: SourceModuleLoaderFactory;
|
|
31
48
|
}
|
|
32
49
|
/**
|
|
33
|
-
* Loads
|
|
50
|
+
* Loads page-like modules through the Ecopages build pipeline.
|
|
34
51
|
*
|
|
35
|
-
* This service centralizes the
|
|
36
|
-
* scanning, page data loading, and request-time page inspection. In
|
|
37
|
-
*
|
|
38
|
-
*
|
|
52
|
+
* This service centralizes the shared build-first import strategy used by route
|
|
53
|
+
* scanning, page data loading, and request-time page inspection. In Node
|
|
54
|
+
* development it can still delegate compatible source imports to the active
|
|
55
|
+
* host loader, but the public contract remains one transpile-targeted module
|
|
56
|
+
* loading path.
|
|
39
57
|
*
|
|
40
58
|
* Keeping this logic in one place prevents subtle drift in cache-busting,
|
|
41
59
|
* transpilation settings, and error semantics across the different callers.
|
|
@@ -71,6 +89,7 @@ export declare class PageModuleImportService {
|
|
|
71
89
|
* @param options Runtime-specific import settings.
|
|
72
90
|
* @returns The loaded module.
|
|
73
91
|
*/
|
|
74
|
-
importModule<T = unknown>(options:
|
|
92
|
+
importModule<T = unknown>(options: PageModuleBuildImportOptions): Promise<T>;
|
|
75
93
|
private loadModule;
|
|
76
94
|
}
|
|
95
|
+
export {};
|
|
@@ -47,8 +47,9 @@ class PageModuleImportService {
|
|
|
47
47
|
* @returns The loaded module.
|
|
48
48
|
*/
|
|
49
49
|
async importModule(options) {
|
|
50
|
-
const { filePath
|
|
50
|
+
const { filePath } = options;
|
|
51
51
|
const invalidationVersion = options.invalidationVersion ?? this.developmentInvalidationVersion;
|
|
52
|
+
const { externalPackages, splitting } = options;
|
|
52
53
|
const fileHash = this.dependencies.hashFile(filePath);
|
|
53
54
|
const hostModuleLoader = typeof Bun === "undefined" && process.env.NODE_ENV === "development" && this.dependencies.canLoadSourceModuleFromHost(filePath) ? this.dependencies.getHostModuleLoader() : void 0;
|
|
54
55
|
if (hostModuleLoader) {
|
|
@@ -67,10 +68,12 @@ class PageModuleImportService {
|
|
|
67
68
|
const cacheKey = [
|
|
68
69
|
runtime,
|
|
69
70
|
filePath,
|
|
70
|
-
rootDir,
|
|
71
|
+
options.rootDir,
|
|
71
72
|
splitting ?? "default",
|
|
72
73
|
externalPackages ?? "default",
|
|
73
74
|
options.cacheScope ?? "default",
|
|
75
|
+
createJsxCacheKey(options.jsx),
|
|
76
|
+
createPluginCacheKey(options.plugins),
|
|
74
77
|
fileHash,
|
|
75
78
|
invalidationVersion
|
|
76
79
|
].join("::");
|
|
@@ -91,28 +94,20 @@ class PageModuleImportService {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
async loadModule(options) {
|
|
97
|
+
const { filePath, invalidationVersion = this.developmentInvalidationVersion, cacheScope, fileHash } = options;
|
|
94
98
|
const {
|
|
95
|
-
filePath,
|
|
96
99
|
rootDir,
|
|
97
100
|
outdir,
|
|
98
|
-
invalidationVersion = this.developmentInvalidationVersion,
|
|
99
101
|
splitting,
|
|
100
102
|
externalPackages,
|
|
101
|
-
cacheScope,
|
|
102
103
|
transpileErrorMessage = (details) => `Error transpiling page module: ${details}`,
|
|
103
|
-
noOutputMessage = (targetFilePath) => `No transpiled output generated for page module: ${targetFilePath}
|
|
104
|
-
fileHash
|
|
104
|
+
noOutputMessage = (targetFilePath) => `No transpiled output generated for page module: ${targetFilePath}`
|
|
105
105
|
} = options;
|
|
106
|
-
const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheScope);
|
|
107
|
-
if (typeof Bun !== "undefined") {
|
|
108
|
-
return await import(
|
|
109
|
-
/* @vite-ignore */
|
|
110
|
-
sourceModuleUrl.href
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
106
|
const fileBaseName = path.basename(filePath, path.extname(filePath));
|
|
114
107
|
const cacheScopeSuffix = cacheScope ? `-${sanitizeCacheScope(cacheScope)}` : "";
|
|
115
|
-
const
|
|
108
|
+
const invalidationSuffix = shouldVersionBuildOutputPath(invalidationVersion) ? `-v${invalidationVersion}` : "";
|
|
109
|
+
const outputFileName = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${invalidationSuffix}.js`;
|
|
110
|
+
const outputNamingTemplate = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${invalidationSuffix}.[ext]`;
|
|
116
111
|
const buildResult = await this.dependencies.buildModule(
|
|
117
112
|
{
|
|
118
113
|
entrypoints: [filePath],
|
|
@@ -123,8 +118,9 @@ class PageModuleImportService {
|
|
|
123
118
|
sourcemap: "none",
|
|
124
119
|
splitting: splitting ?? true,
|
|
125
120
|
minify: false,
|
|
126
|
-
naming:
|
|
121
|
+
naming: outputNamingTemplate,
|
|
127
122
|
externalPackages: true,
|
|
123
|
+
jsx: options.jsx,
|
|
128
124
|
plugins: options.plugins,
|
|
129
125
|
...externalPackages !== void 0 ? { externalPackages } : {}
|
|
130
126
|
},
|
|
@@ -140,7 +136,7 @@ class PageModuleImportService {
|
|
|
140
136
|
throw new Error(noOutputMessage(filePath));
|
|
141
137
|
}
|
|
142
138
|
const compiledOutputUrl = pathToFileURL(compiledOutput);
|
|
143
|
-
if (
|
|
139
|
+
if (shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope)) {
|
|
144
140
|
compiledOutputUrl.searchParams.set(
|
|
145
141
|
"update",
|
|
146
142
|
[fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : void 0].filter((value) => value !== void 0).join("-")
|
|
@@ -154,7 +150,7 @@ class PageModuleImportService {
|
|
|
154
150
|
}
|
|
155
151
|
function createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheScope) {
|
|
156
152
|
const moduleUrl = pathToFileURL(filePath);
|
|
157
|
-
if (
|
|
153
|
+
if (shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope)) {
|
|
158
154
|
moduleUrl.searchParams.set(
|
|
159
155
|
"update",
|
|
160
156
|
[fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : void 0].filter((value) => value !== void 0).join("-")
|
|
@@ -162,9 +158,34 @@ function createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheSc
|
|
|
162
158
|
}
|
|
163
159
|
return moduleUrl;
|
|
164
160
|
}
|
|
161
|
+
function shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope) {
|
|
162
|
+
return process.env.NODE_ENV === "development" || invalidationVersion > 0 || !!cacheScope;
|
|
163
|
+
}
|
|
164
|
+
function shouldVersionBuildOutputPath(invalidationVersion) {
|
|
165
|
+
return typeof Bun !== "undefined" && invalidationVersion > 0;
|
|
166
|
+
}
|
|
165
167
|
function sanitizeCacheScope(cacheScope) {
|
|
166
168
|
return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
167
169
|
}
|
|
170
|
+
function createJsxCacheKey(jsx) {
|
|
171
|
+
if (!jsx) {
|
|
172
|
+
return "jsx:default";
|
|
173
|
+
}
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
development: jsx.development ?? false,
|
|
176
|
+
factory: jsx.factory ?? null,
|
|
177
|
+
fragment: jsx.fragment ?? null,
|
|
178
|
+
importSource: jsx.importSource ?? null,
|
|
179
|
+
runtime: jsx.runtime ?? null,
|
|
180
|
+
sideEffects: jsx.sideEffects ?? null
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function createPluginCacheKey(plugins) {
|
|
184
|
+
if (!plugins || plugins.length === 0) {
|
|
185
|
+
return "plugins:default";
|
|
186
|
+
}
|
|
187
|
+
return `plugins:${plugins.map((plugin) => plugin.name).join(",")}`;
|
|
188
|
+
}
|
|
168
189
|
export {
|
|
169
190
|
PageModuleImportService
|
|
170
191
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { BuildExecutor } from '../../build/build-adapter.js';
|
|
2
2
|
import type { EcoBuildPlugin } from '../../build/build-types.js';
|
|
3
3
|
import type { AppModuleLoader } from './app-module-loader.service.js';
|
|
4
|
-
import { type
|
|
4
|
+
import { type PageModuleBuildImportOptions } from './page-module-import.service.js';
|
|
5
5
|
import type { SourceModuleLoaderFactory } from './module-loading-types.js';
|
|
6
|
-
export type ServerModuleTranspilerOptions = Omit<
|
|
6
|
+
export type ServerModuleTranspilerOptions = Omit<PageModuleBuildImportOptions, 'rootDir' | 'buildExecutor'>;
|
|
7
7
|
export type ServerModuleImportDependency = Pick<AppModuleLoader, 'importModule' | 'invalidateDevelopmentGraph'>;
|
|
8
8
|
/**
|
|
9
9
|
* Immutable execution context for one server-transpiler instance.
|
|
@@ -127,7 +127,7 @@ export interface DefaultHmrContext {
|
|
|
127
127
|
/**
|
|
128
128
|
* Server-side module loader owned by the active app/runtime.
|
|
129
129
|
*/
|
|
130
|
-
importServerModule<T = unknown>(filePath: string): Promise<T>;
|
|
130
|
+
importServerModule<T = unknown>(filePath: string | URL): Promise<T>;
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Represents an event broadcast to connected clients via the ClientBridge.
|
|
@@ -823,6 +823,24 @@ export interface ResponseOptions {
|
|
|
823
823
|
* Provides methods to render eco.page views and return formatted responses.
|
|
824
824
|
*/
|
|
825
825
|
export interface RenderContext {
|
|
826
|
+
/**
|
|
827
|
+
* Import a server-executed module through the active Ecopages module loader.
|
|
828
|
+
*
|
|
829
|
+
* This applies the runtime's cache-busting and source-transpilation rules so
|
|
830
|
+
* request-time lazy imports participate in development invalidation.
|
|
831
|
+
*
|
|
832
|
+
* @param filePath - Absolute filesystem path or file URL for the server module
|
|
833
|
+
*/
|
|
834
|
+
importServerModule<T = unknown>(filePath: string | URL): Promise<T>;
|
|
835
|
+
/**
|
|
836
|
+
* Import a server module through the active Ecopages module loader and render
|
|
837
|
+
* its default-exported eco.page view.
|
|
838
|
+
*
|
|
839
|
+
* @param filePath - Absolute filesystem path or file URL for the server module
|
|
840
|
+
* @param props - Props to pass to the default-exported view
|
|
841
|
+
* @param options - Optional status code and headers
|
|
842
|
+
*/
|
|
843
|
+
renderServerModule<P = Record<string, unknown>>(filePath: string | URL, props?: P, options?: RenderOptions): Promise<Response>;
|
|
826
844
|
/**
|
|
827
845
|
* Render an eco.page view with full layout and includes.
|
|
828
846
|
* @param view - The eco.page component to render
|
|
@@ -1020,7 +1038,7 @@ export interface ApiHandlerContext<TRequest extends Request = Request, TServer =
|
|
|
1020
1038
|
* response helpers, but final document rendering stays owned by the page route
|
|
1021
1039
|
* execution path.
|
|
1022
1040
|
*/
|
|
1023
|
-
export interface FileRouteMiddlewareContext<TRequest extends Request = Request, TServer = any> extends Omit<ApiHandlerContext<TRequest, TServer>, 'render' | 'renderPartial'> {
|
|
1041
|
+
export interface FileRouteMiddlewareContext<TRequest extends Request = Request, TServer = any> extends Omit<ApiHandlerContext<TRequest, TServer>, 'render' | 'renderPartial' | 'importServerModule' | 'renderServerModule'> {
|
|
1024
1042
|
}
|
|
1025
1043
|
/**
|
|
1026
1044
|
* Next function for middleware chain.
|