@jsenv/core 37.1.5 → 38.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/js/autoreload.js +2 -2
  2. package/dist/jsenv_core.js +3221 -2483
  3. package/package.json +14 -13
  4. package/src/build/build.js +246 -1028
  5. package/src/build/build_specifier_manager.js +1200 -0
  6. package/src/build/build_urls_generator.js +40 -18
  7. package/src/build/version_mappings_injection.js +14 -16
  8. package/src/dev/file_service.js +0 -10
  9. package/src/dev/start_dev_server.js +0 -2
  10. package/src/kitchen/kitchen.js +54 -37
  11. package/src/kitchen/url_graph/references.js +84 -93
  12. package/src/kitchen/url_graph/url_graph.js +16 -6
  13. package/src/kitchen/url_graph/url_info_transformations.js +124 -55
  14. package/src/plugins/autoreload/client/autoreload.js +6 -2
  15. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +20 -16
  16. package/src/plugins/autoreload/jsenv_plugin_hot_search_param.js +1 -1
  17. package/src/plugins/cache_control/jsenv_plugin_cache_control.js +2 -2
  18. package/src/plugins/clean_html/jsenv_plugin_clean_html.js +16 -0
  19. package/src/plugins/importmap/jsenv_plugin_importmap.js +11 -23
  20. package/src/plugins/inlining/jsenv_plugin_inlining_as_data_url.js +16 -1
  21. package/src/plugins/inlining/jsenv_plugin_inlining_into_html.js +14 -24
  22. package/src/plugins/plugin_controller.js +37 -25
  23. package/src/plugins/plugins.js +2 -0
  24. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +31 -16
  25. package/src/plugins/reference_analysis/directory/jsenv_plugin_directory_reference_analysis.js +12 -6
  26. package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +33 -54
  27. package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +2 -9
  28. package/src/plugins/reference_analysis/jsenv_plugin_reference_analysis.js +15 -8
  29. package/src/build/build_versions_manager.js +0 -492
@@ -23,7 +23,17 @@ const HOOK_NAMES = [
23
23
  "destroy",
24
24
  ];
25
25
 
