@jsenv/core 41.4.17 → 41.4.19

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 (30) hide show
  1. package/dist/build/build.js +693 -509
  2. package/dist/build/jsenv_core_packages.js +3 -187
  3. package/dist/client/directory_listing/js/directory_listing.js +23 -2
  4. package/dist/start_dev_server/start_dev_server.js +813 -625
  5. package/package.json +8 -8
  6. package/src/build/build.js +66 -15
  7. package/src/build/build_content_report.js +24 -15
  8. package/src/build/build_specifier_manager.js +2 -2
  9. package/src/dev/dev_server_plugins/dev_server_plugin_omega_error_handler.js +4 -1
  10. package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +116 -68
  11. package/src/dev/start_dev_server.js +24 -4
  12. package/src/helpers/watch_source_files.js +25 -20
  13. package/src/kitchen/kitchen.js +6 -0
  14. package/src/kitchen/prepend_content.js +4 -4
  15. package/src/kitchen/url_graph/references.js +30 -13
  16. package/src/kitchen/url_graph/url_graph.js +0 -1
  17. package/src/kitchen/url_graph/url_info_transformations.js +16 -6
  18. package/src/plugins/import_meta_css/jsenv_plugin_import_meta_css.js +10 -40
  19. package/src/plugins/import_meta_hot/import_meta_hot_analysis.js +70 -0
  20. package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +35 -20
  21. package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +22 -56
  22. package/src/plugins/modulepreload/jsenv_plugin_modulepreload.js +128 -0
  23. package/src/plugins/plugins.js +3 -0
  24. package/src/plugins/protocol_file/client/directory_listing.jsx +27 -1
  25. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +20 -0
  26. package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +32 -15
  27. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +11 -2
  28. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -2
  29. package/src/plugins/resolution_node_esm/node_esm_resolver.js +0 -5
  30. package/src/plugins/import_meta_hot/babel_plugin_metadata_import_meta_hot.js +0 -97
@@ -1,10 +1,10 @@
1
- import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
1
+ import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, visitJsAst, getImportMetaPropertyName, visitJsAstUntil, analyzeLinkNode, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
2
2
  import { bundleJsModules, jsenvPluginBundling } from "@jsenv/plugin-bundling";
3
3
  import { jsenvPluginMinification } from "@jsenv/plugin-minification";
