@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.
- package/dist/build/build.js +573 -469
- package/dist/client/directory_listing/js/directory_listing.js +23 -2
- package/dist/start_dev_server/start_dev_server.js +780 -612
- package/package.json +7 -7
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +116 -68
- package/src/dev/start_dev_server.js +24 -4
- package/src/helpers/watch_source_files.js +25 -20
- package/src/kitchen/url_graph/references.js +30 -13
- package/src/kitchen/url_graph/url_graph.js +0 -1
- package/src/plugins/import_meta_css/jsenv_plugin_import_meta_css.js +8 -40
- package/src/plugins/import_meta_hot/import_meta_hot_analysis.js +70 -0
- package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +35 -20
- package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +22 -56
- package/src/plugins/modulepreload/jsenv_plugin_modulepreload.js +128 -0
- package/src/plugins/plugins.js +3 -0
- package/src/plugins/protocol_file/client/directory_listing.jsx +27 -1
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +20 -0
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +32 -15
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +11 -2
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +0 -5
- package/src/plugins/import_meta_hot/babel_plugin_metadata_import_meta_hot.js +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.4.
|
|
3
|
+
"version": "41.4.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@jsenv/ast": "6.9.
|
|
76
|
-
"@jsenv/js-module-fallback": "1.5.
|
|
77
|
-
"@jsenv/plugin-bundling": "2.10.
|
|
78
|
-
"@jsenv/plugin-minification": "1.7.
|
|
79
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
80
|
-
"@jsenv/plugin-transpilation": "1.6.
|
|
75
|
+
"@jsenv/ast": "6.9.2",
|
|
76
|
+
"@jsenv/js-module-fallback": "1.5.1",
|
|
77
|
+
"@jsenv/plugin-bundling": "2.10.17",
|
|
78
|
+
"@jsenv/plugin-minification": "1.7.9",
|
|
79
|
+
"@jsenv/plugin-supervisor": "1.8.13",
|
|
80
|
+
"@jsenv/plugin-transpilation": "1.6.1",
|
|
81
81
|
"@jsenv/server": "17.6.1",
|
|
82
82
|
"@jsenv/sourcemap": "1.4.2",
|
|
83
83
|
"react-table": "7.8.0"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { bufferToEtag } from "@jsenv/filesystem";
|
|
2
2
|
import { formatError } from "@jsenv/humanize";
|
|
3
3
|
import { composeTwoResponses, fetchDirectory } from "@jsenv/server";
|
|
4
|
-
import { URL_META } from "@jsenv/url-meta";
|
|
5
4
|
import { normalizeUrl } from "@jsenv/urls";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
6
|
|
|
8
7
|
import { watchSourceFiles } from "../../helpers/watch_source_files.js";
|
|
9
8
|
import { WEB_URL_CONVERTER } from "../../helpers/web_url_converter.js";
|
|
@@ -56,11 +55,10 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
56
55
|
if (existing) {
|
|
57
56
|
return existing;
|
|
58
57
|
}
|
|
59
|
-
const watchAssociations = URL_META.resolveAssociations(
|
|
60
|
-
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
61
|
-
sourceDirectoryUrl,
|
|
62
|
-
);
|
|
63
58
|
let kitchen;
|
|
59
|
+
// per url info, the file stat under which its content was last compared
|
|
60
|
+
// with the file on disk and found identical (see isValid)
|
|
61
|
+
const fileStatValidatedMap = new WeakMap();
|
|
64
62
|
clientFileChangeEventEmitter.on(({ url, event }) => {
|
|
65
63
|
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
66
64
|
if (urlInfo) {
|
|
@@ -101,11 +99,6 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
101
99
|
packageDirectory,
|
|
102
100
|
});
|
|
103
101
|
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
104
|
-
const { watch } = URL_META.applyAssociations({
|
|
105
|
-
url: urlInfoCreated.url,
|
|
106
|
-
associations: watchAssociations,
|
|
107
|
-
});
|
|
108
|
-
urlInfoCreated.isWatched = watch;
|
|
109
102
|
// when an url depends on many others, we check all these (like package.json)
|
|
110
103
|
urlInfoCreated.isValid = () => {
|
|
111
104
|
const seenSet = new Set();
|
|
@@ -125,37 +118,64 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
125
118
|
// was compared using etag and it has changed
|
|
126
119
|
return false;
|
|
127
120
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// the
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
// The content held in memory is the file as it was read; a request
|
|
122
|
+
// must never win a stale answer, whatever the watcher's latency,
|
|
123
|
+
// and the ?v= a package.json decides even ends up in the browser's
|
|
124
|
+
// immutable cache. So the file is checked on disk at every
|
|
125
|
+
// validation — cheaply: a stat, compared with the one taken when
|
|
126
|
+
// the content was read (or last found identical). Only a file
|
|
127
|
+
// whose stat moved is read and hashed again.
|
|
128
|
+
// Inline content (a <script> inside an html) has no file of its
|
|
129
|
+
// own: it is as fresh as the html holding it, checked by the caller.
|
|
130
|
+
if (!urlInfo.isInline) {
|
|
131
|
+
let fileStat;
|
|
137
132
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
133
|
+
fileStat = statSync(new URL(urlInfo.url), {
|
|
134
|
+
throwIfNoEntry: false,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
144
137
|
return false;
|
|
145
138
|
}
|
|
146
|
-
|
|
147
|
-
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
139
|
+
if (!fileStat) {
|
|
148
140
|
urlInfo.onModified();
|
|
149
|
-
// restore content to be able to compare it again later
|
|
150
|
-
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
151
|
-
urlInfo,
|
|
152
|
-
String(fileContentAsBuffer),
|
|
153
|
-
{
|
|
154
|
-
contentEtag: fileContentEtag,
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
141
|
return false;
|
|
158
142
|
}
|
|
143
|
+
const fileStatKnown =
|
|
144
|
+
fileStatValidatedMap.get(urlInfo) || urlInfo.data.fileStat;
|
|
145
|
+
const fileUnchanged =
|
|
146
|
+
fileStatKnown &&
|
|
147
|
+
fileStatKnown.mtimeMs === fileStat.mtimeMs &&
|
|
148
|
+
fileStatKnown.size === fileStat.size;
|
|
149
|
+
if (!fileUnchanged) {
|
|
150
|
+
let fileContentAsBuffer;
|
|
151
|
+
try {
|
|
152
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfo.url));
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e.code === "ENOENT") {
|
|
155
|
+
urlInfo.onModified();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
161
|
+
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
162
|
+
fileStatValidatedMap.delete(urlInfo);
|
|
163
|
+
urlInfo.onModified();
|
|
164
|
+
// restore content to be able to compare it again later
|
|
165
|
+
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
166
|
+
urlInfo,
|
|
167
|
+
String(fileContentAsBuffer),
|
|
168
|
+
{
|
|
169
|
+
contentEtag: fileContentEtag,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
fileStatValidatedMap.set(urlInfo, {
|
|
175
|
+
mtimeMs: fileStat.mtimeMs,
|
|
176
|
+
size: fileStat.size,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
for (const implicitUrl of urlInfo.implicitUrlSet) {
|
|
161
181
|
const implicitUrlInfo = urlInfo.graph.getUrlInfo(implicitUrl);
|
|
@@ -271,6 +291,40 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
271
291
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
272
292
|
const inlineParentUrlInfo = urlInfo.findParentIfInline();
|
|
273
293
|
const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
|
|
294
|
+
// The content held in memory is the response when it is finalized
|
|
295
|
+
// and still valid. Content can be defined while a cook is still in
|
|
296
|
+
// flight (a file watcher invalidation re-cooking in the background,
|
|
297
|
+
// for instance): at that point it holds the raw fetched content,
|
|
298
|
+
// transformations not applied yet. Serving that would send an html
|
|
299
|
+
// without any of the injected scripts. An inline url info (a
|
|
300
|
+
// <script> inside an html) is cooked again whenever the html
|
|
301
|
+
// containing it is cooked: its content is as fresh as the html's,
|
|
302
|
+
// so both must be valid.
|
|
303
|
+
const hasFreshContent = (urlInfo) =>
|
|
304
|
+
urlInfo.content !== undefined &&
|
|
305
|
+
urlInfo.contentFinalized &&
|
|
306
|
+
urlInfo.isValid();
|
|
307
|
+
const memoryContentIsFresh = () =>
|
|
308
|
+
inlineParentUrlInfo
|
|
309
|
+
? hasFreshContent(urlInfo) && hasFreshContent(inlineParentUrlInfo)
|
|
310
|
+
: hasFreshContent(urlInfo);
|
|
311
|
+
// a 304 goes through the hooks too: its headers stand for the
|
|
312
|
+
// cached response's, they must say the same
|
|
313
|
+
const augmentResponse = (response) => {
|
|
314
|
+
const augmentResponseInfo = {
|
|
315
|
+
...kitchen.context,
|
|
316
|
+
reference,
|
|
317
|
+
urlInfo,
|
|
318
|
+
};
|
|
319
|
+
kitchen.jsenvPluginsController.callHooks(
|
|
320
|
+
"augmentResponse",
|
|
321
|
+
augmentResponseInfo,
|
|
322
|
+
(returnValue) => {
|
|
323
|
+
response = composeTwoResponses(response, returnValue);
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
return response;
|
|
327
|
+
};
|
|
274
328
|
const respondWithNotModified = () => {
|
|
275
329
|
const headers = {
|
|
276
330
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
@@ -280,23 +334,21 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
280
334
|
headers[key] = urlInfo.headers[key];
|
|
281
335
|
}
|
|
282
336
|
});
|
|
283
|
-
return {
|
|
337
|
+
return augmentResponse({
|
|
284
338
|
status: 304,
|
|
285
339
|
headers,
|
|
286
|
-
};
|
|
340
|
+
});
|
|
287
341
|
};
|
|
288
342
|
|
|
289
343
|
try {
|
|
290
|
-
|
|
291
|
-
// is cooked, so its content is only known after cooking; its etag is
|
|
292
|
-
// compared below, once cooked
|
|
293
|
-
if (!urlInfo.error && ifNoneMatch && !inlineParentUrlInfo) {
|
|
344
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
294
345
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
295
346
|
ifNoneMatch.split("_");
|
|
296
347
|
if (
|
|
297
|
-
|
|
348
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
349
|
+
clientOriginalContentEtag &&
|
|
298
350
|
urlInfo.contentEtag === clientContentEtag &&
|
|
299
|
-
|
|
351
|
+
memoryContentIsFresh()
|
|
300
352
|
) {
|
|
301
353
|
return respondWithNotModified();
|
|
302
354
|
}
|
|
@@ -314,18 +366,9 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
314
366
|
// client etag to match).
|
|
315
367
|
const servableFromMemory =
|
|
316
368
|
!urlInfo.error &&
|
|
317
|
-
!inlineParentUrlInfo &&
|
|
318
369
|
!urlInfo.response &&
|
|
319
|
-
urlInfo.content !== undefined &&
|
|
320
|
-
// content can be defined while a cook is still in flight (a file
|
|
321
|
-
// watcher invalidation re-cooking in the background, for
|
|
322
|
-
// instance): at that point it holds the raw fetched content,
|
|
323
|
-
// transformations not applied yet. Serving that would send an
|
|
324
|
-
// html without any of the injected scripts. Only finalized
|
|
325
|
-
// content is a complete response; anything else must go through
|
|
326
|
-
// cook() below, which joins the pending cook (see debounceCook).
|
|
327
|
-
urlInfo.contentFinalized &&
|
|
328
370
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
371
|
+
!cacheIsDisabledInResponseHeader(urlInfoTargetedByCache) &&
|
|
329
372
|
// a "?hot" request exists to bypass every cache, this one
|
|
330
373
|
// included: it must be cooked, because cooking is what rewrites
|
|
331
374
|
// its references so "?hot" cascades to the modified files below
|
|
@@ -344,7 +387,7 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
344
387
|
// signal registry throwing on duplicate ids). Re-cooking under
|
|
345
388
|
// the normal request rewrites the references clean.
|
|
346
389
|
!urlInfo.contentCookedForHotRequest &&
|
|
347
|
-
|
|
390
|
+
memoryContentIsFresh();
|
|
348
391
|
if (!servableFromMemory) {
|
|
349
392
|
await urlInfo.cook({ request, reference });
|
|
350
393
|
urlInfo.contentCookedForHotRequest =
|
|
@@ -402,19 +445,7 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
402
445
|
? { "served from memory cache": null }
|
|
403
446
|
: urlInfo.timing,
|
|
404
447
|
};
|
|
405
|
-
|
|
406
|
-
...kitchen.context,
|
|
407
|
-
reference,
|
|
408
|
-
urlInfo,
|
|
409
|
-
};
|
|
410
|
-
kitchen.jsenvPluginsController.callHooks(
|
|
411
|
-
"augmentResponse",
|
|
412
|
-
augmentResponseInfo,
|
|
413
|
-
(returnValue) => {
|
|
414
|
-
response = composeTwoResponses(response, returnValue);
|
|
415
|
-
},
|
|
416
|
-
);
|
|
417
|
-
return response;
|
|
448
|
+
return augmentResponse(response);
|
|
418
449
|
} catch (error) {
|
|
419
450
|
const originalError = error ? error.cause || error : error;
|
|
420
451
|
if (originalError.asResponse) {
|
|
@@ -492,6 +523,14 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
492
523
|
"cache-control": "no-store",
|
|
493
524
|
},
|
|
494
525
|
};
|
|
526
|
+
} finally {
|
|
527
|
+
// What the request put on the url info context is for this
|
|
528
|
+
// request only: a cook happening later for another reason (the
|
|
529
|
+
// html holding an inline script is cooked again) must not see a
|
|
530
|
+
// stale "requestedUrl" and take the inline script for a direct
|
|
531
|
+
// request, which would make it pick the content of a previous
|
|
532
|
+
// reference (see jsenv:inline_content_fetcher).
|
|
533
|
+
forgetRequestFromContext(urlInfo.context);
|
|
495
534
|
}
|
|
496
535
|
},
|
|
497
536
|
},
|
|
@@ -501,6 +540,15 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
501
540
|
return [devServerPluginRoutes, ...devServerJsenvPluginStore.allServerPlugins];
|
|
502
541
|
};
|
|
503
542
|
|
|
543
|
+
const forgetRequestFromContext = (context) => {
|
|
544
|
+
// own properties only: what is inherited from the owner context stays
|
|
545
|
+
for (const key of ["request", "requestedUrl", "reference"]) {
|
|
546
|
+
if (Object.hasOwn(context, key)) {
|
|
547
|
+
delete context[key];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
504
552
|
const cacheIsDisabledInResponseHeader = (urlInfo) => {
|
|
505
553
|
return (
|
|
506
554
|
urlInfo.headers["cache-control"] === "no-store" ||
|
|
@@ -8,8 +8,13 @@ import {
|
|
|
8
8
|
serverPluginCORS,
|
|
9
9
|
startServer,
|
|
10
10
|
} from "@jsenv/server";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
urlIsOrIsInsideOf,
|
|
13
|
+
urlToFileSystemPath,
|
|
14
|
+
urlToRelativeUrl,
|
|
15
|
+
} from "@jsenv/urls";
|
|
12
16
|
import { existsSync } from "node:fs";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
13
18
|
|
|
14
19
|
import { defaultRuntimeCompat } from "../build/build_params.js";
|
|
15
20
|
import { createEventEmitter } from "../helpers/event_emitter.js";
|
|
@@ -50,6 +55,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
50
55
|
* @param {boolean|object} [params.serverTiming={ minDuration: 0.5 }] - server-timing response headers; `minDuration` (ms) drops entries that took less (0 when run by the test plan, so tests see every entry).
|
|
51
56
|
* @param {boolean|object} [params.ribbon=true] - The "ribbon" overlay marking the page as non-production. As an object: `text` (defaults to `"DEV"`), `color` (background, defaults to `"orange"`), `textColor` (defaults to dark or light depending on `color`), `href` (turns the ribbon into a link; only then does it capture clicks), `target` (defaults to `"_blank"`), `position` (`"top-right"` (default), `"top-left"`, `"bottom-right"`, `"bottom-left"` draw a diagonal corner ribbon; `"top"`, `"bottom"` draw a full width band), `htmlInclude` (defaults to `"/**\/*.html"`).
|
|
52
57
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
58
|
+
* @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
|
|
53
59
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
54
60
|
* @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
|
|
55
61
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
@@ -91,6 +97,7 @@ export const startDevServer = async ({
|
|
|
91
97
|
sourceFilesConfig = {},
|
|
92
98
|
clientAutoreload = true,
|
|
93
99
|
clientAutoreloadOnServerRestart = true,
|
|
100
|
+
modulepreload = false,
|
|
94
101
|
// server-timing response headers: devtools show how the time to answer is
|
|
95
102
|
// spent (cook measures come from the kitchen, see urlInfo.timing). Entries
|
|
96
103
|
// under minDuration are dropped so a human reads the measures that matter;
|
|
@@ -181,9 +188,12 @@ export const startDevServer = async ({
|
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
const logger = createLogger({ logLevel });
|
|
184
|
-
const startDevServerTask = createTaskLog(
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const startDevServerTask = createTaskLog(
|
|
192
|
+
`start dev server for ${humanizeSourceDirectory(sourceDirectoryUrl)}`,
|
|
193
|
+
{
|
|
194
|
+
disabled: !logger.levels.info,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
187
197
|
|
|
188
198
|
const serverStopCallbackSet = new Set();
|
|
189
199
|
const serverStopAbortController = new AbortController();
|
|
@@ -262,6 +272,7 @@ export const startDevServer = async ({
|
|
|
262
272
|
|
|
263
273
|
clientAutoreload,
|
|
264
274
|
clientAutoreloadOnServerRestart,
|
|
275
|
+
modulepreload,
|
|
265
276
|
dependencyStatus: {
|
|
266
277
|
dependencyProblemEventEmitter,
|
|
267
278
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
@@ -366,3 +377,12 @@ export const startDevServer = async ({
|
|
|
366
377
|
kitchenCache,
|
|
367
378
|
};
|
|
368
379
|
};
|
|
380
|
+
|
|
381
|
+
// "./src/" when inside the current working directory, the full path otherwise
|
|
382
|
+
const humanizeSourceDirectory = (sourceDirectoryUrl) => {
|
|
383
|
+
const cwdUrl = pathToFileURL(`${process.cwd()}/`).href;
|
|
384
|
+
if (urlIsOrIsInsideOf(sourceDirectoryUrl, cwdUrl)) {
|
|
385
|
+
return `./${urlToRelativeUrl(sourceDirectoryUrl, cwdUrl)}`;
|
|
386
|
+
}
|
|
387
|
+
return urlToFileSystemPath(sourceDirectoryUrl);
|
|
388
|
+
};
|
|
@@ -2,24 +2,27 @@ import {
|
|
|
2
2
|
lookupPackageDirectory,
|
|
3
3
|
registerDirectoryLifecycle,
|
|
4
4
|
} from "@jsenv/filesystem";
|
|
5
|
-
import { urlToRelativeUrl } from "@jsenv/urls";
|
|
5
|
+
import { urlIsOrIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
|
|
8
8
|
export const getDirectoryWatchPatterns = (
|
|
9
9
|
directoryUrl,
|
|
10
10
|
watchedDirectoryUrl,
|
|
11
|
-
{ sourceFilesConfig },
|
|
11
|
+
{ sourceFilesConfig, defaultPatterns = true },
|
|
12
12
|
) => {
|
|
13
13
|
const directoryUrlRelativeToWatchedDirectory = urlToRelativeUrl(
|
|
14
14
|
directoryUrl,
|
|
15
15
|
watchedDirectoryUrl,
|
|
16
16
|
);
|
|
17
|
-
const watchPatterns = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const watchPatterns = {};
|
|
18
|
+
if (defaultPatterns) {
|
|
19
|
+
Object.assign(watchPatterns, {
|
|
20
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
|
|
21
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
|
|
22
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
|
|
23
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
|
|
24
|
+
});
|
|
25
|
+
}
|
|
23
26
|
for (const key of Object.keys(sourceFilesConfig)) {
|
|
24
27
|
watchPatterns[`${directoryUrlRelativeToWatchedDirectory}${key}`] =
|
|
25
28
|
sourceFilesConfig[key];
|
|
@@ -40,11 +43,12 @@ export const watchSourceFiles = (
|
|
|
40
43
|
// And jsenv should not consider these as source files and watch them (to not hurt performances)
|
|
41
44
|
const watchPatterns = {};
|
|
42
45
|
let watchedDirectoryUrl = "";
|
|
43
|
-
const addDirectoryToWatch = (directoryUrl) => {
|
|
46
|
+
const addDirectoryToWatch = (directoryUrl, { defaultPatterns } = {}) => {
|
|
44
47
|
Object.assign(
|
|
45
48
|
watchPatterns,
|
|
46
49
|
getDirectoryWatchPatterns(directoryUrl, watchedDirectoryUrl, {
|
|
47
50
|
sourceFilesConfig,
|
|
51
|
+
defaultPatterns,
|
|
48
52
|
}),
|
|
49
53
|
);
|
|
50
54
|
};
|
|
@@ -76,7 +80,6 @@ export const watchSourceFiles = (
|
|
|
76
80
|
},
|
|
77
81
|
},
|
|
78
82
|
);
|
|
79
|
-
stopWatchingSourceFiles.watchPatterns = watchPatterns;
|
|
80
83
|
return stopWatchingSourceFiles;
|
|
81
84
|
};
|
|
82
85
|
|
|
@@ -96,16 +99,18 @@ export const watchSourceFiles = (
|
|
|
96
99
|
}
|
|
97
100
|
watchedDirectoryUrl = packageDirectoryUrl;
|
|
98
101
|
for (const workspace of workspaces) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const workspaceDirectoryUrl = workspace.endsWith("*")
|
|
103
|
+
? new URL(workspace.slice(0, -1), packageDirectoryUrl)
|
|
104
|
+
: new URL(workspace, packageDirectoryUrl);
|
|
105
|
+
addDirectoryToWatch(workspaceDirectoryUrl, {
|
|
106
|
+
// The source directory patterns already cover a workspace inside it.
|
|
107
|
+
// Every pattern is tested against every file found while watching, so
|
|
108
|
+
// the same patterns rooted at each workspace only multiply that work.
|
|
109
|
+
defaultPatterns: !urlIsOrIsInsideOf(
|
|
110
|
+
workspaceDirectoryUrl,
|
|
111
|
+
sourceDirectoryUrl,
|
|
112
|
+
),
|
|
113
|
+
});
|
|
109
114
|
}
|
|
110
115
|
// we are updating the root directory
|
|
111
116
|
// we must make the patterns relative to source directory relative to the new root directory
|
|
@@ -634,20 +634,37 @@ const checkForDependencyRemovalEffects = (reference) => {
|
|
|
634
634
|
};
|
|
635
635
|
|
|
636
636
|
const traceFromUrlSite = (urlSite) => {
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
: ""
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
637
|
+
const { url, line, column, content } = urlSite;
|
|
638
|
+
const trace = { url, line, column };
|
|
639
|
+
// A file references many urls and an error on one of them is rare: the code
|
|
640
|
+
// frame (and the message embedding it) is built the first time it is read,
|
|
641
|
+
// not for every reference found.
|
|
642
|
+
defineLazyProperty(trace, "codeFrame", () =>
|
|
643
|
+
content ? generateContentFrame({ content, line, column }) : "",
|
|
644
|
+
);
|
|
645
|
+
defineLazyProperty(trace, "message", () => stringifyUrlSite(urlSite));
|
|
646
|
+
return trace;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const defineLazyProperty = (object, property, compute) => {
|
|
650
|
+
const setValue = (value) => {
|
|
651
|
+
Object.defineProperty(object, property, {
|
|
652
|
+
enumerable: true,
|
|
653
|
+
configurable: true,
|
|
654
|
+
writable: true,
|
|
655
|
+
value,
|
|
656
|
+
});
|
|
650
657
|
};
|
|
658
|
+
Object.defineProperty(object, property, {
|
|
659
|
+
enumerable: true,
|
|
660
|
+
configurable: true,
|
|
661
|
+
get: () => {
|
|
662
|
+
const value = compute();
|
|
663
|
+
setValue(value);
|
|
664
|
+
return value;
|
|
665
|
+
},
|
|
666
|
+
set: setValue,
|
|
667
|
+
});
|
|
651
668
|
};
|
|
652
669
|
|
|
653
670
|
const adjustUrlSite = (urlInfo, { url, line, column }) => {
|
|
@@ -185,7 +185,6 @@ const createUrlInfo = (url, context) => {
|
|
|
185
185
|
dereferencedTimestamp: 0,
|
|
186
186
|
originalContentEtag: null,
|
|
187
187
|
contentEtag: null,
|
|
188
|
-
isWatched: false,
|
|
189
188
|
isValid: () => false,
|
|
190
189
|
data: {}, // plugins can put whatever they want here
|
|
191
190
|
referenceToOthersSet: new Set(),
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
applyBabelPlugins,
|
|
28
|
+
getImportMetaPropertyName,
|
|
29
|
+
visitJsAstUntil,
|
|
30
|
+
} from "@jsenv/ast";
|
|
27
31
|
|
|
28
32
|
export const jsenvPluginImportMetaCss = () => {
|
|
29
33
|
const importMetaCssDevClientFileUrl = import.meta
|
|
@@ -47,15 +51,10 @@ export const jsenvPluginImportMetaCss = () => {
|
|
|
47
51
|
if (!urlInfo.content.includes("import.meta.css")) {
|
|
48
52
|
return null;
|
|
49
53
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
input: urlInfo.content,
|
|
53
|
-
inputIsJsModule: true,
|
|
54
|
-
inputUrl: urlInfo.originalUrl,
|
|
55
|
-
outputUrl: urlInfo.generatedUrl,
|
|
54
|
+
const importMetaCssNode = visitJsAstUntil(urlInfo.contentAst, {
|
|
55
|
+
MemberExpression: (node) => getImportMetaPropertyName(node) === "css",
|
|
56
56
|
});
|
|
57
|
-
|
|
58
|
-
if (!usesImportMetaCss) {
|
|
57
|
+
if (!importMetaCssNode) {
|
|
59
58
|
return null;
|
|
60
59
|
}
|
|
61
60
|
if (urlInfo.context.build) {
|
|
@@ -135,37 +134,6 @@ const babelPluginRewriteImportMetaCssAssignment = (
|
|
|
135
134
|
};
|
|
136
135
|
};
|
|
137
136
|
|
|
138
|
-
const babelPluginMetadataUsesImportMetaCss = () => {
|
|
139
|
-
return {
|
|
140
|
-
name: "metadata-uses-import-meta-css",
|
|
141
|
-
visitor: {
|
|
142
|
-
Program(programPath, state) {
|
|
143
|
-
let usesImportMetaCss = false;
|
|
144
|
-
programPath.traverse({
|
|
145
|
-
MemberExpression(path) {
|
|
146
|
-
const { node } = path;
|
|
147
|
-
const { object } = node;
|
|
148
|
-
if (object.type !== "MetaProperty") {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const { property: objectProperty } = object;
|
|
152
|
-
if (objectProperty.name !== "meta") {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const { property } = node;
|
|
156
|
-
const { name } = property;
|
|
157
|
-
if (name === "css") {
|
|
158
|
-
usesImportMetaCss = true;
|
|
159
|
-
path.stop();
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
state.file.metadata.usesImportMetaCss = usesImportMetaCss;
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
};
|
|
168
|
-
|
|
169
137
|
const injectImportMetaCss = (
|
|
170
138
|
urlInfo,
|
|
171
139
|
{ content, importFrom, importName, importAs, hot },
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getImportMetaPropertyName, visitJsAst } from "@jsenv/ast";
|
|
2
|
+
|
|
3
|
+
export const analyzeImportMetaHot = (ast) => {
|
|
4
|
+
const importMetaHotNodes = [];
|
|
5
|
+
let hotDecline = false;
|
|
6
|
+
let hotAcceptSelf = false;
|
|
7
|
+
let hotAcceptSpecifiers = [];
|
|
8
|
+
visitJsAst(ast, {
|
|
9
|
+
MemberExpression: (node) => {
|
|
10
|
+
if (getImportMetaPropertyName(node) === "hot") {
|
|
11
|
+
importMetaHotNodes.push(node);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
CallExpression: (node) => {
|
|
15
|
+
const methodName = getImportMetaHotMethodName(node);
|
|
16
|
+
if (methodName === "accept") {
|
|
17
|
+
const args = node.arguments;
|
|
18
|
+
if (args.length === 0) {
|
|
19
|
+
hotAcceptSelf = true;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const [firstArg] = args;
|
|
23
|
+
if (isStringLiteral(firstArg)) {
|
|
24
|
+
hotAcceptSpecifiers = [firstArg.value];
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (firstArg.type === "ArrayExpression") {
|
|
28
|
+
hotAcceptSpecifiers = firstArg.elements.map((element) => {
|
|
29
|
+
if (!isStringLiteral(element)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return element.value;
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// accept first arg can be "anything" such as
|
|
39
|
+
// `const cb = () => {}; import.meta.hot.accept(cb)`
|
|
40
|
+
hotAcceptSelf = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (methodName === "decline") {
|
|
44
|
+
hotDecline = true;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
importMetaHotNodes,
|
|
50
|
+
hotDecline,
|
|
51
|
+
hotAcceptSelf,
|
|
52
|
+
hotAcceptSpecifiers,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// "import.meta.hot.<method>(...)"
|
|
57
|
+
const getImportMetaHotMethodName = (callNode) => {
|
|
58
|
+
const { callee } = callNode;
|
|
59
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (getImportMetaPropertyName(callee.object) !== "hot") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return callee.property.name;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const isStringLiteral = (node) => {
|
|
69
|
+
return node.type === "Literal" && typeof node.value === "string";
|
|
70
|
+
};
|