evolit 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,7 +60,16 @@ Evolit resolves package subpaths with ESM `import` conditions, emits exported CS
60
60
  route static-asset pipeline, and adds the resulting stylesheet URLs to the rendered document.
61
61
  Relative `@import` rules and `url(...)` references inside package CSS are emitted and rewritten
62
62
  from their location within `node_modules`. Bare package imports that resolve to JavaScript continue
63
- to use the shared vendor runtime; CSS and other static assets do not enter vendor chunks.
63
+ to use the shared vendor runtime; hydration metadata uses that same canonical package URL so an
64
+ external component is evaluated only once. CSS and other static assets do not enter vendor chunks.
65
+
66
+ Packages declared by the application remain package dependencies even when a workspace manager
67
+ links them to sources elsewhere in a monorepo. Evolit uses the declared package name and its
68
+ browser `exports` subpath as the public identity; the physical symlink or real filesystem path is
69
+ used only to verify package ownership and is never exposed as an `__unmanaged__` client module.
70
+ During `evolit build`, a package reached by the production SSR graph that is declared only in
71
+ `devDependencies` produces a warning. Its package identity is preserved, but applications should
72
+ move it to `dependencies` or keep development dependencies installed in the production runtime.
64
73
 
65
74
  ## Commands
66
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
package/src/build.js CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  import {
11
11
  collectTransitiveAssetPreloads,
12
12
  collectTransitiveStyleUrls,
13
+ buildSharedVendorRuntime,
14
+ canonicalizePackageModuleId,
13
15
  createAssetResolver,
14
16
  createHydrationBootstrap,
15
17
  createStaticAssetPublicUrlMap,
@@ -70,6 +72,31 @@ const CONTENT_TYPE_BY_EXTENSION = new Map([
70
72
  [".js", "text/javascript; charset=utf-8"],
71
73
  ]);
72
74
 
75
+ function getBarePackageName(specifier) {
76
+ if (typeof specifier !== "string" || specifier.length === 0) return null;
77
+ const segments = specifier.split("/");
78
+ return specifier.startsWith("@")
79
+ ? segments.length >= 2 ? `${segments[0]}/${segments[1]}` : null
80
+ : segments[0];
81
+ }
82
+
83
+ export function collectSsrDevDependencyWarnings(packageJson, packageSpecifiers) {
84
+ const productionDependencies = new Set([
85
+ ...Object.keys(packageJson?.dependencies ?? {}),
86
+ ...Object.keys(packageJson?.optionalDependencies ?? {}),
87
+ ...Object.keys(packageJson?.peerDependencies ?? {}),
88
+ ]);
89
+ const devDependencies = new Set(Object.keys(packageJson?.devDependencies ?? {}));
90
+ return [...new Set(packageSpecifiers
91
+ .map(getBarePackageName)
92
+ .filter((packageName) => (
93
+ packageName
94
+ && devDependencies.has(packageName)
95
+ && !productionDependencies.has(packageName)
96
+ )))]
97
+ .sort();
98
+ }
99
+
73
100
  function getContentTypeForBuildArtifact(filePath) {
74
101
  return CONTENT_TYPE_BY_EXTENSION.get(path.extname(filePath)) ?? "application/octet-stream";
75
102
  }
@@ -128,12 +155,15 @@ async function writeDeploymentRuntimeEntry(buildRoot) {
128
155
  return runtimeEntryPath;
129
156
  }
130
157
 
131
- export async function buildProject(projectRoot) {
158
+ export async function buildProject(projectRoot, options = {}) {
132
159
  const evolitConfig = await loadEvolitConfig(projectRoot);
133
160
  const extensions = resolveEvolitExtensions(evolitConfig);
134
161
  const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
162
+ const packageClientSpecifiers = new Set(
163
+ extensionClientDescriptors.map((descriptor) => descriptor.module),
164
+ );
135
165
  const sharedVendorOptions = {
136
- additionalEntrySpecifiers: extensionClientDescriptors.map((descriptor) => descriptor.module),
166
+ additionalEntrySpecifiers: [],
137
167
  };
138
168
  const routes = await discoverAppRoutes(projectRoot);
139
169
  const routeHandlers = await discoverAppRouteHandlers(projectRoot);
@@ -144,6 +174,19 @@ export async function buildProject(projectRoot) {
144
174
  const deployHandlers = [];
145
175
  const compiledClientBoundaries = new Map();
146
176
  const inventoriesBySourceEntry = new Map();
177
+ const ssrPackageImports = new Set();
178
+
179
+ async function compileProductionServerEntry(sourcePath) {
180
+ const result = await compileModuleGraph(sourcePath, {
181
+ projectRoot,
182
+ mode: "production",
183
+ sourceMaps: false,
184
+ ssr: true,
185
+ target: "server",
186
+ });
187
+ result.packageImports.forEach((specifier) => ssrPackageImports.add(specifier));
188
+ return result;
189
+ }
147
190
 
148
191
  function getEntryInventory(entryPath) {
149
192
  let inventory = inventoriesBySourceEntry.get(entryPath);
@@ -174,13 +217,7 @@ export async function buildProject(projectRoot) {
174
217
  await ensureDirectory(buildRoot);
175
218
 
176
219
  for (const routeHandler of routeHandlers) {
177
- await compileModuleGraph(routeHandler.handler, {
178
- projectRoot,
179
- mode: "production",
180
- sourceMaps: false,
181
- ssr: true,
182
- target: "server",
183
- });
220
+ await compileProductionServerEntry(routeHandler.handler);
184
221
 
185
222
  const handlerModule = await importCompiledModule(routeHandler.handler, {
186
223
  projectRoot,
@@ -204,22 +241,10 @@ export async function buildProject(projectRoot) {
204
241
  }
205
242
 
206
243
  for (const route of routes) {
207
- await compileModuleGraph(route.page, {
208
- projectRoot,
209
- mode: "production",
210
- sourceMaps: false,
211
- ssr: true,
212
- target: "server",
213
- });
244
+ await compileProductionServerEntry(route.page);
214
245
 
215
246
  for (const layoutPath of route.layouts) {
216
- await compileModuleGraph(layoutPath, {
217
- projectRoot,
218
- mode: "production",
219
- sourceMaps: false,
220
- ssr: true,
221
- target: "server",
222
- });
247
+ await compileProductionServerEntry(layoutPath);
223
248
 
224
249
  }
225
250
 
@@ -228,13 +253,7 @@ export async function buildProject(projectRoot) {
228
253
  ...(route.errorBoundaries ?? []).map((boundary) => boundary.module),
229
254
  ];
230
255
  for (const boundaryPath of new Set(boundaryModules)) {
231
- await compileModuleGraph(boundaryPath, {
232
- projectRoot,
233
- mode: "production",
234
- sourceMaps: false,
235
- ssr: true,
236
- target: "server",
237
- });
256
+ await compileProductionServerEntry(boundaryPath);
238
257
  }
239
258
 
240
259
  const toProjectRelative = (filePath) => path.relative(projectRoot, filePath).split(path.sep).join("/");
@@ -247,13 +266,21 @@ export async function buildProject(projectRoot) {
247
266
  const allAssets = new Set();
248
267
  const allClientBoundaries = new Set();
249
268
  for (const [entryPath, entryInventory] of inventoriesByEntry) {
269
+ const packageBoundarySources = new Set(
270
+ (entryInventory.packageClientBoundaries ?? []).map((entry) => entry.sourcePath),
271
+ );
272
+ for (const packageBoundary of entryInventory.packageClientBoundaries ?? []) {
273
+ packageClientSpecifiers.add(packageBoundary.specifier);
274
+ }
250
275
  serverAssetImportsByEntry[toProjectRelative(entryPath)] = {
251
276
  styles: entryInventory.styles.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
252
277
  assets: entryInventory.assets.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
253
278
  };
254
279
  entryInventory.styles.forEach((filePath) => allStyles.add(filePath));
255
280
  entryInventory.assets.forEach((filePath) => allAssets.add(filePath));
256
- entryInventory.clientBoundaries.forEach((filePath) => allClientBoundaries.add(filePath));
281
+ entryInventory.clientBoundaries
282
+ .filter((filePath) => !packageBoundarySources.has(filePath))
283
+ .forEach((filePath) => allClientBoundaries.add(filePath));
257
284
  }
258
285
  await emitClientStaticAssets([...allStyles, ...allAssets], {
259
286
  projectRoot,
@@ -268,12 +295,23 @@ export async function buildProject(projectRoot) {
268
295
  }
269
296
  for (const [entryPath, entryInventory] of inventoriesByEntry) {
270
297
  clientBoundariesByEntry[toProjectRelative(entryPath)] = entryInventory.clientBoundaries
298
+ .filter((clientBoundary) => !(entryInventory.packageClientBoundaries ?? [])
299
+ .some((packageBoundary) => packageBoundary.sourcePath === clientBoundary))
271
300
  .map((clientBoundary) => compiledBoundaryModules.get(clientBoundary))
272
301
  .filter(Boolean)
273
302
  .sort();
274
303
  }
275
304
  }
276
305
 
306
+ const projectPackageJson = JSON.parse(await fs.readFile(path.join(projectRoot, "package.json"), "utf8"));
307
+ const warn = options.onWarning ?? console.warn;
308
+ for (const packageName of collectSsrDevDependencyWarnings(projectPackageJson, [...ssrPackageImports])) {
309
+ warn(
310
+ `[evolit] Production SSR imports ${JSON.stringify(packageName)}, but it is declared only in devDependencies. `
311
+ + "Move it to dependencies or ensure production installations include development dependencies.",
312
+ );
313
+ }
314
+
277
315
  const configuredClientBoundaries = evolitConfig.clientBoundaries ?? [];
278
316
  if (!Array.isArray(configuredClientBoundaries)) {
279
317
  throw new Error("Expected clientBoundaries in evolit.config.js to be an array of module specifiers.");
@@ -295,8 +333,10 @@ export async function buildProject(projectRoot) {
295
333
  await compileProductionClientBoundary(sourcePath);
296
334
  }
297
335
 
336
+ sharedVendorOptions.additionalEntrySpecifiers = [...packageClientSpecifiers].sort();
298
337
  const clientAssets = await emitBundledClientAssets(projectRoot, {
299
338
  entryClientModules,
339
+ additionalVendorSpecifiers: sharedVendorOptions.additionalEntrySpecifiers,
300
340
  serverAssetImportsByEntry,
301
341
  clientBoundariesByEntry,
302
342
  });
@@ -306,8 +346,12 @@ export async function buildProject(projectRoot) {
306
346
  const routeResolver = await createRouteResolver(projectRoot, "production", {
307
347
  staticAssetPublicUrls,
308
348
  });
349
+ const sharedRuntime = await buildSharedVendorRuntime(projectRoot, "production", sharedVendorOptions);
350
+ clientAssets.sharedImports = { ...sharedRuntime.imports };
351
+ const packageImports = sharedRuntime.imports;
309
352
  const assetResolver = createAssetResolver(projectRoot, {
310
353
  assetManifest: clientAssets,
354
+ packageImports,
311
355
  });
312
356
  const hydrationModuleUrl = await resolveSharedVendorModuleUrl(
313
357
  projectRoot,
@@ -341,6 +385,33 @@ export async function buildProject(projectRoot) {
341
385
  ].filter((moduleId) => typeof moduleId === "string" && moduleId.length > 0))];
342
386
  const unresolved = [];
343
387
  for (const moduleId of renderedModules) {
388
+ const packageSpecifier = await canonicalizePackageModuleId(
389
+ projectRoot,
390
+ moduleId,
391
+ packageClientSpecifiers,
392
+ );
393
+ if (packageSpecifier) {
394
+ const publicUrl = packageImports[packageSpecifier] ?? null;
395
+ if (!publicUrl) {
396
+ unresolved.push(moduleId);
397
+ continue;
398
+ }
399
+ const previousPublicUrl = assetResolver(moduleId);
400
+ for (const root of result.hydrationData?.roots ?? []) {
401
+ if (root?.moduleId === moduleId) root.moduleId = packageSpecifier;
402
+ }
403
+ if (Array.isArray(result.clientImports)) {
404
+ result.clientImports = result.clientImports.map((value) => (
405
+ value === moduleId || value === previousPublicUrl ? publicUrl : value
406
+ ));
407
+ }
408
+ if (Array.isArray(result.hydrationData?.clientImports)) {
409
+ result.hydrationData.clientImports = result.hydrationData.clientImports.map((value) => (
410
+ value === moduleId || value === previousPublicUrl ? publicUrl : value
411
+ ));
412
+ }
413
+ continue;
414
+ }
344
415
  if (assetResolver(moduleId) || clientAssets.byPublicPath?.[moduleId]) continue;
345
416
  const importerPath = routeResult.boundaryModule
346
417
  ?? routeResult.route?.page
@@ -406,6 +477,7 @@ export async function buildProject(projectRoot) {
406
477
  result.hydrationData,
407
478
  projectRoot,
408
479
  resolveHydrationRootClientImports(result.hydrationData, assetResolver),
480
+ assetResolver,
409
481
  ),
410
482
  assetResolver,
411
483
  hydrationModuleUrl,
@@ -420,6 +492,7 @@ export async function buildProject(projectRoot) {
420
492
  result.hydrationData,
421
493
  projectRoot,
422
494
  resolveHydrationRootClientImports(result.hydrationData, assetResolver),
495
+ assetResolver,
423
496
  ),
424
497
  );
425
498
  },
@@ -172,32 +172,56 @@ export async function resolvePackageRoot(packageName, options = {}) {
172
172
  resolutionError = error;
173
173
  }
174
174
  }
175
- if (!packageEntryPath) {
176
- throw resolutionError ?? new Error(`Unable to resolve package entry for ${packageName}`);
177
- }
178
- let currentPath = path.dirname(packageEntryPath);
175
+ if (packageEntryPath) {
176
+ let currentPath = path.dirname(packageEntryPath);
179
177
 
180
- while (true) {
181
- const packageJsonPath = path.join(currentPath, "package.json");
178
+ while (true) {
179
+ const packageJsonPath = path.join(currentPath, "package.json");
182
180
 
183
- try {
184
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
185
- if (packageJson?.name === packageName) {
186
- return { packageRoot: currentPath, packageJson };
181
+ try {
182
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
183
+ if (packageJson?.name === packageName) {
184
+ return { packageRoot: currentPath, packageJson };
185
+ }
186
+ } catch {
187
+ // Keep walking up until we find the owning package root.
187
188
  }
188
- } catch {
189
- // Keep walking up until we find the owning package root.
190
- }
191
189
 
192
- const parentPath = path.dirname(currentPath);
193
- if (parentPath === currentPath) {
194
- break;
190
+ const parentPath = path.dirname(currentPath);
191
+ if (parentPath === currentPath) {
192
+ break;
193
+ }
194
+
195
+ currentPath = parentPath;
195
196
  }
197
+ }
196
198
 
197
- currentPath = parentPath;
199
+ const packageSegments = packageName.split("/");
200
+ const validPackageSegments = packageSegments.length === 1
201
+ || (packageSegments.length === 2 && packageSegments[0].startsWith("@"));
202
+ if (validPackageSegments && packageSegments.every((segment) => segment && segment !== "." && segment !== "..")) {
203
+ let currentPath = resolveFrom;
204
+ while (true) {
205
+ const packageJsonPath = path.join(currentPath, "node_modules", ...packageSegments, "package.json");
206
+ try {
207
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
208
+ if (packageJson?.name === packageName) {
209
+ return {
210
+ packageRoot: await fs.realpath(path.dirname(packageJsonPath)),
211
+ packageJson,
212
+ };
213
+ }
214
+ } catch {
215
+ // Continue through the normal node_modules ancestor lookup.
216
+ }
217
+
218
+ const parentPath = path.dirname(currentPath);
219
+ if (parentPath === currentPath) break;
220
+ currentPath = parentPath;
221
+ }
198
222
  }
199
223
 
200
- throw new Error(`Unable to resolve package root for ${packageName}`);
224
+ throw resolutionError ?? new Error(`Unable to resolve package root for ${packageName}`);
201
225
  })();
202
226
 
203
227
  packageRootCache.set(cacheKey, pendingResolution);
@@ -322,6 +346,79 @@ export async function resolveBrowserSpecifierFilePath(specifier, options = {}) {
322
346
  }
323
347
  }
324
348
 
349
+ export async function resolveDeclaredPackageNames(projectRoot) {
350
+ try {
351
+ const packageJson = JSON.parse(await fs.readFile(path.join(projectRoot, "package.json"), "utf8"));
352
+ return new Set([
353
+ ...Object.keys(packageJson.dependencies ?? {}),
354
+ ...Object.keys(packageJson.optionalDependencies ?? {}),
355
+ ...Object.keys(packageJson.peerDependencies ?? {}),
356
+ ...Object.keys(packageJson.devDependencies ?? {}),
357
+ ]);
358
+ } catch (error) {
359
+ if (error?.code === "ENOENT") return new Set();
360
+ throw error;
361
+ }
362
+ }
363
+
364
+ export async function canonicalizePackageModuleId(projectRoot, moduleId, packageSpecifiers = []) {
365
+ const declaredPackageNames = await resolveDeclaredPackageNames(projectRoot);
366
+ const specifiers = [...new Set(packageSpecifiers)]
367
+ .filter((specifier) => {
368
+ const parsed = parsePackageSpecifier(specifier);
369
+ return parsed && declaredPackageNames.has(parsed.packageName);
370
+ });
371
+ if (specifiers.includes(moduleId)) {
372
+ return moduleId;
373
+ }
374
+
375
+ let sourcePath = null;
376
+ if (typeof moduleId === "string" && moduleId.startsWith("file:")) {
377
+ try { sourcePath = fileURLToPath(moduleId); } catch { return null; }
378
+ } else if (typeof moduleId === "string" && path.isAbsolute(moduleId)) {
379
+ sourcePath = moduleId;
380
+ }
381
+ if (!sourcePath || sourcePath.startsWith(`${path.sep}_evolit${path.sep}`)) {
382
+ return null;
383
+ }
384
+
385
+ let realSourcePath;
386
+ try {
387
+ realSourcePath = await fs.realpath(sourcePath);
388
+ } catch {
389
+ realSourcePath = path.resolve(sourcePath);
390
+ }
391
+
392
+ const exactMatches = [];
393
+ const packageMatches = [];
394
+ for (const specifier of specifiers) {
395
+ const parsed = parsePackageSpecifier(specifier);
396
+ try {
397
+ const [{ packageRoot }, entryPath] = await Promise.all([
398
+ resolvePackageRoot(parsed.packageName, { projectRoot }),
399
+ resolveBrowserSpecifierFilePath(specifier, { projectRoot }),
400
+ ]);
401
+ const [realPackageRoot, realEntryPath] = await Promise.all([
402
+ fs.realpath(packageRoot),
403
+ fs.realpath(entryPath),
404
+ ]);
405
+ const relativePath = path.relative(realPackageRoot, realSourcePath);
406
+ const belongsToPackage = relativePath === ""
407
+ || (relativePath !== ".."
408
+ && !relativePath.startsWith(`..${path.sep}`)
409
+ && !path.isAbsolute(relativePath));
410
+ if (!belongsToPackage) continue;
411
+ packageMatches.push(specifier);
412
+ if (realEntryPath === realSourcePath) exactMatches.push(specifier);
413
+ } catch {
414
+ // An unrelated or unavailable optional package cannot own this module.
415
+ }
416
+ }
417
+
418
+ if (exactMatches.length === 1) return exactMatches[0];
419
+ return packageMatches.length === 1 ? packageMatches[0] : null;
420
+ }
421
+
325
422
  export function getSharedOutputRoot(projectRoot, mode) {
326
423
  return path.join(
327
424
  projectRoot,
@@ -1140,6 +1237,10 @@ function toPublicHydrationModuleId(projectRoot, moduleId) {
1140
1237
  return `/${relativePath}`;
1141
1238
  }
1142
1239
 
1240
+ if (isBareSpecifier(moduleId)) {
1241
+ return moduleId;
1242
+ }
1243
+
1143
1244
  if (moduleId.startsWith("/")) {
1144
1245
  return moduleId;
1145
1246
  }
@@ -1171,12 +1272,19 @@ function getServerOutputRoot(projectRoot) {
1171
1272
 
1172
1273
  export function createAssetResolver(projectRoot, options = {}) {
1173
1274
  const assetManifest = normalizeClientAssetManifest(options.assetManifest);
1275
+ const packageImports = options.packageImports instanceof Map
1276
+ ? options.packageImports
1277
+ : new Map(Object.entries(options.packageImports ?? {}));
1174
1278
 
1175
1279
  return function assetResolver(moduleId) {
1176
1280
  if (typeof moduleId !== "string" || moduleId.length === 0) {
1177
1281
  return null;
1178
1282
  }
1179
1283
 
1284
+ if (packageImports.has(moduleId)) {
1285
+ return packageImports.get(moduleId);
1286
+ }
1287
+
1180
1288
  let relativeClientModule = null;
1181
1289
  if (path.isAbsolute(moduleId) && !isVirtualClientModuleId(moduleId)) {
1182
1290
  relativeClientModule = toClientModuleRelativePath(projectRoot, moduleId);
@@ -1278,6 +1386,7 @@ export function normalizeHydrationDataForClient(
1278
1386
  hydrationData,
1279
1387
  projectRoot = null,
1280
1388
  additionalClientImports = [],
1389
+ assetResolver = null,
1281
1390
  ) {
1282
1391
  if (!hydrationData || typeof hydrationData !== "object") {
1283
1392
  return hydrationData ?? null;
@@ -1314,7 +1423,9 @@ export function normalizeHydrationDataForClient(
1314
1423
  const clientImports = [...new Set([
1315
1424
  ...(Array.isArray(hydrationData.clientImports) ? hydrationData.clientImports : []),
1316
1425
  ...(Array.isArray(additionalClientImports) ? additionalClientImports : []),
1317
- ].filter((value) => typeof value === "string" && value.length > 0))];
1426
+ ]
1427
+ .map((value) => typeof assetResolver === "function" ? assetResolver(value) ?? value : value)
1428
+ .filter((value) => typeof value === "string" && value.length > 0))];
1318
1429
 
1319
1430
  Object.defineProperties(normalizedHydrationData, {
1320
1431
  payload: {
@@ -1844,10 +1955,15 @@ async function bundleClientAssets(projectRoot, options = {}) {
1844
1955
  const scriptAssetRecords = [];
1845
1956
  let nextRollupCache = options.rollupCache ?? null;
1846
1957
  if (Object.keys(inputEntries).length > 0) {
1847
- const sharedVendorSpecifiers = await collectClientVendorSpecifiers(projectRoot, [], {
1848
- mode,
1849
- entryClientModules: [...entryClientModules],
1850
- });
1958
+ const sharedVendorSpecifiers = [...new Set([
1959
+ ...await collectClientVendorSpecifiers(projectRoot, [], {
1960
+ mode,
1961
+ entryClientModules: [...entryClientModules],
1962
+ }),
1963
+ ...(Array.isArray(options.additionalVendorSpecifiers)
1964
+ ? options.additionalVendorSpecifiers.filter((specifier) => isBareSpecifier(specifier))
1965
+ : []),
1966
+ ])].sort();
1851
1967
  const sharedRuntime = await buildSharedVendorRuntime(projectRoot, mode, {
1852
1968
  additionalEntrySpecifiers: sharedVendorSpecifiers,
1853
1969
  });
@@ -2285,6 +2401,9 @@ export function normalizeClientAssetManifest(manifest) {
2285
2401
  chunks: Array.isArray(manifest.chunks) ? manifest.chunks : [],
2286
2402
  styles: Array.isArray(manifest.styles) ? manifest.styles : [],
2287
2403
  resources: Array.isArray(manifest.resources) ? manifest.resources : [],
2404
+ sharedImports: manifest.sharedImports && typeof manifest.sharedImports === "object"
2405
+ ? manifest.sharedImports
2406
+ : {},
2288
2407
  serverAssetImportsByEntry:
2289
2408
  manifest.serverAssetImportsByEntry && typeof manifest.serverAssetImportsByEntry === "object"
2290
2409
  ? manifest.serverAssetImportsByEntry
package/src/compiler.js CHANGED
@@ -840,6 +840,7 @@ async function rewriteRelativeSpecifiers({
840
840
  staticAssetFiles,
841
841
  managedSourceRoots,
842
842
  serverExportsByModule,
843
+ packageImports,
843
844
  }) {
844
845
  const magicSource = new MagicString(code);
845
846
  let didRewrite = false;
@@ -863,6 +864,15 @@ async function rewriteRelativeSpecifiers({
863
864
  ? packageImportPath
864
865
  : null;
865
866
 
867
+ if (
868
+ target === "server"
869
+ && isBareSpecifier(specifier)
870
+ && !aliasedImportPath
871
+ && !packageAssetPath
872
+ ) {
873
+ packageImports?.add(specifier);
874
+ }
875
+
866
876
  if (
867
877
  target === "client"
868
878
  && isBareSpecifier(specifier)
@@ -1152,6 +1162,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
1152
1162
  const staticAssetFiles = new Set();
1153
1163
  const moduleMetadata = new Map();
1154
1164
  const serverExportsByModule = new Map();
1165
+ const packageImports = new Set();
1155
1166
  const serverImportQuery = target === "server" && mode === "development"
1156
1167
  ? `t=${Date.now()}`
1157
1168
  : null;
@@ -1221,6 +1232,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
1221
1232
  staticAssetFiles,
1222
1233
  managedSourceRoots,
1223
1234
  serverExportsByModule,
1235
+ packageImports,
1224
1236
  });
1225
1237
 
1226
1238
  await fs.writeFile(outputPath, rewritten.code, "utf8");
@@ -1250,6 +1262,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
1250
1262
  entrypoint: await compileModule(entryPath),
1251
1263
  outputRoot,
1252
1264
  sourceFiles: [...new Set([...visited.keys(), ...staticAssetFiles])],
1265
+ packageImports: [...packageImports].sort(),
1253
1266
  };
1254
1267
  }
1255
1268
 
@@ -1314,6 +1327,7 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
1314
1327
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1315
1328
  const visited = new Set();
1316
1329
  const boundaries = new Set();
1330
+ const packageBoundaries = new Map();
1317
1331
  const styles = new Set();
1318
1332
  const assets = new Set();
1319
1333
 
@@ -1422,7 +1436,10 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
1422
1436
  if (!resolved) continue;
1423
1437
  if (shouldCompileModule(resolved)) {
1424
1438
  if (isBareSpecifier(specifier) && aliasResolved == null) {
1425
- if (componentImportSpecifiers.has(specifier)) boundaries.add(resolved);
1439
+ if (componentImportSpecifiers.has(specifier)) {
1440
+ boundaries.add(resolved);
1441
+ packageBoundaries.set(specifier, resolved);
1442
+ }
1426
1443
  } else {
1427
1444
  await visit(resolved, true);
1428
1445
  }
@@ -1438,6 +1455,9 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
1438
1455
  for (const entryPath of entryPaths) await visit(entryPath);
1439
1456
  return {
1440
1457
  clientBoundaries: [...boundaries].sort(),
1458
+ packageClientBoundaries: [...packageBoundaries]
1459
+ .map(([specifier, sourcePath]) => ({ specifier, sourcePath }))
1460
+ .sort((left, right) => left.specifier.localeCompare(right.specifier)),
1441
1461
  styles: [...styles].sort(),
1442
1462
  assets: [...assets].sort(),
1443
1463
  sourceFiles: [...visited].sort(),
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  collectTransitiveAssetPreloads,
15
15
  collectTransitiveStyleUrls,
16
+ canonicalizePackageModuleId,
16
17
  createAssetResolver,
17
18
  createHydrationBootstrap,
18
19
  createStaticAssetPublicUrlMap,
@@ -46,6 +47,20 @@ import {
46
47
  resolveEvolitExtensions,
47
48
  runRequestExtensions,
48
49
  } from "./extensions.js";
50
+
51
+ function isBarePackageModuleId(value) {
52
+ return typeof value === "string"
53
+ && value.length > 0
54
+ && !value.startsWith(".")
55
+ && !value.startsWith("/")
56
+ && !value.startsWith("@/")
57
+ && !value.startsWith("#")
58
+ && !value.startsWith("file:")
59
+ && !value.startsWith("node:")
60
+ && !value.startsWith("data:")
61
+ && !value.startsWith("http:")
62
+ && !value.startsWith("https:");
63
+ }
49
64
  const CONTENT_TYPE_BY_EXTENSION = new Map([
50
65
  [".css", "text/css; charset=utf-8"],
51
66
  [".svg", "image/svg+xml"],
@@ -240,27 +255,32 @@ export async function createRequestRenderer({
240
255
  extensions = [],
241
256
  }) {
242
257
  let currentAssetManifest = normalizeClientAssetManifest(assetManifest);
258
+ const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
259
+ const extensionClientSpecifiers = extensionClientDescriptors.map((descriptor) => descriptor.module);
260
+ const packageClientSpecifiers = new Set(extensionClientSpecifiers);
261
+ const persistedSharedImports = currentAssetManifest?.sharedImports ?? {};
262
+ const packageImports = new Map(Object.entries(persistedSharedImports));
263
+ const packageImportUrls = new Set(packageImports.values());
243
264
  let currentAssetResolver = createAssetResolver(projectRoot, {
244
265
  assetManifest: currentAssetManifest,
266
+ packageImports,
245
267
  });
246
- const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
247
- const extensionClientSpecifiers = extensionClientDescriptors.map((descriptor) => descriptor.module);
248
- const sharedVendorOptions = { additionalEntrySpecifiers: extensionClientSpecifiers };
249
- let currentHydrationModuleUrl = await resolveSharedVendorModuleUrl(
250
- projectRoot,
251
- mode,
268
+ const sharedVendorOptions = { additionalEntrySpecifiers: [...packageClientSpecifiers] };
269
+ const resolveSharedModuleUrl = (specifier, options = sharedVendorOptions) => (
270
+ persistedSharedImports[specifier]
271
+ ?? (mode === "development"
272
+ ? resolveSharedVendorModuleUrl(projectRoot, mode, specifier, options)
273
+ : null)
274
+ );
275
+ let currentHydrationModuleUrl = await resolveSharedModuleUrl(
252
276
  "@litsx/ssr/hydration",
253
- sharedVendorOptions,
254
277
  );
255
- let currentNavigationModuleUrl = await resolveSharedVendorModuleUrl(
256
- projectRoot,
257
- mode,
278
+ let currentNavigationModuleUrl = await resolveSharedModuleUrl(
258
279
  "evolit/navigation",
259
- sharedVendorOptions,
260
280
  );
261
281
  let currentNavigationExtensions = await Promise.all(extensionClientDescriptors.map(async (descriptor) => ({
262
282
  ...descriptor,
263
- module: await resolveSharedVendorModuleUrl(projectRoot, mode, descriptor.module, sharedVendorOptions),
283
+ module: await resolveSharedModuleUrl(descriptor.module),
264
284
  })));
265
285
  const devBundledEntries = new Set();
266
286
  const devPreparedClientModules = new Set();
@@ -359,11 +379,13 @@ export async function createRequestRenderer({
359
379
  retainOutputPaths: devPreviousAssetOutputPaths,
360
380
  serverAssetImportsByEntry: devServerAssetImportsByEntry,
361
381
  clientBoundariesByEntry: devClientBoundariesByEntry,
382
+ additionalVendorSpecifiers: [...packageClientSpecifiers],
362
383
  });
363
384
  currentAssetManifest = bundledClientAssets.manifest;
364
385
  devPreviousAssetOutputPaths = new Set();
365
386
  currentAssetResolver = createAssetResolver(projectRoot, {
366
387
  assetManifest: currentAssetManifest,
388
+ packageImports,
367
389
  });
368
390
  currentHydrationModuleUrl = await resolveSharedVendorModuleUrl(
369
391
  projectRoot,
@@ -405,6 +427,7 @@ export async function createRequestRenderer({
405
427
  return (
406
428
  typeof currentAssetResolver(moduleId) === "string"
407
429
  || typeof currentAssetManifest?.byPublicPath?.[moduleId] === "string"
430
+ || packageImportUrls.has(moduleId)
408
431
  );
409
432
  }
410
433
 
@@ -447,6 +470,46 @@ export async function createRequestRenderer({
447
470
  ].filter((moduleId) => typeof moduleId === "string" && moduleId.length > 0))];
448
471
  const unresolvedModules = [];
449
472
  for (const moduleId of renderedModules) {
473
+ const packageSpecifier = isBarePackageModuleId(moduleId)
474
+ ? moduleId
475
+ : await canonicalizePackageModuleId(projectRoot, moduleId, packageClientSpecifiers);
476
+ if (packageSpecifier) {
477
+ packageClientSpecifiers.add(packageSpecifier);
478
+ sharedVendorOptions.additionalEntrySpecifiers = [...packageClientSpecifiers].sort();
479
+ const publicUrl = packageImports.get(packageSpecifier) ?? await resolveSharedModuleUrl(
480
+ packageSpecifier,
481
+ {
482
+ assetManifest: currentAssetManifest,
483
+ entryClientModules: [...devBundledEntries],
484
+ additionalEntrySpecifiers: sharedVendorOptions.additionalEntrySpecifiers,
485
+ },
486
+ );
487
+ if (publicUrl) {
488
+ const previousPublicUrl = currentAssetResolver(moduleId);
489
+ packageImports.set(packageSpecifier, publicUrl);
490
+ packageImportUrls.add(publicUrl);
491
+ currentAssetResolver = createAssetResolver(projectRoot, {
492
+ assetManifest: currentAssetManifest,
493
+ packageImports,
494
+ });
495
+ for (const root of result.hydrationData?.roots ?? []) {
496
+ if (root?.moduleId === moduleId) root.moduleId = packageSpecifier;
497
+ }
498
+ if (Array.isArray(result.clientImports)) {
499
+ result.clientImports = result.clientImports.map((value) => (
500
+ value === moduleId || value === previousPublicUrl ? publicUrl : value
501
+ ));
502
+ }
503
+ if (Array.isArray(result.hydrationData?.clientImports)) {
504
+ result.hydrationData.clientImports = result.hydrationData.clientImports.map((value) => (
505
+ value === moduleId || value === previousPublicUrl ? publicUrl : value
506
+ ));
507
+ }
508
+ continue;
509
+ }
510
+ unresolvedModules.push(moduleId);
511
+ continue;
512
+ }
450
513
  const sourcePath = await resolveRenderedClientSource(moduleId, routeResult);
451
514
  if (sourcePath) devKnownClientBoundarySources.add(path.resolve(sourcePath));
452
515
  if (hasClientArtifact(moduleId)) continue;
@@ -575,6 +638,7 @@ export async function createRequestRenderer({
575
638
  result.hydrationData,
576
639
  projectRoot,
577
640
  resolveHydrationRootClientImports(result.hydrationData, currentAssetResolver),
641
+ currentAssetResolver,
578
642
  ),
579
643
  assetResolver(moduleId) {
580
644
  return currentAssetResolver(moduleId);
@@ -591,9 +655,10 @@ export async function createRequestRenderer({
591
655
  result.hydrationData,
592
656
  projectRoot,
593
657
  resolveHydrationRootClientImports(
594
- result.hydrationData,
595
- currentAssetResolver,
596
- ),
658
+ result.hydrationData,
659
+ currentAssetResolver,
660
+ ),
661
+ currentAssetResolver,
597
662
  ),
598
663
  );
599
664
  },
@@ -678,6 +743,7 @@ export async function createRequestRenderer({
678
743
  currentAssetManifest = null;
679
744
  currentAssetResolver = createAssetResolver(projectRoot, {
680
745
  assetManifest: currentAssetManifest,
746
+ packageImports,
681
747
  });
682
748
  devRollupCache = null;
683
749
  for (const clientModule of affectedClientModules) {
@@ -726,11 +792,24 @@ export async function createRequestRenderer({
726
792
  ])));
727
793
  const inventory = {
728
794
  clientBoundaries: [...new Set([...inventoriesByEntry.values()].flatMap((entry) => entry.clientBoundaries))].sort(),
795
+ packageClientBoundaries: [...new Map(
796
+ [...inventoriesByEntry.values()]
797
+ .flatMap((entry) => entry.packageClientBoundaries ?? [])
798
+ .map((entry) => [entry.specifier, entry]),
799
+ ).values()].sort((left, right) => left.specifier.localeCompare(right.specifier)),
729
800
  styles: [...new Set([...inventoriesByEntry.values()].flatMap((entry) => entry.styles))].sort(),
730
801
  assets: [...new Set([...inventoriesByEntry.values()].flatMap((entry) => entry.assets))].sort(),
731
802
  sourceFiles: [...new Set([...inventoriesByEntry.values()].flatMap((entry) => entry.sourceFiles))].sort(),
732
803
  };
733
- const clientBoundaries = inventory.clientBoundaries;
804
+ const packageBoundarySources = new Set(
805
+ inventory.packageClientBoundaries.map((entry) => entry.sourcePath),
806
+ );
807
+ for (const packageBoundary of inventory.packageClientBoundaries) {
808
+ packageClientSpecifiers.add(packageBoundary.specifier);
809
+ }
810
+ sharedVendorOptions.additionalEntrySpecifiers = [...packageClientSpecifiers].sort();
811
+ const clientBoundaries = inventory.clientBoundaries
812
+ .filter((clientBoundary) => !packageBoundarySources.has(clientBoundary));
734
813
  for (const clientBoundary of clientBoundaries) {
735
814
  devKnownClientBoundarySources.add(path.resolve(clientBoundary));
736
815
  }
@@ -747,6 +826,8 @@ export async function createRequestRenderer({
747
826
  assets: entryInventory.assets.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
748
827
  };
749
828
  devClientBoundariesByEntry[entryKey] = entryInventory.clientBoundaries
829
+ .filter((filePath) => !(entryInventory.packageClientBoundaries ?? [])
830
+ .some((packageBoundary) => packageBoundary.sourcePath === filePath))
750
831
  .map((filePath) => getCompiledClientModule(projectRoot, filePath))
751
832
  .sort();
752
833
  }