@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.48",
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.48",
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(filePath);
189
- return await this.collectPageBrowserGraphContribution({ file: filePath, pageModule });
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.setPagePackage(createPagePackage(mergedDependencies));
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(routeOptions.file)
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 processedDependencies = contribution.dependencies?.length ? await this.assetProcessingService.processDependencies(
173
- contribution.dependencies,
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 = [];
@@ -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
  });