26
- export const createPluginController = (kitchenContext) => {
26
+ export const createPluginController = (
27
+ kitchenContext,
28
+ initialPuginsMeta = {},
29
+ ) => {
30
+ const pluginsMeta = initialPuginsMeta;
31
+
32
+ kitchenContext.getPluginMeta = (id) => {
33
+ const value = pluginsMeta[id];
34
+ return value;
35
+ };
36
+
27
37
  const plugins = [];
28
38
  // precompute a list of hooks per hookName for one major reason:
29
39
  // - When debugging, there is less iteration
@@ -42,29 +52,43 @@ export const createPluginController = (kitchenContext) => {
42
52
  if (plugin === null || typeof plugin !== "object") {
43
53
  throw new TypeError(`plugin must be objects, got ${plugin}`);
44
54
  }
55
+ if (!plugin.name) {
56
+ plugin.name = "anonymous";
57
+ }
45
58
  if (!testAppliesDuring(plugin) || !initPlugin(plugin)) {
46
59
  if (plugin.destroy) {
47
60
  plugin.destroy();
48
61
  }
49
62
  return;
50
63
  }
51
- if (!plugin.name) {
52
- plugin.name = "anonymous";
53
- }
54
64
  plugins.push(plugin);
55
- Object.keys(plugin).forEach((key) => {
65
+ for (const key of Object.keys(plugin)) {
66
+ if (key === "meta") {
67
+ const value = plugin[key];
68
+ if (typeof value !== "object" || value === null) {
69
+ console.warn(`plugin.meta must be an object, got ${value}`);
70
+ continue;
71
+ }
72
+ Object.assign(pluginsMeta, value);
73
+ // any extension/modification on plugin.meta
74
+ // won't be taken into account so we freeze object
75
+ // to throw in case it happen
76
+ Object.freeze(value);
77
+ continue;
78
+ }
79
+
56
80
  if (
57
81
  key === "name" ||
58
82
  key === "appliesDuring" ||
59
83
  key === "init" ||
60
- key === "serverEvents" ||
61
- key === "meta"
84
+ key === "serverEvents"
62
85
  ) {
63
- return;
86
+ continue;
64
87
  }
65
88
  const isHook = HOOK_NAMES.includes(key);
66
89
  if (!isHook) {
67
90
  console.warn(`Unexpected "${key}" property on "${plugin.name}" plugin`);
91
+ continue;
68
92
  }
69
93
  const hookName = key;
70
94
  const hookValue = plugin[hookName];
@@ -81,7 +105,7 @@ export const createPluginController = (kitchenContext) => {
81
105
  group.push(hook);
82
106
  }
83
107
  }
84
- });
108
+ }
85
109
  };
86
110
  const testAppliesDuring = (plugin) => {
87
111
  const { appliesDuring } = plugin;
@@ -121,7 +145,7 @@ export const createPluginController = (kitchenContext) => {
121
145
  };
122
146
  const initPlugin = (plugin) => {
123
147
  if (plugin.init) {
124
- const initReturnValue = plugin.init(kitchenContext);
148
+ const initReturnValue = plugin.init(kitchenContext, plugin);
125
149
  if (initReturnValue === false) {
126
150
  return false;
127
151
  }
@@ -204,13 +228,12 @@ export const createPluginController = (kitchenContext) => {
204
228
  const callAsyncHooks = async (hookName, info, callback) => {
205
229
  const hooks = hookGroups[hookName];
206
230
  if (hooks) {
207
- await hooks.reduce(async (previous, hook) => {
208
- await previous;
231
+ for (const hook of hooks) {
209
232
  const returnValue = await callAsyncHook(hook, info);
210
233
  if (returnValue && callback) {
211
234
  await callback(returnValue, hook.plugin);
212
235
  }
213
- }, Promise.resolve());
236
+ }
214
237
  }
215
238
  };
216
239
 
@@ -252,17 +275,8 @@ export const createPluginController = (kitchenContext) => {
252
275
  });
253
276
  };
254
277
 
255
- const getPluginMeta = (id) => {
256
- for (const plugin of plugins) {
257
- const { meta } = plugin;
258
- if (meta && meta[id] !== undefined) {
259
- return meta[id];
260
- }
261
- }
262
- return undefined;
263
- };
264
-
265
278
  return {
279
+ pluginsMeta,
266
280
  plugins,
267
281
  pushPlugin,
268
282
  unshiftPlugin,
@@ -275,8 +289,6 @@ export const createPluginController = (kitchenContext) => {
275
289
  callAsyncHooks,
276
290
  callAsyncHooksUntil,
277
291
 
278
- getPluginMeta,
279
-
280
292
  getLastPluginUsed: () => lastPluginUsed,
281
293
  getCurrentPlugin: () => currentPlugin,
282
294
  getCurrentHookName: () => currentHookName,
@@ -19,6 +19,7 @@ import { jsenvPluginAutoreload } from "./autoreload/jsenv_plugin_autoreload.js";
19
19
  import { jsenvPluginCacheControl } from "./cache_control/jsenv_plugin_cache_control.js";
20
20
  // other
21
21
  import { jsenvPluginRibbon } from "./ribbon/jsenv_plugin_ribbon.js";
22
+ import { jsenvPluginCleanHTML } from "./clean_html/jsenv_plugin_clean_html.js";
22
23
 
23
24
  export const getCorePlugins = ({
24
25
  rootDirectoryUrl,
@@ -85,5 +86,6 @@ export const getCorePlugins = ({
85
86
  : []),
86
87
  ...(cacheControl ? [jsenvPluginCacheControl(cacheControl)] : []),
87
88
  ...(ribbon ? [jsenvPluginRibbon({ rootDirectoryUrl, ...ribbon })] : []),
89
+ jsenvPluginCleanHTML(),
88
90
  ];
89
91
  };
@@ -92,7 +92,11 @@ export const jsenvPluginProtocolFile = ({
92
92
  url = filesystemResolution.url;
93
93
  }
94
94
  }
95
- if (stat && stat.isDirectory()) {
95
+ if (!stat) {
96
+ return null;
97
+ }
98
+ reference.leadsToADirectory = stat && stat.isDirectory();
99
+ if (reference.leadsToADirectory) {
96
100
  const directoryAllowed =
97
101
  reference.type === "filesystem" ||
98
102
  (typeof directoryReferenceAllowed === "function" &&
@@ -104,13 +108,9 @@ export const jsenvPluginProtocolFile = ({
104
108
  throw error;
105
109
  }
106
110
  }
107
- reference.leadsToADirectory = stat && stat.isDirectory();
108
- if (stat) {
109
- const urlRaw = preserveSymlinks ? url : resolveSymlink(url);
110
- const resolvedUrl = `${urlRaw}${search}${hash}`;
111
- return resolvedUrl;
112
- }
113
- return null;
111
+ const urlRaw = preserveSymlinks ? url : resolveSymlink(url);
112
+ const resolvedUrl = `${urlRaw}${search}${hash}`;
113
+ return resolvedUrl;
114
114
  },
115
115
  },
116
116
  {
@@ -120,7 +120,7 @@ export const jsenvPluginProtocolFile = ({
120
120
  filesystem: (reference) => {
121
121
  const ownerUrlInfo = reference.ownerUrlInfo;
122
122
  const baseUrl =
123
- ownerUrlInfo && ownerUrlInfo.type === "directory"
123
+ ownerUrlInfo.type === "directory"
124
124
  ? ensurePathnameTrailingSlash(ownerUrlInfo.url)
125
125
  : ownerUrlInfo.url;
126
126
  return new URL(reference.specifier, baseUrl).href;
@@ -164,19 +164,28 @@ export const jsenvPluginProtocolFile = ({
164
164
  const urlObject = new URL(urlInfo.url);
165
165
  if (urlInfo.firstReference.leadsToADirectory) {
166
166
  const directoryEntries = readdirSync(urlObject);
167
- let filename;
168
- if (urlInfo.firstReference.type === "filesystem") {
169
- filename = `${urlInfo.firstReference.ownerUrlInfo.filename}${urlInfo.firstReference.specifier}/`;
170
- } else {
171
- filename = `${urlToFilename(urlInfo.url)}/`;
167
+ if (!urlInfo.filenameHint) {
168
+ if (urlInfo.firstReference.type === "filesystem") {
169
+ urlInfo.filenameHint = `${
170
+ urlInfo.firstReference.ownerUrlInfo.filenameHint
171
+ }${urlToFilename(urlInfo.url)}/`;
172
+ } else {
173
+ urlInfo.filenameHint = `${urlToFilename(urlInfo.url)}/`;
174
+ }
172
175
  }
173
176
  return {
174
177
  type: "directory",
175
178
  contentType: "application/json",
176
179
  content: JSON.stringify(directoryEntries, null, " "),
177
- filename,
178
180
  };
179
181
  }
182
+ if (
183
+ !urlInfo.dirnameHint &&
184
+ urlInfo.firstReference.ownerUrlInfo.type === "directory"
185
+ ) {
186
+ urlInfo.dirnameHint =
187
+ urlInfo.firstReference.ownerUrlInfo.filenameHint;
188
+ }
180
189
  const fileBuffer = readFileSync(urlObject);
181
190
  const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
182
191
  const content = CONTENT_TYPE.isTextual(contentType)
@@ -192,5 +201,11 @@ export const jsenvPluginProtocolFile = ({
192
201
  };
193
202
 
194
203
  const resolveSymlink = (fileUrl) => {
195
- return pathToFileURL(realpathSync(new URL(fileUrl))).href;
204
+ const urlObject = new URL(fileUrl);
205
+ const realpath = realpathSync(urlObject);
206
+ const realUrlObject = pathToFileURL(realpath);
207
+ if (urlObject.pathname.endsWith("/")) {
208
+ realUrlObject.pathname += `/`;
209
+ }
210
+ return realUrlObject.href;
196
211
  };
@@ -4,23 +4,29 @@ export const jsenvPluginDirectoryReferenceAnalysis = () => {
4
4
  return {
5
5
  name: "jsenv:directory_reference_analysis",
6
6
  transformUrlContent: {
7
- directory: (urlInfo) => {
7
+ directory: async (urlInfo) => {
8
8
  const originalDirectoryReference =
9
9
  findOriginalDirectoryReference(urlInfo);
10
10
  const directoryRelativeUrl = urlToRelativeUrl(
11
11
  urlInfo.url,
12
12
  urlInfo.context.rootDirectoryUrl,
13
13
  );
14
- JSON.parse(urlInfo.content).forEach((directoryEntryName) => {
15
- urlInfo.dependencies.found({
14
+ const entryNames = JSON.parse(urlInfo.content);
15
+ const newEntryNames = [];
16
+ for (const entryName of entryNames) {
17
+ const entryReference = urlInfo.dependencies.found({
16
18
  type: "filesystem",
17
19
  subtype: "directory_entry",
18
- specifier: directoryEntryName,
20
+ specifier: entryName,
19
21
  trace: {
20
- message: `"${directoryRelativeUrl}${directoryEntryName}" entry in directory referenced by ${originalDirectoryReference.trace.message}`,
22
+ message: `"${directoryRelativeUrl}${entryName}" entry in directory referenced by ${originalDirectoryReference.trace.message}`,
21
23
  },
22
24
  });
23
- });
25
+ await entryReference.readGeneratedSpecifier();
26
+ const replacement = entryReference.generatedSpecifier;
27
+ newEntryNames.push(replacement);
28
+ }
29
+ return JSON.stringify(newEntryNames);
24
30
  },
25
31
  },
26
32
  };
@@ -11,9 +11,8 @@ import {
11
11
  analyzeScriptNode,
12
12
  parseSrcSet,
13
13
  stringifyHtmlAst,
14
+ getUrlForContentInsideHtml,
14
15
  } from "@jsenv/ast";
15
- import { generateInlineContentUrl } from "@jsenv/urls";
16
- import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
17
16
 
18
17
  export const jsenvPluginHtmlReferenceAnalysis = ({
19
18
  inlineContent,
@@ -146,18 +145,14 @@ const parseAndTransformHtmlReferences = async (
146
145
  const createInlineReference = (
147
146
  node,
148
147
  inlineContent,
149
- { extension, type, subtype, expectedType, contentType },
148
+ { type, expectedType, contentType },
150
149
  ) => {
151
150
  const hotAccept = getHtmlNodeAttribute(node, "hot-accept") !== undefined;
152
- const { line, column, lineEnd, columnEnd, isOriginal } =
153
- getHtmlNodePosition(node, { preferOriginal: true });
154
- const inlineContentUrl = generateInlineContentUrl({
155
- url: urlInfo.url,
156
- extension,
157
- line,
158
- column,
159
- lineEnd,
160
- columnEnd,
151
+ const { line, column, isOriginal } = getHtmlNodePosition(node, {
152
+ preferOriginal: true,
153
+ });
154
+ const inlineContentUrl = getUrlForContentInsideHtml(node, {
155
+ htmlUrl: urlInfo.url,
161
156
  });
162
157
  const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
163
158
  const inlineReference = urlInfo.dependencies.foundInline({
@@ -176,33 +171,6 @@ const parseAndTransformHtmlReferences = async (
176
171
  astInfo: { node },
177
172
  });
178
173
 
179
- const externalSpecifierAttributeName =
180
- type === "script"
181
- ? "inlined-from-src"
182
- : type === "style"
183
- ? "inlined-from-href"
184
- : null;
185
- if (externalSpecifierAttributeName) {
186
- const externalSpecifier = getHtmlNodeAttribute(
187
- node,
188
- externalSpecifierAttributeName,
189
- );
190
- if (externalSpecifier) {
191
- // create an external ref
192
- // the goal is only to have the url in the graph (and in dependencies/implicit urls for reload)
193
- // not to consider the url is actually used (at least during build)
194
- // maybe we can just exclude these urls in a special if during build, we'll see
195
- const externalRef = createExternalReference(
196
- node,
197
- externalSpecifierAttributeName,
198
- externalSpecifier,
199
- { type, subtype, expectedType, next: inlineReference },
200
- );
201
- inlineReference.prev = externalRef;
202
- inlineReference.original = externalRef;
203
- }
204
- }
205
-
206
174
  actions.push(async () => {
207
175
  await inlineReference.urlInfo.cook();
208
176
  mutations.push(() => {
@@ -225,14 +193,13 @@ const parseAndTransformHtmlReferences = async (
225
193
  };
226
194
  const visitTextContent = (
227
195
  node,
228
- { extension, type, subtype, expectedType, contentType },
196
+ { type, subtype, expectedType, contentType },
229
197
  ) => {
230
198
  const inlineContent = getHtmlNodeText(node);
231
199
  if (!inlineContent) {
232
200
  return null;
233
201
  }
234
202
  return createInlineReference(node, inlineContent, {
235
- extension,
236
203
  type,
237
204
  subtype,
238
205
  expectedType,
@@ -252,14 +219,17 @@ const parseAndTransformHtmlReferences = async (
252
219
  });
253
220
  if (ref) {
254
221
  finalizeCallbacks.push(() => {
255
- ref.expectedType = decideLinkExpectedType(ref, urlInfo);
222
+ if (ref.expectedType) {
223
+ // might be set by other plugins, in that case respect it
224
+ } else {
225
+ ref.expectedType = decideLinkExpectedType(ref, urlInfo);
226
+ }
256
227
  });
257
228
  }
258
229
  },
259
230
  style: inlineContent
260
231
  ? (styleNode) => {
261
232
  visitTextContent(styleNode, {
262
- extension: ".css",
263
233
  type: "style",
264
234
  expectedType: "css",
265
235
  contentType: "text/css",
@@ -307,20 +277,28 @@ const parseAndTransformHtmlReferences = async (
307
277
  }
308
278
 
309
279
  const inlineRef = visitTextContent(scriptNode, {
310
- extension: extension || CONTENT_TYPE.asFileExtension(contentType),
311
280
  type: "script",
312
281
  subtype,
313
282
  expectedType: type,
314
283
  contentType,
315
284
  });
316
- if (inlineRef && extension) {
285
+ if (inlineRef) {
317
286
  // 1. <script type="jsx"> becomes <script>
287
+ if (type === "js_classic" && extension !== ".js") {
288
+ mutations.push(() => {
289
+ setHtmlNodeAttributes(scriptNode, {
290
+ type: undefined,
291
+ });
292
+ });
293
+ }
318
294
  // 2. <script type="module/jsx"> becomes <script type="module">
319
- mutations.push(() => {
320
- setHtmlNodeAttributes(scriptNode, {
321
- type: type === "js_module" ? "module" : undefined,
295
+ if (type === "js_module" && extension !== ".js") {
296
+ mutations.push(() => {
297
+ setHtmlNodeAttributes(scriptNode, {
298
+ type: "module",
299
+ });
322
300
  });
323
- });
301
+ }
324
302
  }
325
303
  },
326
304
  a: (aNode) => {
@@ -412,12 +390,13 @@ const decideLinkExpectedType = (linkReference, htmlUrlInfo) => {
412
390
  }
413
391
  if (as === "script") {
414
392
  for (const referenceToOther of htmlUrlInfo.referenceToOthersSet) {
415
- if (
416
- referenceToOther.url === linkReference.url &&
417
- referenceToOther.type === "script"
418
- ) {
419
- return referenceToOther.expectedType;
393
+ if (referenceToOther.url !== linkReference.url) {
394
+ continue;
395
+ }
396
+ if (referenceToOther.type !== "script") {
397
+ continue;
420
398
  }
399
+ return referenceToOther.expectedType;
421
400
  }
422
401
  return undefined;
423
402
  }
@@ -1,8 +1,6 @@
1
1
  import { createMagicSource } from "@jsenv/sourcemap";
2
- import { parseJsUrls } from "@jsenv/ast";
3
- import { generateInlineContentUrl } from "@jsenv/urls";
2
+ import { parseJsUrls, getUrlForContentInsideJs } from "@jsenv/ast";
4
3
  import { JS_QUOTES } from "@jsenv/utils/src/string/js_quotes.js";
5
- import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
6
4
 
7
5
  import { isWebWorkerUrlInfo } from "@jsenv/core/src/kitchen/web_workers.js";
8
6
 
@@ -38,13 +36,8 @@ const parseAndTransformJsReferences = async (
38
36
  const sequentialActions = [];
39
37
 
40
38
  const onInlineReference = (inlineReferenceInfo) => {
41
- const inlineUrl = generateInlineContentUrl({
39
+ const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, {
42
40
  url: urlInfo.url,
43
- extension: CONTENT_TYPE.asFileExtension(inlineReferenceInfo.contentType),
44
- line: inlineReferenceInfo.line,
45
- column: inlineReferenceInfo.column,
46
- lineEnd: inlineReferenceInfo.lineEnd,
47
- columnEnd: inlineReferenceInfo.columnEnd,
48
41
  });
49
42
  let { quote } = inlineReferenceInfo;
50
43
  if (quote === "`" && !canUseTemplateLiterals) {
@@ -38,22 +38,29 @@ const jsenvPluginInlineContentFetcher = () => {
38
38
  if (!urlInfo.isInline) {
39
39
  return null;
40
40
  }
41
- const { firstReference } = urlInfo;
42
- const { prev } = firstReference;
41
+ // - we must use last reference because
42
+ // when updating the file, first reference is the previous version
43
+ // - we cannot use urlInfo.lastReference because it can be the reference created by "http_request"
44
+ let lastInlineReference;
45
+ for (const reference of urlInfo.referenceFromOthersSet) {
46
+ if (reference.isInline) {
47
+ lastInlineReference = reference;
48
+ }
49
+ }
50
+ const { prev } = lastInlineReference;
43
51
  if (prev && !prev.isInline) {
44
52
  // got inlined, cook original url
45
- if (firstReference.content === undefined) {
53
+ if (lastInlineReference.content === undefined) {
46
54
  const originalUrlInfo = prev.urlInfo;
47
55
  await originalUrlInfo.cook();
48
- firstReference.content = originalUrlInfo.content;
49
- firstReference.contentType = originalUrlInfo.contentType;
56
+ lastInlineReference.content = originalUrlInfo.content;
57
+ lastInlineReference.contentType = originalUrlInfo.contentType;
50
58
  }
51
59
  }
52
-
53
60
  return {
54
61
  originalContent: urlInfo.originalContent,
55
- content: firstReference.content,
56
- contentType: firstReference.contentType,
62
+ content: lastInlineReference.content,
63
+ contentType: lastInlineReference.contentType,
57
64
  };
58
65
  },
59
66
  };