@jsenv/core 41.4.16 → 41.4.18

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.
@@ -1,8 +1,8 @@
1
- import { applyBabelPlugins, parseHtml } from "@jsenv/ast";
1
+ import { parseHtml } from "@jsenv/ast";
2
2
  import { createMagicSource } from "@jsenv/sourcemap";
3
3
 
4
- import { babelPluginMetadataImportMetaHot } from "./babel_plugin_metadata_import_meta_hot.js";
5
4
  import { collectHotDataFromHtmlAst } from "./html_hot_dependencies.js";
5
+ import { analyzeImportMetaHot } from "./import_meta_hot_analysis.js";
6
6
 
7
7
  export const jsenvPluginImportMetaHot = () => {
8
8
  const importMetaHotClientFileUrl = import.meta
@@ -52,7 +52,7 @@ export const jsenvPluginImportMetaHot = () => {
52
52
  cssUrlInfo.data.hotAcceptSelf = false;
53
53
  cssUrlInfo.data.hotAcceptDependencies = [];
54
54
  },
55
- js_module: async (urlInfo) => {
55
+ js_module: (urlInfo) => {
56
56
  // Do not scan node modules for import.meta.hot
57
57
  // - unlikely to be there
58
58
  // - we don't watch node modules (too expensive)
@@ -64,27 +64,22 @@ export const jsenvPluginImportMetaHot = () => {
64
64
  if (!urlInfo.content.includes("import.meta.hot")) {
65
65
  return null;
66
66
  }
67
- const { metadata } = await applyBabelPlugins({
68
- babelPlugins: [babelPluginMetadataImportMetaHot],
69
- input: urlInfo.content,
70
- inputIsJsModule: true,
71
- inputUrl: urlInfo.originalUrl,
72
- outputUrl: urlInfo.generatedUrl,
73
- });
74
67
  const {
75
- importMetaHotPaths,
68
+ importMetaHotNodes,
76
69
  hotDecline,
77
70
  hotAcceptSelf,
78
- hotAcceptDependencies,
79
- } = metadata;
71
+ hotAcceptSpecifiers,
72
+ } = analyzeImportMetaHot(urlInfo.contentAst);
80
73
  urlInfo.data.hotDecline = hotDecline;
81
74
  urlInfo.data.hotAcceptSelf = hotAcceptSelf;
82
- urlInfo.data.hotAcceptDependencies = hotAcceptDependencies;
83
- if (importMetaHotPaths.length === 0) {
75
+ urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
76
+ (specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
77
+ );
78
+ if (importMetaHotNodes.length === 0) {
84
79
  return null;
85
80
  }
86
81
  if (urlInfo.context.build) {
87
- return removeImportMetaHots(urlInfo, importMetaHotPaths);
82
+ return removeImportMetaHots(urlInfo, importMetaHotNodes);
88
83
  }
89
84
  return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
90
85
  },
@@ -92,12 +87,32 @@ export const jsenvPluginImportMetaHot = () => {
92
87
  };
93
88
  };
94
89
 
95
- const removeImportMetaHots = (urlInfo, importMetaHotPaths) => {
90
+ // The specifier given to import.meta.hot.accept() is one the file imports,
91
+ // so its url is known from that import (autoreload compares urls). A
92
+ // specifier the file does not import accepts nothing: it is kept as a plain
93
+ // url, never resolved as a dependency (that could throw on a bare specifier).
94
+ const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
95
+ for (const referenceToOther of urlInfo.referenceToOthersSet) {
96
+ if (
97
+ referenceToOther.type === "js_import" &&
98
+ referenceToOther.specifier === specifier
99
+ ) {
100
+ return referenceToOther.url;
101
+ }
102
+ }
103
+ try {
104
+ return new URL(specifier, urlInfo.url).href;
105
+ } catch {
106
+ return specifier;
107
+ }
108
+ };
109
+
110
+ const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
96
111
  const magicSource = createMagicSource(urlInfo.content);
97
- importMetaHotPaths.forEach((path) => {
112
+ importMetaHotNodes.forEach((node) => {
98
113
  magicSource.replace({
99
- start: path.node.start,
100
- end: path.node.end,
114
+ start: node.start,
115
+ end: node.end,
101
116
  replacement: "undefined",
102
117
  });
103
118
  });
@@ -10,7 +10,7 @@
10
10
  * TODO: ideally during dev we would keep import.meta.dev and ensure we set it to true rather than replacing it with true?
11
11
  */
12
12
 
13
- import { applyBabelPlugins } from "@jsenv/ast";
13
+ import { getImportMetaPropertyName, visitJsAst } from "@jsenv/ast";
14
14
  import { createMagicSource } from "@jsenv/sourcemap";
15
15
 
16
16
  export const jsenvPluginImportMetaScenarios = () => {
@@ -18,7 +18,7 @@ export const jsenvPluginImportMetaScenarios = () => {
18
18
  name: "jsenv:import_meta_scenario",
19
19
  appliesDuring: "*",
20
20
  transformUrlContent: {
21
- js_module: async (urlInfo) => {
21
+ js_module: (urlInfo) => {
22
22
  // Do not scan node modules for import.meta.dev/import.meta.build
23
23
  // - node modules won't have this in their code
24
24
  // - ;or should use other an other technic as this one won't be available
@@ -34,40 +34,42 @@ export const jsenvPluginImportMetaScenarios = () => {
34
34
  ) {
35
35
  return null;
36
36
  }
37
- const { metadata } = await applyBabelPlugins({
38
- babelPlugins: [babelPluginMetadataImportMetaScenarios],
39
- input: urlInfo.content,
40
- inputIsJsModule: true,
41
- inputUrl: urlInfo.originalUrl,
42
- outputUrl: urlInfo.generatedUrl,
37
+ const importMetaScenarioNodes = { dev: [], build: [] };
38
+ visitJsAst(urlInfo.contentAst, {
39
+ MemberExpression: (node) => {
40
+ const name = getImportMetaPropertyName(node);
41
+ if (name === "dev" || name === "build") {
42
+ importMetaScenarioNodes[name].push(node);
43
+ }
44
+ },
43
45
  });
44
- const { dev = [], build = [] } = metadata.importMetaScenarios;
46
+ const { dev, build } = importMetaScenarioNodes;
45
47
  const replacements = [];
46
- const replace = (path, value) => {
47
- replacements.push({ path, value });
48
+ const replace = (node, value) => {
49
+ replacements.push({ node, value });
48
50
  };
49
51
  if (urlInfo.context.build) {
50
52
  // during build ensure replacement for tree-shaking
51
- dev.forEach((path) => {
52
- replace(path, "undefined");
53
+ dev.forEach((node) => {
54
+ replace(node, "undefined");
53
55
  });
54
- build.forEach((path) => {
55
- replace(path, "true");
56
+ build.forEach((node) => {
57
+ replace(node, "true");
56
58
  });
57
59
  } else {
58
60
  // during dev we can let "import.meta.build" untouched
59
61
  // it will be evaluated to undefined.
60
62
  // Moreover it can be surprising to see some "undefined"
61
63
  // when source file contains "import.meta.build"
62
- dev.forEach((path) => {
63
- replace(path, "true");
64
+ dev.forEach((node) => {
65
+ replace(node, "true");
64
66
  });
65
67
  }
66
68
  const magicSource = createMagicSource(urlInfo.content);
67
- replacements.forEach(({ path, value }) => {
69
+ replacements.forEach(({ node, value }) => {
68
70
  magicSource.replace({
69
- start: path.node.start,
70
- end: path.node.end,
71
+ start: node.start,
72
+ end: node.end,
71
73
  replacement: value,
72
74
  });
73
75
  });
@@ -76,39 +78,3 @@ export const jsenvPluginImportMetaScenarios = () => {
76
78
  },
77
79
  };
78
80
  };
79
-
80
- const babelPluginMetadataImportMetaScenarios = () => {
81
- return {
82
- name: "metadata-import-meta-scenarios",
83
- visitor: {
84
- Program(programPath, state) {
85
- const importMetas = {};
86
- programPath.traverse({
87
- MemberExpression(path) {
88
- const { node } = path;
89
- const { object } = node;
90
- if (object.type !== "MetaProperty") {
91
- return;
92
- }
93
- const { property: objectProperty } = object;
94
- if (objectProperty.name !== "meta") {
95
- return;
96
- }
97
- const { property } = node;
98
- const { name } = property;
99
- const importMetaPaths = importMetas[name];
100
- if (importMetaPaths) {
101
- importMetaPaths.push(path);
102
- } else {
103
- importMetas[name] = [path];
104
- }
105
- },
106
- });
107
- state.file.metadata.importMetaScenarios = {
108
- dev: importMetas.dev,
109
- build: importMetas.build,
110
- };
111
- },
112
- },
113
- };
114
- };
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Tells the browser the static import graph of a page's module scripts, as
3
+ * `Link: <url>; rel=modulepreload` response headers, so it can fetch every
4
+ * module as soon as the page's headers arrive, without waiting to discover
5
+ * them level by level (each level costing a round trip and, the first time,
6
+ * a cook).
7
+ *
8
+ * Only what the graph already knows is listed: a module never cooked has no
9
+ * known imports. The header is computed at each response from the graph as it
10
+ * is then: a page gets its full list from its second load on, the first load
11
+ * keeps the plain waterfall.
12
+ *
13
+ * Headers rather than <link> elements written into the page: the page content
14
+ * stays what it is (served from memory, revalidated by etag) and the list can
15
+ * grow without cooking the page again. And not references either: a reference
16
+ * from the html to every module would make the html a dependent of each of
17
+ * them, and a hot update propagating up the importers would reach the html
18
+ * (which declines) instead of stopping at the module accepting it.
19
+ *
20
+ * OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
21
+ * Over http/1.1 it makes pages slower — measured on a 500 modules page: first
22
+ * paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
23
+ * browser no extra resource: what it has is 6 connections per origin, and
24
+ * every request goes through them in the order it was learned.
25
+ * - Without the header, requests come in execution order: the parser asks
26
+ * for the render-blocking supervisor script first and gets a connection at
27
+ * once; transfer and execution overlap, a module executing and discovering
28
+ * its imports while the others download. On localhost the server answers
29
+ * from memory in 0.1ms, so the 6 connections are busy from the start.
30
+ * - With the header, hundreds of requests take the 6 connections before the
31
+ * parser asks for the supervisor script, which then waits for a free
32
+ * connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
33
+ * Meanwhile nothing renders and nothing executes: transfer, then execution,
34
+ * serialized. http/1.1 cannot reprioritize a queued request, and a
35
+ * modulepreload has the same priority as a script.
36
+ * - And there is nothing to hide: the discovery latency a preload removes is
37
+ * ~1ms per graph level on localhost; with a real RTT a wide graph (more
38
+ * than 6 modules pending at any time) keeps the 6 connections saturated
39
+ * anyway, and the load takes requests / 6 * RTT whatever the order.
40
+ * It pays off for a deep and narrow graph (connections idle, waiting for a
41
+ * response to learn the next level), and over http/2 or http/3: no
42
+ * connection limit, and the browser prioritizes the blocking script.
43
+ */
44
+
45
+ import { injectQueryParamsIntoSpecifier } from "@jsenv/urls";
46
+
47
+ import { urlSpecifierEncoding } from "../../kitchen/url_graph/url_specifier_encoding.js";
48
+
49
+ export const jsenvPluginModulepreload = () => {
50
+ return {
51
+ name: "jsenv:modulepreload",
52
+ appliesDuring: "dev",
53
+ augmentResponse: ({ urlInfo }) => {
54
+ if (urlInfo.type !== "html") {
55
+ return null;
56
+ }
57
+ const hrefs = collectStaticImportHrefs(urlInfo);
58
+ if (hrefs.length === 0) {
59
+ return null;
60
+ }
61
+ return {
62
+ headers: {
63
+ link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
64
+ },
65
+ };
66
+ },
67
+ };
68
+ };
69
+
70
+ const collectStaticImportHrefs = (htmlUrlInfo) => {
71
+ const hrefSet = new Set();
72
+ const visitedSet = new Set();
73
+ const addHref = (reference) => {
74
+ const specifier = urlSpecifierEncoding.decode(reference);
75
+ if (typeof specifier !== "string") {
76
+ return;
77
+ }
78
+ // "?hot" belongs to the request that cooked the importer, the page loading
79
+ // now imports the url without it
80
+ hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
81
+ };
82
+ const visitJsModule = (jsModuleUrlInfo) => {
83
+ if (visitedSet.has(jsModuleUrlInfo)) {
84
+ return;
85
+ }
86
+ visitedSet.add(jsModuleUrlInfo);
87
+ for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
88
+ if (
89
+ reference.type !== "js_import" ||
90
+ reference.isWeak ||
91
+ reference.isImplicit ||
92
+ !STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
93
+ !reference.url.startsWith("file:")
94
+ ) {
95
+ continue;
96
+ }
97
+ const importedUrlInfo = reference.urlInfo;
98
+ // never cooked: its content, hence its own imports, are unknown
99
+ if (importedUrlInfo.type !== "js_module") {
100
+ continue;
101
+ }
102
+ addHref(reference);
103
+ visitJsModule(importedUrlInfo);
104
+ }
105
+ };
106
+ for (const reference of htmlUrlInfo.referenceToOthersSet) {
107
+ if (
108
+ reference.type !== "script" ||
109
+ reference.expectedType !== "js_module" ||
110
+ // the scripts jsenv adds to every page are few, small and in memory
111
+ reference.injected ||
112
+ reference.isWeak
113
+ ) {
114
+ continue;
115
+ }
116
+ const scriptUrlInfo = reference.urlInfo;
117
+ if (scriptUrlInfo.type !== "js_module") {
118
+ continue;
119
+ }
120
+ if (!reference.isInline && reference.url.startsWith("file:")) {
121
+ addHref(reference);
122
+ }
123
+ visitJsModule(scriptUrlInfo);
124
+ }
125
+ return Array.from(hrefSet);
126
+ };
127
+
128
+ const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
@@ -20,6 +20,7 @@ import { jsenvPluginImportMetaCss } from "./import_meta_css/jsenv_plugin_import_
20
20
  // autoreload
21
21
  import { jsenvPluginImportMetaHot } from "./import_meta_hot/jsenv_plugin_import_meta_hot.js";
22
22
  import { jsenvPluginAutoreload } from "./autoreload/jsenv_plugin_autoreload.js";
23
+ import { jsenvPluginModulepreload } from "./modulepreload/jsenv_plugin_modulepreload.js";
23
24
  import { jsenvPluginDependencyStatus } from "./dependency_status/jsenv_plugin_dependency_status.js";
24
25
  import { jsenvPluginCacheControl } from "./cache_control/jsenv_plugin_cache_control.js";
25
26
  import { jsenvPluginCustomElementsRedefine } from "./custom_elements_redefine/jsenv_plugin_custom_elements_redefine.js";
@@ -56,6 +57,7 @@ export const getCorePlugins = ({
56
57
 
57
58
  clientAutoreload,
58
59
  clientAutoreloadOnServerRestart,
60
+ modulepreload = false,
59
61
  dependencyStatus,
60
62
  cacheControl,
61
63
  scenarioPlaceholders = true,
@@ -157,6 +159,7 @@ export const getCorePlugins = ({
157
159
  ...(clientAutoreload && clientAutoreload.enabled
158
160
  ? [jsenvPluginAutoreload(clientAutoreload)]
159
161
  : []),
162
+ ...(modulepreload ? [jsenvPluginModulepreload()] : []),
160
163
  ...(dependencyStatus
161
164
  ? [jsenvPluginDependencyStatus(dependencyStatus)]
162
165
  : []),
@@ -56,10 +56,31 @@ const DirectoryListing = () => {
56
56
  };
57
57
 
58
58
  const ErrorMessage = () => {
59
- const { filePathExisting, filePathNotFound } = enoentDetails;
59
+ const { filePathExisting, filePathNotFound, spaFallbackFilePaths } =
60
+ enoentDetails;
60
61
 
61
62
  let errorText;
62
63
  let errorSuggestion;
64
+ let spaExplanation = null;
65
+ if (spaFallbackFilePaths) {
66
+ spaExplanation = (
67
+ <>
68
+ <strong>SPA mode:</strong> a url without extension is a route, served
69
+ with the closest html file. None of these exists:
70
+ <ul
71
+ className="spa_fallback_list"
72
+ style="margin: 6px 0 0; padding-left: 20px;"
73
+ >
74
+ {spaFallbackFilePaths.map((filePath) => (
75
+ <li key={filePath}>
76
+ <code>{filePath}</code>
77
+ {filePath === `/${mainFilePath}` ? " (main file)" : ""}
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ </>
82
+ );
83
+ }
63
84
  errorText = (
64
85
  <>
65
86
  <strong>File not found:</strong>&nbsp;
@@ -82,6 +103,11 @@ const ErrorMessage = () => {
82
103
  return (
83
104
  <div className="error_message">
84
105
  <p className="error_text">{errorText}</p>
106
+ {spaExplanation ? (
107
+ <p className="error_spa" style="margin-top: 10px;">
108
+ {spaExplanation}
109
+ </p>
110
+ ) : null}
85
111
  <p
86
112
  className="error_suggestion"
87
113
  style="font-size: 0.8em; margin-top: 10px;"
@@ -32,12 +32,14 @@ import {
32
32
  asUrlWithoutSearch,
33
33
  ensurePathnameTrailingSlash,
34
34
  urlIsOrIsInsideOf,
35
+ urlToExtension,
35
36
  urlToFilename,
36
37
  urlToRelativeUrl,
37
38
  } from "@jsenv/urls";
38
39
  import { existsSync, lstatSync, readdirSync } from "node:fs";
39
40
  import { getDirectoryWatchPatterns } from "../../helpers/watch_source_files.js";
40
41
  import { FILE_AND_SERVER_URLS_CONVERTER } from "../../kitchen/file_and_server_urls_converter.js";
42
+ import { listSpaFallbackFileUrls } from "./jsenv_plugin_fs_redirection.js";
41
43
 
42
44
  const htmlFileUrlForDirectory = import.meta
43
45
  .resolve("./client/directory_listing.html");
@@ -391,6 +393,24 @@ const generateDirectoryListingInjection = (
391
393
  filePathExisting: `/${filePathExisting}`,
392
394
  filePathNotFound,
393
395
  });
396
+ // a url without extension nor trailing slash is a route: in spa mode it
397
+ // was answered with the closest html file, the 404 means none was found
398
+ const urlNotFoundObject = new URL(urlNotFound);
399
+ if (
400
+ spa &&
401
+ !urlToExtension(urlNotFoundObject) &&
402
+ !urlNotFoundObject.pathname.endsWith("/")
403
+ ) {
404
+ enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
405
+ urlNotFound,
406
+ { rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
407
+ ).map((fileUrl) =>
408
+ FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
409
+ fileUrl,
410
+ serverRootDirectoryUrl,
411
+ ),
412
+ );
413
+ }
394
414
  }
395
415
 
396
416
  return {
@@ -201,25 +201,42 @@ const getSpaFallbackUrl = (reference) => {
201
201
  // the SPA fallback answers a request; during build there is none
202
202
  return null;
203
203
  }
204
- const closestHtmlRootFile = getClosestHtmlRootFile(
205
- requestedUrl,
204
+ const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
206
205
  rootDirectoryUrl,
207
- );
208
- if (closestHtmlRootFile) {
209
- return closestHtmlRootFile;
206
+ mainFilePath,
207
+ });
208
+ for (const spaFallbackFileUrl of spaFallbackFileUrls) {
209
+ if (existsSync(new URL(spaFallbackFileUrl))) {
210
+ return spaFallbackFileUrl;
211
+ }
210
212
  }
211
- return String(new URL(mainFilePath, rootDirectoryUrl));
213
+ // none exists: the main file it is, and the 404 answering for it lists
214
+ // what was tried (see directory listing)
215
+ return new URL(mainFilePath, rootDirectoryUrl).href;
212
216
  };
213
- const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
214
- let directoryUrl = new URL("./", requestedUrl);
215
- while (true) {
216
- const directoryEntryFileUrl = getDirectoryEntryFileUrl(directoryUrl);
217
- if (directoryEntryFileUrl) {
218
- return directoryEntryFileUrl;
217
+ // The html files that can answer a route, closest first: the entry file of
218
+ // the route's own directory ("index.html", then "<dirname>.html"), then of
219
+ // each directory above it up to the server root, then the main file.
220
+ export const listSpaFallbackFileUrls = (
221
+ requestedUrl,
222
+ { rootDirectoryUrl, mainFilePath },
223
+ ) => {
224
+ const fileUrls = [];
225
+ let directoryUrl = new URL("./", requestedUrl).href;
226
+ while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
227
+ fileUrls.push(new URL("index.html", directoryUrl).href);
228
+ const filename = urlToFilename(directoryUrl);
229
+ if (filename) {
230
+ fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
219
231
  }
220
- if (!urlIsOrIsInsideOf(directoryUrl, serverRootDirectoryUrl)) {
221
- return null;
232
+ if (directoryUrl === String(rootDirectoryUrl)) {
233
+ break;
222
234
  }
223
- directoryUrl = new URL("../", directoryUrl);
235
+ directoryUrl = new URL("../", directoryUrl).href;
236
+ }
237
+ const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
238
+ if (!fileUrls.includes(mainFileUrl)) {
239
+ fileUrls.push(mainFileUrl);
224
240
  }
241
+ return fileUrls;
225
242
  };
@@ -1,7 +1,7 @@
1
1
  import { readEntryStatSync } from "@jsenv/filesystem";
2
2
  import { ensurePathnameTrailingSlash } from "@jsenv/urls";
3
3
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
4
- import { readFileSync, readdirSync } from "node:fs";
4
+ import { readFileSync, readdirSync, statSync } from "node:fs";
5
5
  import { FILE_AND_SERVER_URLS_CONVERTER } from "../../kitchen/file_and_server_urls_converter.js";
6
6
  import { createHtmlPageLister } from "./html_pages.js";
7
7
  import { jsenvPluginDirectoryListing } from "./jsenv_plugin_directory_listing.js";
@@ -160,7 +160,16 @@ export const jsenvPluginProtocolFile = ({
160
160
  }
161
161
  const serveFile = (url) => {
162
162
  const contentType = CONTENT_TYPE.fromUrlExtension(url);
163
- const fileBuffer = readFileSync(new URL(url));
163
+ const urlObject = new URL(url);
164
+ // taken before the read: a write landing in between moves the
165
+ // stat past this one, so the dev server sees the content as
166
+ // outdated (see isValid in the dev server) rather than the reverse
167
+ const fileStat = statSync(urlObject);
168
+ const fileBuffer = readFileSync(urlObject);
169
+ urlInfo.data.fileStat = {
170
+ mtimeMs: fileStat.mtimeMs,
171
+ size: fileStat.size,
172
+ };
164
173
  const content = CONTENT_TYPE.isTextual(contentType)
165
174
  ? String(fileBuffer)
166
175
  : fileBuffer;
@@ -478,11 +478,6 @@ const addRelationshipWithPackageJson = ({
478
478
  String(packageJsonContentAsBuffer),
479
479
  );
480
480
  }
481
- // Checked on disk at every validation rather than trusted to the watcher:
482
- // what this file decides (the package version, hence the ?v= importers
483
- // embed) ends up in the browser's immutable cache, so a request racing the
484
- // watcher must never be answered from a stale package.json.
485
- packageJsonReference.urlInfo.revalidateOnFileSystem = true;
486
481
  };
487
482
 
488
483
  const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
@@ -1,97 +0,0 @@
1
- // https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-stages-of-babel
2
- // https://github.com/cfware/babel-plugin-bundled-import-meta/blob/master/index.js
3
- // https://github.com/babel/babel/blob/f4edf62f6beeab8ae9f2b7f0b82f1b3b12a581af/packages/babel-helper-module-imports/src/index.js#L7
4
-
5
- export const babelPluginMetadataImportMetaHot = () => {
6
- return {
7
- name: "metadata-import-meta-hot",
8
- visitor: {
9
- Program(programPath, state) {
10
- Object.assign(
11
- state.file.metadata,
12
- collectImportMetaHotProperties(programPath),
13
- );
14
- },
15
- },
16
- };
17
- };
18
- const collectImportMetaHotProperties = (programPath) => {
19
- const importMetaHotPaths = [];
20
- let hotDecline = false;
21
- let hotAcceptSelf = false;
22
- let hotAcceptDependencies = [];
23
- programPath.traverse({
24
- MemberExpression(path) {
25
- const { node } = path;
26
- const { object } = node;
27
- if (object.type !== "MetaProperty") {
28
- return;
29
- }
30
- const { property: objectProperty } = object;
31
- if (objectProperty.name !== "meta") {
32
- return;
33
- }
34
- const { property } = node;
35
- const { name } = property;
36
- if (name === "hot") {
37
- importMetaHotPaths.push(path);
38
- }
39
- },
40
- CallExpression(path) {
41
- if (isImportMetaHotMethodCall(path, "accept")) {
42
- const callNode = path.node;
43
- const args = callNode.arguments;
44
- if (args.length === 0) {
45
- hotAcceptSelf = true;
46
- return;
47
- }
48
- const firstArg = args[0];
49
- if (firstArg.type === "StringLiteral") {
50
- hotAcceptDependencies = [
51
- {
52
- specifierPath: path.get("arguments")[0],
53
- },
54
- ];
55
- return;
56
- }
57
- if (firstArg.type === "ArrayExpression") {
58
- const firstArgPath = path.get("arguments")[0];
59
- hotAcceptDependencies = firstArg.elements.map((arrayNode, index) => {
60
- if (arrayNode.type !== "StringLiteral") {
61
- throw new Error(
62
- `all array elements must be strings in "import.meta.hot.accept(array)"`,
63
- );
64
- }
65
- return {
66
- specifierPath: firstArgPath.get(String(index)),
67
- };
68
- });
69
- return;
70
- }
71
- // accept first arg can be "anything" such as
72
- // `const cb = () => {}; import.meta.accept(cb)`
73
- hotAcceptSelf = true;
74
- }
75
- if (isImportMetaHotMethodCall(path, "decline")) {
76
- hotDecline = true;
77
- }
78
- },
79
- });
80
- return {
81
- importMetaHotPaths,
82
- hotDecline,
83
- hotAcceptSelf,
84
- hotAcceptDependencies,
85
- };
86
- };
87
- const isImportMetaHotMethodCall = (path, methodName) => {
88
- const { property, object } = path.node.callee;
89
- return (
90
- property &&
91
- property.name === methodName &&
92
- object &&
93
- object.property &&
94
- object.property.name === "hot" &&
95
- object.object.type === "MetaProperty"
96
- );
97
- };