@ecopages/core 0.2.0-alpha.48 → 0.2.0-alpha.49
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 +2 -2
- package/src/build/build-adapter.js +6 -0
- package/src/build/runtime-specifier-aliases.js +1 -1
- package/src/route-renderer/orchestration/integration-renderer.js +9 -4
- package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +23 -1
- package/src/route-renderer/orchestration/route-render-orchestrator.js +140 -5
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js +1 -0
- package/src/services/module-loading/node-bootstrap-plugin.js +26 -0
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.49",
|
|
4
4
|
"description": "Core package for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -17,7 +17,7 @@
|
|
|
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.49",
|
|
21
21
|
"@ecopages/logger": "^0.2.3",
|
|
22
22
|
"@ecopages/scripts-injector": "^0.1.5",
|
|
23
23
|
"@worker-tools/html-rewriter": "0.1.0-pre.19",
|
|
@@ -227,6 +227,12 @@ class BunBuildAdapter {
|
|
|
227
227
|
if (fs.existsSync(outputPath)) {
|
|
228
228
|
return outputPath;
|
|
229
229
|
}
|
|
230
|
+
for (const extension of [".js", ".mjs", ".cjs"]) {
|
|
231
|
+
const outputPathWithExtension = `${outputPath}${extension}`;
|
|
232
|
+
if (fs.existsSync(outputPathWithExtension)) {
|
|
233
|
+
return outputPathWithExtension;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
230
236
|
if (!outputPath.includes("[hash]")) {
|
|
231
237
|
return outputPath;
|
|
232
238
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseSync } from "oxc-parser";
|
|
2
|
-
const RUNTIME_SPECIFIER_ALIAS_MAP = /* @__PURE__ */ Symbol("ecopages.runtimeSpecifierAliasMap");
|
|
2
|
+
const RUNTIME_SPECIFIER_ALIAS_MAP = /* @__PURE__ */ Symbol.for("ecopages.runtimeSpecifierAliasMap");
|
|
3
3
|
function attachRuntimeSpecifierAliasMap(plugin, specifierMap) {
|
|
4
4
|
plugin[RUNTIME_SPECIFIER_ALIAS_MAP] = specifierMap;
|
|
5
5
|
return plugin;
|
|
@@ -184,9 +184,9 @@ class IntegrationRenderer {
|
|
|
184
184
|
return await this.routeRenderOrchestrator.resolveDeclaredPageBrowserGraph({
|
|
185
185
|
routeFile: filePath,
|
|
186
186
|
integrationName: this.name,
|
|
187
|
-
collectContribution: async () => {
|
|
188
|
-
const pageModule = await this.importPageFile(
|
|
189
|
-
return await this.collectPageBrowserGraphContribution({ file:
|
|
187
|
+
collectContribution: async (routeFile) => {
|
|
188
|
+
const pageModule = await this.importPageFile(routeFile);
|
|
189
|
+
return await this.collectPageBrowserGraphContribution({ file: routeFile, pageModule });
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -241,7 +241,12 @@ class IntegrationRenderer {
|
|
|
241
241
|
...this.htmlTransformer.getProcessedDependencies(),
|
|
242
242
|
...nextDependencies
|
|
243
243
|
]);
|
|
244
|
-
this.htmlTransformer.
|
|
244
|
+
const currentPageBrowserGraph = this.htmlTransformer.getPagePackage()?.pageBrowserGraph;
|
|
245
|
+
this.htmlTransformer.setPagePackage(
|
|
246
|
+
createPagePackage(mergedDependencies, {
|
|
247
|
+
pageBrowserGraph: currentPageBrowserGraph
|
|
248
|
+
})
|
|
249
|
+
);
|
|
245
250
|
return nextDependencies;
|
|
246
251
|
}
|
|
247
252
|
/**
|
|
@@ -94,6 +94,7 @@ export declare class RouteRenderOrchestrator {
|
|
|
94
94
|
private readonly ownershipPlanningService;
|
|
95
95
|
private readonly ownershipValidationService;
|
|
96
96
|
private readonly pageBrowserGraphCache;
|
|
97
|
+
private readonly groupedPageBrowserGraphCache;
|
|
97
98
|
constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService, dependencies?: RouteRenderOrchestratorDependencies);
|
|
98
99
|
/**
|
|
99
100
|
* Builds normalized route render options before the integration render runs.
|
|
@@ -106,11 +107,32 @@ export declare class RouteRenderOrchestrator {
|
|
|
106
107
|
resolveDeclaredPageBrowserGraph(input: {
|
|
107
108
|
routeFile: string;
|
|
108
109
|
integrationName: string;
|
|
109
|
-
collectContribution: () => Promise<PageBrowserGraphContribution | undefined>;
|
|
110
|
+
collectContribution: (routeFile: string) => Promise<PageBrowserGraphContribution | undefined>;
|
|
110
111
|
}): Promise<PageBrowserGraphResult | undefined>;
|
|
111
112
|
private resolvePageBrowserGraph;
|
|
112
113
|
private isHmrEnabled;
|
|
113
114
|
private buildPageBrowserGraph;
|
|
115
|
+
/**
|
|
116
|
+
* Resolves grouped page-browser assets for all routes owned by one integration.
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* This keeps one shared browser-build result available across sibling routes so
|
|
120
|
+
* navigation can reuse the same emitted client graph instead of rebuilding one
|
|
121
|
+
* page entry at a time. HMR bypasses this cache because the grouped build must
|
|
122
|
+
* reflect the latest source on every request.
|
|
123
|
+
*/
|
|
124
|
+
private resolveGroupedPageBrowserAssets;
|
|
125
|
+
/**
|
|
126
|
+
* Builds the shared grouped page-browser asset map for one integration.
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* Each route can declare grouped content-script dependencies that should be
|
|
130
|
+
* emitted together. This method collects those declarations across the owning
|
|
131
|
+
* integration, runs the grouped processor once, and then maps the emitted
|
|
132
|
+
* assets back onto the routes that reference them.
|
|
133
|
+
*/
|
|
134
|
+
private buildGroupedPageBrowserAssets;
|
|
135
|
+
private listIntegrationRouteFiles;
|
|
114
136
|
private partitionPageBrowserGraphAssets;
|
|
115
137
|
/**
|
|
116
138
|
* Captures one route render body as HTML while preserving a replayable body value.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
3
4
|
import {
|
|
4
5
|
AssetFactory,
|
|
5
6
|
createPagePackage
|
|
6
7
|
} from "../../services/assets/asset-processing-service/index.js";
|
|
7
8
|
import { buildGlobalInjectorBootstrapContent, buildGlobalInjectorMapScript } from "../../eco/global-injector-map.js";
|
|
8
9
|
import { LocalsAccessError } from "../../errors/locals-access-error.js";
|
|
10
|
+
import { appLogger } from "../../global/app-logger.js";
|
|
9
11
|
import { inspectUnresolvedMarkerArtifactHtml } from "./render-output.utils.js";
|
|
10
12
|
import { OwnershipValidationService } from "./ownership-validation.service.js";
|
|
11
13
|
import { OwnershipPlanningService } from "./ownership-planning.service.js";
|
|
@@ -39,12 +41,19 @@ function createPageLocalsProxy(filePath) {
|
|
|
39
41
|
}
|
|
40
42
|
);
|
|
41
43
|
}
|
|
44
|
+
function isGroupedContentScriptAsset(asset) {
|
|
45
|
+
return asset.kind === "script" && asset.source === "content" && Boolean(asset.groupedBundle?.id);
|
|
46
|
+
}
|
|
47
|
+
function getGroupedBundleAssetKey(groupedBundle) {
|
|
48
|
+
return `${groupedBundle.id}:${groupedBundle.entryName}`;
|
|
49
|
+
}
|
|
42
50
|
class RouteRenderOrchestrator {
|
|
43
51
|
appConfig;
|
|
44
52
|
assetProcessingService;
|
|
45
53
|
ownershipPlanningService;
|
|
46
54
|
ownershipValidationService;
|
|
47
55
|
pageBrowserGraphCache = /* @__PURE__ */ new Map();
|
|
56
|
+
groupedPageBrowserGraphCache = /* @__PURE__ */ new Map();
|
|
48
57
|
constructor(appConfig, assetProcessingService, dependencies = {}) {
|
|
49
58
|
this.appConfig = appConfig;
|
|
50
59
|
this.assetProcessingService = assetProcessingService;
|
|
@@ -84,7 +93,7 @@ class RouteRenderOrchestrator {
|
|
|
84
93
|
const pageBrowserGraph = await this.resolvePageBrowserGraph({
|
|
85
94
|
routeFile: routeOptions.file,
|
|
86
95
|
integrationName: adapter.name,
|
|
87
|
-
collectContribution: async () => await adapter.collectPageBrowserGraphContribution(
|
|
96
|
+
collectContribution: async (routeFile) => await adapter.collectPageBrowserGraphContribution(routeFile)
|
|
88
97
|
});
|
|
89
98
|
const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(componentsToResolve, adapter.name);
|
|
90
99
|
const allDependencies = [...resolvedDependencies, ...usedIntegrationDependencies];
|
|
@@ -165,17 +174,143 @@ class RouteRenderOrchestrator {
|
|
|
165
174
|
return typeof this.assetProcessingService.getHmrManager === "function" && this.assetProcessingService.getHmrManager()?.isEnabled() === true;
|
|
166
175
|
}
|
|
167
176
|
async buildPageBrowserGraph(input) {
|
|
168
|
-
const contribution = await input.collectContribution();
|
|
177
|
+
const contribution = await input.collectContribution(input.routeFile);
|
|
169
178
|
if (!contribution) {
|
|
170
179
|
return void 0;
|
|
171
180
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
181
|
+
const groupedDependencies = (contribution.dependencies ?? []).filter((dep) => isGroupedContentScriptAsset(dep));
|
|
182
|
+
const ungroupedDependencies = (contribution.dependencies ?? []).filter(
|
|
183
|
+
(dep) => !isGroupedContentScriptAsset(dep)
|
|
184
|
+
);
|
|
185
|
+
const groupedAssets = groupedDependencies.length ? (await this.resolveGroupedPageBrowserAssets(input, contribution)).get(input.routeFile) ?? [] : [];
|
|
186
|
+
const processedDependencies = ungroupedDependencies.length ? await this.assetProcessingService.processDependencies(
|
|
187
|
+
ungroupedDependencies,
|
|
174
188
|
`${input.integrationName}:${input.routeFile}`
|
|
175
189
|
) : [];
|
|
176
|
-
const resolvedAssets = [...processedDependencies, ...contribution.assets ?? []];
|
|
190
|
+
const resolvedAssets = [...processedDependencies, ...groupedAssets, ...contribution.assets ?? []];
|
|
177
191
|
return this.partitionPageBrowserGraphAssets(resolvedAssets);
|
|
178
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolves grouped page-browser assets for all routes owned by one integration.
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* This keeps one shared browser-build result available across sibling routes so
|
|
198
|
+
* navigation can reuse the same emitted client graph instead of rebuilding one
|
|
199
|
+
* page entry at a time. HMR bypasses this cache because the grouped build must
|
|
200
|
+
* reflect the latest source on every request.
|
|
201
|
+
*/
|
|
202
|
+
async resolveGroupedPageBrowserAssets(input, currentContribution) {
|
|
203
|
+
if (this.isHmrEnabled()) {
|
|
204
|
+
const result = await this.buildGroupedPageBrowserAssets(input, currentContribution);
|
|
205
|
+
return result.assetsByRoute;
|
|
206
|
+
}
|
|
207
|
+
const cacheKey = input.integrationName;
|
|
208
|
+
const cached = this.groupedPageBrowserGraphCache.get(cacheKey);
|
|
209
|
+
if (cached) {
|
|
210
|
+
return await cached;
|
|
211
|
+
}
|
|
212
|
+
const pendingGroupedAssets = this.buildGroupedPageBrowserAssets(input, currentContribution).then((result) => {
|
|
213
|
+
if (result.hasCollectionFailures) {
|
|
214
|
+
this.groupedPageBrowserGraphCache.delete(cacheKey);
|
|
215
|
+
}
|
|
216
|
+
return result.assetsByRoute;
|
|
217
|
+
}).catch((error) => {
|
|
218
|
+
this.groupedPageBrowserGraphCache.delete(cacheKey);
|
|
219
|
+
throw error;
|
|
220
|
+
});
|
|
221
|
+
this.groupedPageBrowserGraphCache.set(cacheKey, pendingGroupedAssets);
|
|
222
|
+
return await pendingGroupedAssets;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Builds the shared grouped page-browser asset map for one integration.
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Each route can declare grouped content-script dependencies that should be
|
|
229
|
+
* emitted together. This method collects those declarations across the owning
|
|
230
|
+
* integration, runs the grouped processor once, and then maps the emitted
|
|
231
|
+
* assets back onto the routes that reference them.
|
|
232
|
+
*/
|
|
233
|
+
async buildGroupedPageBrowserAssets(input, currentContribution) {
|
|
234
|
+
const routeFiles = await this.listIntegrationRouteFiles(input.integrationName);
|
|
235
|
+
const currentRouteGroupedDependencies = (currentContribution.dependencies ?? []).filter(
|
|
236
|
+
(dep) => isGroupedContentScriptAsset(dep)
|
|
237
|
+
);
|
|
238
|
+
const groupedDependencies = [...currentRouteGroupedDependencies];
|
|
239
|
+
const groupedAssetKeysByRoute = /* @__PURE__ */ new Map();
|
|
240
|
+
let hasCollectionFailures = false;
|
|
241
|
+
if (currentRouteGroupedDependencies.length > 0) {
|
|
242
|
+
groupedAssetKeysByRoute.set(
|
|
243
|
+
input.routeFile,
|
|
244
|
+
new Set(currentRouteGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle)))
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
for (const routeFile of routeFiles) {
|
|
248
|
+
if (routeFile === input.routeFile) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let contribution;
|
|
252
|
+
try {
|
|
253
|
+
contribution = await input.collectContribution(routeFile);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
hasCollectionFailures = true;
|
|
256
|
+
appLogger.warn(
|
|
257
|
+
`Skipping grouped page-browser contribution for ${routeFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
258
|
+
);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!contribution?.dependencies?.length) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const routeGroupedDependencies = contribution.dependencies.filter(
|
|
265
|
+
(dep) => isGroupedContentScriptAsset(dep)
|
|
266
|
+
);
|
|
267
|
+
if (routeGroupedDependencies.length === 0) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
groupedDependencies.push(...routeGroupedDependencies);
|
|
271
|
+
groupedAssetKeysByRoute.set(
|
|
272
|
+
routeFile,
|
|
273
|
+
new Set(routeGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle)))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (groupedDependencies.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
assetsByRoute: /* @__PURE__ */ new Map(),
|
|
279
|
+
hasCollectionFailures
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const processedGroupedDependencies = await this.assetProcessingService.processDependencies(
|
|
283
|
+
groupedDependencies,
|
|
284
|
+
`${input.integrationName}:grouped-page-browser-graph`
|
|
285
|
+
);
|
|
286
|
+
const groupedAssetsByRoute = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const [routeFile, groupedAssetKeys] of groupedAssetKeysByRoute) {
|
|
288
|
+
groupedAssetsByRoute.set(
|
|
289
|
+
routeFile,
|
|
290
|
+
processedGroupedDependencies.filter((asset) => {
|
|
291
|
+
if (!asset.groupedBundle) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
return groupedAssetKeys.has(getGroupedBundleAssetKey(asset.groupedBundle));
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
assetsByRoute: groupedAssetsByRoute,
|
|
300
|
+
hasCollectionFailures
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async listIntegrationRouteFiles(integrationName) {
|
|
304
|
+
const integration = this.appConfig.integrations.find((plugin) => plugin.name === integrationName);
|
|
305
|
+
if (!integration) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const scannedFiles = await fileSystem.glob(
|
|
309
|
+
integration.extensions.map((extension) => `**/*${extension}`),
|
|
310
|
+
{ cwd: this.appConfig.absolutePaths.pagesDir }
|
|
311
|
+
);
|
|
312
|
+
return scannedFiles.filter((file) => !file.includes(".ecopages-node.")).map((file) => path.join(this.appConfig.absolutePaths.pagesDir, file)).sort((left, right) => left.localeCompare(right));
|
|
313
|
+
}
|
|
179
314
|
partitionPageBrowserGraphAssets(assets) {
|
|
180
315
|
const entryAssets = [];
|
|
181
316
|
const chunkAssets = [];
|
package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js
CHANGED
|
@@ -75,6 +75,7 @@ class ContentScriptProcessor extends BaseScriptProcessor {
|
|
|
75
75
|
inline: dep.inline,
|
|
76
76
|
excludeFromHtml: dep.excludeFromHtml,
|
|
77
77
|
packageRole: dep.packageRole,
|
|
78
|
+
groupedBundle: dep.groupedBundle,
|
|
78
79
|
bundledSourceFilepaths: dep.bundledSourceFilepaths
|
|
79
80
|
};
|
|
80
81
|
this.writeCacheFile(filename, unbundledProcessedAsset);
|
|
@@ -71,6 +71,18 @@ function resolvePackageExportTarget(packageDir, target) {
|
|
|
71
71
|
const record = target;
|
|
72
72
|
return resolvePackageExportTarget(packageDir, record.import) ?? resolvePackageExportTarget(packageDir, record.default) ?? resolvePackageExportTarget(packageDir, record.require);
|
|
73
73
|
}
|
|
74
|
+
function shouldRewriteProjectImportMeta(filePath, projectDir) {
|
|
75
|
+
const normalizedFilePath = path.normalize(filePath);
|
|
76
|
+
const normalizedProjectDir = path.normalize(projectDir);
|
|
77
|
+
return (normalizedFilePath === normalizedProjectDir || normalizedFilePath.startsWith(`${normalizedProjectDir}${path.sep}`)) && !normalizedFilePath.includes(`${path.sep}node_modules${path.sep}`);
|
|
78
|
+
}
|
|
79
|
+
function getLoaderForPath(filePath) {
|
|
80
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
81
|
+
if (extension === ".tsx") return "tsx";
|
|
82
|
+
if (extension === ".ts") return "ts";
|
|
83
|
+
if (extension === ".jsx") return "jsx";
|
|
84
|
+
return "js";
|
|
85
|
+
}
|
|
74
86
|
function resolveInstalledPackageTarget(specifier, parentPath) {
|
|
75
87
|
const packageName = getPackageNameFromSpecifier(specifier);
|
|
76
88
|
const packageDir = findInstalledPackageDir(packageName, parentPath);
|
|
@@ -168,6 +180,20 @@ function createNodeBootstrapPlugin(options) {
|
|
|
168
180
|
build.onResolve({ filter: /^bun:/ }, (args) => {
|
|
169
181
|
throw new Error(getNodeUnsupportedBuiltinError(args.path, args.importer));
|
|
170
182
|
});
|
|
183
|
+
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
184
|
+
if (!shouldRewriteProjectImportMeta(args.path, options.projectDir)) {
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
const originalContents = readFileSync(args.path, "utf8");
|
|
188
|
+
const contents = originalContents.replaceAll("import.meta.env", "process.env").replaceAll("import.meta.dirname", JSON.stringify(path.dirname(args.path))).replaceAll("import.meta.filename", JSON.stringify(args.path)).replaceAll("import.meta.dir", JSON.stringify(path.dirname(args.path))).replaceAll("import.meta.path", JSON.stringify(args.path));
|
|
189
|
+
if (contents === originalContents) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
contents,
|
|
194
|
+
loader: getLoaderForPath(args.path)
|
|
195
|
+
};
|
|
196
|
+
});
|
|
171
197
|
build.onResolve({ filter: /^[@A-Za-z0-9][^:]*$/ }, (args) => {
|
|
172
198
|
return resolveNodeBootstrapDependency(args, options);
|
|
173
199
|
});
|