@jsenv/core 40.0.9 → 40.1.0

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.
Files changed (27) hide show
  1. package/dist/build/build.js +1792 -1098
  2. package/dist/client/autoreload/autoreload.js +2 -45
  3. package/dist/client/autoreload/jsenv_core_packages.js +20 -0
  4. package/dist/client/directory_listing/directory_listing.html +2 -2
  5. package/dist/client/directory_listing/js/directory_listing.js +8 -14
  6. package/dist/client/directory_listing/jsenv_core_node_modules.js +9 -0
  7. package/dist/jsenv_core_node_modules.js +2045 -0
  8. package/dist/jsenv_core_packages.js +9397 -5830
  9. package/dist/start_build_server/start_build_server.js +4 -2
  10. package/dist/start_dev_server/start_dev_server.js +228 -70
  11. package/package.json +16 -15
  12. package/src/build/build.js +1084 -559
  13. package/src/build/build_content_report.js +377 -0
  14. package/src/build/build_params.js +1 -4
  15. package/src/build/build_specifier_manager.js +70 -21
  16. package/src/build/build_urls_generator.js +29 -28
  17. package/src/kitchen/fetched_content_compliance.js +2 -2
  18. package/src/kitchen/kitchen.js +1 -1
  19. package/src/plugins/directory_reference_effect/jsenv_plugin_directory_reference_effect.js +20 -5
  20. package/src/plugins/plugins.js +5 -2
  21. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +3 -4
  22. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +1 -4
  23. package/src/plugins/resolution_node_esm/jsenv_plugin_node_esm_resolution.js +56 -34
  24. package/src/plugins/resolution_node_esm/node_esm_resolver.js +132 -11
  25. package/src/build/jsenv_plugin_subbuilds.js +0 -74
  26. package/src/kitchen/url_graph/url_graph_report.js +0 -202
  27. /package/dist/client/{server_events_client → server_events}/server_events_client.js +0 -0
@@ -21,19 +21,43 @@ import { parseHtml, stringifyHtmlAst } from "@jsenv/ast";
21
21
  import {
22
22
  assertAndNormalizeDirectoryUrl,
23
23
  clearDirectorySync,
24
+ compareFileUrls,
24
25
  ensureEmptyDirectory,
25
26
  lookupPackageDirectory,
26
27
  writeFileSync,
27
28
  } from "@jsenv/filesystem";
28
- import { createLogger, createTaskLog } from "@jsenv/humanize";
29
+ import {
30
+ ANSI,
31
+ createDynamicLog,
32
+ createLogger,
33
+ createTaskLog,
34
+ humanizeDuration,
35
+ humanizeMemory,
36
+ UNICODE,
37
+ } from "@jsenv/humanize";
38
+ import { applyNodeEsmResolution } from "@jsenv/node-esm-resolution";
39
+ import {
40
+ startMonitoringCpuUsage,
41
+ startMonitoringMemoryUsage,
42
+ } from "@jsenv/os-metrics";
29
43
  import { jsenvPluginBundling } from "@jsenv/plugin-bundling";
30
44
  import { jsenvPluginMinification } from "@jsenv/plugin-minification";