4
4
  import { jsenvPluginTranspilation, jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
5
5
  import { memoryUsage } from "node:process";
6
- import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
7
- import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, urlIsOrIsInsideOf, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringCpuUsage, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
6
+ import { readFileSync, existsSync, realpathSync, readdirSync, lstatSync, statSync } from "node:fs";
7
+ import { lookupPackageDirectory, urlIsOrIsInsideOf, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
10
10
  import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
@@ -20,18 +20,21 @@ import "node:util";
20
20
  const getDirectoryWatchPatterns = (
21
21
  directoryUrl,
22
22
  watchedDirectoryUrl,
23
- { sourceFilesConfig },
23
+ { sourceFilesConfig, defaultPatterns = true },
24
24
  ) => {
25
25
  const directoryUrlRelativeToWatchedDirectory = urlToRelativeUrl(
26
26
  directoryUrl,
27
27
  watchedDirectoryUrl,
28
28
  );
29
- const watchPatterns = {
30
- [`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
31
- [`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
32
- [`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
33
- [`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
34
- };
29
+ const watchPatterns = {};
30
+ if (defaultPatterns) {
31
+ Object.assign(watchPatterns, {
32
+ [`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
33
+ [`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
34
+ [`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
35
+ [`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
36
+ });
37
+ }
35
38
  for (const key of Object.keys(sourceFilesConfig)) {
36
39
  watchPatterns[`${directoryUrlRelativeToWatchedDirectory}${key}`] =
37
40
  sourceFilesConfig[key];
@@ -52,11 +55,12 @@ const watchSourceFiles = (
52
55
  // And jsenv should not consider these as source files and watch them (to not hurt performances)
53
56
  const watchPatterns = {};
54
57
  let watchedDirectoryUrl = "";
55
- const addDirectoryToWatch = (directoryUrl) => {
58
+ const addDirectoryToWatch = (directoryUrl, { defaultPatterns } = {}) => {
56
59
  Object.assign(
57
60
  watchPatterns,
58
61
  getDirectoryWatchPatterns(directoryUrl, watchedDirectoryUrl, {
59
62
  sourceFilesConfig,
63
+ defaultPatterns,
60
64
  }),
61
65
  );
62
66
  };
@@ -88,7 +92,6 @@ const watchSourceFiles = (
88
92
  },
89
93
  },
90
94
  );
91
- stopWatchingSourceFiles.watchPatterns = watchPatterns;
92
95
  return stopWatchingSourceFiles;
93
96
  };
94
97
 
@@ -108,16 +111,18 @@ const watchSourceFiles = (
108
111
  }
109
112
  watchedDirectoryUrl = packageDirectoryUrl;
110
113
  for (const workspace of workspaces) {
111
- if (workspace.endsWith("*")) {
112
- const workspaceDirectoryUrl = new URL(
113
- workspace.slice(0, -1),
114
- packageDirectoryUrl,
115
- );
116
- addDirectoryToWatch(workspaceDirectoryUrl);
117
- } else {
118
- const workspaceRelativeUrl = new URL(workspace, packageDirectoryUrl);
119
- addDirectoryToWatch(workspaceRelativeUrl);
120
- }
114
+ const workspaceDirectoryUrl = workspace.endsWith("*")
115
+ ? new URL(workspace.slice(0, -1), packageDirectoryUrl)
116
+ : new URL(workspace, packageDirectoryUrl);
117
+ addDirectoryToWatch(workspaceDirectoryUrl, {
118
+ // The source directory patterns already cover a workspace inside it.
119
+ // Every pattern is tested against every file found while watching, so
120
+ // the same patterns rooted at each workspace only multiply that work.
121
+ defaultPatterns: !urlIsOrIsInsideOf(
122
+ workspaceDirectoryUrl,
123
+ sourceDirectoryUrl,
124
+ ),
125
+ });
121
126
  }
122
127
  // we are updating the root directory
123
128
  // we must make the patterns relative to source directory relative to the new root directory
@@ -905,10 +910,9 @@ const prependJsClassicInJsClassic = (jsUrlInfo, urlInfoToPrepend) => {
905
910
  const magicSource = createMagicSource(jsUrlInfo.content);
906
911
  magicSource.prepend(`${urlInfoToPrepend.content}\n\n`);
907
912
  const magicResult = magicSource.toContentAndSourcemap();
908
- const sourcemap = composeTwoSourcemaps(
909
- jsUrlInfo.sourcemap,
910
- magicResult.sourcemap,
911
- );
913
+ const sourcemap = jsUrlInfo.context.sourcemapsEnabled
914
+ ? composeTwoSourcemaps(jsUrlInfo.sourcemap, magicResult.sourcemap)
915
+ : null;
912
916
  jsUrlInfo.mutateContent({
913
917
  content: magicResult.content,
914
918
  sourcemap,
@@ -926,6 +930,7 @@ const prependJsClassicInJsModule = async (jsUrlInfo, urlInfoToPrepend) => {
926
930
  input: jsUrlInfo.content,
927
931
  inputIsJsModule: true,
928
932
  inputUrl: jsUrlInfo.originalUrl,
933
+ options: { sourceMaps: jsUrlInfo.context.sourcemapsEnabled },
929
934
  });
930
935
  jsUrlInfo.mutateContent({
931
936
  content: code,
@@ -1578,20 +1583,37 @@ const checkForDependencyRemovalEffects = (reference) => {
1578
1583
  };
1579
1584
 
1580
1585
  const traceFromUrlSite = (urlSite) => {
1581
- const codeFrame = urlSite.content
1582
- ? generateContentFrame({
1583
- content: urlSite.content,
1584
- line: urlSite.line,
1585
- column: urlSite.column,
1586
- })
1587
- : "";
1588
- return {
1589
- codeFrame,
1590
- message: stringifyUrlSite(urlSite),
1591
- url: urlSite.url,
1592
- line: urlSite.line,
1593
- column: urlSite.column,
1586
+ const { url, line, column, content } = urlSite;
1587
+ const trace = { url, line, column };
1588
+ // A file references many urls and an error on one of them is rare: the code
1589
+ // frame (and the message embedding it) is built the first time it is read,
1590
+ // not for every reference found.
1591
+ defineLazyProperty(trace, "codeFrame", () =>
1592
+ content ? generateContentFrame({ content, line, column }) : "",
1593
+ );
1594
+ defineLazyProperty(trace, "message", () => stringifyUrlSite(urlSite));
1595
+ return trace;
1596
+ };
1597
+
1598
+ const defineLazyProperty = (object, property, compute) => {
1599
+ const setValue = (value) => {
1600
+ Object.defineProperty(object, property, {
1601
+ enumerable: true,
1602
+ configurable: true,
1603
+ writable: true,
1604
+ value,
1605
+ });
1594
1606
  };
1607
+ Object.defineProperty(object, property, {
1608
+ enumerable: true,
1609
+ configurable: true,
1610
+ get: () => {
1611
+ const value = compute();
1612
+ setValue(value);
1613
+ return value;
1614
+ },
1615
+ set: setValue,
1616
+ });
1595
1617
  };
1596
1618
 
1597
1619
  const adjustUrlSite = (urlInfo, { url, line, column }) => {
@@ -2090,7 +2112,6 @@ const createUrlInfo = (url, context) => {
2090
2112
  dereferencedTimestamp: 0,
2091
2113
  originalContentEtag: null,
2092
2114
  contentEtag: null,
2093
- isWatched: false,
2094
2115
  isValid: () => false,
2095
2116
  data: {}, // plugins can put whatever they want here
2096
2117
  referenceToOthersSet: new Set(),
@@ -2899,7 +2920,6 @@ const createUrlInfoTransformer = ({
2899
2920
  contentAst, // undefined most of the time
2900
2921
  contentEtag, // in practice always undefined
2901
2922
  contentLength,
2902
- sourcemap,
2903
2923
  sourcemapIsWrong,
2904
2924
  contentInjections,
2905
2925
  } = transformations;
@@ -2926,11 +2946,14 @@ const createUrlInfoTransformer = ({
2926
2946
  contentLength,
2927
2947
  });
2928
2948
  }
2929
- if (
2930
- sourcemap &&
2931
- mayHaveSourcemap(urlInfo) &&
2932
- shouldHandleSourcemap(urlInfo)
2933
- ) {
2949
+ // "sourcemap" is read last, and only when it will be used: a plugin can
2950
+ // hand it back as a getter that generates the map on first read, so that
2951
+ // nothing is generated for a kitchen that throws sourcemaps away
2952
+ const sourcemap =
2953
+ mayHaveSourcemap(urlInfo) && shouldHandleSourcemap(urlInfo)
2954
+ ? transformations.sourcemap
2955
+ : null;
2956
+ if (sourcemap) {
2934
2957
  const sourcemapNormalized = normalizeSourcemap(urlInfo, sourcemap);
2935
2958
  let currentSourcemap = urlInfo.sourcemap;
2936
2959
  const finalSourcemap = composeTwoSourcemaps(
@@ -2963,6 +2986,14 @@ const createUrlInfoTransformer = ({
2963
2986
  writeInsideOutDirectory(urlInfo);
2964
2987
  };
2965
2988
 
2989
+ // Written synchronously on purpose, and measured: async is the tempting
2990
+ // choice, but here it loses. A cold load cooks hundreds of files; their
2991
+ // synchronous writes block the event loop ~160ms in total, while
2992
+ // asynchronous writes queue in the threadpool behind tens of MB of content
2993
+ // and sourcemaps, and a response that waits for its own write then waits
2994
+ // ~36ms on average (18s summed over 500 responses). Not waiting is not an
2995
+ // option either: the last write lands after the test that cooked it has
2996
+ // ended, and the side-effect snapshots lose it.
2966
2997
  const writeInsideOutDirectory = (urlInfo) => {
2967
2998
  // writing result inside ".jsenv" directory (debug purposes)
2968
2999
  if (!outDirectoryUrl) {
@@ -3282,6 +3313,12 @@ const createKitchen = ({
3282
3313
  INJECTIONS,
3283
3314
  getPluginMeta: null,
3284
3315
  sourcemaps,
3316
+ // a plugin producing a sourcemap can skip the work when it would be
3317
+ // thrown away (see shouldHandleSourcemap in url_info_transformations.js)
3318
+ sourcemapsEnabled:
3319
+ sourcemaps === "inline" ||
3320
+ sourcemaps === "file" ||
3321
+ sourcemaps === "programmatic",
3285
3322
  outDirectoryUrl,
3286
3323
  },
3287
3324
  resolve: (specifier, importer = rootDirectoryUrl) => {
@@ -5751,8 +5788,7 @@ const parseAndTransformJsReferences = async (
5751
5788
  for (const sequentialAction of sequentialActions) {
5752
5789
  await sequentialAction();
5753
5790
  }
5754
- const { content, sourcemap } = magicSource.toContentAndSourcemap();
5755
- return { content, sourcemap };
5791
+ return magicSource.toContentAndSourcemap();
5756
5792
  };
5757
5793
 
5758
5794
  const jsenvPluginReferenceExpectedTypes = () => {
@@ -6413,11 +6449,6 @@ const addRelationshipWithPackageJson = ({
6413
6449
  String(packageJsonContentAsBuffer),
6414
6450
  );
6415
6451
  }
6416
- // Checked on disk at every validation rather than trusted to the watcher:
6417
- // what this file decides (the package version, hence the ?v= importers
6418
- // embed) ends up in the browser's immutable cache, so a request racing the
6419
- // watcher must never be answered from a stale package.json.
6420
- packageJsonReference.urlInfo.revalidateOnFileSystem = true;
6421
6452
  };
6422
6453
 
6423
6454
  const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
@@ -6759,6 +6790,240 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
6759
6790
  };
6760
6791
  };
6761
6792
 
6793
+ const jsenvPluginFsRedirection = ({
6794
+ spa,
6795
+ directoryContentMagicName,
6796
+ magicExtensions = ["inherit", ".js"],
6797
+ magicDirectoryIndex = true,
6798
+ preserveSymlinks = false,
6799
+ }) => {
6800
+ return {
6801
+ name: "jsenv:fs_redirection",
6802
+ appliesDuring: "*",
6803
+ redirectReference: (reference) => {
6804
+ if (reference.url === "file:///") {
6805
+ return `ignore:file:///`;
6806
+ }
6807
+ if (reference.url === "file://") {
6808
+ return `ignore:file://`;
6809
+ }
6810
+ // ignore all new URL second arg
6811
+ if (reference.subtype === "new_url_second_arg") {
6812
+ if (reference.original) {
6813
+ return `ignore:${reference.original.specifier}`;
6814
+ }
6815
+ return `ignore:${reference.specifier}`;
6816
+ }
6817
+ // http, https, data, about, ...
6818
+ if (!reference.url.startsWith("file:")) {
6819
+ return null;
6820
+ }
6821
+ if (reference.original && !reference.original.url.startsWith("file:")) {
6822
+ return null;
6823
+ }
6824
+ if (reference.isInline) {
6825
+ return null;
6826
+ }
6827
+
6828
+ if (
6829
+ reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
6830
+ ) {
6831
+ const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
6832
+ const directoryUrl = new URL(
6833
+ reference.specifierPathname
6834
+ .replace(`/${directoryContentMagicName}`, "/")
6835
+ .slice(1),
6836
+ rootDirectoryUrl,
6837
+ ).href;
6838
+ return directoryUrl;
6839
+ }
6840
+ // ignore "./" on new URL("./")
6841
+ // if (
6842
+ // reference.subtype === "new_url_first_arg" &&
6843
+ // reference.specifier === "./"
6844
+ // ) {
6845
+ // return `ignore:${reference.url}`;
6846
+ // }
6847
+ const urlObject = new URL(reference.url);
6848
+ let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
6849
+ reference.fsStat = fsStat;
6850
+ const { search, hash } = urlObject;
6851
+ urlObject.search = "";
6852
+ urlObject.hash = "";
6853
+ // must be read before applyFsStatEffectsOnUrlObject which forces the
6854
+ // trailing slash on directories
6855
+ const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
6856
+ applyFsStatEffectsOnUrlObject(urlObject, fsStat);
6857
+ const shouldApplyFilesystemMagicResolution =
6858
+ reference.type === "js_import";
6859
+ if (shouldApplyFilesystemMagicResolution) {
6860
+ const filesystemResolution = applyFileSystemMagicResolution(
6861
+ urlObject.href,
6862
+ {
6863
+ fileStat: fsStat,
6864
+ magicDirectoryIndex,
6865
+ magicExtensions: getExtensionsToTry(
6866
+ magicExtensions,
6867
+ reference.ownerUrlInfo.url,
6868
+ ),
6869
+ },
6870
+ );
6871
+ if (filesystemResolution.stat) {
6872
+ fsStat = filesystemResolution.stat;
6873
+ reference.fsStat = fsStat;
6874
+ urlObject.href = filesystemResolution.url;
6875
+ applyFsStatEffectsOnUrlObject(urlObject, fsStat);
6876
+ }
6877
+ }
6878
+ if (spa) {
6879
+ // for SPA we want to serve the root HTML file most of the time
6880
+ if (!fsStat) {
6881
+ if (urlToExtension(urlObject)) {
6882
+ // url has an extension, we assume it's a file request -> let 404 happen
6883
+ return null;
6884
+ }
6885
+ if (specifierUsesTrailingSlash) {
6886
+ // the trailing slash asks for a directory and there is none here
6887
+ // -> let 404 happen (same reasoning as the extension above)
6888
+ return null;
6889
+ }
6890
+ const spaFallbackUrl = getSpaFallbackUrl(reference);
6891
+ if (spaFallbackUrl) {
6892
+ return spaFallbackUrl;
6893
+ }
6894
+ return null;
6895
+ }
6896
+ if (fsStat.isDirectory()) {
6897
+ // When requesting a directory, check if we have an HTML entry file for that directory
6898
+ const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
6899
+ if (directoryEntryFileUrl) {
6900
+ reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
6901
+ return directoryEntryFileUrl;
6902
+ }
6903
+ if (!specifierUsesTrailingSlash) {
6904
+ // the trailing slash is what tells a directory apart from a route:
6905
+ // "/join/" is the directory, "/join" is a route owned by the SPA
6906
+ // even when "join/" exists in the source files.
6907
+ // Without this a source directory would shadow the route having
6908
+ // the same name and the SPA would be unreachable in dev while
6909
+ // being perfectly fine once built
6910
+ const spaFallbackUrl = getSpaFallbackUrl(reference);
6911
+ if (spaFallbackUrl) {
6912
+ reference.fsStat = readEntryStatSync(spaFallbackUrl, {
6913
+ nullIfNotFound: true,
6914
+ });
6915
+ return spaFallbackUrl;
6916
+ }
6917
+ }
6918
+ }
6919
+ }
6920
+ if (!fsStat) {
6921
+ return null;
6922
+ }
6923
+ const urlBeforeSymlinkResolution = urlObject.href;
6924
+ if (preserveSymlinks) {
6925
+ return `${urlBeforeSymlinkResolution}${search}${hash}`;
6926
+ }
6927
+ const urlAfterSymlinkResolution = resolveSymlink(
6928
+ urlBeforeSymlinkResolution,
6929
+ );
6930
+ if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
6931
+ reference.leadsToASymlink = true;
6932
+ // reference.baseUrl = urlBeforeSymlinkResolution;
6933
+ }
6934
+ const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
6935
+ return resolvedUrl;
6936
+ },
6937
+ };
6938
+ };
6939
+
6940
+ const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
6941
+ if (!fsStat) {
6942
+ return;
6943
+ }
6944
+ const { pathname } = urlObject;
6945
+ const pathnameUsesTrailingSlash = pathname.endsWith("/");
6946
+ // force trailing slash on directories
6947
+ if (fsStat.isDirectory()) {
6948
+ if (!pathnameUsesTrailingSlash) {
6949
+ urlObject.pathname = `${pathname}/`;
6950
+ }
6951
+ } else if (pathnameUsesTrailingSlash) {
6952
+ // otherwise remove trailing slash if any
6953
+ // a warning here? (because it's strange to reference a file with a trailing slash)
6954
+ urlObject.pathname = pathname.slice(0, -1);
6955
+ }
6956
+ };
6957
+
6958
+ const resolveSymlink = (fileUrl) => {
6959
+ const urlObject = new URL(fileUrl);
6960
+ const realpath = realpathSync(urlObject);
6961
+ const realUrlObject = pathToFileURL(realpath);
6962
+ if (urlObject.pathname.endsWith("/")) {
6963
+ realUrlObject.pathname += `/`;
6964
+ }
6965
+ return realUrlObject.href;
6966
+ };
6967
+
6968
+ const getDirectoryEntryFileUrl = (directoryUrl) => {
6969
+ const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
6970
+ if (existsSync(indexHtmlFileUrl)) {
6971
+ return indexHtmlFileUrl.href;
6972
+ }
6973
+ const filename = urlToFilename(directoryUrl);
6974
+ const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
6975
+ if (existsSync(htmlFileUrlCandidate)) {
6976
+ return htmlFileUrlCandidate.href;
6977
+ }
6978
+ return null;
6979
+ };
6980
+ const getSpaFallbackUrl = (reference) => {
6981
+ const { requestedUrl, rootDirectoryUrl, mainFilePath } =
6982
+ reference.ownerUrlInfo.context;
6983
+ if (!requestedUrl) {
6984
+ // the SPA fallback answers a request; during build there is none
6985
+ return null;
6986
+ }
6987
+ const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
6988
+ rootDirectoryUrl,
6989
+ mainFilePath,
6990
+ });
6991
+ for (const spaFallbackFileUrl of spaFallbackFileUrls) {
6992
+ if (existsSync(new URL(spaFallbackFileUrl))) {
6993
+ return spaFallbackFileUrl;
6994
+ }
6995
+ }
6996
+ // none exists: the main file it is, and the 404 answering for it lists
6997
+ // what was tried (see directory listing)
6998
+ return new URL(mainFilePath, rootDirectoryUrl).href;
6999
+ };
7000
+ // The html files that can answer a route, closest first: the entry file of
7001
+ // the route's own directory ("index.html", then "<dirname>.html"), then of
7002
+ // each directory above it up to the server root, then the main file.
7003
+ const listSpaFallbackFileUrls = (
7004
+ requestedUrl,
7005
+ { rootDirectoryUrl, mainFilePath },
7006
+ ) => {
7007
+ const fileUrls = [];
7008
+ let directoryUrl = new URL("./", requestedUrl).href;
7009
+ while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
7010
+ fileUrls.push(new URL("index.html", directoryUrl).href);
7011
+ const filename = urlToFilename(directoryUrl);
7012
+ if (filename) {
7013
+ fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
7014
+ }
7015
+ if (directoryUrl === String(rootDirectoryUrl)) {
7016
+ break;
7017
+ }
7018
+ directoryUrl = new URL("../", directoryUrl).href;
7019
+ }
7020
+ const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
7021
+ if (!fileUrls.includes(mainFileUrl)) {
7022
+ fileUrls.push(mainFileUrl);
7023
+ }
7024
+ return fileUrls;
7025
+ };
7026
+
6762
7027
  /*
6763
7028
  * NICE TO HAVE:
6764
7029
  *
@@ -7136,6 +7401,24 @@ const generateDirectoryListingInjection = (
7136
7401
  filePathExisting: `/${filePathExisting}`,
7137
7402
  filePathNotFound,
7138
7403
  });
7404
+ // a url without extension nor trailing slash is a route: in spa mode it
7405
+ // was answered with the closest html file, the 404 means none was found
7406
+ const urlNotFoundObject = new URL(urlNotFound);
7407
+ if (
7408
+ spa &&
7409
+ !urlToExtension(urlNotFoundObject) &&
7410
+ !urlNotFoundObject.pathname.endsWith("/")
7411
+ ) {
7412
+ enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
7413
+ urlNotFound,
7414
+ { rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
7415
+ ).map((fileUrl) =>
7416
+ FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
7417
+ fileUrl,
7418
+ serverRootDirectoryUrl,
7419
+ ),
7420
+ );
7421
+ }
7139
7422
  }
7140
7423
 
7141
7424
  return {
@@ -7207,223 +7490,6 @@ const getDirectoryContentItems = ({
7207
7490
  return items;
7208
7491
  };
7209
7492
 
7210
- const jsenvPluginFsRedirection = ({
7211
- spa,
7212
- directoryContentMagicName,
7213
- magicExtensions = ["inherit", ".js"],
7214
- magicDirectoryIndex = true,
7215
- preserveSymlinks = false,
7216
- }) => {
7217
- return {
7218
- name: "jsenv:fs_redirection",
7219
- appliesDuring: "*",
7220
- redirectReference: (reference) => {
7221
- if (reference.url === "file:///") {
7222
- return `ignore:file:///`;
7223
- }
7224
- if (reference.url === "file://") {
7225
- return `ignore:file://`;
7226
- }
7227
- // ignore all new URL second arg
7228
- if (reference.subtype === "new_url_second_arg") {
7229
- if (reference.original) {
7230
- return `ignore:${reference.original.specifier}`;
7231
- }
7232
- return `ignore:${reference.specifier}`;
7233
- }
7234
- // http, https, data, about, ...
7235
- if (!reference.url.startsWith("file:")) {
7236
- return null;
7237
- }
7238
- if (reference.original && !reference.original.url.startsWith("file:")) {
7239
- return null;
7240
- }
7241
- if (reference.isInline) {
7242
- return null;
7243
- }
7244
-
7245
- if (
7246
- reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
7247
- ) {
7248
- const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
7249
- const directoryUrl = new URL(
7250
- reference.specifierPathname
7251
- .replace(`/${directoryContentMagicName}`, "/")
7252
- .slice(1),
7253
- rootDirectoryUrl,
7254
- ).href;
7255
- return directoryUrl;
7256
- }
7257
- // ignore "./" on new URL("./")
7258
- // if (
7259
- // reference.subtype === "new_url_first_arg" &&
7260
- // reference.specifier === "./"
7261
- // ) {
7262
- // return `ignore:${reference.url}`;
7263
- // }
7264
- const urlObject = new URL(reference.url);
7265
- let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
7266
- reference.fsStat = fsStat;
7267
- const { search, hash } = urlObject;
7268
- urlObject.search = "";
7269
- urlObject.hash = "";
7270
- // must be read before applyFsStatEffectsOnUrlObject which forces the
7271
- // trailing slash on directories
7272
- const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
7273
- applyFsStatEffectsOnUrlObject(urlObject, fsStat);
7274
- const shouldApplyFilesystemMagicResolution =
7275
- reference.type === "js_import";
7276
- if (shouldApplyFilesystemMagicResolution) {
7277
- const filesystemResolution = applyFileSystemMagicResolution(
7278
- urlObject.href,
7279
- {
7280
- fileStat: fsStat,
7281
- magicDirectoryIndex,
7282
- magicExtensions: getExtensionsToTry(
7283
- magicExtensions,
7284
- reference.ownerUrlInfo.url,
7285
- ),
7286
- },
7287
- );
7288
- if (filesystemResolution.stat) {
7289
- fsStat = filesystemResolution.stat;
7290
- reference.fsStat = fsStat;
7291
- urlObject.href = filesystemResolution.url;
7292
- applyFsStatEffectsOnUrlObject(urlObject, fsStat);
7293
- }
7294
- }
7295
- if (spa) {
7296
- // for SPA we want to serve the root HTML file most of the time
7297
- if (!fsStat) {
7298
- if (urlToExtension(urlObject)) {
7299
- // url has an extension, we assume it's a file request -> let 404 happen
7300
- return null;
7301
- }
7302
- if (specifierUsesTrailingSlash) {
7303
- // the trailing slash asks for a directory and there is none here
7304
- // -> let 404 happen (same reasoning as the extension above)
7305
- return null;
7306
- }
7307
- const spaFallbackUrl = getSpaFallbackUrl(reference);
7308
- if (spaFallbackUrl) {
7309
- return spaFallbackUrl;
7310
- }
7311
- return null;
7312
- }
7313
- if (fsStat.isDirectory()) {
7314
- // When requesting a directory, check if we have an HTML entry file for that directory
7315
- const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
7316
- if (directoryEntryFileUrl) {
7317
- reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
7318
- return directoryEntryFileUrl;
7319
- }
7320
- if (!specifierUsesTrailingSlash) {
7321
- // the trailing slash is what tells a directory apart from a route:
7322
- // "/join/" is the directory, "/join" is a route owned by the SPA
7323
- // even when "join/" exists in the source files.
7324
- // Without this a source directory would shadow the route having
7325
- // the same name and the SPA would be unreachable in dev while
7326
- // being perfectly fine once built
7327
- const spaFallbackUrl = getSpaFallbackUrl(reference);
7328
- if (spaFallbackUrl) {
7329
- reference.fsStat = readEntryStatSync(spaFallbackUrl, {
7330
- nullIfNotFound: true,
7331
- });
7332
- return spaFallbackUrl;
7333
- }
7334
- }
7335
- }
7336
- }
7337
- if (!fsStat) {
7338
- return null;
7339
- }
7340
- const urlBeforeSymlinkResolution = urlObject.href;
7341
- if (preserveSymlinks) {
7342
- return `${urlBeforeSymlinkResolution}${search}${hash}`;
7343
- }
7344
- const urlAfterSymlinkResolution = resolveSymlink(
7345
- urlBeforeSymlinkResolution,
7346
- );
7347
- if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
7348
- reference.leadsToASymlink = true;
7349
- // reference.baseUrl = urlBeforeSymlinkResolution;
7350
- }
7351
- const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
7352
- return resolvedUrl;
7353
- },
7354
- };
7355
- };
7356
-
7357
- const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
7358
- if (!fsStat) {
7359
- return;
7360
- }
7361
- const { pathname } = urlObject;
7362
- const pathnameUsesTrailingSlash = pathname.endsWith("/");
7363
- // force trailing slash on directories
7364
- if (fsStat.isDirectory()) {
7365
- if (!pathnameUsesTrailingSlash) {
7366
- urlObject.pathname = `${pathname}/`;
7367
- }
7368
- } else if (pathnameUsesTrailingSlash) {
7369
- // otherwise remove trailing slash if any
7370
- // a warning here? (because it's strange to reference a file with a trailing slash)
7371
- urlObject.pathname = pathname.slice(0, -1);
7372
- }
7373
- };
7374
-
7375
- const resolveSymlink = (fileUrl) => {
7376
- const urlObject = new URL(fileUrl);
7377
- const realpath = realpathSync(urlObject);
7378
- const realUrlObject = pathToFileURL(realpath);
7379
- if (urlObject.pathname.endsWith("/")) {
7380
- realUrlObject.pathname += `/`;
7381
- }
7382
- return realUrlObject.href;
7383
- };
7384
-
7385
- const getDirectoryEntryFileUrl = (directoryUrl) => {
7386
- const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
7387
- if (existsSync(indexHtmlFileUrl)) {
7388
- return indexHtmlFileUrl.href;
7389
- }
7390
- const filename = urlToFilename(directoryUrl);
7391
- const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
7392
- if (existsSync(htmlFileUrlCandidate)) {
7393
- return htmlFileUrlCandidate.href;
7394
- }
7395
- return null;
7396
- };
7397
- const getSpaFallbackUrl = (reference) => {
7398
- const { requestedUrl, rootDirectoryUrl, mainFilePath } =
7399
- reference.ownerUrlInfo.context;
7400
- if (!requestedUrl) {
7401
- // the SPA fallback answers a request; during build there is none
7402
- return null;
7403
- }
7404
- const closestHtmlRootFile = getClosestHtmlRootFile(
7405
- requestedUrl,
7406
- rootDirectoryUrl,
7407
- );
7408
- if (closestHtmlRootFile) {
7409
- return closestHtmlRootFile;
7410
- }
7411
- return String(new URL(mainFilePath, rootDirectoryUrl));
7412
- };
7413
- const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
7414
- let directoryUrl = new URL("./", requestedUrl);
7415
- while (true) {
7416
- const directoryEntryFileUrl = getDirectoryEntryFileUrl(directoryUrl);
7417
- if (directoryEntryFileUrl) {
7418
- return directoryEntryFileUrl;
7419
- }
7420
- if (!urlIsOrIsInsideOf(directoryUrl, serverRootDirectoryUrl)) {
7421
- return null;
7422
- }
7423
- directoryUrl = new URL("../", directoryUrl);
7424
- }
7425
- };
7426
-
7427
7493
  const directoryContentMagicName = "...";
7428
7494
 
7429
7495
  const jsenvPluginProtocolFile = ({
@@ -7577,7 +7643,16 @@ const jsenvPluginProtocolFile = ({
7577
7643
  }
7578
7644
  const serveFile = (url) => {
7579
7645
  const contentType = CONTENT_TYPE.fromUrlExtension(url);
7580
- const fileBuffer = readFileSync(new URL(url));
7646
+ const urlObject = new URL(url);
7647
+ // taken before the read: a write landing in between moves the
7648
+ // stat past this one, so the dev server sees the content as
7649
+ // outdated (see isValid in the dev server) rather than the reverse
7650
+ const fileStat = statSync(urlObject);
7651
+ const fileBuffer = readFileSync(urlObject);
7652
+ urlInfo.data.fileStat = {
7653
+ mtimeMs: fileStat.mtimeMs,
7654
+ size: fileStat.size,
7655
+ };
7581
7656
  const content = CONTENT_TYPE.isTextual(contentType)
7582
7657
  ? String(fileBuffer)
7583
7658
  : fileBuffer;
@@ -8084,7 +8159,7 @@ const jsenvPluginImportMetaScenarios = () => {
8084
8159
  name: "jsenv:import_meta_scenario",
8085
8160
  appliesDuring: "*",
8086
8161
  transformUrlContent: {
8087
- js_module: async (urlInfo) => {
8162
+ js_module: (urlInfo) => {
8088
8163
  // Do not scan node modules for import.meta.dev/import.meta.build
8089
8164
  // - node modules won't have this in their code
8090
8165
  // - ;or should use other an other technic as this one won't be available
@@ -8100,40 +8175,42 @@ const jsenvPluginImportMetaScenarios = () => {
8100
8175
  ) {
8101
8176
  return null;
8102
8177
  }
8103
- const { metadata } = await applyBabelPlugins({
8104
- babelPlugins: [babelPluginMetadataImportMetaScenarios],
8105
- input: urlInfo.content,
8106
- inputIsJsModule: true,
8107
- inputUrl: urlInfo.originalUrl,
8108
- outputUrl: urlInfo.generatedUrl,
8178
+ const importMetaScenarioNodes = { dev: [], build: [] };
8179
+ visitJsAst(urlInfo.contentAst, {
8180
+ MemberExpression: (node) => {
8181
+ const name = getImportMetaPropertyName(node);
8182
+ if (name === "dev" || name === "build") {
8183
+ importMetaScenarioNodes[name].push(node);
8184
+ }
8185
+ },
8109
8186
  });
8110
- const { dev = [], build = [] } = metadata.importMetaScenarios;
8187
+ const { dev, build } = importMetaScenarioNodes;
8111
8188
  const replacements = [];
8112
- const replace = (path, value) => {
8113
- replacements.push({ path, value });
8189
+ const replace = (node, value) => {
8190
+ replacements.push({ node, value });
8114
8191
  };
8115
8192
  if (urlInfo.context.build) {
8116
8193
  // during build ensure replacement for tree-shaking
8117
- dev.forEach((path) => {
8118
- replace(path, "undefined");
8194
+ dev.forEach((node) => {
8195
+ replace(node, "undefined");
8119
8196
  });
8120
- build.forEach((path) => {
8121
- replace(path, "true");
8197
+ build.forEach((node) => {
8198
+ replace(node, "true");
8122
8199
  });
8123
8200
  } else {
8124
8201
  // during dev we can let "import.meta.build" untouched
8125
8202
  // it will be evaluated to undefined.
8126
8203
  // Moreover it can be surprising to see some "undefined"
8127
8204
  // when source file contains "import.meta.build"
8128
- dev.forEach((path) => {
8129
- replace(path, "true");
8205
+ dev.forEach((node) => {
8206
+ replace(node, "true");
8130
8207
  });
8131
8208
  }
8132
8209
  const magicSource = createMagicSource(urlInfo.content);
8133
- replacements.forEach(({ path, value }) => {
8210
+ replacements.forEach(({ node, value }) => {
8134
8211
  magicSource.replace({
8135
- start: path.node.start,
8136
- end: path.node.end,
8212
+ start: node.start,
8213
+ end: node.end,
8137
8214
  replacement: value,
8138
8215
  });
8139
8216
  });
@@ -8143,42 +8220,6 @@ const jsenvPluginImportMetaScenarios = () => {
8143
8220
  };
8144
8221
  };
8145
8222
 
8146
- const babelPluginMetadataImportMetaScenarios = () => {
8147
- return {
8148
- name: "metadata-import-meta-scenarios",
8149
- visitor: {
8150
- Program(programPath, state) {
8151
- const importMetas = {};
8152
- programPath.traverse({
8153
- MemberExpression(path) {
8154
- const { node } = path;
8155
- const { object } = node;
8156
- if (object.type !== "MetaProperty") {
8157
- return;
8158
- }
8159
- const { property: objectProperty } = object;
8160
- if (objectProperty.name !== "meta") {
8161
- return;
8162
- }
8163
- const { property } = node;
8164
- const { name } = property;
8165
- const importMetaPaths = importMetas[name];
8166
- if (importMetaPaths) {
8167
- importMetaPaths.push(path);
8168
- } else {
8169
- importMetas[name] = [path];
8170
- }
8171
- },
8172
- });
8173
- state.file.metadata.importMetaScenarios = {
8174
- dev: importMetas.dev,
8175
- build: importMetas.build,
8176
- };
8177
- },
8178
- },
8179
- };
8180
- };
8181
-
8182
8223
  /*
8183
8224
  * Source code can contain the following
8184
8225
  * - __DEV__
@@ -8277,15 +8318,10 @@ const jsenvPluginImportMetaCss = () => {
8277
8318
  if (!urlInfo.content.includes("import.meta.css")) {
8278
8319
  return null;
8279
8320
  }
8280
- const { metadata } = await applyBabelPlugins({
8281
- babelPlugins: [babelPluginMetadataUsesImportMetaCss],
8282
- input: urlInfo.content,
8283
- inputIsJsModule: true,
8284
- inputUrl: urlInfo.originalUrl,
8285
- outputUrl: urlInfo.generatedUrl,
8321
+ const importMetaCssNode = visitJsAstUntil(urlInfo.contentAst, {
8322
+ MemberExpression: (node) => getImportMetaPropertyName(node) === "css",
8286
8323
  });
8287
- const { usesImportMetaCss } = metadata;
8288
- if (!usesImportMetaCss) {
8324
+ if (!importMetaCssNode) {
8289
8325
  return null;
8290
8326
  }
8291
8327
  if (urlInfo.context.build) {
@@ -8306,6 +8342,8 @@ const jsenvPluginImportMetaCss = () => {
8306
8342
  inputIsJsModule: true,
8307
8343
  inputUrl: urlInfo.originalUrl,
8308
8344
  outputUrl: urlInfo.generatedUrl,
8345
+ // the map would be dropped: the content sent back carries none
8346
+ options: { sourceMaps: false },
8309
8347
  });
8310
8348
  if (code === urlInfo.content) {
8311
8349
  // all assignments were already in array form (pre-built file) — nothing to do
@@ -8365,37 +8403,6 @@ const babelPluginRewriteImportMetaCssAssignment = (
8365
8403
  };
8366
8404
  };
8367
8405
 
8368
- const babelPluginMetadataUsesImportMetaCss = () => {
8369
- return {
8370
- name: "metadata-uses-import-meta-css",
8371
- visitor: {
8372
- Program(programPath, state) {
8373
- let usesImportMetaCss = false;
8374
- programPath.traverse({
8375
- MemberExpression(path) {
8376
- const { node } = path;
8377
- const { object } = node;
8378
- if (object.type !== "MetaProperty") {
8379
- return;
8380
- }
8381
- const { property: objectProperty } = object;
8382
- if (objectProperty.name !== "meta") {
8383
- return;
8384
- }
8385
- const { property } = node;
8386
- const { name } = property;
8387
- if (name === "css") {
8388
- usesImportMetaCss = true;
8389
- path.stop();
8390
- }
8391
- },
8392
- });
8393
- state.file.metadata.usesImportMetaCss = usesImportMetaCss;
8394
- },
8395
- },
8396
- };
8397
- };
8398
-
8399
8406
  const injectImportMetaCss = (
8400
8407
  urlInfo,
8401
8408
  { content, importFrom, importName, importAs, hot },
@@ -8438,104 +8445,6 @@ ${importVariableName}(import.meta);
8438
8445
  };
8439
8446
  };
8440
8447
 
8441
- // https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-stages-of-babel
8442
- // https://github.com/cfware/babel-plugin-bundled-import-meta/blob/master/index.js
8443
- // https://github.com/babel/babel/blob/f4edf62f6beeab8ae9f2b7f0b82f1b3b12a581af/packages/babel-helper-module-imports/src/index.js#L7
8444
-
8445
- const babelPluginMetadataImportMetaHot = () => {
8446
- return {
8447
- name: "metadata-import-meta-hot",
8448
- visitor: {
8449
- Program(programPath, state) {
8450
- Object.assign(
8451
- state.file.metadata,
8452
- collectImportMetaHotProperties(programPath),
8453
- );
8454
- },
8455
- },
8456
- };
8457
- };
8458
- const collectImportMetaHotProperties = (programPath) => {
8459
- const importMetaHotPaths = [];
8460
- let hotDecline = false;
8461
- let hotAcceptSelf = false;
8462
- let hotAcceptDependencies = [];
8463
- programPath.traverse({
8464
- MemberExpression(path) {
8465
- const { node } = path;
8466
- const { object } = node;
8467
- if (object.type !== "MetaProperty") {
8468
- return;
8469
- }
8470
- const { property: objectProperty } = object;
8471
- if (objectProperty.name !== "meta") {
8472
- return;
8473
- }
8474
- const { property } = node;
8475
- const { name } = property;
8476
- if (name === "hot") {
8477
- importMetaHotPaths.push(path);
8478
- }
8479
- },
8480
- CallExpression(path) {
8481
- if (isImportMetaHotMethodCall(path, "accept")) {
8482
- const callNode = path.node;
8483
- const args = callNode.arguments;
8484
- if (args.length === 0) {
8485
- hotAcceptSelf = true;
8486
- return;
8487
- }
8488
- const firstArg = args[0];
8489
- if (firstArg.type === "StringLiteral") {
8490
- hotAcceptDependencies = [
8491
- {
8492
- specifierPath: path.get("arguments")[0],
8493
- },
8494
- ];
8495
- return;
8496
- }
8497
- if (firstArg.type === "ArrayExpression") {
8498
- const firstArgPath = path.get("arguments")[0];
8499
- hotAcceptDependencies = firstArg.elements.map((arrayNode, index) => {
8500
- if (arrayNode.type !== "StringLiteral") {
8501
- throw new Error(
8502
- `all array elements must be strings in "import.meta.hot.accept(array)"`,
8503
- );
8504
- }
8505
- return {
8506
- specifierPath: firstArgPath.get(String(index)),
8507
- };
8508
- });
8509
- return;
8510
- }
8511
- // accept first arg can be "anything" such as
8512
- // `const cb = () => {}; import.meta.accept(cb)`
8513
- hotAcceptSelf = true;
8514
- }
8515
- if (isImportMetaHotMethodCall(path, "decline")) {
8516
- hotDecline = true;
8517
- }
8518
- },
8519
- });
8520
- return {
8521
- importMetaHotPaths,
8522
- hotDecline,
8523
- hotAcceptSelf,
8524
- hotAcceptDependencies,
8525
- };
8526
- };
8527
- const isImportMetaHotMethodCall = (path, methodName) => {
8528
- const { property, object } = path.node.callee;
8529
- return (
8530
- property &&
8531
- property.name === methodName &&
8532
- object &&
8533
- object.property &&
8534
- object.property.name === "hot" &&
8535
- object.object.type === "MetaProperty"
8536
- );
8537
- };
8538
-
8539
8448
  // Some "smart" default applied to decide what should hot reload / fullreload:
8540
8449
  // By default:
8541
8450
  // - hot reload on <img src="./image.png" />
@@ -8683,6 +8592,75 @@ const htmlNodeCanHotReload = (node) => {
8683
8592
  ].includes(node.nodeName);
8684
8593
  };
8685
8594
 
8595
+ const analyzeImportMetaHot = (ast) => {
8596
+ const importMetaHotNodes = [];
8597
+ let hotDecline = false;
8598
+ let hotAcceptSelf = false;
8599
+ let hotAcceptSpecifiers = [];
8600
+ visitJsAst(ast, {
8601
+ MemberExpression: (node) => {
8602
+ if (getImportMetaPropertyName(node) === "hot") {
8603
+ importMetaHotNodes.push(node);
8604
+ }
8605
+ },
8606
+ CallExpression: (node) => {
8607
+ const methodName = getImportMetaHotMethodName(node);
8608
+ if (methodName === "accept") {
8609
+ const args = node.arguments;
8610
+ if (args.length === 0) {
8611
+ hotAcceptSelf = true;
8612
+ return;
8613
+ }
8614
+ const [firstArg] = args;
8615
+ if (isStringLiteral(firstArg)) {
8616
+ hotAcceptSpecifiers = [firstArg.value];
8617
+ return;
8618
+ }
8619
+ if (firstArg.type === "ArrayExpression") {
8620
+ hotAcceptSpecifiers = firstArg.elements.map((element) => {
8621
+ if (!isStringLiteral(element)) {
8622
+ throw new Error(
8623
+ `all array elements must be strings in "import.meta.hot.accept(array)"`,
8624
+ );
8625
+ }
8626
+ return element.value;
8627
+ });
8628
+ return;
8629
+ }
8630
+ // accept first arg can be "anything" such as
8631
+ // `const cb = () => {}; import.meta.hot.accept(cb)`
8632
+ hotAcceptSelf = true;
8633
+ return;
8634
+ }
8635
+ if (methodName === "decline") {
8636
+ hotDecline = true;
8637
+ }
8638
+ },
8639
+ });
8640
+ return {
8641
+ importMetaHotNodes,
8642
+ hotDecline,
8643
+ hotAcceptSelf,
8644
+ hotAcceptSpecifiers,
8645
+ };
8646
+ };
8647
+
8648
+ // "import.meta.hot.<method>(...)"
8649
+ const getImportMetaHotMethodName = (callNode) => {
8650
+ const { callee } = callNode;
8651
+ if (callee.type !== "MemberExpression" || callee.computed) {
8652
+ return null;
8653
+ }
8654
+ if (getImportMetaPropertyName(callee.object) !== "hot") {
8655
+ return null;
8656
+ }
8657
+ return callee.property.name;
8658
+ };
8659
+
8660
+ const isStringLiteral = (node) => {
8661
+ return node.type === "Literal" && typeof node.value === "string";
8662
+ };
8663
+
8686
8664
  const jsenvPluginImportMetaHot = () => {
8687
8665
  const importMetaHotClientFileUrl = import.meta
8688
8666
  .resolve("../client/import_meta_hot/import_meta_hot.js");
@@ -8731,7 +8709,7 @@ const jsenvPluginImportMetaHot = () => {
8731
8709
  cssUrlInfo.data.hotAcceptSelf = false;
8732
8710
  cssUrlInfo.data.hotAcceptDependencies = [];
8733
8711
  },
8734
- js_module: async (urlInfo) => {
8712
+ js_module: (urlInfo) => {
8735
8713
  // Do not scan node modules for import.meta.hot
8736
8714
  // - unlikely to be there
8737
8715
  // - we don't watch node modules (too expensive)
@@ -8743,27 +8721,22 @@ const jsenvPluginImportMetaHot = () => {
8743
8721
  if (!urlInfo.content.includes("import.meta.hot")) {
8744
8722
  return null;
8745
8723
  }
8746
- const { metadata } = await applyBabelPlugins({
8747
- babelPlugins: [babelPluginMetadataImportMetaHot],
8748
- input: urlInfo.content,
8749
- inputIsJsModule: true,
8750
- inputUrl: urlInfo.originalUrl,
8751
- outputUrl: urlInfo.generatedUrl,
8752
- });
8753
8724
  const {
8754
- importMetaHotPaths,
8725
+ importMetaHotNodes,
8755
8726
  hotDecline,
8756
8727
  hotAcceptSelf,
8757
- hotAcceptDependencies,
8758
- } = metadata;
8728
+ hotAcceptSpecifiers,
8729
+ } = analyzeImportMetaHot(urlInfo.contentAst);
8759
8730
  urlInfo.data.hotDecline = hotDecline;
8760
8731
  urlInfo.data.hotAcceptSelf = hotAcceptSelf;
8761
- urlInfo.data.hotAcceptDependencies = hotAcceptDependencies;
8762
- if (importMetaHotPaths.length === 0) {
8732
+ urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
8733
+ (specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
8734
+ );
8735
+ if (importMetaHotNodes.length === 0) {
8763
8736
  return null;
8764
8737
  }
8765
8738
  if (urlInfo.context.build) {
8766
- return removeImportMetaHots(urlInfo, importMetaHotPaths);
8739
+ return removeImportMetaHots(urlInfo, importMetaHotNodes);
8767
8740
  }
8768
8741
  return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
8769
8742
  },
@@ -8771,12 +8744,32 @@ const jsenvPluginImportMetaHot = () => {
8771
8744
  };
8772
8745
  };
8773
8746
 
8774
- const removeImportMetaHots = (urlInfo, importMetaHotPaths) => {
8747
+ // The specifier given to import.meta.hot.accept() is one the file imports,
8748
+ // so its url is known from that import (autoreload compares urls). A
8749
+ // specifier the file does not import accepts nothing: it is kept as a plain
8750
+ // url, never resolved as a dependency (that could throw on a bare specifier).
8751
+ const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
8752
+ for (const referenceToOther of urlInfo.referenceToOthersSet) {
8753
+ if (
8754
+ referenceToOther.type === "js_import" &&
8755
+ referenceToOther.specifier === specifier
8756
+ ) {
8757
+ return referenceToOther.url;
8758
+ }
8759
+ }
8760
+ try {
8761
+ return new URL(specifier, urlInfo.url).href;
8762
+ } catch {
8763
+ return specifier;
8764
+ }
8765
+ };
8766
+
8767
+ const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
8775
8768
  const magicSource = createMagicSource(urlInfo.content);
8776
- importMetaHotPaths.forEach((path) => {
8769
+ importMetaHotNodes.forEach((node) => {
8777
8770
  magicSource.replace({
8778
- start: path.node.start,
8779
- end: path.node.end,
8771
+ start: node.start,
8772
+ end: node.end,
8780
8773
  replacement: "undefined",
8781
8774
  });
8782
8775
  });
@@ -9305,6 +9298,132 @@ const jsenvPluginAutoreload = ({
9305
9298
  ];
9306
9299
  };
9307
9300
 
9301
+ /*
9302
+ * Tells the browser the static import graph of a page's module scripts, as
9303
+ * `Link: <url>; rel=modulepreload` response headers, so it can fetch every
9304
+ * module as soon as the page's headers arrive, without waiting to discover
9305
+ * them level by level (each level costing a round trip and, the first time,
9306
+ * a cook).
9307
+ *
9308
+ * Only what the graph already knows is listed: a module never cooked has no
9309
+ * known imports. The header is computed at each response from the graph as it
9310
+ * is then: a page gets its full list from its second load on, the first load
9311
+ * keeps the plain waterfall.
9312
+ *
9313
+ * Headers rather than <link> elements written into the page: the page content
9314
+ * stays what it is (served from memory, revalidated by etag) and the list can
9315
+ * grow without cooking the page again. And not references either: a reference
9316
+ * from the html to every module would make the html a dependent of each of
9317
+ * them, and a hot update propagating up the importers would reach the html
9318
+ * (which declines) instead of stopping at the module accepting it.
9319
+ *
9320
+ * OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
9321
+ * Over http/1.1 it makes pages slower — measured on a 500 modules page: first
9322
+ * paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
9323
+ * browser no extra resource: what it has is 6 connections per origin, and
9324
+ * every request goes through them in the order it was learned.
9325
+ * - Without the header, requests come in execution order: the parser asks
9326
+ * for the render-blocking supervisor script first and gets a connection at
9327
+ * once; transfer and execution overlap, a module executing and discovering
9328
+ * its imports while the others download. On localhost the server answers
9329
+ * from memory in 0.1ms, so the 6 connections are busy from the start.
9330
+ * - With the header, hundreds of requests take the 6 connections before the
9331
+ * parser asks for the supervisor script, which then waits for a free
9332
+ * connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
9333
+ * Meanwhile nothing renders and nothing executes: transfer, then execution,
9334
+ * serialized. http/1.1 cannot reprioritize a queued request, and a
9335
+ * modulepreload has the same priority as a script.
9336
+ * - And there is nothing to hide: the discovery latency a preload removes is
9337
+ * ~1ms per graph level on localhost; with a real RTT a wide graph (more
9338
+ * than 6 modules pending at any time) keeps the 6 connections saturated
9339
+ * anyway, and the load takes requests / 6 * RTT whatever the order.
9340
+ * It pays off for a deep and narrow graph (connections idle, waiting for a
9341
+ * response to learn the next level), and over http/2 or http/3: no
9342
+ * connection limit, and the browser prioritizes the blocking script.
9343
+ */
9344
+
9345
+
9346
+ const jsenvPluginModulepreload = () => {
9347
+ return {
9348
+ name: "jsenv:modulepreload",
9349
+ appliesDuring: "dev",
9350
+ augmentResponse: ({ urlInfo }) => {
9351
+ if (urlInfo.type !== "html") {
9352
+ return null;
9353
+ }
9354
+ const hrefs = collectStaticImportHrefs(urlInfo);
9355
+ if (hrefs.length === 0) {
9356
+ return null;
9357
+ }
9358
+ return {
9359
+ headers: {
9360
+ link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
9361
+ },
9362
+ };
9363
+ },
9364
+ };
9365
+ };
9366
+
9367
+ const collectStaticImportHrefs = (htmlUrlInfo) => {
9368
+ const hrefSet = new Set();
9369
+ const visitedSet = new Set();
9370
+ const addHref = (reference) => {
9371
+ const specifier = urlSpecifierEncoding.decode(reference);
9372
+ if (typeof specifier !== "string") {
9373
+ return;
9374
+ }
9375
+ // "?hot" belongs to the request that cooked the importer, the page loading
9376
+ // now imports the url without it
9377
+ hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
9378
+ };
9379
+ const visitJsModule = (jsModuleUrlInfo) => {
9380
+ if (visitedSet.has(jsModuleUrlInfo)) {
9381
+ return;
9382
+ }
9383
+ visitedSet.add(jsModuleUrlInfo);
9384
+ for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
9385
+ if (
9386
+ reference.type !== "js_import" ||
9387
+ reference.isWeak ||
9388
+ reference.isImplicit ||
9389
+ !STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
9390
+ !reference.url.startsWith("file:")
9391
+ ) {
9392
+ continue;
9393
+ }
9394
+ const importedUrlInfo = reference.urlInfo;
9395
+ // never cooked: its content, hence its own imports, are unknown
9396
+ if (importedUrlInfo.type !== "js_module") {
9397
+ continue;
9398
+ }
9399
+ addHref(reference);
9400
+ visitJsModule(importedUrlInfo);
9401
+ }
9402
+ };
9403
+ for (const reference of htmlUrlInfo.referenceToOthersSet) {
9404
+ if (
9405
+ reference.type !== "script" ||
9406
+ reference.expectedType !== "js_module" ||
9407
+ // the scripts jsenv adds to every page are few, small and in memory
9408
+ reference.injected ||
9409
+ reference.isWeak
9410
+ ) {
9411
+ continue;
9412
+ }
9413
+ const scriptUrlInfo = reference.urlInfo;
9414
+ if (scriptUrlInfo.type !== "js_module") {
9415
+ continue;
9416
+ }
9417
+ if (!reference.isInline && reference.url.startsWith("file:")) {
9418
+ addHref(reference);
9419
+ }
9420
+ visitJsModule(scriptUrlInfo);
9421
+ }
9422
+ return Array.from(hrefSet);
9423
+ };
9424
+
9425
+ const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
9426
+
9308
9427
  /*
9309
9428
  * Tells the browser about the dependencies declared in package.json that
9310
9429
  * node_modules does not match, so the page can say it is running with something
@@ -9946,6 +10065,7 @@ const getCorePlugins = ({
9946
10065
 
9947
10066
  clientAutoreload,
9948
10067
  clientAutoreloadOnServerRestart,
10068
+ modulepreload = false,
9949
10069
  dependencyStatus,
9950
10070
  cacheControl,
9951
10071
  scenarioPlaceholders = true,
@@ -10047,6 +10167,7 @@ const getCorePlugins = ({
10047
10167
  ...(clientAutoreload && clientAutoreload.enabled
10048
10168
  ? [jsenvPluginAutoreload(clientAutoreload)]
10049
10169
  : []),
10170
+ ...(modulepreload ? [jsenvPluginModulepreload()] : []),
10050
10171
  ...(dependencyStatus
10051
10172
  ? [jsenvPluginDependencyStatus(dependencyStatus)]
10052
10173
  : []),
@@ -10061,11 +10182,12 @@ const getCorePlugins = ({
10061
10182
  ];
10062
10183
  };
10063
10184
 
10064
- const humanizeProcessCpuUsage = (ratio) => {
10065
- const percentageAsNumber = ratio * 100;
10066
- const percentageAsNumberRounded = Math.round(percentageAsNumber);
10067
- const percentage = `${percentageAsNumberRounded}%`;
10068
- return percentage;
10185
+ // a phase can be a few milliseconds long (refine, mostly): shown as such
10186
+ const humanizePhaseDuration = (ms) => {
10187
+ if (ms < 1000) {
10188
+ return `${Math.round(ms)}ms`;
10189
+ }
10190
+ return humanizeDuration(ms, { short: true });
10069
10191
  };
10070
10192
  const humanizeProcessMemoryUsage = (value) => {
10071
10193
  return humanizeMemory(value, { short: true, decimals: 0 });
@@ -10074,7 +10196,8 @@ const renderBuildDoneLog = ({
10074
10196
  entryPointArray,
10075
10197
  duration,
10076
10198
  buildFileContents,
10077
- processCpuUsage,
10199
+ cpuUsage,
10200
+ phaseTimings,
10078
10201
  processMemoryUsage,
10079
10202
  }) => {
10080
10203
  const buildContentReport = createBuildContentReport(buildFileContents);
@@ -10089,15 +10212,10 @@ const renderBuildDoneLog = ({
10089
10212
  title = `build done (${entryPointCount} entry points)`;
10090
10213
  }
10091
10214
 
10092
- // cpu usage
10093
- let cpuUsageLine = "cpu: ";
10094
- cpuUsageLine += `${humanizeProcessCpuUsage(processCpuUsage.end)}`;
10095
- cpuUsageLine += renderDetails({
10096
- med: humanizeProcessCpuUsage(processCpuUsage.median),
10097
- min: humanizeProcessCpuUsage(processCpuUsage.min),
10098
- max: humanizeProcessCpuUsage(processCpuUsage.max),
10099
- });
10100
- lines.push(cpuUsageLine);
10215
+ // cpu: the process cpu time over the build duration, 100% being one core
10216
+ // busy the whole time (more when a native module works on other threads)
10217
+ const cpuMs = (cpuUsage.user + cpuUsage.system) / 1000;
10218
+ lines.push(`cpu: ${Math.round((cpuMs / duration) * 100)}%`);
10101
10219
 
10102
10220
  // memory usage
10103
10221
  let memoryUsageLine = "memory: ";
@@ -10112,6 +10230,16 @@ const renderBuildDoneLog = ({
10112
10230
  // duration
10113
10231
  let durationLine = `duration: `;
10114
10232
  durationLine += humanizeDuration(duration, { short: true });
10233
+ if (phaseTimings) {
10234
+ durationLine += renderDetails(
10235
+ Object.fromEntries(
10236
+ Object.keys(phaseTimings).map((phaseName) => [
10237
+ phaseName,
10238
+ humanizePhaseDuration(phaseTimings[phaseName]),
10239
+ ]),
10240
+ ),
10241
+ );
10242
+ }
10115
10243
  lines.push(durationLine);
10116
10244
 
10117
10245
  // content
@@ -10302,6 +10430,8 @@ const createBuildContentReport = (buildFileContents) => {
10302
10430
  };
10303
10431
 
10304
10432
  const determineCategory = (buildRelativeUrl) => {
10433
+ // a versioned url ("main.js?v=abc") is categorized by its file
10434
+ buildRelativeUrl = buildRelativeUrl.replace(/[?#].*$/, "");
10305
10435
  if (buildRelativeUrl.endsWith(".map")) {
10306
10436
  return "sourcemap";
10307
10437
  }
@@ -11228,7 +11358,7 @@ const createBuildSpecifierManager = ({
11228
11358
  }
11229
11359
  if (mayUsePlaceholder(urlInfo)) {
11230
11360
  const contentBeforeReplace = urlInfo.content;
11231
- const { content, sourcemap } = placeholderAPI.replaceAll(
11361
+ const replaceResult = placeholderAPI.replaceAll(
11232
11362
  contentBeforeReplace,
11233
11363
  (placeholder) => {
11234
11364
  const reference = placeholderToReferenceMap.get(placeholder);
@@ -11236,7 +11366,7 @@ const createBuildSpecifierManager = ({
11236
11366
  return value;
11237
11367
  },
11238
11368
  );
11239
- urlInfo.mutateContent({ content, sourcemap });
11369
+ urlInfo.mutateContent(replaceResult);
11240
11370
  }
11241
11371
  },
11242
11372
  );
@@ -12477,13 +12607,12 @@ entryPoints: {
12477
12607
  });
12478
12608
  }
12479
12609
 
12480
- const cpuMonitoring = startMonitoringCpuUsage();
12481
- operation.addEndCallback(cpuMonitoring.stop);
12482
- const [processCpuUsageMonitoring] = cpuMonitoring;
12610
+ // cpu is not sampled: cooking, rollup and terser hold the event loop for
12611
+ // seconds at a time, a sampler only runs in the gaps and would report the
12612
+ // idle time. What the build cost is process.cpuUsage() over its duration.
12483
12613
  const memoryMonitoring = startMonitoringMemoryUsage();
12484
12614
  const [processMemoryUsageMonitoring] = memoryMonitoring;
12485
12615
  const interval = setInterval(() => {
12486
- processCpuUsageMonitoring.measure();
12487
12616
  processMemoryUsageMonitoring.measure();
12488
12617
  }, 500).unref();
12489
12618
  operation.addEndCallback(() => {
@@ -12553,19 +12682,24 @@ entryPoints: {
12553
12682
  content += "\n";
12554
12683
  return content;
12555
12684
  };
12556
- const renderBuildEndLog = ({ duration, buildFileContents }) => {
12685
+ const renderBuildEndLog = ({
12686
+ duration,
12687
+ buildFileContents,
12688
+ cpuUsage,
12689
+ phaseTimings,
12690
+ }) => {
12557
12691
  // tell how many files are generated in build directory
12558
12692
  // tell the repartition?
12559
12693
  // this is not really useful for single build right?
12560
12694
 
12561
- processCpuUsageMonitoring.end();
12562
12695
  processMemoryUsageMonitoring.end();
12563
12696
 
12564
12697
  return renderBuildDoneLog({
12565
12698
  entryPointArray,
12566
12699
  duration,
12567
12700
  buildFileContents,
12568
- processCpuUsage: processCpuUsageMonitoring.info,
12701
+ cpuUsage,
12702
+ phaseTimings,
12569
12703
  processMemoryUsage: processMemoryUsageMonitoring.info,
12570
12704
  });
12571
12705
  };
@@ -12640,13 +12774,25 @@ entryPoints: {
12640
12774
  }, renderDynamicLog());
12641
12775
  };
12642
12776
  },
12643
- onBuildEnd: ({ buildFileContents, duration }) => {
12777
+ onBuildEnd: ({
12778
+ buildFileContents,
12779
+ duration,
12780
+ cpuUsage,
12781
+ phaseTimings,
12782
+ }) => {
12644
12783
  clearInterval(interval);
12645
12784
  dynamicLog.update("");
12646
12785
  dynamicLog.destroy();
12647
12786
  dynamicLog = null;
12648
12787
  logger.info("");
12649
- logger.info(renderBuildEndLog({ duration, buildFileContents }));
12788
+ logger.info(
12789
+ renderBuildEndLog({
12790
+ duration,
12791
+ buildFileContents,
12792
+ cpuUsage,
12793
+ phaseTimings,
12794
+ }),
12795
+ );
12650
12796
  },
12651
12797
  };
12652
12798
  };
@@ -12673,8 +12819,20 @@ entryPoints: {
12673
12819
  );
12674
12820
  };
12675
12821
  },
12676
- onBuildEnd: ({ buildFileContents, duration }) => {
12677
- logger.info(renderBuildEndLog({ duration, buildFileContents }));
12822
+ onBuildEnd: ({
12823
+ buildFileContents,
12824
+ duration,
12825
+ cpuUsage,
12826
+ phaseTimings,
12827
+ }) => {
12828
+ logger.info(
12829
+ renderBuildEndLog({
12830
+ duration,
12831
+ buildFileContents,
12832
+ cpuUsage,
12833
+ phaseTimings,
12834
+ }),
12835
+ );
12678
12836
  },
12679
12837
  };
12680
12838
  };
@@ -12723,6 +12881,7 @@ entryPoints: {
12723
12881
 
12724
12882
  const runBuild = async ({ signal }) => {
12725
12883
  const startDate = Date.now();
12884
+ const cpuUsageAtStart = process.cpuUsage();
12726
12885
  const { onBuildEnd, onEntryPointBuildStart } = startBuildLogs();
12727
12886
 
12728
12887
  const buildUrlsGenerator = createBuildUrlsGenerator({
@@ -12831,6 +12990,7 @@ entryPoints: {
12831
12990
  entryBuildInfo.buildInlineContents = result.buildInlineContents;
12832
12991
  entryBuildInfo.buildManifest = result.buildManifest;
12833
12992
  entryBuildInfo.buildSideEffectFiles = result.buildSideEffectFiles;
12993
+ entryBuildInfo.phaseTimings = result.phaseTimings;
12834
12994
  entryBuildInfo.duration = Date.now() - entryPointBuildStartMs;
12835
12995
  onEntryPointBuildEnd();
12836
12996
  })();
@@ -12935,11 +13095,19 @@ entryPoints: {
12935
13095
  }
12936
13096
  }
12937
13097
  }
13098
+ // entry points are built concurrently, interleaved on the same thread:
13099
+ // their phases overlap and only a single one can be told apart
13100
+ const phaseTimings =
13101
+ entryBuildInfoMap.size === 1
13102
+ ? entryBuildInfoMap.values().next().value.phaseTimings
13103
+ : null;
12938
13104
  onBuildEnd({
12939
13105
  buildFileContents,
12940
13106
  buildInlineContents,
12941
13107
  buildManifest,
12942
13108
  duration: Date.now() - startDate,
13109
+ cpuUsage: process.cpuUsage(cpuUsageAtStart),
13110
+ phaseTimings,
12943
13111
  });
12944
13112
  return {
12945
13113
  ...(returnBuildInlineContents ? { buildInlineContents } : {}),
@@ -13303,6 +13471,14 @@ const prepareEntryPointBuild = async (
13303
13471
  return {
13304
13472
  entryReference,
13305
13473
  buildEntryPoint: async ({ getOtherEntryBuildInfo }) => {
13474
+ const phaseTimings = {};
13475
+ let phaseStartMs = Date.now();
13476
+ const endPhase = (name) => {
13477
+ const nowMs = Date.now();
13478
+ phaseTimings[name] = nowMs - phaseStartMs;
13479
+ phaseStartMs = nowMs;
13480
+ };
13481
+
13306
13482
  {
13307
13483
  _getOtherEntryBuildInfo = getOtherEntryBuildInfo;
13308
13484
  if (outDirectoryUrl) {
@@ -13310,6 +13486,7 @@ const prepareEntryPointBuild = async (
13310
13486
  }
13311
13487
  await rawRootUrlInfo.cookDependencies({ operation: buildOperation });
13312
13488
  }
13489
+ endPhase("craft");
13313
13490
 
13314
13491
  const finalKitchen = createKitchen({
13315
13492
  name: "shape",
@@ -13522,6 +13699,8 @@ const prepareEntryPointBuild = async (
13522
13699
  }
13523
13700
  }
13524
13701
 
13702
+ endPhase("bundle");
13703
+
13525
13704
  {
13526
13705
  finalKitchen.context.buildStep = "shape";
13527
13706
  if (outDirectoryUrl) {
@@ -13541,6 +13720,8 @@ const prepareEntryPointBuild = async (
13541
13720
  });
13542
13721
  }
13543
13722
 
13723
+ endPhase("shape");
13724
+
13544
13725
  const buildSideEffectFiles = [];
13545
13726
  {
13546
13727
  finalKitchen.context.buildStep = "refine";
@@ -13648,6 +13829,8 @@ const prepareEntryPointBuild = async (
13648
13829
  }
13649
13830
  }
13650
13831
  }
13832
+ endPhase("refine");
13833
+
13651
13834
  const {
13652
13835
  buildFileContents,
13653
13836
  buildFileVersions,
@@ -13667,6 +13850,7 @@ const prepareEntryPointBuild = async (
13667
13850
  buildInlineContents,
13668
13851
  buildManifest,
13669
13852
  buildSideEffectFiles,
13853
+ phaseTimings,
13670
13854
  };
13671
13855
  },
13672
13856
  };