31
45
  import { jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
32
- import { urlIsInsideOf } from "@jsenv/urls";
46
+ import {
47
+ browserDefaultRuntimeCompat,
48
+ inferRuntimeCompatFromClosestPackage,
49
+ nodeDefaultRuntimeCompat,
50
+ } from "@jsenv/runtime-compat";
51
+ import {
52
+ urlIsInsideOf,
53
+ urlToBasename,
54
+ urlToExtension,
55
+ urlToRelativeUrl,
56
+ } from "@jsenv/urls";
57
+ import { memoryUsage as processMemoryUsage } from "node:process";
33
58
  import { watchSourceFiles } from "../helpers/watch_source_files.js";
34
59
  import { jsenvCoreDirectoryUrl } from "../jsenv_core_directory_url.js";
35
60
  import { createKitchen } from "../kitchen/kitchen.js";
36
- import { createUrlGraphSummary } from "../kitchen/url_graph/url_graph_report.js";
37
61
  import { GRAPH_VISITOR } from "../kitchen/url_graph/url_graph_visitor.js";
38
62
  import { jsenvPluginDirectoryReferenceEffect } from "../plugins/directory_reference_effect/jsenv_plugin_directory_reference_effect.js";
39
63
  import { jsenvPluginInlining } from "../plugins/inlining/jsenv_plugin_inlining.js";
@@ -43,15 +67,12 @@ import {
43
67
  } from "../plugins/plugin_controller.js";
44
68
  import { getCorePlugins } from "../plugins/plugins.js";
45
69
  import { jsenvPluginReferenceAnalysis } from "../plugins/reference_analysis/jsenv_plugin_reference_analysis.js";
46
- import {
47
- defaultRuntimeCompat,
48
- getDefaultBase,
49
- logsDefault,
50
- } from "./build_params.js";
70
+ import { renderBuildDoneLog } from "./build_content_report.js";
71
+ import { defaultRuntimeCompat, logsDefault } from "./build_params.js";
51
72
  import { createBuildSpecifierManager } from "./build_specifier_manager.js";
73
+ import { createBuildUrlsGenerator } from "./build_urls_generator.js";
52
74
  import { jsenvPluginLineBreakNormalization } from "./jsenv_plugin_line_break_normalization.js";
53
75
  import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
54
- import { jsenvPluginSubbuilds } from "./jsenv_plugin_subbuilds.js";
55
76
 
56
77
  /**
57
78
  * Generate an optimized version of source files into a directory.
@@ -66,8 +87,8 @@ import { jsenvPluginSubbuilds } from "./jsenv_plugin_subbuilds.js";
66
87
  * Keys are relative to sourceDirectoryUrl
67
88
  * @param {object} params.runtimeCompat
68
89
  * Code generated will be compatible with these runtimes
69
- * @param {string} [params.assetsDirectory=""]
70
- * Directory where asset files will be written
90
+ * @param {string} [params.assetsDirectory]
91
+ * Directory where asset files will be written. By default sibling to the entry build file.
71
92
  * @param {string|url} [params.base=""]
72
93
  * Urls in build file contents will be prefixed with this string
73
94
  * @param {boolean|object} [params.bundling=true]
@@ -89,54 +110,29 @@ import { jsenvPluginSubbuilds } from "./jsenv_plugin_subbuilds.js";
89
110
  * Map build file paths without versioning to versioned file paths
90
111
  */
91
112
  export const build = async ({
92
- signal = new AbortController().signal,
93
- handleSIGINT = true,
94
- logs = logsDefault,
95
113
  sourceDirectoryUrl,
96
114
  buildDirectoryUrl,
97
115
  entryPoints = {},
98
- assetsDirectory = "",
99
- runtimeCompat = defaultRuntimeCompat,
100
- base = getDefaultBase(runtimeCompat),
101
- ignore,
102
-
103
- mappings,
104
- subbuilds = [],
105
- plugins = [],
106
- referenceAnalysis = {},
107
- nodeEsmResolution,
108
- magicExtensions,
109
- magicDirectoryIndex,
110
- directoryReferenceEffect,
111
- scenarioPlaceholders,
112
- injections,
113
- transpilation = {},
114
- bundling = true,
115
- minification = !runtimeCompat.node,
116
- versioning = !runtimeCompat.node,
117
- versioningMethod = "search_param", // "filename", "search_param"
118
- versioningViaImportmap = true,
119
- versionLength = 8,
120
- lineBreakNormalization = process.platform === "win32",
116
+ logs,
121
117
 
122
- sourceFilesConfig = {},
123
- cooldownBetweenFileEvents,
124
- watch = false,
125
- http = false,
126
-
127
- buildDirectoryCleanPatterns = {
128
- "**/*": true,
129
- },
130
- sourcemaps = "none",
131
- sourcemapsSourcesContent,
132
- writeOnFileSystem = true,
133
118
  outDirectoryUrl,
134
- assetManifest = versioningMethod === "filename",
135
- assetManifestFileRelativeUrl = "asset-manifest.json",
119
+ buildDirectoryCleanPatterns = { "**/*": true },
136
120
  returnBuildInlineContents,
137
121
  returnBuildManifest,
122
+ returnBuildFileVersions,
123
+ signal = new AbortController().signal,
124
+ handleSIGINT = true,
125
+
126
+ writeOnFileSystem = true,
127
+
128
+ watch = false,
129
+ sourceFilesConfig = {},
130
+ cooldownBetweenFileEvents,
131
+
138
132
  ...rest
139
133
  }) => {
134
+ const entryPointArray = [];
135
+
140
136
  // param validation
141
137
  {
142
138
  const unexpectedParamNames = Object.keys(rest);
@@ -145,88 +141,166 @@ export const build = async ({
145
141
  `${unexpectedParamNames.join(",")}: there is no such param`,
146
142
  );
147
143
  }
148
- // logs
144
+ // source and build directory
149
145
  {
150
- if (typeof logs !== "object") {
151
- throw new TypeError(`logs must be an object, got ${logs}`);
152
- }
153
- const unexpectedLogsKeys = Object.keys(logs).filter(
154
- (key) => !Object.hasOwn(logsDefault, key),
146
+ sourceDirectoryUrl = assertAndNormalizeDirectoryUrl(
147
+ sourceDirectoryUrl,
148
+ "sourceDirectoryUrl",
155
149
  );
156
- if (unexpectedLogsKeys.length > 0) {
150
+ buildDirectoryUrl = assertAndNormalizeDirectoryUrl(
151
+ buildDirectoryUrl,
152
+ "buildDirectoryUrl",
153
+ );
154
+ }
155
+ // entry points
156
+ {
157
+ if (typeof entryPoints !== "object" || entryPoints === null) {
157
158
  throw new TypeError(
158
- `${unexpectedLogsKeys.join(",")}: no such key on logs`,
159
+ `The value "${entryPoints}" for "entryPoints" is invalid: it must be an object.`,
159
160
  );
160
161
  }
161
- logs = { ...logsDefault, ...logs };
162
- }
163
- sourceDirectoryUrl = assertAndNormalizeDirectoryUrl(
164
- sourceDirectoryUrl,
165
- "sourceDirectoryUrl",
166
- );
167
- buildDirectoryUrl = assertAndNormalizeDirectoryUrl(
168
- buildDirectoryUrl,
169
- "buildDirectoryUrl",
170
- );
171
- if (outDirectoryUrl === undefined) {
172
- if (
173
- process.env.CAPTURING_SIDE_EFFECTS ||
174
- (!import.meta.build &&
175
- urlIsInsideOf(sourceDirectoryUrl, jsenvCoreDirectoryUrl))
176
- ) {
177
- outDirectoryUrl = new URL("../.jsenv_b/", sourceDirectoryUrl);
178
- } else {
179
- const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
180
- if (packageDirectoryUrl) {
181
- outDirectoryUrl = `${packageDirectoryUrl}.jsenv/`;
162
+ const keys = Object.keys(entryPoints);
163
+ const isSingleEntryPoint = keys.length === 1;
164
+ for (const key of keys) {
165
+ // key (sourceRelativeUrl)
166
+ let sourceUrl;
167
+ let runtimeType;
168
+ {
169
+ if (isBareSpecifier(key)) {
170
+ const packageConditions = ["development", "node", "import"];
171
+ try {
172
+ const { url, type } = applyNodeEsmResolution({
173
+ conditions: packageConditions,
174
+ parentUrl: sourceDirectoryUrl,
175
+ specifier: key,
176
+ });
177
+ if (type === "field:browser") {
178
+ runtimeType = "browser";
179
+ }
180
+ sourceUrl = url;
181
+ } catch (e) {
182
+ throw new Error(
183
+ `The key "${key}" in "entryPoints" is invalid: it cannot be resolved.`,
184
+ { cause: e },
185
+ );
186
+ }
187
+ } else {
188
+ if (!key.startsWith("./")) {
189
+ throw new TypeError(
190
+ `The key "${key}" in "entryPoints" is invalid: it must start with "./".`,
191
+ );
192
+ }
193
+
194
+ try {
195
+ sourceUrl = new URL(key, sourceDirectoryUrl).href;
196
+ } catch {
197
+ throw new TypeError(
198
+ `The key "${key}" in "entryPoints" is invalid: it must be a relative url.`,
199
+ );
200
+ }
201
+ }
202
+ if (!urlIsInsideOf(sourceUrl, sourceDirectoryUrl)) {
203
+ throw new Error(
204
+ `The key "${key}" in "entryPoints" is invalid: it must be inside the source directory at ${sourceDirectoryUrl}.`,
205
+ );
206
+ }
207
+
208
+ if (!runtimeType) {
209
+ const ext = urlToExtension(sourceUrl);
210
+ if (ext === ".html" || ext === ".css") {
211
+ runtimeType = "browser";
212
+ }
213
+ }
182
214
  }
183
- }
184
- } else if (outDirectoryUrl) {
185
- outDirectoryUrl = assertAndNormalizeDirectoryUrl(
186
- outDirectoryUrl,
187
- "outDirectoryUrl",
188
- );
189
- }
190
215
 
191
- if (typeof entryPoints !== "object" || entryPoints === null) {
192
- throw new TypeError(`entryPoints must be an object, got ${entryPoints}`);
193
- }
194
- const keys = Object.keys(entryPoints);
195
- keys.forEach((key) => {
196
- if (!key.startsWith("./")) {
197
- throw new TypeError(
198
- `entryPoints keys must start with "./", found ${key}`,
199
- );
216
+ // value (entryPointParams)
217
+ const value = entryPoints[key];
218
+ {
219
+ if (value === null || typeof value !== "object") {
220
+ throw new TypeError(
221
+ `The value "${value}" in "entryPoints" is invalid: it must be an object.`,
222
+ );
223
+ }
224
+ const forEntryPointOrEmpty = isSingleEntryPoint
225
+ ? ""
226
+ : ` for entry point "${key}"`;
227
+ const unexpectedEntryPointParamNames = Object.keys(value).filter(
228
+ (key) => !Object.hasOwn(entryPointDefaultParams, key),
229
+ );
230
+ if (unexpectedEntryPointParamNames.length) {
231
+ throw new TypeError(
232
+ `The value${forEntryPointOrEmpty} contains unknown keys: ${unexpectedEntryPointParamNames.join(",")}.`,
233
+ );
234
+ }
235
+ const { versioningMethod } = value;
236
+ if (versioningMethod !== undefined) {
237
+ if (!["filename", "search_param"].includes(versioningMethod)) {
238
+ throw new TypeError(
239
+ `The versioningMethod "${versioningMethod}"${forEntryPointOrEmpty} is invalid: it must be "filename" or "search_param".`,
240
+ );
241
+ }
242
+ }
243
+ const { buildRelativeUrl } = value;
244
+ if (buildRelativeUrl !== undefined) {
245
+ let buildUrl;
246
+ try {
247
+ buildUrl = new URL(buildRelativeUrl, buildDirectoryUrl);
248
+ } catch {
249
+ throw new TypeError(
250
+ `The buildRelativeUrl "${buildRelativeUrl}"${forEntryPointOrEmpty} is invalid: it must be a relative url.`,
251
+ );
252
+ }
253
+ if (!urlIsInsideOf(buildUrl, buildDirectoryUrl)) {
254
+ throw new Error(
255
+ `The buildRelativeUrl "${buildRelativeUrl}"${forEntryPointOrEmpty} is invalid: it must be inside the build directory at ${buildDirectoryUrl}.`,
256
+ );
257
+ }
258
+ }
259
+ const { runtimeCompat } = value;
260
+ if (runtimeCompat !== undefined) {
261
+ if (runtimeCompat === null || typeof runtimeCompat !== "object") {
262
+ throw new TypeError(
263
+ `The runtimeCompat "${runtimeCompat}"${forEntryPointOrEmpty} is invalid: it must be an object.`,
264
+ );
265
+ }
266
+ }
267
+ }
268
+
269
+ entryPointArray.push({
270
+ key,
271
+ sourceUrl,
272
+ sourceRelativeUrl: `./${urlToRelativeUrl(sourceUrl, sourceDirectoryUrl)}`,
273
+ params: { ...value },
274
+ runtimeType,
275
+ });
200
276
  }
201
- const value = entryPoints[key];
202
- if (typeof value !== "string") {
277
+ }
278
+ // logs
279
+ if (logs === undefined) {
280
+ logs = logsDefault;
281
+ } else {
282
+ if (typeof logs !== "object") {
203
283
  throw new TypeError(
204
- `entryPoints values must be strings, found "${value}" on key "${key}"`,
284
+ `The value "${logs}" is invalid for param logs: it must be an object.`,
205
285
  );
206
286
  }
207
- if (value.includes("/")) {
287
+ const unexpectedLogsKeys = Object.keys(logs).filter(
288
+ (key) => !Object.hasOwn(logsDefault, key),
289
+ );
290
+ if (unexpectedLogsKeys.length > 0) {
208
291
  throw new TypeError(
209
- `entryPoints values must be plain strings (no "/"), found "${value}" on key "${key}"`,
292
+ `The param logs have unknown params: ${unexpectedLogsKeys.join(",")}.`,
210
293
  );
211
294
  }
212
- });
213
- if (!["filename", "search_param"].includes(versioningMethod)) {
214
- throw new TypeError(
215
- `versioningMethod must be "filename" or "search_param", got ${versioning}`,
216
- );
217
- }
218
- if (bundling === true) {
219
- bundling = {};
220
295
  }
221
- if (minification === true) {
222
- minification = {};
296
+ if (outDirectoryUrl !== undefined) {
297
+ outDirectoryUrl = assertAndNormalizeDirectoryUrl(
298
+ outDirectoryUrl,
299
+ "outDirectoryUrl",
300
+ );
223
301
  }
224
302
  }
225
303
 
226
- if (assetsDirectory && assetsDirectory[assetsDirectory.length - 1] !== "/") {
227
- assetsDirectory = `${assetsDirectory}/`;
228
- }
229
-
230
304
  const operation = Abort.startOperation();
231
305
  operation.addAbortSignal(signal);
232
306
  if (handleSIGINT) {
@@ -240,463 +314,340 @@ export const build = async ({
240
314
  });
241
315
  }
242
316
 
243
- const runBuild = async ({ signal, logLevel }) => {
244
- const logger = createLogger({ logLevel });
245
- const createBuildTask = (label) => {
246
- return createTaskLog(label, {
247
- disabled:
248
- logs.disabled || (!logger.levels.debug && !logger.levels.info),
249
- animated: logs.animation && !logger.levels.debug,
250
- });
251
- };
317
+ const cpuMonitoring = startMonitoringCpuUsage();
318
+ operation.addEndCallback(cpuMonitoring.stop);
319
+ const [processCpuUsageMonitoring] = cpuMonitoring;
320
+ const memoryMonitoring = startMonitoringMemoryUsage();
321
+ const [processMemoryUsageMonitoring] = memoryMonitoring;
322
+ const interval = setInterval(() => {
323
+ processCpuUsageMonitoring.measure();
324
+ processMemoryUsageMonitoring.measure();
325
+ }, 500).unref();
326
+ operation.addEndCallback(() => {
327
+ clearInterval(interval);
328
+ });
252
329
 
253
- const buildOperation = Abort.startOperation();
254
- buildOperation.addAbortSignal(signal);
255
- const entryPointKeys = Object.keys(entryPoints);
256
- if (entryPointKeys.length === 1) {
257
- logger.info(`
258
- build "${entryPointKeys[0]}"`);
259
- } else {
260
- logger.info(`
261
- build ${entryPointKeys.length} entry points`);
262
- }
263
- let explicitJsModuleConversion = false;
264
- for (const entryPointKey of entryPointKeys) {
265
- if (entryPointKey.includes("?js_module_fallback")) {
266
- explicitJsModuleConversion = true;
267
- break;
268
- }
269
- if (entryPointKey.includes("?as_js_classic")) {
270
- explicitJsModuleConversion = true;
271
- break;
272
- }
273
- }
274
- const entryUrls = [];
275
- const contextSharedDuringBuild = {
276
- buildStep: "craft",
277
- buildDirectoryUrl,
278
- assetsDirectory,
279
- versioning,
280
- versioningViaImportmap,
281
- };
282
- const rawKitchen = createKitchen({
283
- signal,
284
- logLevel: logs.level,
285
- rootDirectoryUrl: sourceDirectoryUrl,
286
- ignore,
287
- // during first pass (craft) we keep "ignore:" when a reference is ignored
288
- // so that the second pass (shape) properly ignore those urls
289
- ignoreProtocol: "keep",
290
- build: true,
291
- runtimeCompat,
292
- initialContext: contextSharedDuringBuild,
293
- sourcemaps,
294
- sourcemapsSourcesContent,
295
- outDirectoryUrl: outDirectoryUrl
296
- ? new URL("craft/", outDirectoryUrl)
297
- : undefined,
330
+ const logLevel = logs.level;
331
+ const logger = createLogger({ logLevel });
332
+ const animatedLogEnabled =
333
+ logs.animated &&
334
+ // canEraseProcessStdout
335
+ process.stdout.isTTY &&
336
+ // if there is an error during execution npm will mess up the output
337
+ // (happens when npm runs several command in a workspace)
338
+ // so we enable hot replace only when !process.exitCode (no error so far)
339
+ process.exitCode !== 1;
340
+ let startBuildLogs = () => {};
341
+
342
+ const renderEntyPointBuildDoneLog = (
343
+ entryBuildInfo,
344
+ { sourceUrlToLog, buildUrlToLog },
345
+ ) => {
346
+ let content = "";
347
+ content += `${UNICODE.OK} ${ANSI.color(sourceUrlToLog, ANSI.GREY)} ${ANSI.color("->", ANSI.GREY)} ${ANSI.color(buildUrlToLog, "")}`;
348
+ // content += " ";
349
+ // content += ANSI.color("(", ANSI.GREY);
350
+ // content += ANSI.color(
351
+ // humanizeDuration(entryBuildInfo.duration, { short: true }),
352
+ // ANSI.GREY,
353
+ // );
354
+ // content += ANSI.color(")", ANSI.GREY);
355
+ content += "\n";
356
+ return content;
357
+ };
358
+ const renderBuildEndLog = ({ duration, buildFileContents }) => {
359
+ // tell how many files are generated in build directory
360
+ // tell the repartition?
361
+ // this is not really useful for single build right?
362
+
363
+ processCpuUsageMonitoring.end();
364
+ processMemoryUsageMonitoring.end();
365
+
366
+ return renderBuildDoneLog({
367
+ duration,
368
+ buildFileContents,
369
+ processCpuUsage: processCpuUsageMonitoring.info,
370
+ processMemoryUsage: processMemoryUsageMonitoring.info,
298
371
  });
372
+ };
299
373
 
300
- let subbuildResults = [];
374
+ if (animatedLogEnabled) {
375
+ startBuildLogs = () => {
376
+ const startMs = Date.now();
377
+ let dynamicLog = createDynamicLog();
378
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
379
+ let frameIndex = 0;
380
+ let oneWrite = false;
381
+ const memoryHeapUsedAtStart = processMemoryUsage().heapUsed;
382
+ const renderDynamicLog = () => {
383
+ frameIndex = frameIndex === frames.length - 1 ? 0 : frameIndex + 1;
384
+ let dynamicLogContent = "";
385
+ dynamicLogContent += `${frames[frameIndex]} `;
386
+ dynamicLogContent += `building ${entryPointArray.length} entry points`;
301
387
 
302
- const rawPluginStore = createPluginStore([
303
- ...(mappings ? [jsenvPluginMappings(mappings)] : []),
304
- ...jsenvPluginSubbuilds(subbuilds, {
305
- parentBuildParams: {
306
- sourceDirectoryUrl,
307
- buildDirectoryUrl,
308
- runtimeCompat,
309
- bundling,
310
- minification,
311
- versioning,
312
- versioningMethod,
313
- outDirectoryUrl,
314
- },
315
- onCustomBuildDirectory: (subBuildRelativeUrl) => {
316
- buildDirectoryCleanPatterns = {
317
- ...buildDirectoryCleanPatterns,
318
- [`${subBuildRelativeUrl}**/*`]: false,
388
+ const msEllapsed = Date.now() - startMs;
389
+ const infos = [];
390
+ const duration = humanizeDuration(msEllapsed, {
391
+ short: true,
392
+ decimals: 0,
393
+ rounded: false,
394
+ });
395
+ infos.push(ANSI.color(duration, ANSI.GREY));
396
+ let memoryUsageColor = ANSI.GREY;
397
+ const memoryHeapUsed = processMemoryUsage().heapUsed;
398
+ if (memoryHeapUsed > 2.5 * memoryHeapUsedAtStart) {
399
+ memoryUsageColor = ANSI.YELLOW;
400
+ } else if (memoryHeapUsed > 1.5 * memoryHeapUsedAtStart) {
401
+ memoryUsageColor = null;
402
+ }
403
+ const memoryHeapUsedFormatted = humanizeMemory(memoryHeapUsed, {
404
+ short: true,
405
+ decimals: 0,
406
+ });
407
+ infos.push(ANSI.color(memoryHeapUsedFormatted, memoryUsageColor));
408
+
409
+ const infoFormatted = infos.join(ANSI.color(`/`, ANSI.GREY));
410
+ dynamicLogContent += ` ${ANSI.color(
411
+ "[",
412
+ ANSI.GREY,
413
+ )}${infoFormatted}${ANSI.color("]", ANSI.GREY)}`;
414
+
415
+ if (oneWrite) {
416
+ dynamicLogContent = `\n${dynamicLogContent}`;
417
+ }
418
+ dynamicLogContent = `${dynamicLogContent}\n`;
419
+ return dynamicLogContent;
420
+ };
421
+ dynamicLog.update(renderDynamicLog());
422
+ const interval = setInterval(() => {
423
+ dynamicLog.update(renderDynamicLog());
424
+ }, 150).unref();
425
+ signal.addEventListener("abort", () => {
426
+ clearInterval(interval);
427
+ });
428
+ return {
429
+ onEntryPointBuildStart: (
430
+ entryBuildInfo,
431
+ { sourceUrlToLog, buildUrlToLog },
432
+ ) => {
433
+ return () => {
434
+ oneWrite = true;
435
+ dynamicLog.clearDuringFunctionCall((write) => {
436
+ const log = renderEntyPointBuildDoneLog(entryBuildInfo, {
437
+ sourceUrlToLog,
438
+ buildUrlToLog,
439
+ });
440
+ write(log);
441
+ }, renderDynamicLog());
319
442
  };
320
443
  },
321
- buildStart: async (params, index) => {
322
- const result = await build({
323
- ...params,
324
- signal,
325
- handleSIGINT: false,
326
- });
327
- subbuildResults[index] = result;
328
- return result;
444
+ onBuildEnd: ({ buildFileContents, duration }) => {
445
+ clearInterval(interval);
446
+ dynamicLog.update("");
447
+ dynamicLog.destroy();
448
+ dynamicLog = null;
449
+ logger.info("");
450
+ logger.info(renderBuildEndLog({ duration, buildFileContents }));
329
451
  },
330
- }),
331
- ...plugins,
332
- ...(bundling ? [jsenvPluginBundling(bundling)] : []),
333
- ...(minification ? [jsenvPluginMinification(minification)] : []),
334
- ...getCorePlugins({
335
- rootDirectoryUrl: sourceDirectoryUrl,
336
- runtimeCompat,
337
- referenceAnalysis,
338
- nodeEsmResolution,
339
- magicExtensions,
340
- magicDirectoryIndex,
341
- directoryReferenceEffect,
342
- injections,
343
- transpilation: {
344
- babelHelpersAsImport: !explicitJsModuleConversion,
345
- ...transpilation,
346
- jsModuleFallback: false,
452
+ };
453
+ };
454
+ } else {
455
+ startBuildLogs = () => {
456
+ if (entryPointArray.length === 1) {
457
+ const [singleEntryPoint] = entryPointArray;
458
+ logger.info(`building ${singleEntryPoint.key}`);
459
+ } else {
460
+ logger.info(`building ${entryPointArray.length} entry points`);
461
+ }
462
+ logger.info("");
463
+ return {
464
+ onEntryPointBuildStart: (
465
+ entryBuildInfo,
466
+ { sourceUrlToLog, buildUrlToLog },
467
+ ) => {
468
+ return () => {
469
+ logger.info(
470
+ renderEntyPointBuildDoneLog(entryBuildInfo, {
471
+ sourceUrlToLog,
472
+ buildUrlToLog,
473
+ }),
474
+ );
475
+ };
347
476
  },
348
- inlining: false,
349
- http,
350
- scenarioPlaceholders,
351
- }),
352
- ]);
353
- const rawPluginController = createPluginController(
354
- rawPluginStore,
355
- rawKitchen,
356
- );
357
- rawKitchen.setPluginController(rawPluginController);
477
+ onBuildEnd: ({ buildFileContents, duration }) => {
478
+ logger.info("");
479
+ logger.info(renderBuildEndLog({ duration, buildFileContents }));
480
+ },
481
+ };
482
+ };
483
+ }
358
484
 
359
- craft: {
360
- const generateSourceGraph = createBuildTask("generate source graph");
361
- try {
362
- if (outDirectoryUrl) {
363
- await ensureEmptyDirectory(new URL(`craft/`, outDirectoryUrl));
364
- }
365
- const rawRootUrlInfo = rawKitchen.graph.rootUrlInfo;
366
- await rawRootUrlInfo.dependencies.startCollecting(() => {
367
- Object.keys(entryPoints).forEach((key) => {
368
- const entryReference = rawRootUrlInfo.dependencies.found({
369
- trace: { message: `"${key}" in entryPoints parameter` },
370
- isEntryPoint: true,
371
- type: "entry_point",
372
- specifier: key,
373
- filenameHint: entryPoints[key],
374
- });
375
- entryUrls.push(entryReference.url);
376
- });
377
- });
378
- await rawRootUrlInfo.cookDependencies({
379
- operation: buildOperation,
380
- });
381
- } catch (e) {
382
- generateSourceGraph.fail();
383
- throw e;
384
- }
385
- generateSourceGraph.done();
485
+ // we want to start building the entry point that are deeper
486
+ // - they are more likely to be small
487
+ // - they are more likely to be referenced by highter files that will depend on them
488
+ entryPointArray.sort((a, b) => {
489
+ return compareFileUrls(a.sourceUrl, b.sourceUrl);
490
+ });
491
+
492
+ const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
493
+ if (outDirectoryUrl === undefined) {
494
+ if (
495
+ process.env.CAPTURING_SIDE_EFFECTS ||
496
+ (!import.meta.build &&
497
+ urlIsInsideOf(sourceDirectoryUrl, jsenvCoreDirectoryUrl))
498
+ ) {
499
+ outDirectoryUrl = new URL("../.jsenv_b/", sourceDirectoryUrl).href;
500
+ } else if (packageDirectoryUrl) {
501
+ outDirectoryUrl = `${packageDirectoryUrl}.jsenv/`;
386
502
  }
503
+ }
504
+ let rootPackageDirectoryUrl = packageDirectoryUrl;
505
+ if (packageDirectoryUrl) {
506
+ const parentPackageDirectoryUrl = lookupPackageDirectory(
507
+ new URL("../", packageDirectoryUrl),
508
+ );
509
+ if (parentPackageDirectoryUrl) {
510
+ rootPackageDirectoryUrl = parentPackageDirectoryUrl;
511
+ }
512
+ }
387
513
 
388
- const finalKitchen = createKitchen({
389
- name: "shape",
390
- logLevel: logs.level,
391
- rootDirectoryUrl: sourceDirectoryUrl,
392
- // here most plugins are not there
393
- // - no external plugin
394
- // - no plugin putting reference.mustIgnore on https urls
395
- // At this stage it's only about redirecting urls to the build directory
396
- // consequently only a subset or urls are supported
397
- supportedProtocols: ["file:", "data:", "virtual:", "ignore:"],
398
- ignore,
399
- ignoreProtocol: "remove",
400
- build: true,
401
- runtimeCompat,
402
- initialContext: contextSharedDuringBuild,
403
- sourcemaps,
404
- sourcemapsComment: "relative",
405
- sourcemapsSourcesContent,
406
- outDirectoryUrl: outDirectoryUrl
407
- ? new URL("shape/", outDirectoryUrl)
408
- : undefined,
409
- });
410
- const buildSpecifierManager = createBuildSpecifierManager({
411
- rawKitchen,
412
- finalKitchen,
413
- logger,
514
+ const runBuild = async ({ signal }) => {
515
+ const startDate = Date.now();
516
+ const { onBuildEnd, onEntryPointBuildStart } = startBuildLogs();
517
+
518
+ const buildUrlsGenerator = createBuildUrlsGenerator({
414
519
  sourceDirectoryUrl,
415
520
  buildDirectoryUrl,
416
- base,
417
- assetsDirectory,
418
-
419
- versioning,
420
- versioningMethod,
421
- versionLength,
422
- canUseImportmap:
423
- versioningViaImportmap &&
424
- entryUrls.every((finalEntryUrl) => {
425
- const entryUrlInfo = rawKitchen.graph.getUrlInfo(finalEntryUrl);
426
- return entryUrlInfo.type === "html";
427
- }) &&
428
- rawKitchen.context.isSupportedOnCurrentClients("importmap"),
429
521
  });
430
- const finalPluginStore = createPluginStore([
431
- jsenvPluginReferenceAnalysis({
432
- ...referenceAnalysis,
433
- fetchInlineUrls: false,
434
- // inlineContent: false,
435
- }),
436
- jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
437
- ...(lineBreakNormalization ? [jsenvPluginLineBreakNormalization()] : []),
438
- jsenvPluginJsModuleFallback({
439
- remapImportSpecifier: (specifier, parentUrl) => {
440
- return buildSpecifierManager.remapPlaceholder(specifier, parentUrl);
441
- },
442
- }),
443
- jsenvPluginInlining(),
444
- {
445
- name: "jsenv:optimize",
446
- appliesDuring: "build",
447
- transformUrlContent: async (urlInfo) => {
448
- await rawKitchen.pluginController.callAsyncHooks(
449
- "optimizeUrlContent",
450
- urlInfo,
451
- (optimizeReturnValue) => {
452
- urlInfo.mutateContent(optimizeReturnValue);
453
- },
454
- );
455
- },
456
- },
457
- buildSpecifierManager.jsenvPluginMoveToBuildDirectory,
458
- ]);
459
- const finalPluginController = createPluginController(
460
- finalPluginStore,
461
- finalKitchen,
462
- {
463
- initialPuginsMeta: rawKitchen.pluginController.pluginsMeta,
464
- },
465
- );
466
- finalKitchen.setPluginController(finalPluginController);
467
-
468
- const bundlers = {};
469
- bundle: {
470
- for (const plugin of rawKitchen.pluginController.activePlugins) {
471
- const bundle = plugin.bundle;
472
- if (!bundle) {
473
- continue;
474
- }
475
- if (typeof bundle !== "object") {
476
- throw new Error(
477
- `bundle must be an object, found "${bundle}" on plugin named "${plugin.name}"`,
478
- );
479
- }
480
- for (const type of Object.keys(bundle)) {
481
- const bundleFunction = bundle[type];
482
- if (!bundleFunction) {
483
- continue;
484
- }
485
- const bundlerForThatType = bundlers[type];
486
- if (bundlerForThatType) {
487
- // first plugin to define a bundle hook wins
488
- continue;
489
- }
490
- bundlers[type] = {
491
- plugin,
492
- bundleFunction: bundle[type],
493
- urlInfoMap: new Map(),
494
- };
522
+
523
+ let someEntryPointUseNode = false;
524
+ for (const entryPoint of entryPointArray) {
525
+ let { runtimeCompat } = entryPoint.params;
526
+ if (runtimeCompat === undefined) {
527
+ const runtimeCompatFromPackage = inferRuntimeCompatFromClosestPackage(
528
+ entryPoint.sourceUrl,
529
+ {
530
+ runtimeType: entryPoint.runtimeType,
531
+ },
532
+ );
533
+ if (runtimeCompatFromPackage) {
534
+ entryPoint.params.runtimeCompat = runtimeCompat =
535
+ runtimeCompatFromPackage;
536
+ } else {
537
+ entryPoint.params.runtimeCompat = runtimeCompat =
538
+ entryPoint.runtimeType === "browser"
539
+ ? browserDefaultRuntimeCompat
540
+ : nodeDefaultRuntimeCompat;
495
541
  }
496
542
  }
497
- const addToBundlerIfAny = (rawUrlInfo) => {
498
- const bundler = bundlers[rawUrlInfo.type];
499
- if (bundler) {
500
- bundler.urlInfoMap.set(rawUrlInfo.url, rawUrlInfo);
501
- }
502
- };
503
- // ignore unused urls thanks to "forEachUrlInfoStronglyReferenced"
504
- // it avoid bundling things that are not actually used
505
- // happens for:
506
- // - js import assertions
507
- // - conversion to js classic using ?as_js_classic or ?js_module_fallback
508
- GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
509
- rawKitchen.graph.rootUrlInfo,
510
- (rawUrlInfo) => {
511
- if (rawUrlInfo.isEntryPoint) {
512
- addToBundlerIfAny(rawUrlInfo);
513
- }
514
- if (rawUrlInfo.type === "html") {
515
- for (const referenceToOther of rawUrlInfo.referenceToOthersSet) {
516
- if (
517
- referenceToOther.isResourceHint &&
518
- referenceToOther.expectedType === "js_module"
519
- ) {
520
- const referencedUrlInfo = referenceToOther.urlInfo;
521
- if (
522
- referencedUrlInfo &&
523
- // something else than the resource hint is using this url
524
- referencedUrlInfo.referenceFromOthersSet.size > 0
525
- ) {
526
- addToBundlerIfAny(referencedUrlInfo);
527
- continue;
528
- }
529
- }
530
- if (referenceToOther.isWeak) {
531
- continue;
532
- }
533
- const referencedUrlInfo = referenceToOther.urlInfo;
534
- if (referencedUrlInfo.isInline) {
535
- if (referencedUrlInfo.type !== "js_module") {
536
- continue;
537
- }
538
- addToBundlerIfAny(referencedUrlInfo);
539
- continue;
540
- }
541
- addToBundlerIfAny(referencedUrlInfo);
542
- }
543
- return;
544
- }
545
- // File referenced with
546
- // - new URL("./file.js", import.meta.url)
547
- // - import.meta.resolve("./file.js")
548
- // are entry points that should be bundled
549
- // For instance we will bundle service worker/workers detected like this
550
- if (rawUrlInfo.type === "js_module") {
551
- for (const referenceToOther of rawUrlInfo.referenceToOthersSet) {
552
- if (
553
- referenceToOther.type === "js_url" ||
554
- referenceToOther.subtype === "import_meta_resolve"
555
- ) {
556
- const referencedUrlInfo = referenceToOther.urlInfo;
557
- let isAlreadyBundled = false;
558
- for (const referenceFromOther of referencedUrlInfo.referenceFromOthersSet) {
559
- if (referenceFromOther.url === referencedUrlInfo.url) {
560
- if (
561
- referenceFromOther.subtype === "import_dynamic" ||
562
- referenceFromOther.type === "script"
563
- ) {
564
- isAlreadyBundled = true;
565
- break;
566
- }
567
- }
568
- }
569
- if (!isAlreadyBundled) {
570
- addToBundlerIfAny(referencedUrlInfo);
571
- }
572
- continue;
573
- }
574
- if (referenceToOther.type === "js_inline_content") {
575
- // we should bundle it too right?
576
- }
577
- }
578
- }
579
- },
580
- );
581
- for (const type of Object.keys(bundlers)) {
582
- const bundler = bundlers[type];
583
- const urlInfosToBundle = Array.from(bundler.urlInfoMap.values());
584
- if (urlInfosToBundle.length === 0) {
585
- continue;
586
- }
587
- const bundleTask = createBuildTask(`bundle "${type}"`);
588
- try {
589
- await buildSpecifierManager.applyBundling({
590
- bundler,
591
- urlInfosToBundle,
592
- });
593
- } catch (e) {
594
- bundleTask.fail();
595
- throw e;
596
- }
597
- bundleTask.done();
543
+ if (!someEntryPointUseNode && "node" in runtimeCompat) {
544
+ someEntryPointUseNode = true;
598
545
  }
599
546
  }
600
547
 
601
- shape: {
602
- finalKitchen.context.buildStep = "shape";
603
- const generateBuildGraph = createBuildTask("generate build graph");
604
- try {
605
- if (outDirectoryUrl) {
606
- await ensureEmptyDirectory(new URL(`shape/`, outDirectoryUrl));
607
- }
608
- const finalRootUrlInfo = finalKitchen.graph.rootUrlInfo;
609
- await finalRootUrlInfo.dependencies.startCollecting(() => {
610
- entryUrls.forEach((entryUrl) => {
611
- finalRootUrlInfo.dependencies.found({
612
- trace: { message: `entryPoint` },
613
- isEntryPoint: true,
614
- type: "entry_point",
615
- specifier: entryUrl,
616
- });
617
- });
618
- });
619
- await finalRootUrlInfo.cookDependencies({
620
- operation: buildOperation,
621
- });
622
- } catch (e) {
623
- generateBuildGraph.fail();
624
- throw e;
548
+ const entryBuildInfoMap = new Map();
549
+ let entryPointIndex = 0;
550
+ const entryOutDirSet = new Set();
551
+ for (const entryPoint of entryPointArray) {
552
+ let entryOutDirCandidate = `entry_${urlToBasename(entryPoint.sourceRelativeUrl)}/`;
553
+ let entryInteger = 1;
554
+ while (entryOutDirSet.has(entryOutDirCandidate)) {
555
+ entryInteger++;
556
+ entryOutDirCandidate = `entry_${urlToBasename(entryPoint.sourceRelativeUrl)}_${entryInteger}/`;
625
557
  }
626
- generateBuildGraph.done();
627
- }
628
-
629
- refine: {
630
- finalKitchen.context.buildStep = "refine";
558
+ const entryOutDirname = entryOutDirCandidate;
559
+ entryOutDirSet.add(entryOutDirname);
560
+ const entryOutDirectoryUrl = new URL(entryOutDirname, outDirectoryUrl);
561
+ const { entryReference, buildEntryPoint } = await prepareEntryPointBuild(
562
+ {
563
+ signal,
564
+ sourceDirectoryUrl,
565
+ buildDirectoryUrl,
566
+ outDirectoryUrl: entryOutDirectoryUrl,
567
+ sourceRelativeUrl: entryPoint.sourceRelativeUrl,
568
+ buildUrlsGenerator,
569
+ someEntryPointUseNode,
570
+ },
571
+ entryPoint.params,
572
+ );
573
+ const entryPointBuildRelativeUrl = entryPoint.params.buildRelativeUrl;
574
+ const entryBuildInfo = {
575
+ index: entryPointIndex,
576
+ entryReference,
577
+ entryUrlInfo: entryReference.urlInfo,
578
+ buildRelativeUrl: entryPointBuildRelativeUrl,
579
+ buildFileContents: undefined,
580
+ buildFileVersions: undefined,
581
+ buildInlineContents: undefined,
582
+ buildManifest: undefined,
583
+ duration: null,
584
+ buildEntryPoint: () => {
585
+ const sourceUrl = new URL(
586
+ entryPoint.sourceRelativeUrl,
587
+ sourceDirectoryUrl,
588
+ );
589
+ const buildUrl = new URL(
590
+ entryPointBuildRelativeUrl,
591
+ buildDirectoryUrl,
592
+ );
593
+ const sourceUrlToLog = rootPackageDirectoryUrl
594
+ ? urlToRelativeUrl(sourceUrl, rootPackageDirectoryUrl)
595
+ : entryPoint.key;
596
+ const buildUrlToLog = rootPackageDirectoryUrl
597
+ ? urlToRelativeUrl(buildUrl, rootPackageDirectoryUrl)
598
+ : entryPointBuildRelativeUrl;
631
599
 
632
- const htmlRefineSet = new Set();
633
- const registerHtmlRefine = (htmlRefine) => {
634
- htmlRefineSet.add(htmlRefine);
600
+ const entryPointBuildStartMs = Date.now();
601
+ const onEntryPointBuildEnd = onEntryPointBuildStart(entryBuildInfo, {
602
+ sourceUrlToLog,
603
+ buildUrlToLog,
604
+ });
605
+ const promise = (async () => {
606
+ const result = await buildEntryPoint({
607
+ getOtherEntryBuildInfo: (url) => {
608
+ if (url === entryReference.url) {
609
+ return null;
610
+ }
611
+ const otherEntryBuildInfo = entryBuildInfoMap.get(url);
612
+ if (!otherEntryBuildInfo) {
613
+ return null;
614
+ }
615
+ return otherEntryBuildInfo;
616
+ },
617
+ });
618
+ entryBuildInfo.buildFileContents = result.buildFileContents;
619
+ entryBuildInfo.buildFileVersions = result.buildFileVersions;
620
+ entryBuildInfo.buildInlineContents = result.buildInlineContents;
621
+ entryBuildInfo.buildManifest = result.buildManifest;
622
+ entryBuildInfo.duration = Date.now() - entryPointBuildStartMs;
623
+ onEntryPointBuildEnd();
624
+ })();
625
+ entryBuildInfo.promise = promise;
626
+ return promise;
627
+ },
635
628
  };
629
+ entryBuildInfoMap.set(entryReference.url, entryBuildInfo);
630
+ entryPointIndex++;
631
+ }
636
632
 
637
- replace_placeholders: {
638
- await buildSpecifierManager.replacePlaceholders();
639
- }
640
-
641
- /*
642
- * Update <link rel="preload"> and friends after build (once we know everything)
643
- * - Used to remove resource hint targeting an url that is no longer used:
644
- * - because of bundlings
645
- * - because of import assertions transpilation (file is inlined into JS)
646
- */
647
- resync_resource_hints: {
648
- buildSpecifierManager.prepareResyncResourceHints({
649
- registerHtmlRefine,
650
- });
651
- }
652
-
653
- mutate_html: {
654
- GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
655
- if (!urlInfo.url.startsWith("file:")) {
656
- return;
657
- }
658
- if (urlInfo.type !== "html") {
659
- return;
660
- }
661
- const htmlAst = parseHtml({
662
- html: urlInfo.content,
663
- url: urlInfo.url,
664
- storeOriginalPositions: false,
665
- });
666
- for (const htmlRefine of htmlRefineSet) {
667
- const htmlMutationCallbackSet = new Set();
668
- const registerHtmlMutation = (callback) => {
669
- htmlMutationCallbackSet.add(callback);
670
- };
671
- htmlRefine(htmlAst, { registerHtmlMutation });
672
- for (const htmlMutationCallback of htmlMutationCallbackSet) {
673
- htmlMutationCallback();
674
- }
675
- }
676
- // cleanup jsenv attributes from html as a last step
677
- urlInfo.content = stringifyHtmlAst(htmlAst, {
678
- cleanupJsenvAttributes: true,
679
- cleanupPositionAttributes: true,
680
- });
681
- });
682
- }
633
+ const promises = [];
634
+ for (const [, entryBuildInfo] of entryBuildInfoMap) {
635
+ const promise = entryBuildInfo.buildEntryPoint();
636
+ promises.push(promise);
637
+ }
638
+ await Promise.all(promises);
683
639
 
684
- inject_urls_in_service_workers: {
685
- const inject = buildSpecifierManager.prepareServiceWorkerUrlInjection();
686
- if (inject) {
687
- const urlsInjectionInSw = createBuildTask(
688
- "inject urls in service worker",
689
- );
690
- await inject();
691
- urlsInjectionInSw.done();
692
- buildOperation.throwIfAborted();
693
- }
694
- }
640
+ const buildFileContents = {};
641
+ const buildFileVersions = {};
642
+ const buildInlineContents = {};
643
+ const buildManifest = {};
644
+ for (const [, entryBuildInfo] of entryBuildInfoMap) {
645
+ Object.assign(buildFileContents, entryBuildInfo.buildFileContents);
646
+ Object.assign(buildFileVersions, entryBuildInfo.buildFileVersions);
647
+ Object.assign(buildInlineContents, entryBuildInfo.buildInlineContents);
648
+ Object.assign(buildManifest, entryBuildInfo.buildManifest);
695
649
  }
696
- const { buildFileContents, buildInlineContents, buildManifest } =
697
- buildSpecifierManager.getBuildInfo();
698
650
  if (writeOnFileSystem) {
699
- const writingFiles = createBuildTask("write files in build directory");
700
651
  clearDirectorySync(buildDirectoryUrl, buildDirectoryCleanPatterns);
701
652
  const buildRelativeUrls = Object.keys(buildFileContents);
702
653
  buildRelativeUrls.forEach((buildRelativeUrl) => {
@@ -705,23 +656,17 @@ build ${entryPointKeys.length} entry points`);
705
656
  buildFileContents[buildRelativeUrl],
706
657
  );
707
658
  });
708
- if (versioning && assetManifest && Object.keys(buildManifest).length) {
709
- writeFileSync(
710
- new URL(assetManifestFileRelativeUrl, buildDirectoryUrl),
711
- JSON.stringify(buildManifest, null, " "),
712
- );
713
- }
714
- writingFiles.done();
715
659
  }
716
- logger.info(
717
- createUrlGraphSummary(finalKitchen.graph, {
718
- title: "build files",
719
- }),
720
- );
660
+ onBuildEnd({
661
+ buildFileContents,
662
+ buildInlineContents,
663
+ buildManifest,
664
+ duration: Date.now() - startDate,
665
+ });
721
666
  return {
722
667
  ...(returnBuildInlineContents ? { buildInlineContents } : {}),
723
668
  ...(returnBuildManifest ? { buildManifest } : {}),
724
- ...(subbuilds.length ? { subbuilds: subbuildResults } : {}),
669
+ ...(returnBuildFileVersions ? { buildFileVersions } : {}),
725
670
  };
726
671
  };
727
672
 
@@ -729,7 +674,6 @@ build ${entryPointKeys.length} entry points`);
729
674
  try {
730
675
  const result = await runBuild({
731
676
  signal: operation.signal,
732
- logLevel: logs.level,
733
677
  });
734
678
  return result;
735
679
  } finally {
@@ -801,3 +745,584 @@ build ${entryPointKeys.length} entry points`);
801
745
  await firstBuildPromise;
802
746
  return stopWatchingSourceFiles;
803
747
  };
748
+
749
+ const entryPointDefaultParams = {
750
+ buildRelativeUrl: undefined,
751
+ runtimeCompat: defaultRuntimeCompat,
752
+ plugins: [],
753
+ mappings: undefined,
754
+ assetsDirectory: undefined,
755
+ base: undefined,
756
+ ignore: undefined,
757
+
758
+ bundling: true,
759
+ minification: true,
760
+ versioning: true,
761
+
762
+ referenceAnalysis: {},
763
+ nodeEsmResolution: undefined,
764
+ packageConditions: undefined,
765
+ magicExtensions: undefined,
766
+ magicDirectoryIndex: undefined,
767
+ directoryReferenceEffect: undefined,
768
+ scenarioPlaceholders: undefined,
769
+ injections: undefined,
770
+ transpilation: {},
771
+
772
+ versioningMethod: "search_param", // "filename", "search_param"
773
+ versioningViaImportmap: true,
774
+ versionLength: 8,
775
+ lineBreakNormalization: process.platform === "win32",
776
+
777
+ http: false,
778
+
779
+ sourcemaps: "none",
780
+ sourcemapsSourcesContent: undefined,
781
+ assetManifest: false,
782
+ assetManifestFileRelativeUrl: "asset-manifest.json",
783
+ };
784
+
785
+ const prepareEntryPointBuild = async (
786
+ {
787
+ signal,
788
+ sourceDirectoryUrl,
789
+ buildDirectoryUrl,
790
+ sourceRelativeUrl,
791
+ outDirectoryUrl,
792
+ buildUrlsGenerator,
793
+ someEntryPointUseNode,
794
+ },
795
+ entryPointParams,
796
+ ) => {
797
+ let {
798
+ buildRelativeUrl,
799
+ runtimeCompat,
800
+ plugins,
801
+ mappings,
802
+ assetsDirectory,
803
+ base,
804
+ ignore,
805
+
806
+ bundling,
807
+ minification,
808
+ versioning,
809
+
810
+ referenceAnalysis,
811
+ nodeEsmResolution,
812
+ packageConditions,
813
+ magicExtensions,
814
+ magicDirectoryIndex,
815
+ directoryReferenceEffect,
816
+ scenarioPlaceholders,
817
+ injections,
818
+ transpilation,
819
+
820
+ versioningMethod,
821
+ versioningViaImportmap,
822
+ versionLength,
823
+ lineBreakNormalization,
824
+
825
+ http,
826
+
827
+ sourcemaps,
828
+ sourcemapsSourcesContent,
829
+ assetManifest,
830
+ assetManifestFileRelativeUrl,
831
+ } = {
832
+ ...entryPointDefaultParams,
833
+ ...entryPointParams,
834
+ };
835
+
836
+ // param defaults and normalization
837
+ {
838
+ if (entryPointParams.buildRelativeUrl === undefined) {
839
+ buildRelativeUrl = entryPointParams.buildRelativeUrl = sourceRelativeUrl;
840
+ }
841
+ const buildUrl = new URL(buildRelativeUrl, buildDirectoryUrl);
842
+ entryPointParams.buildRelativeUrl = buildRelativeUrl = urlToRelativeUrl(
843
+ buildUrl,
844
+ buildDirectoryUrl,
845
+ );
846
+ if (entryPointParams.assetsDirectory === undefined) {
847
+ const entryBuildUrl = new URL(buildRelativeUrl, buildDirectoryUrl).href;
848
+ const entryBuildRelativeUrl = urlToRelativeUrl(
849
+ entryBuildUrl,
850
+ buildDirectoryUrl,
851
+ );
852
+ if (entryBuildRelativeUrl.includes("/")) {
853
+ const assetDirectoryUrl = new URL("./", entryBuildUrl);
854
+ assetsDirectory = urlToRelativeUrl(
855
+ assetDirectoryUrl,
856
+ buildDirectoryUrl,
857
+ );
858
+ } else {
859
+ assetsDirectory = "";
860
+ }
861
+ }
862
+ if (
863
+ assetsDirectory &&
864
+ assetsDirectory[assetsDirectory.length - 1] !== "/"
865
+ ) {
866
+ assetsDirectory = `${assetsDirectory}/`;
867
+ }
868
+ if (entryPointParams.base === undefined) {
869
+ base = someEntryPointUseNode ? "./" : "/";
870
+ }
871
+ if (entryPointParams.bundling === undefined) {
872
+ bundling = true;
873
+ }
874
+ if (bundling === true) {
875
+ bundling = {};
876
+ }
877
+ if (entryPointParams.minification === undefined) {
878
+ minification = !someEntryPointUseNode;
879
+ }
880
+ if (minification === true) {
881
+ minification = {};
882
+ }
883
+ if (entryPointParams.versioning === undefined) {
884
+ versioning = !someEntryPointUseNode;
885
+ }
886
+ if (entryPointParams.versioningMethod === undefined) {
887
+ versioningMethod = entryPointDefaultParams.versioningMethod;
888
+ }
889
+ if (entryPointParams.assetManifest === undefined) {
890
+ assetManifest = versioningMethod === "filename";
891
+ }
892
+ }
893
+
894
+ const buildOperation = Abort.startOperation();
895
+ buildOperation.addAbortSignal(signal);
896
+
897
+ const explicitJsModuleConversion =
898
+ sourceRelativeUrl.includes("?js_module_fallback") ||
899
+ sourceRelativeUrl.includes("?as_js_classic");
900
+ const contextSharedDuringBuild = {
901
+ buildStep: "craft",
902
+ buildDirectoryUrl,
903
+ assetsDirectory,
904
+ versioning,
905
+ versioningViaImportmap,
906
+ };
907
+ const rawKitchen = createKitchen({
908
+ signal,
909
+ logLevel: "warn",
910
+ rootDirectoryUrl: sourceDirectoryUrl,
911
+ ignore,
912
+ // during first pass (craft) we keep "ignore:" when a reference is ignored
913
+ // so that the second pass (shape) properly ignore those urls
914
+ ignoreProtocol: "keep",
915
+ build: true,
916
+ runtimeCompat,
917
+ initialContext: contextSharedDuringBuild,
918
+ sourcemaps,
919
+ sourcemapsSourcesContent,
920
+ outDirectoryUrl: outDirectoryUrl
921
+ ? new URL("craft/", outDirectoryUrl)
922
+ : undefined,
923
+ });
924
+
925
+ let _getOtherEntryBuildInfo;
926
+ const rawPluginStore = createPluginStore([
927
+ ...(mappings ? [jsenvPluginMappings(mappings)] : []),
928
+ {
929
+ name: "jsenv:other_entry_point_build_during_craft",
930
+ fetchUrlContent: async (urlInfo) => {
931
+ if (!_getOtherEntryBuildInfo) {
932
+ return null;
933
+ }
934
+ const otherEntryBuildInfo = _getOtherEntryBuildInfo(urlInfo.url);
935
+ if (!otherEntryBuildInfo) {
936
+ return null;
937
+ }
938
+ urlInfo.otherEntryBuildInfo = otherEntryBuildInfo;
939
+ return {
940
+ type: "entry_build", // this ensure the rest of jsenv do not try to scan or modify the content
941
+ content: "", // we don't know yet the content it will be known later
942
+ filenameHint: otherEntryBuildInfo.entryUrlInfo.filenameHint,
943
+ };
944
+ },
945
+ },
946
+ ...plugins,
947
+ ...(bundling ? [jsenvPluginBundling(bundling)] : []),
948
+ ...(minification ? [jsenvPluginMinification(minification)] : []),
949
+ ...getCorePlugins({
950
+ rootDirectoryUrl: sourceDirectoryUrl,
951
+ runtimeCompat,
952
+ referenceAnalysis,
953
+ nodeEsmResolution,
954
+ packageConditions,
955
+ magicExtensions,
956
+ magicDirectoryIndex,
957
+ directoryReferenceEffect,
958
+ injections,
959
+ transpilation: {
960
+ babelHelpersAsImport: !explicitJsModuleConversion,
961
+ ...transpilation,
962
+ jsModuleFallback: false,
963
+ },
964
+ inlining: false,
965
+ http,
966
+ scenarioPlaceholders,
967
+ }),
968
+ ]);
969
+ const rawPluginController = createPluginController(
970
+ rawPluginStore,
971
+ rawKitchen,
972
+ );
973
+ rawKitchen.setPluginController(rawPluginController);
974
+
975
+ const rawRootUrlInfo = rawKitchen.graph.rootUrlInfo;
976
+ let entryReference;
977
+ await rawRootUrlInfo.dependencies.startCollecting(() => {
978
+ entryReference = rawRootUrlInfo.dependencies.found({
979
+ trace: { message: `"${sourceRelativeUrl}" from "entryPoints"` },
980
+ isEntryPoint: true,
981
+ type: "entry_point",
982
+ specifier: sourceRelativeUrl,
983
+ filenameHint: buildRelativeUrl,
984
+ });
985
+ });
986
+
987
+ return {
988
+ entryReference,
989
+ buildEntryPoint: async ({ getOtherEntryBuildInfo }) => {
990
+ craft: {
991
+ _getOtherEntryBuildInfo = getOtherEntryBuildInfo;
992
+ if (outDirectoryUrl) {
993
+ await ensureEmptyDirectory(new URL(`craft/`, outDirectoryUrl));
994
+ }
995
+ await rawRootUrlInfo.cookDependencies({ operation: buildOperation });
996
+ }
997
+
998
+ const finalKitchen = createKitchen({
999
+ name: "shape",
1000
+ logLevel: "warn",
1001
+ rootDirectoryUrl: sourceDirectoryUrl,
1002
+ // here most plugins are not there
1003
+ // - no external plugin
1004
+ // - no plugin putting reference.mustIgnore on https urls
1005
+ // At this stage it's only about redirecting urls to the build directory
1006
+ // consequently only a subset or urls are supported
1007
+ supportedProtocols: ["file:", "data:", "virtual:", "ignore:"],
1008
+ ignore,
1009
+ ignoreProtocol: "remove",
1010
+ build: true,
1011
+ runtimeCompat,
1012
+ initialContext: contextSharedDuringBuild,
1013
+ sourcemaps,
1014
+ sourcemapsComment: "relative",
1015
+ sourcemapsSourcesContent,
1016
+ outDirectoryUrl: outDirectoryUrl
1017
+ ? new URL("shape/", outDirectoryUrl)
1018
+ : undefined,
1019
+ });
1020
+ const buildSpecifierManager = createBuildSpecifierManager({
1021
+ rawKitchen,
1022
+ finalKitchen,
1023
+ logger: createLogger({ logLevel: "warn" }),
1024
+ sourceDirectoryUrl,
1025
+ buildDirectoryUrl,
1026
+ base,
1027
+ assetsDirectory,
1028
+ buildUrlsGenerator,
1029
+
1030
+ versioning,
1031
+ versioningMethod,
1032
+ versionLength,
1033
+ canUseImportmap:
1034
+ versioningViaImportmap &&
1035
+ rawKitchen.graph.getUrlInfo(entryReference.url).type === "html" &&
1036
+ rawKitchen.context.isSupportedOnCurrentClients("importmap"),
1037
+ });
1038
+ const finalPluginStore = createPluginStore([
1039
+ jsenvPluginReferenceAnalysis({
1040
+ ...referenceAnalysis,
1041
+ fetchInlineUrls: false,
1042
+ // inlineContent: false,
1043
+ }),
1044
+ jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect, {
1045
+ rootDirectoryUrl: sourceDirectoryUrl,
1046
+ }),
1047
+ ...(lineBreakNormalization
1048
+ ? [jsenvPluginLineBreakNormalization()]
1049
+ : []),
1050
+ jsenvPluginJsModuleFallback({
1051
+ remapImportSpecifier: (specifier, parentUrl) => {
1052
+ return buildSpecifierManager.remapPlaceholder(specifier, parentUrl);
1053
+ },
1054
+ }),
1055
+ jsenvPluginInlining(),
1056
+ {
1057
+ name: "jsenv:optimize",
1058
+ appliesDuring: "build",
1059
+ transformUrlContent: async (urlInfo) => {
1060
+ await rawKitchen.pluginController.callAsyncHooks(
1061
+ "optimizeUrlContent",
1062
+ urlInfo,
1063
+ (optimizeReturnValue) => {
1064
+ urlInfo.mutateContent(optimizeReturnValue);
1065
+ },
1066
+ );
1067
+ },
1068
+ },
1069
+ buildSpecifierManager.jsenvPluginMoveToBuildDirectory,
1070
+ ]);
1071
+ const finalPluginController = createPluginController(
1072
+ finalPluginStore,
1073
+ finalKitchen,
1074
+ {
1075
+ initialPuginsMeta: rawKitchen.pluginController.pluginsMeta,
1076
+ },
1077
+ );
1078
+ finalKitchen.setPluginController(finalPluginController);
1079
+
1080
+ bundle: {
1081
+ const bundlerMap = new Map();
1082
+ for (const plugin of rawKitchen.pluginController.activePlugins) {
1083
+ const bundle = plugin.bundle;
1084
+ if (!bundle) {
1085
+ continue;
1086
+ }
1087
+ if (typeof bundle !== "object") {
1088
+ throw new Error(
1089
+ `bundle must be an object, found "${bundle}" on plugin named "${plugin.name}"`,
1090
+ );
1091
+ }
1092
+ for (const type of Object.keys(bundle)) {
1093
+ const bundleFunction = bundle[type];
1094
+ if (!bundleFunction) {
1095
+ continue;
1096
+ }
1097
+ if (bundlerMap.has(type)) {
1098
+ // first plugin to define a bundle hook wins
1099
+ continue;
1100
+ }
1101
+ bundlerMap.set(type, {
1102
+ plugin,
1103
+ bundleFunction: bundle[type],
1104
+ urlInfoMap: new Map(),
1105
+ });
1106
+ }
1107
+ }
1108
+ if (bundlerMap.size === 0) {
1109
+ break bundle;
1110
+ }
1111
+ const addToBundlerIfAny = (rawUrlInfo) => {
1112
+ const bundler = bundlerMap.get(rawUrlInfo.type);
1113
+ if (bundler) {
1114
+ bundler.urlInfoMap.set(rawUrlInfo.url, rawUrlInfo);
1115
+ }
1116
+ };
1117
+ // ignore unused urls thanks to "forEachUrlInfoStronglyReferenced"
1118
+ // it avoid bundling things that are not actually used
1119
+ // happens for:
1120
+ // - js import assertions
1121
+ // - conversion to js classic using ?as_js_classic or ?js_module_fallback
1122
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
1123
+ rawKitchen.graph.rootUrlInfo,
1124
+ (rawUrlInfo) => {
1125
+ if (rawUrlInfo.isEntryPoint) {
1126
+ addToBundlerIfAny(rawUrlInfo);
1127
+ }
1128
+ if (rawUrlInfo.type === "html") {
1129
+ for (const referenceToOther of rawUrlInfo.referenceToOthersSet) {
1130
+ if (
1131
+ referenceToOther.isResourceHint &&
1132
+ referenceToOther.expectedType === "js_module"
1133
+ ) {
1134
+ const referencedUrlInfo = referenceToOther.urlInfo;
1135
+ if (
1136
+ referencedUrlInfo &&
1137
+ // something else than the resource hint is using this url
1138
+ referencedUrlInfo.referenceFromOthersSet.size > 0
1139
+ ) {
1140
+ addToBundlerIfAny(referencedUrlInfo);
1141
+ continue;
1142
+ }
1143
+ }
1144
+ if (referenceToOther.isWeak) {
1145
+ continue;
1146
+ }
1147
+ const referencedUrlInfo = referenceToOther.urlInfo;
1148
+ if (referencedUrlInfo.isInline) {
1149
+ if (referencedUrlInfo.type !== "js_module") {
1150
+ continue;
1151
+ }
1152
+ addToBundlerIfAny(referencedUrlInfo);
1153
+ continue;
1154
+ }
1155
+ addToBundlerIfAny(referencedUrlInfo);
1156
+ }
1157
+ return;
1158
+ }
1159
+ // File referenced with
1160
+ // - new URL("./file.js", import.meta.url)
1161
+ // - import.meta.resolve("./file.js")
1162
+ // are entry points that should be bundled
1163
+ // For instance we will bundle service worker/workers detected like this
1164
+ if (rawUrlInfo.type === "js_module") {
1165
+ for (const referenceToOther of rawUrlInfo.referenceToOthersSet) {
1166
+ if (
1167
+ referenceToOther.type === "js_url" ||
1168
+ referenceToOther.subtype === "import_meta_resolve"
1169
+ ) {
1170
+ const referencedUrlInfo = referenceToOther.urlInfo;
1171
+ let isAlreadyBundled = false;
1172
+ for (const referenceFromOther of referencedUrlInfo.referenceFromOthersSet) {
1173
+ if (referenceFromOther.url === referencedUrlInfo.url) {
1174
+ if (
1175
+ referenceFromOther.subtype === "import_dynamic" ||
1176
+ referenceFromOther.type === "script"
1177
+ ) {
1178
+ isAlreadyBundled = true;
1179
+ break;
1180
+ }
1181
+ }
1182
+ }
1183
+ if (!isAlreadyBundled) {
1184
+ addToBundlerIfAny(referencedUrlInfo);
1185
+ }
1186
+ continue;
1187
+ }
1188
+ if (referenceToOther.type === "js_inline_content") {
1189
+ // we should bundle it too right?
1190
+ }
1191
+ }
1192
+ }
1193
+ },
1194
+ );
1195
+ for (const [, bundler] of bundlerMap) {
1196
+ const urlInfosToBundle = Array.from(bundler.urlInfoMap.values());
1197
+ if (urlInfosToBundle.length === 0) {
1198
+ continue;
1199
+ }
1200
+ await buildSpecifierManager.applyBundling({
1201
+ bundler,
1202
+ urlInfosToBundle,
1203
+ });
1204
+ }
1205
+ }
1206
+
1207
+ shape: {
1208
+ finalKitchen.context.buildStep = "shape";
1209
+ if (outDirectoryUrl) {
1210
+ await ensureEmptyDirectory(new URL(`shape/`, outDirectoryUrl));
1211
+ }
1212
+ const finalRootUrlInfo = finalKitchen.graph.rootUrlInfo;
1213
+ await finalRootUrlInfo.dependencies.startCollecting(() => {
1214
+ finalRootUrlInfo.dependencies.found({
1215
+ trace: { message: `entryPoint` },
1216
+ isEntryPoint: true,
1217
+ type: "entry_point",
1218
+ specifier: entryReference.url,
1219
+ });
1220
+ });
1221
+ await finalRootUrlInfo.cookDependencies({
1222
+ operation: buildOperation,
1223
+ });
1224
+ }
1225
+
1226
+ refine: {
1227
+ finalKitchen.context.buildStep = "refine";
1228
+
1229
+ const htmlRefineSet = new Set();
1230
+ const registerHtmlRefine = (htmlRefine) => {
1231
+ htmlRefineSet.add(htmlRefine);
1232
+ };
1233
+
1234
+ replace_placeholders: {
1235
+ await buildSpecifierManager.replacePlaceholders();
1236
+ }
1237
+
1238
+ /*
1239
+ * Update <link rel="preload"> and friends after build (once we know everything)
1240
+ * - Used to remove resource hint targeting an url that is no longer used:
1241
+ * - because of bundlings
1242
+ * - because of import assertions transpilation (file is inlined into JS)
1243
+ */
1244
+ resync_resource_hints: {
1245
+ buildSpecifierManager.prepareResyncResourceHints({
1246
+ registerHtmlRefine,
1247
+ });
1248
+ }
1249
+
1250
+ mutate_html: {
1251
+ GRAPH_VISITOR.forEach(finalKitchen.graph, (urlInfo) => {
1252
+ if (!urlInfo.url.startsWith("file:")) {
1253
+ return;
1254
+ }
1255
+ if (urlInfo.type !== "html") {
1256
+ return;
1257
+ }
1258
+ const htmlAst = parseHtml({
1259
+ html: urlInfo.content,
1260
+ url: urlInfo.url,
1261
+ storeOriginalPositions: false,
1262
+ });
1263
+ for (const htmlRefine of htmlRefineSet) {
1264
+ const htmlMutationCallbackSet = new Set();
1265
+ const registerHtmlMutation = (callback) => {
1266
+ htmlMutationCallbackSet.add(callback);
1267
+ };
1268
+ htmlRefine(htmlAst, { registerHtmlMutation });
1269
+ for (const htmlMutationCallback of htmlMutationCallbackSet) {
1270
+ htmlMutationCallback();
1271
+ }
1272
+ }
1273
+ // cleanup jsenv attributes from html as a last step
1274
+ urlInfo.content = stringifyHtmlAst(htmlAst, {
1275
+ cleanupJsenvAttributes: true,
1276
+ cleanupPositionAttributes: true,
1277
+ });
1278
+ });
1279
+ }
1280
+
1281
+ inject_urls_in_service_workers: {
1282
+ const inject =
1283
+ buildSpecifierManager.prepareServiceWorkerUrlInjection();
1284
+ if (inject) {
1285
+ await inject();
1286
+ buildOperation.throwIfAborted();
1287
+ }
1288
+ }
1289
+ }
1290
+ const {
1291
+ buildFileContents,
1292
+ buildFileVersions,
1293
+ buildInlineContents,
1294
+ buildManifest,
1295
+ } = buildSpecifierManager.getBuildInfo();
1296
+ if (versioning && assetManifest && Object.keys(buildManifest).length) {
1297
+ buildFileContents[assetManifestFileRelativeUrl] = JSON.stringify(
1298
+ buildManifest,
1299
+ null,
1300
+ " ",
1301
+ );
1302
+ }
1303
+ return {
1304
+ buildFileContents,
1305
+ buildFileVersions,
1306
+ buildInlineContents,
1307
+ buildManifest,
1308
+ };
1309
+ },
1310
+ };
1311
+ };
1312
+
1313
+ const isBareSpecifier = (specifier) => {
1314
+ if (
1315
+ specifier[0] === "/" ||
1316
+ specifier.startsWith("./") ||
1317
+ specifier.startsWith("../")
1318
+ ) {
1319
+ return false;
1320
+ }
1321
+ try {
1322
+ // eslint-disable-next-line no-new
1323
+ new URL(specifier);
1324
+ return false;
1325
+ } catch {
1326
+ return true;
1327
+ }
1328
+ };