@jsenv/core 38.2.10 → 38.3.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.
@@ -13,7 +13,7 @@ import http from "node:http";
13
13
  import { Readable, Stream, Writable } from "node:stream";
14
14
  import { Http2ServerResponse } from "node:http2";
15
15
  import { lookup } from "node:dns";
16
- import { injectJsImport, visitJsAstUntil, applyBabelPlugins, parseHtmlString, visitHtmlNodes, getHtmlNodeAttribute, analyzeScriptNode, getHtmlNodeText, stringifyHtmlAst, setHtmlNodeAttributes, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn, parseSrcSet, getHtmlNodePosition, getHtmlNodeAttributePosition, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, parseCssUrls, parseJsUrls, getUrlForContentInsideJs, findHtmlNode, removeHtmlNode, analyzeLinkNode, injectHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
16
+ import { injectJsImport, visitJsAstUntil, applyBabelPlugins, parseHtmlString, visitHtmlNodes, getHtmlNodeAttribute, analyzeScriptNode, getHtmlNodeText, stringifyHtmlAst, setHtmlNodeAttributes, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn, getHtmlNodePosition, getUrlForContentInsideHtml, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, setHtmlNodeText, removeHtmlNode, parseCssUrls, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, findHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
17
17
  import { sourcemapConverter, createMagicSource, composeTwoSourcemaps, SOURCEMAP, generateSourcemapFileUrl, generateSourcemapDataUrl } from "@jsenv/sourcemap";
18
18
  import { createRequire } from "node:module";
19
19
  import { systemJsClientFileUrlDefault, convertJsModuleToJsClassic } from "@jsenv/js-module-fallback";
@@ -7355,7 +7355,11 @@ const serveDirectory = (
7355
7355
  const directoryContentArray = readdirSync(new URL(url));
7356
7356
  const responseProducers = {
7357
7357
  "application/json": () => {
7358
- const directoryContentJson = JSON.stringify(directoryContentArray);
7358
+ const directoryContentJson = JSON.stringify(
7359
+ directoryContentArray,
7360
+ null,
7361
+ " ",
7362
+ );
7359
7363
  return {
7360
7364
  status: 200,
7361
7365
  headers: {
@@ -9343,6 +9347,7 @@ const babelPluginCompatMap = {
9343
9347
  samsung: "9",
9344
9348
  electron: "3",
9345
9349
  },
9350
+ "proposal-decorators": {},
9346
9351
  "transform-parameters": {
9347
9352
  chrome: "49",
9348
9353
  opera: "36",
@@ -9739,6 +9744,14 @@ const getBaseBabelPluginStructure = ({
9739
9744
  babelPluginStructure["proposal-unicode-property-regex"] =
9740
9745
  requireBabelPlugin("@babel/plugin-proposal-unicode-property-regex");
9741
9746
  }
9747
+ // if (isBabelPluginNeeded("proposal-decorators") && content.includes("@")) {
9748
+ // babelPluginStructure["proposal-decorators"] = [
9749
+ // requireBabelPlugin("@babel/plugin-proposal-decorators"),
9750
+ // {
9751
+ // version: "2023-05",
9752
+ // },
9753
+ // ];
9754
+ // }
9742
9755
  if (isBabelPluginNeeded("transform-async-to-promises")) {
9743
9756
  babelPluginStructure["transform-async-to-promises"] = [
9744
9757
  requireBabelPlugin("babel-plugin-transform-async-to-promises"),
@@ -11801,12 +11814,12 @@ const createReference = ({
11801
11814
  ownerUrlInfo.context.finalizeReference(reference);
11802
11815
  };
11803
11816
 
11804
- // "formatReferencedUrl" can be async BUT this is an exception
11817
+ // "formatReference" can be async BUT this is an exception
11805
11818
  // for most cases it will be sync. We want to favor the sync signature to keep things simpler
11806
11819
  // The only case where it needs to be async is when
11807
11820
  // the specifier is a `data:*` url
11808
11821
  // in this case we'll wait for the promise returned by
11809
- // "formatReferencedUrl"
11822
+ // "formatReference"
11810
11823
  reference.readGeneratedSpecifier = () => {
11811
11824
  if (reference.generatedSpecifier.then) {
11812
11825
  return reference.generatedSpecifier.then((value) => {
@@ -12311,6 +12324,7 @@ const createUrlInfo = (url, context) => {
12311
12324
  context,
12312
12325
  error: null,
12313
12326
  modifiedTimestamp: 0,
12327
+ descendantModifiedTimestamp: 0,
12314
12328
  dereferencedTimestamp: 0,
12315
12329
  originalContentEtag: null,
12316
12330
  contentEtag: null,
@@ -12384,6 +12398,9 @@ const createUrlInfo = (url, context) => {
12384
12398
  continue;
12385
12399
  }
12386
12400
  if (ref.gotInlined()) {
12401
+ if (ref.ownerUrlInfo.isUsed()) {
12402
+ return true;
12403
+ }
12387
12404
  // the url info was inlined, an other reference is required
12388
12405
  // to consider the non-inlined urlInfo as used
12389
12406
  continue;
@@ -12483,7 +12500,7 @@ const createUrlInfo = (url, context) => {
12483
12500
  };
12484
12501
  urlInfo.onModified = ({ modifiedTimestamp = Date.now() } = {}) => {
12485
12502
  const visitedSet = new Set();
12486
- const iterate = (urlInfo) => {
12503
+ const considerModified = (urlInfo) => {
12487
12504
  if (visitedSet.has(urlInfo)) {
12488
12505
  return;
12489
12506
  }
@@ -12493,14 +12510,21 @@ const createUrlInfo = (url, context) => {
12493
12510
  for (const referenceToOther of urlInfo.referenceToOthersSet) {
12494
12511
  const referencedUrlInfo = referenceToOther.urlInfo;
12495
12512
  if (referencedUrlInfo.isInline) {
12496
- iterate(referencedUrlInfo);
12513
+ considerModified(referencedUrlInfo);
12514
+ }
12515
+ }
12516
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
12517
+ if (referenceFromOther.gotInlined()) {
12518
+ const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
12519
+ considerModified(urlInfoReferencingThisOne);
12497
12520
  }
12498
12521
  }
12499
12522
  for (const searchParamVariant of urlInfo.searchParamVariantSet) {
12500
- iterate(searchParamVariant);
12523
+ considerModified(searchParamVariant);
12501
12524
  }
12502
12525
  };
12503
- iterate(urlInfo);
12526
+ considerModified(urlInfo);
12527
+ visitedSet.clear();
12504
12528
  };
12505
12529
  urlInfo.onDereferenced = (lastReferenceFromOther) => {
12506
12530
  urlInfo.dereferencedTimestamp = Date.now();
@@ -14378,7 +14402,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
14378
14402
  // the HTML in itself it still valid
14379
14403
  // keep the syntax error and continue with the HTML
14380
14404
  const errorInfo =
14381
- e.code === "PARSE_ERROR"
14405
+ e.code === "PARSE_ERROR" && e.cause
14382
14406
  ? `${e.cause.reasonCode}\n${e.traceMessage}`
14383
14407
  : e.stack;
14384
14408
  logger.error(
@@ -14885,6 +14909,9 @@ const jsenvPluginDirectoryReferenceAnalysis = () => {
14885
14909
  urlInfo.url,
14886
14910
  urlInfo.context.rootDirectoryUrl,
14887
14911
  );
14912
+ if (urlInfo.contentType !== "application/json") {
14913
+ return null;
14914
+ }
14888
14915
  const entryNames = JSON.parse(urlInfo.content);
14889
14916
  const newEntryNames = [];
14890
14917
  for (const entryName of entryNames) {
@@ -14994,1598 +15021,1586 @@ const base64ToString = (base64String) =>
14994
15021
  Buffer.from(base64String, "base64").toString("utf8");
14995
15022
  const dataToBase64 = (data) => Buffer.from(data).toString("base64");
14996
15023
 
14997
- const jsenvPluginHtmlReferenceAnalysis = ({
14998
- inlineContent,
14999
- inlineConvertedScript,
15000
- }) => {
15001
- return {
15002
- name: "jsenv:html_reference_analysis",
15003
- appliesDuring: "*",
15004
- transformUrlContent: {
15005
- html: (urlInfo) =>
15006
- parseAndTransformHtmlReferences(urlInfo, {
15007
- inlineContent,
15008
- inlineConvertedScript,
15009
- }),
15010
- },
15011
- };
15012
- };
15024
+ // duplicated from @jsenv/log to avoid the dependency
15025
+ const createDetailedMessage = (message, details = {}) => {
15026
+ let string = `${message}`;
15013
15027
 
15014
- const parseAndTransformHtmlReferences = async (
15015
- urlInfo,
15016
- { inlineContent, inlineConvertedScript },
15017
- ) => {
15018
- const content = urlInfo.content;
15019
- const htmlAst = parseHtmlString(content);
15028
+ Object.keys(details).forEach((key) => {
15029
+ const value = details[key];
15030
+ string += `
15031
+ --- ${key} ---
15032
+ ${
15033
+ Array.isArray(value)
15034
+ ? value.join(`
15035
+ `)
15036
+ : value
15037
+ }`;
15038
+ });
15020
15039
 
15021
- const mutations = [];
15022
- const actions = [];
15023
- const finalizeCallbacks = [];
15040
+ return string
15041
+ };
15024
15042
 
15025
- const createExternalReference = (
15026
- node,
15027
- attributeName,
15028
- attributeValue,
15029
- { type, subtype, expectedType, ...rest },
15030
- ) => {
15031
- let position;
15032
- if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
15033
- // when generated from inline content,
15034
- // line, column is not "src" nor "inlined-from-src" but "original-position"
15035
- position = getHtmlNodePosition(node);
15036
- } else {
15037
- position = getHtmlNodeAttributePosition(node, attributeName);
15038
- }
15039
- const {
15040
- line,
15041
- column,
15042
- // originalLine, originalColumn
15043
- } = position;
15044
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
15045
-
15046
- const { crossorigin, integrity } = readFetchMetas(node);
15047
- const isResourceHint = [
15048
- "preconnect",
15049
- "dns-prefetch",
15050
- "prefetch",
15051
- "preload",
15052
- "modulepreload",
15053
- ].includes(subtype);
15054
- let attributeLocation = node.sourceCodeLocation.attrs[attributeName];
15055
- if (
15056
- !attributeLocation &&
15057
- attributeName === "href" &&
15058
- (node.tagName === "use" || node.tagName === "image")
15059
- ) {
15060
- attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
15061
- }
15062
- const attributeStart = attributeLocation.startOffset;
15063
- const attributeValueStart = urlInfo.content.indexOf(
15064
- attributeValue,
15065
- attributeStart + `${attributeName}=`.length,
15066
- );
15067
- const attributeValueEnd = attributeValueStart + attributeValue.length;
15068
- const reference = urlInfo.dependencies.found({
15069
- type,
15070
- subtype,
15071
- expectedType,
15072
- specifier: attributeValue,
15073
- specifierLine: line,
15074
- specifierColumn: column,
15075
- specifierStart: attributeValueStart,
15076
- specifierEnd: attributeValueEnd,
15077
- isResourceHint,
15078
- isWeak: isResourceHint,
15079
- crossorigin,
15080
- integrity,
15081
- debug,
15082
- astInfo: { node, attributeName },
15083
- ...rest,
15084
- });
15085
- actions.push(async () => {
15086
- await reference.readGeneratedSpecifier();
15087
- mutations.push(() => {
15088
- setHtmlNodeAttributes(node, {
15089
- [attributeName]: reference.generatedSpecifier,
15090
- });
15091
- });
15092
- });
15093
- return reference;
15094
- };
15095
- const visitHref = (node, referenceProps) => {
15096
- const href = getHtmlNodeAttribute(node, "href");
15097
- if (href) {
15098
- return createExternalReference(node, "href", href, referenceProps);
15099
- }
15100
- return null;
15101
- };
15102
- const visitSrc = (node, referenceProps) => {
15103
- const src = getHtmlNodeAttribute(node, "src");
15104
- if (src) {
15105
- return createExternalReference(node, "src", src, referenceProps);
15106
- }
15107
- return null;
15108
- };
15109
- const visitSrcset = (node, referenceProps) => {
15110
- const srcset = getHtmlNodeAttribute(node, "srcset");
15111
- if (srcset) {
15112
- const srcCandidates = parseSrcSet(srcset);
15113
- return srcCandidates.map((srcCandidate) => {
15114
- return createExternalReference(
15115
- node,
15116
- "srcset",
15117
- srcCandidate.specifier,
15118
- referenceProps,
15119
- );
15120
- });
15121
- }
15122
- return null;
15123
- };
15043
+ const assertImportMap = (value) => {
15044
+ if (value === null) {
15045
+ throw new TypeError(`an importMap must be an object, got null`)
15046
+ }
15124
15047
 
15125
- const createInlineReference = (
15126
- node,
15127
- inlineContent,
15128
- { type, expectedType, contentType },
15129
- ) => {
15130
- const hotAccept = getHtmlNodeAttribute(node, "hot-accept") !== undefined;
15131
- const { line, column, isOriginal } = getHtmlNodePosition(node, {
15132
- preferOriginal: true,
15133
- });
15134
- const inlineContentUrl = getUrlForContentInsideHtml(node, {
15135
- htmlUrl: urlInfo.url,
15136
- });
15137
- const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
15138
- const inlineReference = urlInfo.dependencies.foundInline({
15139
- type,
15140
- expectedType,
15141
- isOriginalPosition: isOriginal,
15142
- // we remove 1 to the line because imagine the following html:
15143
- // <style>body { color: red; }</style>
15144
- // -> content starts same line as <style> (same for <script>)
15145
- specifierLine: line - 1,
15146
- specifierColumn: column,
15147
- specifier: inlineContentUrl,
15148
- contentType,
15149
- content: inlineContent,
15150
- debug,
15151
- astInfo: { node },
15152
- });
15048
+ const type = typeof value;
15049
+ if (type !== "object") {
15050
+ throw new TypeError(`an importMap must be an object, received ${value}`)
15051
+ }
15153
15052
 
15154
- actions.push(async () => {
15155
- await inlineReference.urlInfo.cook();
15156
- mutations.push(() => {
15157
- if (hotAccept) {
15158
- removeHtmlNodeText(node);
15159
- setHtmlNodeAttributes(node, {
15160
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
15161
- });
15162
- } else {
15163
- setHtmlNodeText(node, inlineReference.urlInfo.content, {
15164
- indentation: false, // indentation would decrease stack trace precision
15165
- });
15166
- setHtmlNodeAttributes(node, {
15167
- "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
15168
- });
15169
- }
15170
- });
15171
- });
15172
- return inlineReference;
15173
- };
15174
- const visitTextContent = (
15175
- node,
15176
- { type, subtype, expectedType, contentType },
15177
- ) => {
15178
- const inlineContent = getHtmlNodeText(node);
15179
- if (!inlineContent) {
15180
- return null;
15181
- }
15182
- return createInlineReference(node, inlineContent, {
15183
- type,
15184
- subtype,
15185
- expectedType,
15186
- contentType,
15187
- });
15188
- };
15053
+ if (Array.isArray(value)) {
15054
+ throw new TypeError(
15055
+ `an importMap must be an object, received array ${value}`,
15056
+ )
15057
+ }
15058
+ };
15189
15059
 
15190
- visitHtmlNodes(htmlAst, {
15191
- link: (linkNode) => {
15192
- const rel = getHtmlNodeAttribute(linkNode, "rel");
15193
- const type = getHtmlNodeAttribute(linkNode, "type");
15194
- const ref = visitHref(linkNode, {
15195
- type: "link_href",
15196
- subtype: rel,
15197
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
15198
- expectedContentType: type,
15199
- });
15200
- if (ref) {
15201
- finalizeCallbacks.push(() => {
15202
- if (ref.expectedType) ; else {
15203
- ref.expectedType = decideLinkExpectedType(ref, urlInfo);
15204
- }
15205
- });
15206
- }
15207
- },
15208
- style: inlineContent
15209
- ? (styleNode) => {
15210
- visitTextContent(styleNode, {
15211
- type: "style",
15212
- expectedType: "css",
15213
- contentType: "text/css",
15214
- });
15215
- }
15216
- : null,
15217
- script: (scriptNode) => {
15218
- // during build the importmap is inlined
15219
- // and shoud not be considered as a dependency anymore
15220
- if (
15221
- getHtmlNodeAttribute(scriptNode, "jsenv-inlined-by") ===
15222
- "jsenv:importmap"
15223
- ) {
15224
- return;
15225
- }
15060
+ const hasScheme = (string) => {
15061
+ return /^[a-zA-Z]{2,}:/.test(string)
15062
+ };
15226
15063
 
15227
- const { type, subtype, contentType, extension } =
15228
- analyzeScriptNode(scriptNode);
15229
- // ignore <script type="whatever">foobar</script>
15230
- // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
15231
- if (type !== "text") {
15232
- const externalRef = visitSrc(scriptNode, {
15233
- type: "script",
15234
- subtype: type,
15235
- expectedType: type,
15236
- });
15237
- if (externalRef) {
15238
- return;
15239
- }
15240
- }
15064
+ const urlToScheme = (urlString) => {
15065
+ const colonIndex = urlString.indexOf(":");
15066
+ if (colonIndex === -1) return ""
15067
+ return urlString.slice(0, colonIndex)
15068
+ };
15241
15069
 
15242
- // now visit the content, if any
15243
- if (!inlineContent) {
15244
- return;
15245
- }
15246
- // If the inline script was already handled by an other plugin, ignore it
15247
- // - we want to preserve inline scripts generated by html supervisor during dev
15248
- // - we want to avoid cooking twice a script during build
15249
- if (
15250
- !inlineConvertedScript &&
15251
- getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
15252
- "jsenv:js_module_fallback"
15253
- ) {
15254
- return;
15255
- }
15070
+ const urlToPathname = (urlString) => {
15071
+ return ressourceToPathname(urlToRessource(urlString))
15072
+ };
15256
15073
 
15257
- const inlineRef = visitTextContent(scriptNode, {
15258
- type: "script",
15259
- subtype,
15260
- expectedType: type,
15261
- contentType,
15262
- });
15263
- if (inlineRef) {
15264
- // 1. <script type="jsx"> becomes <script>
15265
- if (type === "js_classic" && extension !== ".js") {
15266
- mutations.push(() => {
15267
- setHtmlNodeAttributes(scriptNode, {
15268
- type: undefined,
15269
- });
15270
- });
15271
- }
15272
- // 2. <script type="module/jsx"> becomes <script type="module">
15273
- if (type === "js_module" && extension !== ".js") {
15274
- mutations.push(() => {
15275
- setHtmlNodeAttributes(scriptNode, {
15276
- type: "module",
15277
- });
15278
- });
15279
- }
15280
- }
15281
- },
15282
- a: (aNode) => {
15283
- visitHref(aNode, {
15284
- type: "a_href",
15285
- });
15286
- },
15287
- iframe: (iframeNode) => {
15288
- visitSrc(iframeNode, {
15289
- type: "iframe_src",
15290
- });
15291
- },
15292
- img: (imgNode) => {
15293
- visitSrc(imgNode, {
15294
- type: "img_src",
15295
- });
15296
- visitSrcset(imgNode, {
15297
- type: "img_srcset",
15298
- });
15299
- },
15300
- source: (sourceNode) => {
15301
- visitSrc(sourceNode, {
15302
- type: "source_src",
15303
- });
15304
- visitSrcset(sourceNode, {
15305
- type: "source_srcset",
15306
- });
15307
- },
15308
- // svg <image> tag
15309
- image: (imageNode) => {
15310
- visitHref(imageNode, {
15311
- type: "image_href",
15312
- });
15313
- },
15314
- use: (useNode) => {
15315
- visitHref(useNode, {
15316
- type: "use_href",
15317
- });
15318
- },
15319
- });
15320
- finalizeCallbacks.forEach((finalizeCallback) => {
15321
- finalizeCallback();
15322
- });
15074
+ const urlToRessource = (urlString) => {
15075
+ const scheme = urlToScheme(urlString);
15323
15076
 
15324
- if (actions.length > 0) {
15325
- await Promise.all(actions.map((action) => action()));
15077
+ if (scheme === "file") {
15078
+ return urlString.slice("file://".length)
15326
15079
  }
15327
- if (mutations.length === 0) {
15328
- return null;
15080
+
15081
+ if (scheme === "https" || scheme === "http") {
15082
+ // remove origin
15083
+ const afterProtocol = urlString.slice(scheme.length + "://".length);
15084
+ const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
15085
+ return afterProtocol.slice(pathnameSlashIndex)
15329
15086
  }
15330
- mutations.forEach((mutation) => mutation());
15331
- return stringifyHtmlAst(htmlAst);
15087
+
15088
+ return urlString.slice(scheme.length + 1)
15332
15089
  };
15333
15090
 
15334
- const crossOriginCompatibleTagNames = ["script", "link", "img", "source"];
15335
- const integrityCompatibleTagNames = ["script", "link", "img", "source"];
15336
- const readFetchMetas = (node) => {
15337
- const meta = {};
15338
- if (crossOriginCompatibleTagNames.includes(node.nodeName)) {
15339
- const crossorigin = getHtmlNodeAttribute(node, "crossorigin") !== undefined;
15340
- meta.crossorigin = crossorigin;
15091
+ const ressourceToPathname = (ressource) => {
15092
+ const searchSeparatorIndex = ressource.indexOf("?");
15093
+ return searchSeparatorIndex === -1
15094
+ ? ressource
15095
+ : ressource.slice(0, searchSeparatorIndex)
15096
+ };
15097
+
15098
+ const urlToOrigin = (urlString) => {
15099
+ const scheme = urlToScheme(urlString);
15100
+
15101
+ if (scheme === "file") {
15102
+ return "file://"
15341
15103
  }
15342
- if (integrityCompatibleTagNames.includes(node.nodeName)) {
15343
- const integrity = getHtmlNodeAttribute(node, "integrity");
15344
- meta.integrity = integrity;
15104
+
15105
+ if (scheme === "http" || scheme === "https") {
15106
+ const secondProtocolSlashIndex = scheme.length + "://".length;
15107
+ const pathnameSlashIndex = urlString.indexOf("/", secondProtocolSlashIndex);
15108
+
15109
+ if (pathnameSlashIndex === -1) return urlString
15110
+ return urlString.slice(0, pathnameSlashIndex)
15345
15111
  }
15346
- return meta;
15112
+
15113
+ return urlString.slice(0, scheme.length + 1)
15347
15114
  };
15348
15115
 
15349
- const decideLinkExpectedType = (linkReference, htmlUrlInfo) => {
15350
- const rel = getHtmlNodeAttribute(linkReference.astInfo.node, "rel");
15351
- if (rel === "webmanifest") {
15352
- return "webmanifest";
15116
+ const pathnameToParentPathname = (pathname) => {
15117
+ const slashLastIndex = pathname.lastIndexOf("/");
15118
+ if (slashLastIndex === -1) {
15119
+ return "/"
15353
15120
  }
15354
- if (rel === "modulepreload") {
15355
- return "js_module";
15121
+
15122
+ return pathname.slice(0, slashLastIndex + 1)
15123
+ };
15124
+
15125
+ // could be useful: https://url.spec.whatwg.org/#url-miscellaneous
15126
+
15127
+
15128
+ const resolveUrl = (specifier, baseUrl) => {
15129
+ if (baseUrl) {
15130
+ if (typeof baseUrl !== "string") {
15131
+ throw new TypeError(writeBaseUrlMustBeAString({ baseUrl, specifier }))
15132
+ }
15133
+ if (!hasScheme(baseUrl)) {
15134
+ throw new Error(writeBaseUrlMustBeAbsolute({ baseUrl, specifier }))
15135
+ }
15356
15136
  }
15357
- if (rel === "stylesheet") {
15358
- return "css";
15137
+
15138
+ if (hasScheme(specifier)) {
15139
+ return specifier
15359
15140
  }
15360
- if (rel === "preload") {
15361
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#what_types_of_content_can_be_preloaded
15362
- const as = getHtmlNodeAttribute(linkReference.astInfo.node, "as");
15363
- if (as === "document") {
15364
- return "html";
15365
- }
15366
- if (as === "style") {
15367
- return "css";
15368
- }
15369
- if (as === "script") {
15370
- for (const referenceToOther of htmlUrlInfo.referenceToOthersSet) {
15371
- if (referenceToOther.url !== linkReference.url) {
15372
- continue;
15373
- }
15374
- if (referenceToOther.type !== "script") {
15375
- continue;
15376
- }
15377
- return referenceToOther.expectedType;
15378
- }
15379
- return undefined;
15380
- }
15141
+
15142
+ if (!baseUrl) {
15143
+ throw new Error(writeBaseUrlRequired({ baseUrl, specifier }))
15381
15144
  }
15382
- return undefined;
15383
- };
15384
15145
 
15385
- // const applyWebUrlResolution = (url, baseUrl) => {
15386
- // if (url[0] === "/") {
15387
- // return new URL(url.slice(1), baseUrl).href;
15388
- // }
15389
- // return new URL(url, baseUrl).href;
15390
- // };
15146
+ // scheme relative
15147
+ if (specifier.slice(0, 2) === "//") {
15148
+ return `${urlToScheme(baseUrl)}:${specifier}`
15149
+ }
15391
15150
 
15392
- // css: parseAndTransformCssUrls,
15151
+ // origin relative
15152
+ if (specifier[0] === "/") {
15153
+ return `${urlToOrigin(baseUrl)}${specifier}`
15154
+ }
15393
15155
 
15394
- const jsenvPluginWebmanifestReferenceAnalysis = () => {
15395
- return {
15396
- name: "jsenv:webmanifest_reference_analysis",
15397
- appliesDuring: "*",
15398
- transformUrlContent: {
15399
- webmanifest: parseAndTransformWebmanifestUrls,
15400
- },
15401
- };
15402
- };
15156
+ const baseOrigin = urlToOrigin(baseUrl);
15157
+ const basePathname = urlToPathname(baseUrl);
15403
15158
 
15404
- const parseAndTransformWebmanifestUrls = async (urlInfo) => {
15405
- const content = urlInfo.content;
15406
- const manifest = JSON.parse(content);
15407
- const actions = [];
15408
- const { icons = [] } = manifest;
15409
- icons.forEach((icon) => {
15410
- const iconReference = urlInfo.dependencies.found({
15411
- type: "webmanifest_icon_src",
15412
- specifier: icon.src,
15413
- });
15414
- actions.push(async () => {
15415
- await iconReference.readGeneratedSpecifier();
15416
- icon.src = iconReference.generatedSpecifier;
15417
- });
15418
- });
15159
+ if (specifier === ".") {
15160
+ const baseDirectoryPathname = pathnameToParentPathname(basePathname);
15161
+ return `${baseOrigin}${baseDirectoryPathname}`
15162
+ }
15419
15163
 
15420
- if (actions.length === 0) {
15421
- return null;
15164
+ // pathname relative inside
15165
+ if (specifier.slice(0, 2) === "./") {
15166
+ const baseDirectoryPathname = pathnameToParentPathname(basePathname);
15167
+ return `${baseOrigin}${baseDirectoryPathname}${specifier.slice(2)}`
15422
15168
  }
15423
- await Promise.all(actions.map((action) => action()));
15424
- return JSON.stringify(manifest, null, " ");
15425
- };
15426
15169
 
15427
- /*
15428
- * https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/css/src/CSSTransformer.js
15429
- */
15170
+ // pathname relative outside
15171
+ if (specifier.slice(0, 3) === "../") {
15172
+ let unresolvedPathname = specifier;
15173
+ const importerFolders = basePathname.split("/");
15174
+ importerFolders.pop();
15430
15175
 
15176
+ while (unresolvedPathname.slice(0, 3) === "../") {
15177
+ unresolvedPathname = unresolvedPathname.slice(3);
15178
+ // when there is no folder left to resolved
15179
+ // we just ignore '../'
15180
+ if (importerFolders.length) {
15181
+ importerFolders.pop();
15182
+ }
15183
+ }
15431
15184
 
15432
- const jsenvPluginCssReferenceAnalysis = () => {
15433
- return {
15434
- name: "jsenv:css_reference_analysis",
15435
- appliesDuring: "*",
15436
- transformUrlContent: {
15437
- css: parseAndTransformCssUrls,
15438
- },
15439
- };
15440
- };
15185
+ const resolvedPathname = `${importerFolders.join(
15186
+ "/",
15187
+ )}/${unresolvedPathname}`;
15188
+ return `${baseOrigin}${resolvedPathname}`
15189
+ }
15441
15190
 
15442
- const parseAndTransformCssUrls = async (urlInfo) => {
15443
- const cssUrls = await parseCssUrls({
15444
- css: urlInfo.content,
15445
- url: urlInfo.originalUrl,
15446
- });
15447
- const actions = [];
15448
- const magicSource = createMagicSource(urlInfo.content);
15449
- for (const cssUrl of cssUrls) {
15450
- const reference = urlInfo.dependencies.found({
15451
- type: cssUrl.type,
15452
- specifier: cssUrl.specifier,
15453
- specifierStart: cssUrl.start,
15454
- specifierEnd: cssUrl.end,
15455
- specifierLine: cssUrl.line,
15456
- specifierColumn: cssUrl.column,
15457
- });
15458
- actions.push(async () => {
15459
- await reference.readGeneratedSpecifier();
15460
- const replacement = reference.generatedSpecifier;
15461
- magicSource.replace({
15462
- start: cssUrl.start,
15463
- end: cssUrl.end,
15464
- replacement,
15465
- });
15466
- });
15191
+ // bare
15192
+ if (basePathname === "") {
15193
+ return `${baseOrigin}/${specifier}`
15467
15194
  }
15468
- if (actions.length > 0) {
15469
- await Promise.all(actions.map((action) => action()));
15195
+ if (basePathname[basePathname.length] === "/") {
15196
+ return `${baseOrigin}${basePathname}${specifier}`
15470
15197
  }
15471
- return magicSource.toContentAndSourcemap();
15198
+ return `${baseOrigin}${pathnameToParentPathname(basePathname)}${specifier}`
15472
15199
  };
15473
15200
 
15474
- const jsenvPluginJsReferenceAnalysis = ({ inlineContent }) => {
15475
- return [
15476
- {
15477
- name: "jsenv:js_reference_analysis",
15478
- appliesDuring: "*",
15479
- transformUrlContent: {
15480
- js_classic: (urlInfo) =>
15481
- parseAndTransformJsReferences(urlInfo, {
15482
- inlineContent,
15483
- canUseTemplateLiterals:
15484
- urlInfo.context.isSupportedOnCurrentClients("template_literals"),
15485
- }),
15486
- js_module: (urlInfo) =>
15487
- parseAndTransformJsReferences(urlInfo, {
15488
- inlineContent,
15489
- canUseTemplateLiterals:
15490
- urlInfo.context.isSupportedOnCurrentClients("template_literals"),
15491
- }),
15492
- },
15493
- },
15494
- ];
15201
+ const writeBaseUrlMustBeAString = ({
15202
+ baseUrl,
15203
+ specifier,
15204
+ }) => `baseUrl must be a string.
15205
+ --- base url ---
15206
+ ${baseUrl}
15207
+ --- specifier ---
15208
+ ${specifier}`;
15209
+
15210
+ const writeBaseUrlMustBeAbsolute = ({
15211
+ baseUrl,
15212
+ specifier,
15213
+ }) => `baseUrl must be absolute.
15214
+ --- base url ---
15215
+ ${baseUrl}
15216
+ --- specifier ---
15217
+ ${specifier}`;
15218
+
15219
+ const writeBaseUrlRequired = ({
15220
+ baseUrl,
15221
+ specifier,
15222
+ }) => `baseUrl required to resolve relative specifier.
15223
+ --- base url ---
15224
+ ${baseUrl}
15225
+ --- specifier ---
15226
+ ${specifier}`;
15227
+
15228
+ const tryUrlResolution = (string, url) => {
15229
+ const result = resolveUrl(string, url);
15230
+ return hasScheme(result) ? result : null
15495
15231
  };
15496
15232
 
15497
- const parseAndTransformJsReferences = async (
15498
- urlInfo,
15499
- { inlineContent, canUseTemplateLiterals },
15500
- ) => {
15501
- const magicSource = createMagicSource(urlInfo.content);
15502
- const parallelActions = [];
15503
- const sequentialActions = [];
15504
- const isNodeJs =
15505
- Object.keys(urlInfo.context.runtimeCompat).toString() === "node";
15233
+ const resolveSpecifier = (specifier, importer) => {
15234
+ if (
15235
+ specifier === "." ||
15236
+ specifier[0] === "/" ||
15237
+ specifier.startsWith("./") ||
15238
+ specifier.startsWith("../")
15239
+ ) {
15240
+ return resolveUrl(specifier, importer)
15241
+ }
15506
15242
 
15507
- const onInlineReference = (inlineReferenceInfo) => {
15508
- const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, {
15509
- url: urlInfo.url,
15510
- });
15511
- let { quote } = inlineReferenceInfo;
15512
- if (quote === "`" && !canUseTemplateLiterals) {
15513
- // if quote is "`" and template literals are not supported
15514
- // we'll use a regular string (single or double quote)
15515
- // when rendering the string
15516
- quote = JS_QUOTES.pickBest(inlineReferenceInfo.content);
15517
- }
15518
- const inlineReference = urlInfo.dependencies.foundInline({
15519
- type: "js_inline_content",
15520
- subtype: inlineReferenceInfo.type, // "new_blob_first_arg", "new_inline_content_first_arg", "json_parse_first_arg"
15521
- isOriginalPosition: urlInfo.content === urlInfo.originalContent,
15522
- specifierLine: inlineReferenceInfo.line,
15523
- specifierColumn: inlineReferenceInfo.column,
15524
- specifier: inlineUrl,
15525
- contentType: inlineReferenceInfo.contentType,
15526
- content: inlineReferenceInfo.content,
15527
- });
15528
- const inlineUrlInfo = inlineReference.urlInfo;
15529
- inlineUrlInfo.jsQuote = quote;
15530
- inlineReference.escape = (value) => {
15531
- return JS_QUOTES.escapeSpecialChars(value.slice(1, -1), { quote });
15532
- };
15243
+ if (hasScheme(specifier)) {
15244
+ return specifier
15245
+ }
15533
15246
 
15534
- sequentialActions.push(async () => {
15535
- await inlineUrlInfo.cook();
15536
- const replacement = JS_QUOTES.escapeSpecialChars(inlineUrlInfo.content, {
15537
- quote,
15538
- });
15539
- magicSource.replace({
15540
- start: inlineReferenceInfo.start,
15541
- end: inlineReferenceInfo.end,
15542
- replacement,
15543
- });
15544
- });
15545
- };
15546
- const onExternalReference = (externalReferenceInfo) => {
15547
- if (
15548
- externalReferenceInfo.subtype === "import_static" ||
15549
- externalReferenceInfo.subtype === "import_dynamic"
15550
- ) {
15551
- urlInfo.data.usesImport = true;
15552
- }
15553
- if (
15554
- isNodeJs &&
15555
- externalReferenceInfo.type === "js_url" &&
15556
- externalReferenceInfo.expectedSubtype === "worker" &&
15557
- externalReferenceInfo.expectedType === "js_classic" &&
15558
- // TODO: it's true also if closest package.json
15559
- // is type: module
15560
- urlToExtension$1(
15561
- new URL(externalReferenceInfo.specifier, urlInfo.url).href,
15562
- ) === ".mjs"
15563
- ) {
15564
- externalReferenceInfo.expectedType = "js_module";
15247
+ return null
15248
+ };
15249
+
15250
+ const applyImportMap = ({
15251
+ importMap,
15252
+ specifier,
15253
+ importer,
15254
+ createBareSpecifierError = ({ specifier, importer }) => {
15255
+ return new Error(
15256
+ createDetailedMessage(`Unmapped bare specifier.`, {
15257
+ specifier,
15258
+ importer,
15259
+ }),
15260
+ )
15261
+ },
15262
+ onImportMapping = () => {},
15263
+ }) => {
15264
+ assertImportMap(importMap);
15265
+ if (typeof specifier !== "string") {
15266
+ throw new TypeError(
15267
+ createDetailedMessage("specifier must be a string.", {
15268
+ specifier,
15269
+ importer,
15270
+ }),
15271
+ )
15272
+ }
15273
+ if (importer) {
15274
+ if (typeof importer !== "string") {
15275
+ throw new TypeError(
15276
+ createDetailedMessage("importer must be a string.", {
15277
+ importer,
15278
+ specifier,
15279
+ }),
15280
+ )
15565
15281
  }
15566
- const reference = urlInfo.dependencies.found({
15567
- type: externalReferenceInfo.type,
15568
- subtype: externalReferenceInfo.subtype,
15569
- expectedType: externalReferenceInfo.expectedType,
15570
- expectedSubtype: externalReferenceInfo.expectedSubtype || urlInfo.subtype,
15571
- specifier: externalReferenceInfo.specifier,
15572
- specifierStart: externalReferenceInfo.start,
15573
- specifierEnd: externalReferenceInfo.end,
15574
- specifierLine: externalReferenceInfo.line,
15575
- specifierColumn: externalReferenceInfo.column,
15576
- data: externalReferenceInfo.data,
15577
- baseUrl: {
15578
- "StringLiteral": externalReferenceInfo.baseUrl,
15579
- "window.location": urlInfo.url,
15580
- "window.origin": urlInfo.context.rootDirectoryUrl,
15581
- "import.meta.url": urlInfo.url,
15582
- "context.meta.url": urlInfo.url,
15583
- "document.currentScript.src": urlInfo.url,
15584
- }[externalReferenceInfo.baseUrlType],
15585
- importAttributes: externalReferenceInfo.importAttributes,
15586
- astInfo: externalReferenceInfo.astInfo,
15587
- });
15588
- parallelActions.push(async () => {
15589
- await reference.readGeneratedSpecifier();
15590
- const replacement = reference.generatedSpecifier;
15591
- magicSource.replace({
15592
- start: externalReferenceInfo.start,
15593
- end: externalReferenceInfo.end,
15594
- replacement,
15595
- });
15596
- if (reference.mutation) {
15597
- reference.mutation(magicSource, urlInfo);
15598
- }
15599
- });
15600
- };
15601
- const jsReferenceInfos = parseJsUrls({
15602
- js: urlInfo.content,
15603
- url: urlInfo.originalUrl,
15604
- ast: urlInfo.contentAst,
15605
- isJsModule: urlInfo.type === "js_module",
15606
- isWebWorker: isWebWorkerUrlInfo(urlInfo),
15607
- inlineContent,
15608
- isNodeJs,
15609
- });
15610
- for (const jsReferenceInfo of jsReferenceInfos) {
15611
- if (jsReferenceInfo.isInline) {
15612
- onInlineReference(jsReferenceInfo);
15613
- } else {
15614
- onExternalReference(jsReferenceInfo);
15282
+ if (!hasScheme(importer)) {
15283
+ throw new Error(
15284
+ createDetailedMessage(`importer must be an absolute url.`, {
15285
+ importer,
15286
+ specifier,
15287
+ }),
15288
+ )
15615
15289
  }
15616
15290
  }
15617
- if (parallelActions.length > 0) {
15618
- await Promise.all(parallelActions.map((action) => action()));
15619
- }
15620
- if (sequentialActions.length > 0) {
15621
- await sequentialActions.reduce(async (previous, action) => {
15622
- await previous;
15623
- await action();
15624
- }, Promise.resolve());
15625
- }
15626
-
15627
- const { content, sourcemap } = magicSource.toContentAndSourcemap();
15628
- return { content, sourcemap };
15629
- };
15630
15291
 
15631
- const jsenvPluginReferenceAnalysis = ({
15632
- inlineContent = true,
15633
- inlineConvertedScript = false,
15634
- fetchInlineUrls = true,
15635
- }) => {
15636
- return [
15637
- jsenvPluginDirectoryReferenceAnalysis(),
15638
- jsenvPluginHtmlReferenceAnalysis({
15639
- inlineContent,
15640
- inlineConvertedScript,
15641
- }),
15642
- jsenvPluginWebmanifestReferenceAnalysis(),
15643
- jsenvPluginCssReferenceAnalysis(),
15644
- jsenvPluginJsReferenceAnalysis({
15645
- inlineContent,
15646
- }),
15647
- ...(inlineContent ? [jsenvPluginDataUrlsAnalysis()] : []),
15648
- ...(inlineContent && fetchInlineUrls
15649
- ? [jsenvPluginInlineContentFetcher()]
15650
- : []),
15651
- jsenvPluginReferenceExpectedTypes(),
15652
- ];
15653
- };
15292
+ const specifierUrl = resolveSpecifier(specifier, importer);
15293
+ const specifierNormalized = specifierUrl || specifier;
15654
15294
 
15655
- const jsenvPluginInlineContentFetcher = () => {
15656
- return {
15657
- name: "jsenv:inline_content_fetcher",
15658
- appliesDuring: "*",
15659
- fetchUrlContent: async (urlInfo) => {
15660
- if (!urlInfo.isInline) {
15661
- return null;
15662
- }
15663
- // - we must use last reference because
15664
- // when updating the file, first reference is the previous version
15665
- // - we cannot use urlInfo.lastReference because it can be the reference created by "http_request"
15666
- let lastInlineReference;
15667
- for (const reference of urlInfo.referenceFromOthersSet) {
15668
- if (reference.isInline) {
15669
- lastInlineReference = reference;
15670
- }
15671
- }
15672
- const { prev } = lastInlineReference;
15673
- if (prev && !prev.isInline) {
15674
- // got inlined, cook original url
15675
- if (lastInlineReference.content === undefined) {
15676
- const originalUrlInfo = prev.urlInfo;
15677
- await originalUrlInfo.cook();
15678
- lastInlineReference.content = originalUrlInfo.content;
15679
- lastInlineReference.contentType = originalUrlInfo.contentType;
15680
- }
15295
+ const { scopes } = importMap;
15296
+ if (scopes && importer) {
15297
+ const scopeSpecifierMatching = Object.keys(scopes).find(
15298
+ (scopeSpecifier) => {
15299
+ return (
15300
+ scopeSpecifier === importer ||
15301
+ specifierIsPrefixOf(scopeSpecifier, importer)
15302
+ )
15303
+ },
15304
+ );
15305
+ if (scopeSpecifierMatching) {
15306
+ const scopeMappings = scopes[scopeSpecifierMatching];
15307
+ const mappingFromScopes = applyMappings(
15308
+ scopeMappings,
15309
+ specifierNormalized,
15310
+ scopeSpecifierMatching,
15311
+ onImportMapping,
15312
+ );
15313
+ if (mappingFromScopes !== null) {
15314
+ return mappingFromScopes
15681
15315
  }
15682
- return {
15683
- originalContent: urlInfo.originalContent,
15684
- content: lastInlineReference.content,
15685
- contentType: lastInlineReference.contentType,
15686
- };
15687
- },
15688
- };
15689
- };
15316
+ }
15317
+ }
15690
15318
 
15691
- // duplicated from @jsenv/log to avoid the dependency
15692
- const createDetailedMessage = (message, details = {}) => {
15693
- let string = `${message}`;
15319
+ const { imports } = importMap;
15320
+ if (imports) {
15321
+ const mappingFromImports = applyMappings(
15322
+ imports,
15323
+ specifierNormalized,
15324
+ undefined,
15325
+ onImportMapping,
15326
+ );
15327
+ if (mappingFromImports !== null) {
15328
+ return mappingFromImports
15329
+ }
15330
+ }
15694
15331
 
15695
- Object.keys(details).forEach((key) => {
15696
- const value = details[key];
15697
- string += `
15698
- --- ${key} ---
15699
- ${
15700
- Array.isArray(value)
15701
- ? value.join(`
15702
- `)
15703
- : value
15704
- }`;
15705
- });
15332
+ if (specifierUrl) {
15333
+ return specifierUrl
15334
+ }
15706
15335
 
15707
- return string
15336
+ throw createBareSpecifierError({ specifier, importer })
15708
15337
  };
15709
15338
 
15710
- const assertImportMap = (value) => {
15711
- if (value === null) {
15712
- throw new TypeError(`an importMap must be an object, got null`)
15713
- }
15339
+ const applyMappings = (
15340
+ mappings,
15341
+ specifierNormalized,
15342
+ scope,
15343
+ onImportMapping,
15344
+ ) => {
15345
+ const specifierCandidates = Object.keys(mappings);
15714
15346
 
15715
- const type = typeof value;
15716
- if (type !== "object") {
15717
- throw new TypeError(`an importMap must be an object, received ${value}`)
15347
+ let i = 0;
15348
+ while (i < specifierCandidates.length) {
15349
+ const specifierCandidate = specifierCandidates[i];
15350
+ i++;
15351
+ if (specifierCandidate === specifierNormalized) {
15352
+ const address = mappings[specifierCandidate];
15353
+ onImportMapping({
15354
+ scope,
15355
+ from: specifierCandidate,
15356
+ to: address,
15357
+ before: specifierNormalized,
15358
+ after: address,
15359
+ });
15360
+ return address
15361
+ }
15362
+ if (specifierIsPrefixOf(specifierCandidate, specifierNormalized)) {
15363
+ const address = mappings[specifierCandidate];
15364
+ const afterSpecifier = specifierNormalized.slice(
15365
+ specifierCandidate.length,
15366
+ );
15367
+ const addressFinal = tryUrlResolution(afterSpecifier, address);
15368
+ onImportMapping({
15369
+ scope,
15370
+ from: specifierCandidate,
15371
+ to: address,
15372
+ before: specifierNormalized,
15373
+ after: addressFinal,
15374
+ });
15375
+ return addressFinal
15376
+ }
15718
15377
  }
15719
15378
 
15720
- if (Array.isArray(value)) {
15721
- throw new TypeError(
15722
- `an importMap must be an object, received array ${value}`,
15723
- )
15724
- }
15379
+ return null
15725
15380
  };
15726
15381
 
15727
- const hasScheme = (string) => {
15728
- return /^[a-zA-Z]{2,}:/.test(string)
15382
+ const specifierIsPrefixOf = (specifierHref, href) => {
15383
+ return (
15384
+ specifierHref[specifierHref.length - 1] === "/" &&
15385
+ href.startsWith(specifierHref)
15386
+ )
15729
15387
  };
15730
15388
 
15731
- const urlToScheme = (urlString) => {
15732
- const colonIndex = urlString.indexOf(":");
15733
- if (colonIndex === -1) return ""
15734
- return urlString.slice(0, colonIndex)
15735
- };
15389
+ // https://github.com/systemjs/systemjs/blob/89391f92dfeac33919b0223bbf834a1f4eea5750/src/common.js#L136
15736
15390
 
15737
- const urlToPathname = (urlString) => {
15738
- return ressourceToPathname(urlToRessource(urlString))
15739
- };
15391
+ const composeTwoImportMaps = (leftImportMap, rightImportMap) => {
15392
+ assertImportMap(leftImportMap);
15393
+ assertImportMap(rightImportMap);
15740
15394
 
15741
- const urlToRessource = (urlString) => {
15742
- const scheme = urlToScheme(urlString);
15395
+ const importMap = {};
15743
15396
 
15744
- if (scheme === "file") {
15745
- return urlString.slice("file://".length)
15397
+ const leftImports = leftImportMap.imports;
15398
+ const rightImports = rightImportMap.imports;
15399
+ const leftHasImports = Boolean(leftImports);
15400
+ const rightHasImports = Boolean(rightImports);
15401
+ if (leftHasImports && rightHasImports) {
15402
+ importMap.imports = composeTwoMappings(leftImports, rightImports);
15403
+ } else if (leftHasImports) {
15404
+ importMap.imports = { ...leftImports };
15405
+ } else if (rightHasImports) {
15406
+ importMap.imports = { ...rightImports };
15746
15407
  }
15747
15408
 
15748
- if (scheme === "https" || scheme === "http") {
15749
- // remove origin
15750
- const afterProtocol = urlString.slice(scheme.length + "://".length);
15751
- const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
15752
- return afterProtocol.slice(pathnameSlashIndex)
15409
+ const leftScopes = leftImportMap.scopes;
15410
+ const rightScopes = rightImportMap.scopes;
15411
+ const leftHasScopes = Boolean(leftScopes);
15412
+ const rightHasScopes = Boolean(rightScopes);
15413
+ if (leftHasScopes && rightHasScopes) {
15414
+ importMap.scopes = composeTwoScopes(
15415
+ leftScopes,
15416
+ rightScopes,
15417
+ importMap.imports || {},
15418
+ );
15419
+ } else if (leftHasScopes) {
15420
+ importMap.scopes = { ...leftScopes };
15421
+ } else if (rightHasScopes) {
15422
+ importMap.scopes = { ...rightScopes };
15753
15423
  }
15754
15424
 
15755
- return urlString.slice(scheme.length + 1)
15756
- };
15757
-
15758
- const ressourceToPathname = (ressource) => {
15759
- const searchSeparatorIndex = ressource.indexOf("?");
15760
- return searchSeparatorIndex === -1
15761
- ? ressource
15762
- : ressource.slice(0, searchSeparatorIndex)
15425
+ return importMap
15763
15426
  };
15764
15427
 
15765
- const urlToOrigin = (urlString) => {
15766
- const scheme = urlToScheme(urlString);
15767
-
15768
- if (scheme === "file") {
15769
- return "file://"
15770
- }
15428
+ const composeTwoMappings = (leftMappings, rightMappings) => {
15429
+ const mappings = {};
15771
15430
 
15772
- if (scheme === "http" || scheme === "https") {
15773
- const secondProtocolSlashIndex = scheme.length + "://".length;
15774
- const pathnameSlashIndex = urlString.indexOf("/", secondProtocolSlashIndex);
15431
+ Object.keys(leftMappings).forEach((leftSpecifier) => {
15432
+ if (objectHasKey(rightMappings, leftSpecifier)) {
15433
+ // will be overidden
15434
+ return
15435
+ }
15436
+ const leftAddress = leftMappings[leftSpecifier];
15437
+ const rightSpecifier = Object.keys(rightMappings).find((rightSpecifier) => {
15438
+ return compareAddressAndSpecifier(leftAddress, rightSpecifier)
15439
+ });
15440
+ mappings[leftSpecifier] = rightSpecifier
15441
+ ? rightMappings[rightSpecifier]
15442
+ : leftAddress;
15443
+ });
15775
15444
 
15776
- if (pathnameSlashIndex === -1) return urlString
15777
- return urlString.slice(0, pathnameSlashIndex)
15778
- }
15445
+ Object.keys(rightMappings).forEach((rightSpecifier) => {
15446
+ mappings[rightSpecifier] = rightMappings[rightSpecifier];
15447
+ });
15779
15448
 
15780
- return urlString.slice(0, scheme.length + 1)
15449
+ return mappings
15781
15450
  };
15782
15451
 
15783
- const pathnameToParentPathname = (pathname) => {
15784
- const slashLastIndex = pathname.lastIndexOf("/");
15785
- if (slashLastIndex === -1) {
15786
- return "/"
15787
- }
15452
+ const objectHasKey = (object, key) =>
15453
+ Object.prototype.hasOwnProperty.call(object, key);
15788
15454
 
15789
- return pathname.slice(0, slashLastIndex + 1)
15455
+ const compareAddressAndSpecifier = (address, specifier) => {
15456
+ const addressUrl = resolveUrl(address, "file:///");
15457
+ const specifierUrl = resolveUrl(specifier, "file:///");
15458
+ return addressUrl === specifierUrl
15790
15459
  };
15791
15460
 
15792
- // could be useful: https://url.spec.whatwg.org/#url-miscellaneous
15793
-
15461
+ const composeTwoScopes = (leftScopes, rightScopes, imports) => {
15462
+ const scopes = {};
15794
15463
 
15795
- const resolveUrl = (specifier, baseUrl) => {
15796
- if (baseUrl) {
15797
- if (typeof baseUrl !== "string") {
15798
- throw new TypeError(writeBaseUrlMustBeAString({ baseUrl, specifier }))
15464
+ Object.keys(leftScopes).forEach((leftScopeKey) => {
15465
+ if (objectHasKey(rightScopes, leftScopeKey)) {
15466
+ // will be merged
15467
+ scopes[leftScopeKey] = leftScopes[leftScopeKey];
15468
+ return
15799
15469
  }
15800
- if (!hasScheme(baseUrl)) {
15801
- throw new Error(writeBaseUrlMustBeAbsolute({ baseUrl, specifier }))
15470
+ const topLevelSpecifier = Object.keys(imports).find(
15471
+ (topLevelSpecifierCandidate) => {
15472
+ return compareAddressAndSpecifier(
15473
+ leftScopeKey,
15474
+ topLevelSpecifierCandidate,
15475
+ )
15476
+ },
15477
+ );
15478
+ if (topLevelSpecifier) {
15479
+ scopes[imports[topLevelSpecifier]] = leftScopes[leftScopeKey];
15480
+ } else {
15481
+ scopes[leftScopeKey] = leftScopes[leftScopeKey];
15802
15482
  }
15803
- }
15804
-
15805
- if (hasScheme(specifier)) {
15806
- return specifier
15807
- }
15808
-
15809
- if (!baseUrl) {
15810
- throw new Error(writeBaseUrlRequired({ baseUrl, specifier }))
15811
- }
15483
+ });
15812
15484
 
15813
- // scheme relative
15814
- if (specifier.slice(0, 2) === "//") {
15815
- return `${urlToScheme(baseUrl)}:${specifier}`
15816
- }
15485
+ Object.keys(rightScopes).forEach((rightScopeKey) => {
15486
+ if (objectHasKey(scopes, rightScopeKey)) {
15487
+ scopes[rightScopeKey] = composeTwoMappings(
15488
+ scopes[rightScopeKey],
15489
+ rightScopes[rightScopeKey],
15490
+ );
15491
+ } else {
15492
+ scopes[rightScopeKey] = {
15493
+ ...rightScopes[rightScopeKey],
15494
+ };
15495
+ }
15496
+ });
15817
15497
 
15818
- // origin relative
15819
- if (specifier[0] === "/") {
15820
- return `${urlToOrigin(baseUrl)}${specifier}`
15821
- }
15498
+ return scopes
15499
+ };
15822
15500
 
15823
- const baseOrigin = urlToOrigin(baseUrl);
15824
- const basePathname = urlToPathname(baseUrl);
15501
+ const sortImports = (imports) => {
15502
+ const mappingsSorted = {};
15825
15503
 
15826
- if (specifier === ".") {
15827
- const baseDirectoryPathname = pathnameToParentPathname(basePathname);
15828
- return `${baseOrigin}${baseDirectoryPathname}`
15829
- }
15504
+ Object.keys(imports)
15505
+ .sort(compareLengthOrLocaleCompare)
15506
+ .forEach((name) => {
15507
+ mappingsSorted[name] = imports[name];
15508
+ });
15830
15509
 
15831
- // pathname relative inside
15832
- if (specifier.slice(0, 2) === "./") {
15833
- const baseDirectoryPathname = pathnameToParentPathname(basePathname);
15834
- return `${baseOrigin}${baseDirectoryPathname}${specifier.slice(2)}`
15835
- }
15510
+ return mappingsSorted
15511
+ };
15836
15512
 
15837
- // pathname relative outside
15838
- if (specifier.slice(0, 3) === "../") {
15839
- let unresolvedPathname = specifier;
15840
- const importerFolders = basePathname.split("/");
15841
- importerFolders.pop();
15513
+ const sortScopes = (scopes) => {
15514
+ const scopesSorted = {};
15842
15515
 
15843
- while (unresolvedPathname.slice(0, 3) === "../") {
15844
- unresolvedPathname = unresolvedPathname.slice(3);
15845
- // when there is no folder left to resolved
15846
- // we just ignore '../'
15847
- if (importerFolders.length) {
15848
- importerFolders.pop();
15849
- }
15850
- }
15516
+ Object.keys(scopes)
15517
+ .sort(compareLengthOrLocaleCompare)
15518
+ .forEach((scopeSpecifier) => {
15519
+ scopesSorted[scopeSpecifier] = sortImports(scopes[scopeSpecifier]);
15520
+ });
15851
15521
 
15852
- const resolvedPathname = `${importerFolders.join(
15853
- "/",
15854
- )}/${unresolvedPathname}`;
15855
- return `${baseOrigin}${resolvedPathname}`
15856
- }
15522
+ return scopesSorted
15523
+ };
15857
15524
 
15858
- // bare
15859
- if (basePathname === "") {
15860
- return `${baseOrigin}/${specifier}`
15861
- }
15862
- if (basePathname[basePathname.length] === "/") {
15863
- return `${baseOrigin}${basePathname}${specifier}`
15864
- }
15865
- return `${baseOrigin}${pathnameToParentPathname(basePathname)}${specifier}`
15525
+ const compareLengthOrLocaleCompare = (a, b) => {
15526
+ return b.length - a.length || a.localeCompare(b)
15866
15527
  };
15867
15528
 
15868
- const writeBaseUrlMustBeAString = ({
15869
- baseUrl,
15870
- specifier,
15871
- }) => `baseUrl must be a string.
15872
- --- base url ---
15873
- ${baseUrl}
15874
- --- specifier ---
15875
- ${specifier}`;
15529
+ const normalizeImportMap = (importMap, baseUrl) => {
15530
+ assertImportMap(importMap);
15876
15531
 
15877
- const writeBaseUrlMustBeAbsolute = ({
15878
- baseUrl,
15879
- specifier,
15880
- }) => `baseUrl must be absolute.
15881
- --- base url ---
15882
- ${baseUrl}
15883
- --- specifier ---
15884
- ${specifier}`;
15532
+ if (!isStringOrUrl(baseUrl)) {
15533
+ throw new TypeError(formulateBaseUrlMustBeStringOrUrl({ baseUrl }))
15534
+ }
15885
15535
 
15886
- const writeBaseUrlRequired = ({
15887
- baseUrl,
15888
- specifier,
15889
- }) => `baseUrl required to resolve relative specifier.
15890
- --- base url ---
15891
- ${baseUrl}
15892
- --- specifier ---
15893
- ${specifier}`;
15536
+ const { imports, scopes } = importMap;
15894
15537
 
15895
- const tryUrlResolution = (string, url) => {
15896
- const result = resolveUrl(string, url);
15897
- return hasScheme(result) ? result : null
15538
+ return {
15539
+ imports: imports ? normalizeMappings(imports, baseUrl) : undefined,
15540
+ scopes: scopes ? normalizeScopes(scopes, baseUrl) : undefined,
15541
+ }
15898
15542
  };
15899
15543
 
15900
- const resolveSpecifier = (specifier, importer) => {
15901
- if (
15902
- specifier === "." ||
15903
- specifier[0] === "/" ||
15904
- specifier.startsWith("./") ||
15905
- specifier.startsWith("../")
15906
- ) {
15907
- return resolveUrl(specifier, importer)
15544
+ const isStringOrUrl = (value) => {
15545
+ if (typeof value === "string") {
15546
+ return true
15908
15547
  }
15909
15548
 
15910
- if (hasScheme(specifier)) {
15911
- return specifier
15549
+ if (typeof URL === "function" && value instanceof URL) {
15550
+ return true
15912
15551
  }
15913
15552
 
15914
- return null
15553
+ return false
15915
15554
  };
15916
15555
 
15917
- const applyImportMap = ({
15918
- importMap,
15919
- specifier,
15920
- importer,
15921
- createBareSpecifierError = ({ specifier, importer }) => {
15922
- return new Error(
15923
- createDetailedMessage(`Unmapped bare specifier.`, {
15924
- specifier,
15925
- importer,
15926
- }),
15927
- )
15928
- },
15929
- onImportMapping = () => {},
15930
- }) => {
15931
- assertImportMap(importMap);
15932
- if (typeof specifier !== "string") {
15933
- throw new TypeError(
15934
- createDetailedMessage("specifier must be a string.", {
15935
- specifier,
15936
- importer,
15937
- }),
15938
- )
15939
- }
15940
- if (importer) {
15941
- if (typeof importer !== "string") {
15942
- throw new TypeError(
15943
- createDetailedMessage("importer must be a string.", {
15944
- importer,
15945
- specifier,
15946
- }),
15947
- )
15948
- }
15949
- if (!hasScheme(importer)) {
15950
- throw new Error(
15951
- createDetailedMessage(`importer must be an absolute url.`, {
15952
- importer,
15556
+ const normalizeMappings = (mappings, baseUrl) => {
15557
+ const mappingsNormalized = {};
15558
+
15559
+ Object.keys(mappings).forEach((specifier) => {
15560
+ const address = mappings[specifier];
15561
+
15562
+ if (typeof address !== "string") {
15563
+ console.warn(
15564
+ formulateAddressMustBeAString({
15565
+ address,
15953
15566
  specifier,
15954
15567
  }),
15955
- )
15568
+ );
15569
+ return
15956
15570
  }
15957
- }
15958
15571
 
15959
- const specifierUrl = resolveSpecifier(specifier, importer);
15960
- const specifierNormalized = specifierUrl || specifier;
15572
+ const specifierResolved = resolveSpecifier(specifier, baseUrl) || specifier;
15961
15573
 
15962
- const { scopes } = importMap;
15963
- if (scopes && importer) {
15964
- const scopeSpecifierMatching = Object.keys(scopes).find(
15965
- (scopeSpecifier) => {
15966
- return (
15967
- scopeSpecifier === importer ||
15968
- specifierIsPrefixOf(scopeSpecifier, importer)
15969
- )
15970
- },
15971
- );
15972
- if (scopeSpecifierMatching) {
15973
- const scopeMappings = scopes[scopeSpecifierMatching];
15974
- const mappingFromScopes = applyMappings(
15975
- scopeMappings,
15976
- specifierNormalized,
15977
- scopeSpecifierMatching,
15978
- onImportMapping,
15574
+ const addressUrl = tryUrlResolution(address, baseUrl);
15575
+ if (addressUrl === null) {
15576
+ console.warn(
15577
+ formulateAdressResolutionFailed({
15578
+ address,
15579
+ baseUrl,
15580
+ specifier,
15581
+ }),
15979
15582
  );
15980
- if (mappingFromScopes !== null) {
15981
- return mappingFromScopes
15982
- }
15583
+ return
15983
15584
  }
15984
- }
15985
15585
 
15986
- const { imports } = importMap;
15987
- if (imports) {
15988
- const mappingFromImports = applyMappings(
15989
- imports,
15990
- specifierNormalized,
15991
- undefined,
15992
- onImportMapping,
15993
- );
15994
- if (mappingFromImports !== null) {
15995
- return mappingFromImports
15586
+ if (specifier.endsWith("/") && !addressUrl.endsWith("/")) {
15587
+ console.warn(
15588
+ formulateAddressUrlRequiresTrailingSlash({
15589
+ addressUrl,
15590
+ address,
15591
+ specifier,
15592
+ }),
15593
+ );
15594
+ return
15996
15595
  }
15997
- }
15998
-
15999
- if (specifierUrl) {
16000
- return specifierUrl
16001
- }
15596
+ mappingsNormalized[specifierResolved] = addressUrl;
15597
+ });
16002
15598
 
16003
- throw createBareSpecifierError({ specifier, importer })
15599
+ return sortImports(mappingsNormalized)
16004
15600
  };
16005
15601
 
16006
- const applyMappings = (
16007
- mappings,
16008
- specifierNormalized,
16009
- scope,
16010
- onImportMapping,
16011
- ) => {
16012
- const specifierCandidates = Object.keys(mappings);
15602
+ const normalizeScopes = (scopes, baseUrl) => {
15603
+ const scopesNormalized = {};
16013
15604
 
16014
- let i = 0;
16015
- while (i < specifierCandidates.length) {
16016
- const specifierCandidate = specifierCandidates[i];
16017
- i++;
16018
- if (specifierCandidate === specifierNormalized) {
16019
- const address = mappings[specifierCandidate];
16020
- onImportMapping({
16021
- scope,
16022
- from: specifierCandidate,
16023
- to: address,
16024
- before: specifierNormalized,
16025
- after: address,
16026
- });
16027
- return address
16028
- }
16029
- if (specifierIsPrefixOf(specifierCandidate, specifierNormalized)) {
16030
- const address = mappings[specifierCandidate];
16031
- const afterSpecifier = specifierNormalized.slice(
16032
- specifierCandidate.length,
15605
+ Object.keys(scopes).forEach((scopeSpecifier) => {
15606
+ const scopeMappings = scopes[scopeSpecifier];
15607
+ const scopeUrl = tryUrlResolution(scopeSpecifier, baseUrl);
15608
+ if (scopeUrl === null) {
15609
+ console.warn(
15610
+ formulateScopeResolutionFailed({
15611
+ scope: scopeSpecifier,
15612
+ baseUrl,
15613
+ }),
16033
15614
  );
16034
- const addressFinal = tryUrlResolution(afterSpecifier, address);
16035
- onImportMapping({
16036
- scope,
16037
- from: specifierCandidate,
16038
- to: address,
16039
- before: specifierNormalized,
16040
- after: addressFinal,
16041
- });
16042
- return addressFinal
15615
+ return
16043
15616
  }
16044
- }
15617
+ const scopeValueNormalized = normalizeMappings(scopeMappings, baseUrl);
15618
+ scopesNormalized[scopeUrl] = scopeValueNormalized;
15619
+ });
16045
15620
 
16046
- return null
15621
+ return sortScopes(scopesNormalized)
16047
15622
  };
16048
15623
 
16049
- const specifierIsPrefixOf = (specifierHref, href) => {
16050
- return (
16051
- specifierHref[specifierHref.length - 1] === "/" &&
16052
- href.startsWith(specifierHref)
16053
- )
16054
- };
15624
+ const formulateBaseUrlMustBeStringOrUrl = ({
15625
+ baseUrl,
15626
+ }) => `baseUrl must be a string or an url.
15627
+ --- base url ---
15628
+ ${baseUrl}`;
15629
+
15630
+ const formulateAddressMustBeAString = ({
15631
+ specifier,
15632
+ address,
15633
+ }) => `Address must be a string.
15634
+ --- address ---
15635
+ ${address}
15636
+ --- specifier ---
15637
+ ${specifier}`;
15638
+
15639
+ const formulateAdressResolutionFailed = ({
15640
+ address,
15641
+ baseUrl,
15642
+ specifier,
15643
+ }) => `Address url resolution failed.
15644
+ --- address ---
15645
+ ${address}
15646
+ --- base url ---
15647
+ ${baseUrl}
15648
+ --- specifier ---
15649
+ ${specifier}`;
15650
+
15651
+ const formulateAddressUrlRequiresTrailingSlash = ({
15652
+ addressURL,
15653
+ address,
15654
+ specifier,
15655
+ }) => `Address must end with /.
15656
+ --- address url ---
15657
+ ${addressURL}
15658
+ --- address ---
15659
+ ${address}
15660
+ --- specifier ---
15661
+ ${specifier}`;
16055
15662
 
16056
- // https://github.com/systemjs/systemjs/blob/89391f92dfeac33919b0223bbf834a1f4eea5750/src/common.js#L136
15663
+ const formulateScopeResolutionFailed = ({
15664
+ scope,
15665
+ baseUrl,
15666
+ }) => `Scope url resolution failed.
15667
+ --- scope ---
15668
+ ${scope}
15669
+ --- base url ---
15670
+ ${baseUrl}`;
16057
15671
 
16058
- const composeTwoImportMaps = (leftImportMap, rightImportMap) => {
16059
- assertImportMap(leftImportMap);
16060
- assertImportMap(rightImportMap);
15672
+ const pathnameToExtension = (pathname) => {
15673
+ const slashLastIndex = pathname.lastIndexOf("/");
15674
+ if (slashLastIndex !== -1) {
15675
+ pathname = pathname.slice(slashLastIndex + 1);
15676
+ }
16061
15677
 
16062
- const importMap = {};
15678
+ const dotLastIndex = pathname.lastIndexOf(".");
15679
+ if (dotLastIndex === -1) return ""
15680
+ // if (dotLastIndex === pathname.length - 1) return ""
15681
+ return pathname.slice(dotLastIndex)
15682
+ };
16063
15683
 
16064
- const leftImports = leftImportMap.imports;
16065
- const rightImports = rightImportMap.imports;
16066
- const leftHasImports = Boolean(leftImports);
16067
- const rightHasImports = Boolean(rightImports);
16068
- if (leftHasImports && rightHasImports) {
16069
- importMap.imports = composeTwoMappings(leftImports, rightImports);
16070
- } else if (leftHasImports) {
16071
- importMap.imports = { ...leftImports };
16072
- } else if (rightHasImports) {
16073
- importMap.imports = { ...rightImports };
15684
+ const resolveImport = ({
15685
+ specifier,
15686
+ importer,
15687
+ importMap,
15688
+ defaultExtension = false,
15689
+ createBareSpecifierError,
15690
+ onImportMapping = () => {},
15691
+ }) => {
15692
+ let url;
15693
+ if (importMap) {
15694
+ url = applyImportMap({
15695
+ importMap,
15696
+ specifier,
15697
+ importer,
15698
+ createBareSpecifierError,
15699
+ onImportMapping,
15700
+ });
15701
+ } else {
15702
+ url = resolveUrl(specifier, importer);
16074
15703
  }
16075
15704
 
16076
- const leftScopes = leftImportMap.scopes;
16077
- const rightScopes = rightImportMap.scopes;
16078
- const leftHasScopes = Boolean(leftScopes);
16079
- const rightHasScopes = Boolean(rightScopes);
16080
- if (leftHasScopes && rightHasScopes) {
16081
- importMap.scopes = composeTwoScopes(
16082
- leftScopes,
16083
- rightScopes,
16084
- importMap.imports || {},
16085
- );
16086
- } else if (leftHasScopes) {
16087
- importMap.scopes = { ...leftScopes };
16088
- } else if (rightHasScopes) {
16089
- importMap.scopes = { ...rightScopes };
15705
+ if (defaultExtension) {
15706
+ url = applyDefaultExtension({ url, importer, defaultExtension });
16090
15707
  }
16091
15708
 
16092
- return importMap
15709
+ return url
16093
15710
  };
16094
15711
 
16095
- const composeTwoMappings = (leftMappings, rightMappings) => {
16096
- const mappings = {};
15712
+ const applyDefaultExtension = ({ url, importer, defaultExtension }) => {
15713
+ if (urlToPathname(url).endsWith("/")) {
15714
+ return url
15715
+ }
16097
15716
 
16098
- Object.keys(leftMappings).forEach((leftSpecifier) => {
16099
- if (objectHasKey(rightMappings, leftSpecifier)) {
16100
- // will be overidden
16101
- return
15717
+ if (typeof defaultExtension === "string") {
15718
+ const extension = pathnameToExtension(url);
15719
+ if (extension === "") {
15720
+ return `${url}${defaultExtension}`
16102
15721
  }
16103
- const leftAddress = leftMappings[leftSpecifier];
16104
- const rightSpecifier = Object.keys(rightMappings).find((rightSpecifier) => {
16105
- return compareAddressAndSpecifier(leftAddress, rightSpecifier)
16106
- });
16107
- mappings[leftSpecifier] = rightSpecifier
16108
- ? rightMappings[rightSpecifier]
16109
- : leftAddress;
16110
- });
16111
-
16112
- Object.keys(rightMappings).forEach((rightSpecifier) => {
16113
- mappings[rightSpecifier] = rightMappings[rightSpecifier];
16114
- });
16115
-
16116
- return mappings
16117
- };
15722
+ return url
15723
+ }
16118
15724
 
16119
- const objectHasKey = (object, key) =>
16120
- Object.prototype.hasOwnProperty.call(object, key);
15725
+ if (defaultExtension === true) {
15726
+ const extension = pathnameToExtension(url);
15727
+ if (extension === "" && importer) {
15728
+ const importerPathname = urlToPathname(importer);
15729
+ const importerExtension = pathnameToExtension(importerPathname);
15730
+ return `${url}${importerExtension}`
15731
+ }
15732
+ }
16121
15733
 
16122
- const compareAddressAndSpecifier = (address, specifier) => {
16123
- const addressUrl = resolveUrl(address, "file:///");
16124
- const specifierUrl = resolveUrl(specifier, "file:///");
16125
- return addressUrl === specifierUrl
15734
+ return url
16126
15735
  };
16127
15736
 
16128
- const composeTwoScopes = (leftScopes, rightScopes, imports) => {
16129
- const scopes = {};
16130
-
16131
- Object.keys(leftScopes).forEach((leftScopeKey) => {
16132
- if (objectHasKey(rightScopes, leftScopeKey)) {
16133
- // will be merged
16134
- scopes[leftScopeKey] = leftScopes[leftScopeKey];
16135
- return
16136
- }
16137
- const topLevelSpecifier = Object.keys(imports).find(
16138
- (topLevelSpecifierCandidate) => {
16139
- return compareAddressAndSpecifier(
16140
- leftScopeKey,
16141
- topLevelSpecifierCandidate,
16142
- )
16143
- },
16144
- );
16145
- if (topLevelSpecifier) {
16146
- scopes[imports[topLevelSpecifier]] = leftScopes[leftScopeKey];
15737
+ const jsenvPluginHtmlReferenceAnalysis = ({
15738
+ inlineContent,
15739
+ inlineConvertedScript,
15740
+ }) => {
15741
+ /*
15742
+ * About importmap found in HTML files:
15743
+ * - feeds importmap files to jsenv kitchen
15744
+ * - use importmap to resolve import (when there is one + fallback to other resolution mecanism)
15745
+ * - inline importmap with [src=""]
15746
+ *
15747
+ * A correct importmap resolution should scope importmap resolution per html file.
15748
+ * It would be doable by adding ?html_id to each js file in order to track
15749
+ * the html file importing it.
15750
+ * Considering it happens only when all the following conditions are met:
15751
+ * - 2+ html files are using an importmap
15752
+ * - the importmap used is not the same
15753
+ * - the importmap contain conflicting mappings
15754
+ * - these html files are both executed during the same scenario (dev, test, build)
15755
+ * And that it would be ugly to see ?html_id all over the place
15756
+ * -> The importmap resolution implemented here takes a shortcut and does the following:
15757
+ * - All importmap found are merged into a single one that is applied to every import specifiers
15758
+ */
15759
+
15760
+ let globalImportmap = null;
15761
+ const importmaps = {};
15762
+ const onImportmapParsed = (htmlUrl, importmap) => {
15763
+ if (importmap) {
15764
+ importmaps[htmlUrl] = normalizeImportMap(importmap, htmlUrl);
16147
15765
  } else {
16148
- scopes[leftScopeKey] = leftScopes[leftScopeKey];
15766
+ importmaps[htmlUrl] = null;
16149
15767
  }
16150
- });
15768
+ globalImportmap = Object.keys(importmaps).reduce((previous, url) => {
15769
+ const importmap = importmaps[url];
15770
+ if (!previous) {
15771
+ return importmap;
15772
+ }
15773
+ if (!importmap) {
15774
+ return previous;
15775
+ }
15776
+ return composeTwoImportMaps(previous, importmap);
15777
+ }, null);
15778
+ };
16151
15779
 
16152
- Object.keys(rightScopes).forEach((rightScopeKey) => {
16153
- if (objectHasKey(scopes, rightScopeKey)) {
16154
- scopes[rightScopeKey] = composeTwoMappings(
16155
- scopes[rightScopeKey],
16156
- rightScopes[rightScopeKey],
16157
- );
16158
- } else {
16159
- scopes[rightScopeKey] = {
16160
- ...rightScopes[rightScopeKey],
16161
- };
16162
- }
16163
- });
15780
+ return {
15781
+ name: "jsenv:html_reference_analysis",
15782
+ appliesDuring: "*",
15783
+ resolveReference: {
15784
+ js_import: (reference) => {
15785
+ if (!globalImportmap) {
15786
+ return null;
15787
+ }
15788
+ try {
15789
+ let fromMapping = false;
15790
+ const result = resolveImport({
15791
+ specifier: reference.specifier,
15792
+ importer: reference.ownerUrlInfo.url,
15793
+ importMap: globalImportmap,
15794
+ onImportMapping: () => {
15795
+ fromMapping = true;
15796
+ },
15797
+ });
15798
+ if (fromMapping) {
15799
+ return result;
15800
+ }
15801
+ return null;
15802
+ } catch (e) {
15803
+ if (e.message.includes("bare specifier")) {
15804
+ // in theory we should throw to be compliant with web behaviour
15805
+ // but for now it's simpler to return null
15806
+ // and let a chance to other plugins to handle the bare specifier
15807
+ // (node esm resolution)
15808
+ // and we want importmap to be prio over node esm so we cannot put this plugin after
15809
+ return null;
15810
+ }
15811
+ throw e;
15812
+ }
15813
+ },
15814
+ },
15815
+ transformUrlContent: {
15816
+ html: async (urlInfo) => {
15817
+ let importmapFound = false;
15818
+ let importmapParsedCallbackSet = new Set();
15819
+ const onImportmapReady = (importmap) => {
15820
+ onImportmapParsed(urlInfo.url, importmap);
15821
+ importmapParsedCallbackSet.forEach((callback) => {
15822
+ callback();
15823
+ });
15824
+ importmapParsedCallbackSet.clear();
15825
+ };
16164
15826
 
16165
- return scopes
16166
- };
15827
+ const content = urlInfo.content;
15828
+ const htmlAst = parseHtmlString(content);
16167
15829
 
16168
- const sortImports = (imports) => {
16169
- const mappingsSorted = {};
15830
+ const mutations = [];
15831
+ const actions = [];
15832
+ const finalizeCallbacks = [];
16170
15833
 
16171
- Object.keys(imports)
16172
- .sort(compareLengthOrLocaleCompare)
16173
- .forEach((name) => {
16174
- mappingsSorted[name] = imports[name];
16175
- });
15834
+ const createExternalReference = (
15835
+ node,
15836
+ attributeName,
15837
+ attributeValue,
15838
+ { type, subtype, expectedType, ...rest },
15839
+ ) => {
15840
+ let position;
15841
+ if (getHtmlNodeAttribute(node, "jsenv-cooked-by")) {
15842
+ // when generated from inline content,
15843
+ // line, column is not "src" nor "inlined-from-src" but "original-position"
15844
+ position = getHtmlNodePosition(node);
15845
+ } else {
15846
+ position = getHtmlNodeAttributePosition(node, attributeName);
15847
+ }
15848
+ const {
15849
+ line,
15850
+ column,
15851
+ // originalLine, originalColumn
15852
+ } = position;
15853
+ const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
15854
+
15855
+ const { crossorigin, integrity } = readFetchMetas(node);
15856
+ const isResourceHint = [
15857
+ "preconnect",
15858
+ "dns-prefetch",
15859
+ "prefetch",
15860
+ "preload",
15861
+ "modulepreload",
15862
+ ].includes(subtype);
15863
+ let attributeLocation = node.sourceCodeLocation.attrs[attributeName];
15864
+ if (
15865
+ !attributeLocation &&
15866
+ attributeName === "href" &&
15867
+ (node.tagName === "use" || node.tagName === "image")
15868
+ ) {
15869
+ attributeLocation = node.sourceCodeLocation.attrs["xlink:href"];
15870
+ }
15871
+ const attributeStart = attributeLocation.startOffset;
15872
+ const attributeValueStart = urlInfo.content.indexOf(
15873
+ attributeValue,
15874
+ attributeStart + `${attributeName}=`.length,
15875
+ );
15876
+ const attributeValueEnd = attributeValueStart + attributeValue.length;
15877
+ const reference = urlInfo.dependencies.found({
15878
+ type,
15879
+ subtype,
15880
+ expectedType,
15881
+ specifier: attributeValue,
15882
+ specifierLine: line,
15883
+ specifierColumn: column,
15884
+ specifierStart: attributeValueStart,
15885
+ specifierEnd: attributeValueEnd,
15886
+ isResourceHint,
15887
+ isWeak: isResourceHint,
15888
+ crossorigin,
15889
+ integrity,
15890
+ debug,
15891
+ astInfo: { node, attributeName },
15892
+ ...rest,
15893
+ });
15894
+ actions.push(async () => {
15895
+ await reference.readGeneratedSpecifier();
15896
+ mutations.push(() => {
15897
+ setHtmlNodeAttributes(node, {
15898
+ [attributeName]: reference.generatedSpecifier,
15899
+ });
15900
+ });
15901
+ });
15902
+ return reference;
15903
+ };
15904
+ const visitHref = (node, referenceProps) => {
15905
+ const href = getHtmlNodeAttribute(node, "href");
15906
+ if (href) {
15907
+ return createExternalReference(node, "href", href, referenceProps);
15908
+ }
15909
+ return null;
15910
+ };
15911
+ const visitSrc = (node, referenceProps) => {
15912
+ const src = getHtmlNodeAttribute(node, "src");
15913
+ if (src) {
15914
+ return createExternalReference(node, "src", src, referenceProps);
15915
+ }
15916
+ return null;
15917
+ };
15918
+ const visitSrcset = (node, referenceProps) => {
15919
+ const srcset = getHtmlNodeAttribute(node, "srcset");
15920
+ if (srcset) {
15921
+ const srcCandidates = parseSrcSet(srcset);
15922
+ return srcCandidates.map((srcCandidate) => {
15923
+ return createExternalReference(
15924
+ node,
15925
+ "srcset",
15926
+ srcCandidate.specifier,
15927
+ referenceProps,
15928
+ );
15929
+ });
15930
+ }
15931
+ return null;
15932
+ };
15933
+
15934
+ const createInlineReference = (
15935
+ node,
15936
+ inlineContent,
15937
+ { type, expectedType, contentType },
15938
+ ) => {
15939
+ const hotAccept =
15940
+ getHtmlNodeAttribute(node, "hot-accept") !== undefined;
15941
+ const { line, column, isOriginal } = getHtmlNodePosition(node, {
15942
+ preferOriginal: true,
15943
+ });
15944
+ const inlineContentUrl = getUrlForContentInsideHtml(node, {
15945
+ htmlUrl: urlInfo.url,
15946
+ });
15947
+ const debug = getHtmlNodeAttribute(node, "jsenv-debug") !== undefined;
15948
+ const inlineReference = urlInfo.dependencies.foundInline({
15949
+ type,
15950
+ expectedType,
15951
+ isOriginalPosition: isOriginal,
15952
+ // we remove 1 to the line because imagine the following html:
15953
+ // <style>body { color: red; }</style>
15954
+ // -> content starts same line as <style> (same for <script>)
15955
+ specifierLine: line - 1,
15956
+ specifierColumn: column,
15957
+ specifier: inlineContentUrl,
15958
+ contentType,
15959
+ content: inlineContent,
15960
+ debug,
15961
+ astInfo: { node },
15962
+ });
16176
15963
 
16177
- return mappingsSorted
16178
- };
15964
+ actions.push(async () => {
15965
+ if (expectedType === "js_module" && importmapFound) {
15966
+ await new Promise((resolve) => {
15967
+ importmapParsedCallbackSet.add(resolve);
15968
+ });
15969
+ }
15970
+ await inlineReference.urlInfo.cook();
15971
+ mutations.push(() => {
15972
+ if (hotAccept) {
15973
+ removeHtmlNodeText(node);
15974
+ setHtmlNodeAttributes(node, {
15975
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
15976
+ });
15977
+ } else {
15978
+ setHtmlNodeText(node, inlineReference.urlInfo.content, {
15979
+ indentation: false, // indentation would decrease stack trace precision
15980
+ });
15981
+ setHtmlNodeAttributes(node, {
15982
+ "jsenv-cooked-by": "jsenv:html_inline_content_analysis",
15983
+ });
15984
+ }
15985
+ });
15986
+ });
15987
+ return inlineReference;
15988
+ };
15989
+ const visitTextContent = (
15990
+ node,
15991
+ { type, subtype, expectedType, contentType },
15992
+ ) => {
15993
+ const inlineContent = getHtmlNodeText(node);
15994
+ if (!inlineContent) {
15995
+ return null;
15996
+ }
15997
+ return createInlineReference(node, inlineContent, {
15998
+ type,
15999
+ subtype,
16000
+ expectedType,
16001
+ contentType,
16002
+ });
16003
+ };
16179
16004
 
16180
- const sortScopes = (scopes) => {
16181
- const scopesSorted = {};
16005
+ visitHtmlNodes(htmlAst, {
16006
+ link: (linkNode) => {
16007
+ const rel = getHtmlNodeAttribute(linkNode, "rel");
16008
+ const type = getHtmlNodeAttribute(linkNode, "type");
16009
+ const ref = visitHref(linkNode, {
16010
+ type: "link_href",
16011
+ subtype: rel,
16012
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#including_a_mime_type
16013
+ expectedContentType: type,
16014
+ });
16015
+ if (ref) {
16016
+ finalizeCallbacks.push(() => {
16017
+ if (ref.expectedType) ; else {
16018
+ ref.expectedType = decideLinkExpectedType(ref, urlInfo);
16019
+ }
16020
+ });
16021
+ }
16022
+ },
16023
+ style: inlineContent
16024
+ ? (styleNode) => {
16025
+ visitTextContent(styleNode, {
16026
+ type: "style",
16027
+ expectedType: "css",
16028
+ contentType: "text/css",
16029
+ });
16030
+ }
16031
+ : null,
16032
+ script: (scriptNode) => {
16033
+ const { type, subtype, contentType, extension } =
16034
+ analyzeScriptNode(scriptNode);
16035
+ if (type === "text") {
16036
+ // ignore <script type="whatever">foobar</script>
16037
+ // per HTML spec https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
16038
+ return;
16039
+ }
16040
+ if (type === "importmap") {
16041
+ importmapFound = true;
16042
+
16043
+ const src = getHtmlNodeAttribute(scriptNode, "src");
16044
+ if (src) {
16045
+ // Browser would throw on remote importmap
16046
+ // and won't sent a request to the server for it
16047
+ // We must precook the importmap to know its content and inline it into the HTML
16048
+ const importmapReference = createExternalReference(
16049
+ scriptNode,
16050
+ "src",
16051
+ src,
16052
+ {
16053
+ type: "script",
16054
+ subtype: "importmap",
16055
+ expectedType: "importmap",
16056
+ },
16057
+ );
16058
+ const { line, column, isOriginal } = getHtmlNodePosition(
16059
+ scriptNode,
16060
+ {
16061
+ preferOriginal: true,
16062
+ },
16063
+ );
16064
+ const importmapInlineUrl = getUrlForContentInsideHtml(
16065
+ scriptNode,
16066
+ {
16067
+ htmlUrl: urlInfo.url,
16068
+ },
16069
+ );
16070
+ const importmapReferenceInlined = importmapReference.inline({
16071
+ line: line - 1,
16072
+ column,
16073
+ isOriginal,
16074
+ specifier: importmapInlineUrl,
16075
+ contentType: "application/importmap+json",
16076
+ });
16077
+ const importmapInlineUrlInfo =
16078
+ importmapReferenceInlined.urlInfo;
16079
+ actions.push(async () => {
16080
+ await importmapInlineUrlInfo.cook();
16081
+ onImportmapReady(JSON.parse(importmapInlineUrlInfo.content));
16082
+ mutations.push(() => {
16083
+ setHtmlNodeText(
16084
+ scriptNode,
16085
+ importmapInlineUrlInfo.content,
16086
+ {
16087
+ indentation: "auto",
16088
+ },
16089
+ );
16090
+ setHtmlNodeAttributes(scriptNode, {
16091
+ "src": undefined,
16092
+ "jsenv-inlined-by": "jsenv:html_reference_analysis",
16093
+ "inlined-from-src": src,
16094
+ });
16095
+ });
16096
+ });
16097
+ } else {
16098
+ const htmlNodeText = getHtmlNodeText(scriptNode);
16099
+ if (htmlNodeText) {
16100
+ const importmapReference = createInlineReference(
16101
+ scriptNode,
16102
+ htmlNodeText,
16103
+ {
16104
+ type: "script",
16105
+ expectedType: "importmap",
16106
+ contentType: "application/importmap+json",
16107
+ },
16108
+ );
16109
+ const inlineImportmapUrlInfo = importmapReference.urlInfo;
16110
+ actions.push(async () => {
16111
+ await inlineImportmapUrlInfo.cook();
16112
+ onImportmapReady(
16113
+ JSON.parse(inlineImportmapUrlInfo.content),
16114
+ );
16115
+ mutations.push(() => {
16116
+ setHtmlNodeText(
16117
+ scriptNode,
16118
+ inlineImportmapUrlInfo.content,
16119
+ {
16120
+ indentation: "auto",
16121
+ },
16122
+ );
16123
+ setHtmlNodeAttributes(scriptNode, {
16124
+ "jsenv-cooked-by": "jsenv:html_reference_analysis",
16125
+ });
16126
+ });
16127
+ });
16128
+ }
16129
+ }
16130
+ // once this plugin knows the importmap, it will use it
16131
+ // to map imports. These import specifiers will be normalized
16132
+ // by "formatReference" making the importmap presence useless.
16133
+ // In dev/test we keep importmap into the HTML to see it even if useless
16134
+ // Duing build we get rid of it
16135
+ if (urlInfo.context.build) {
16136
+ mutations.push(() => {
16137
+ removeHtmlNode(scriptNode);
16138
+ });
16139
+ }
16140
+ return;
16141
+ }
16142
+ const externalRef = visitSrc(scriptNode, {
16143
+ type: "script",
16144
+ subtype: type,
16145
+ expectedType: type,
16146
+ });
16147
+ if (externalRef) {
16148
+ return;
16149
+ }
16182
16150
 
16183
- Object.keys(scopes)
16184
- .sort(compareLengthOrLocaleCompare)
16185
- .forEach((scopeSpecifier) => {
16186
- scopesSorted[scopeSpecifier] = sortImports(scopes[scopeSpecifier]);
16187
- });
16151
+ // now visit the content, if any
16152
+ if (!inlineContent) {
16153
+ return;
16154
+ }
16155
+ // If the inline script was already handled by an other plugin, ignore it
16156
+ // - we want to preserve inline scripts generated by html supervisor during dev
16157
+ // - we want to avoid cooking twice a script during build
16158
+ if (
16159
+ !inlineConvertedScript &&
16160
+ getHtmlNodeAttribute(scriptNode, "jsenv-injected-by") ===
16161
+ "jsenv:js_module_fallback"
16162
+ ) {
16163
+ return;
16164
+ }
16188
16165
 
16189
- return scopesSorted
16190
- };
16166
+ const inlineRef = visitTextContent(scriptNode, {
16167
+ type: "script",
16168
+ subtype,
16169
+ expectedType: type,
16170
+ contentType,
16171
+ });
16172
+ if (inlineRef) {
16173
+ // 1. <script type="jsx"> becomes <script>
16174
+ if (type === "js_classic" && extension !== ".js") {
16175
+ mutations.push(() => {
16176
+ setHtmlNodeAttributes(scriptNode, {
16177
+ type: undefined,
16178
+ });
16179
+ });
16180
+ }
16181
+ // 2. <script type="module/jsx"> becomes <script type="module">
16182
+ if (type === "js_module" && extension !== ".js") {
16183
+ mutations.push(() => {
16184
+ setHtmlNodeAttributes(scriptNode, {
16185
+ type: "module",
16186
+ });
16187
+ });
16188
+ }
16189
+ }
16190
+ },
16191
+ a: (aNode) => {
16192
+ visitHref(aNode, {
16193
+ type: "a_href",
16194
+ });
16195
+ },
16196
+ iframe: (iframeNode) => {
16197
+ visitSrc(iframeNode, {
16198
+ type: "iframe_src",
16199
+ });
16200
+ },
16201
+ img: (imgNode) => {
16202
+ visitSrc(imgNode, {
16203
+ type: "img_src",
16204
+ });
16205
+ visitSrcset(imgNode, {
16206
+ type: "img_srcset",
16207
+ });
16208
+ },
16209
+ source: (sourceNode) => {
16210
+ visitSrc(sourceNode, {
16211
+ type: "source_src",
16212
+ });
16213
+ visitSrcset(sourceNode, {
16214
+ type: "source_srcset",
16215
+ });
16216
+ },
16217
+ // svg <image> tag
16218
+ image: (imageNode) => {
16219
+ visitHref(imageNode, {
16220
+ type: "image_href",
16221
+ });
16222
+ },
16223
+ use: (useNode) => {
16224
+ visitHref(useNode, {
16225
+ type: "use_href",
16226
+ });
16227
+ },
16228
+ });
16229
+ if (!importmapFound) {
16230
+ onImportmapReady();
16231
+ }
16232
+ finalizeCallbacks.forEach((finalizeCallback) => {
16233
+ finalizeCallback();
16234
+ });
16191
16235
 
16192
- const compareLengthOrLocaleCompare = (a, b) => {
16193
- return b.length - a.length || a.localeCompare(b)
16236
+ if (actions.length > 0) {
16237
+ await Promise.all(actions.map((action) => action()));
16238
+ actions.length = 0;
16239
+ }
16240
+ if (mutations.length === 0) {
16241
+ return null;
16242
+ }
16243
+ mutations.forEach((mutation) => mutation());
16244
+ mutations.length = 0;
16245
+ return stringifyHtmlAst(htmlAst);
16246
+ },
16247
+ },
16248
+ };
16194
16249
  };
16195
16250
 
16196
- const normalizeImportMap = (importMap, baseUrl) => {
16197
- assertImportMap(importMap);
16198
-
16199
- if (!isStringOrUrl(baseUrl)) {
16200
- throw new TypeError(formulateBaseUrlMustBeStringOrUrl({ baseUrl }))
16251
+ const crossOriginCompatibleTagNames = ["script", "link", "img", "source"];
16252
+ const integrityCompatibleTagNames = ["script", "link", "img", "source"];
16253
+ const readFetchMetas = (node) => {
16254
+ const meta = {};
16255
+ if (crossOriginCompatibleTagNames.includes(node.nodeName)) {
16256
+ const crossorigin = getHtmlNodeAttribute(node, "crossorigin") !== undefined;
16257
+ meta.crossorigin = crossorigin;
16201
16258
  }
16202
-
16203
- const { imports, scopes } = importMap;
16204
-
16205
- return {
16206
- imports: imports ? normalizeMappings(imports, baseUrl) : undefined,
16207
- scopes: scopes ? normalizeScopes(scopes, baseUrl) : undefined,
16259
+ if (integrityCompatibleTagNames.includes(node.nodeName)) {
16260
+ const integrity = getHtmlNodeAttribute(node, "integrity");
16261
+ meta.integrity = integrity;
16208
16262
  }
16263
+ return meta;
16209
16264
  };
16210
16265
 
16211
- const isStringOrUrl = (value) => {
16212
- if (typeof value === "string") {
16213
- return true
16266
+ const decideLinkExpectedType = (linkReference, htmlUrlInfo) => {
16267
+ const rel = getHtmlNodeAttribute(linkReference.astInfo.node, "rel");
16268
+ if (rel === "webmanifest") {
16269
+ return "webmanifest";
16214
16270
  }
16215
-
16216
- if (typeof URL === "function" && value instanceof URL) {
16217
- return true
16271
+ if (rel === "modulepreload") {
16272
+ return "js_module";
16218
16273
  }
16219
-
16220
- return false
16221
- };
16222
-
16223
- const normalizeMappings = (mappings, baseUrl) => {
16224
- const mappingsNormalized = {};
16225
-
16226
- Object.keys(mappings).forEach((specifier) => {
16227
- const address = mappings[specifier];
16228
-
16229
- if (typeof address !== "string") {
16230
- console.warn(
16231
- formulateAddressMustBeAString({
16232
- address,
16233
- specifier,
16234
- }),
16235
- );
16236
- return
16237
- }
16238
-
16239
- const specifierResolved = resolveSpecifier(specifier, baseUrl) || specifier;
16240
-
16241
- const addressUrl = tryUrlResolution(address, baseUrl);
16242
- if (addressUrl === null) {
16243
- console.warn(
16244
- formulateAdressResolutionFailed({
16245
- address,
16246
- baseUrl,
16247
- specifier,
16248
- }),
16249
- );
16250
- return
16274
+ if (rel === "stylesheet") {
16275
+ return "css";
16276
+ }
16277
+ if (rel === "preload") {
16278
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#what_types_of_content_can_be_preloaded
16279
+ const as = getHtmlNodeAttribute(linkReference.astInfo.node, "as");
16280
+ if (as === "document") {
16281
+ return "html";
16251
16282
  }
16252
-
16253
- if (specifier.endsWith("/") && !addressUrl.endsWith("/")) {
16254
- console.warn(
16255
- formulateAddressUrlRequiresTrailingSlash({
16256
- addressUrl,
16257
- address,
16258
- specifier,
16259
- }),
16260
- );
16261
- return
16283
+ if (as === "style") {
16284
+ return "css";
16262
16285
  }
16263
- mappingsNormalized[specifierResolved] = addressUrl;
16264
- });
16265
-
16266
- return sortImports(mappingsNormalized)
16267
- };
16268
-
16269
- const normalizeScopes = (scopes, baseUrl) => {
16270
- const scopesNormalized = {};
16271
-
16272
- Object.keys(scopes).forEach((scopeSpecifier) => {
16273
- const scopeMappings = scopes[scopeSpecifier];
16274
- const scopeUrl = tryUrlResolution(scopeSpecifier, baseUrl);
16275
- if (scopeUrl === null) {
16276
- console.warn(
16277
- formulateScopeResolutionFailed({
16278
- scope: scopeSpecifier,
16279
- baseUrl,
16280
- }),
16281
- );
16282
- return
16286
+ if (as === "script") {
16287
+ for (const referenceToOther of htmlUrlInfo.referenceToOthersSet) {
16288
+ if (referenceToOther.url !== linkReference.url) {
16289
+ continue;
16290
+ }
16291
+ if (referenceToOther.type !== "script") {
16292
+ continue;
16293
+ }
16294
+ return referenceToOther.expectedType;
16295
+ }
16296
+ return undefined;
16283
16297
  }
16284
- const scopeValueNormalized = normalizeMappings(scopeMappings, baseUrl);
16285
- scopesNormalized[scopeUrl] = scopeValueNormalized;
16286
- });
16287
-
16288
- return sortScopes(scopesNormalized)
16298
+ }
16299
+ return undefined;
16289
16300
  };
16290
16301
 
16291
- const formulateBaseUrlMustBeStringOrUrl = ({
16292
- baseUrl,
16293
- }) => `baseUrl must be a string or an url.
16294
- --- base url ---
16295
- ${baseUrl}`;
16296
-
16297
- const formulateAddressMustBeAString = ({
16298
- specifier,
16299
- address,
16300
- }) => `Address must be a string.
16301
- --- address ---
16302
- ${address}
16303
- --- specifier ---
16304
- ${specifier}`;
16302
+ // const applyWebUrlResolution = (url, baseUrl) => {
16303
+ // if (url[0] === "/") {
16304
+ // return new URL(url.slice(1), baseUrl).href;
16305
+ // }
16306
+ // return new URL(url, baseUrl).href;
16307
+ // };
16305
16308
 
16306
- const formulateAdressResolutionFailed = ({
16307
- address,
16308
- baseUrl,
16309
- specifier,
16310
- }) => `Address url resolution failed.
16311
- --- address ---
16312
- ${address}
16313
- --- base url ---
16314
- ${baseUrl}
16315
- --- specifier ---
16316
- ${specifier}`;
16309
+ // css: parseAndTransformCssUrls,
16317
16310
 
16318
- const formulateAddressUrlRequiresTrailingSlash = ({
16319
- addressURL,
16320
- address,
16321
- specifier,
16322
- }) => `Address must end with /.
16323
- --- address url ---
16324
- ${addressURL}
16325
- --- address ---
16326
- ${address}
16327
- --- specifier ---
16328
- ${specifier}`;
16311
+ const jsenvPluginWebmanifestReferenceAnalysis = () => {
16312
+ return {
16313
+ name: "jsenv:webmanifest_reference_analysis",
16314
+ appliesDuring: "*",
16315
+ transformUrlContent: {
16316
+ webmanifest: parseAndTransformWebmanifestUrls,
16317
+ },
16318
+ };
16319
+ };
16329
16320
 
16330
- const formulateScopeResolutionFailed = ({
16331
- scope,
16332
- baseUrl,
16333
- }) => `Scope url resolution failed.
16334
- --- scope ---
16335
- ${scope}
16336
- --- base url ---
16337
- ${baseUrl}`;
16321
+ const parseAndTransformWebmanifestUrls = async (urlInfo) => {
16322
+ const content = urlInfo.content;
16323
+ const manifest = JSON.parse(content);
16324
+ const actions = [];
16325
+ const { icons = [] } = manifest;
16326
+ icons.forEach((icon) => {
16327
+ const iconReference = urlInfo.dependencies.found({
16328
+ type: "webmanifest_icon_src",
16329
+ specifier: icon.src,
16330
+ });
16331
+ actions.push(async () => {
16332
+ await iconReference.readGeneratedSpecifier();
16333
+ icon.src = iconReference.generatedSpecifier;
16334
+ });
16335
+ });
16338
16336
 
16339
- const pathnameToExtension = (pathname) => {
16340
- const slashLastIndex = pathname.lastIndexOf("/");
16341
- if (slashLastIndex !== -1) {
16342
- pathname = pathname.slice(slashLastIndex + 1);
16337
+ if (actions.length === 0) {
16338
+ return null;
16343
16339
  }
16340
+ await Promise.all(actions.map((action) => action()));
16341
+ return JSON.stringify(manifest, null, " ");
16342
+ };
16344
16343
 
16345
- const dotLastIndex = pathname.lastIndexOf(".");
16346
- if (dotLastIndex === -1) return ""
16347
- // if (dotLastIndex === pathname.length - 1) return ""
16348
- return pathname.slice(dotLastIndex)
16344
+ /*
16345
+ * https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/css/src/CSSTransformer.js
16346
+ */
16347
+
16348
+
16349
+ const jsenvPluginCssReferenceAnalysis = () => {
16350
+ return {
16351
+ name: "jsenv:css_reference_analysis",
16352
+ appliesDuring: "*",
16353
+ transformUrlContent: {
16354
+ css: parseAndTransformCssUrls,
16355
+ },
16356
+ };
16349
16357
  };
16350
16358
 
16351
- const resolveImport = ({
16352
- specifier,
16353
- importer,
16354
- importMap,
16355
- defaultExtension = false,
16356
- createBareSpecifierError,
16357
- onImportMapping = () => {},
16358
- }) => {
16359
- let url;
16360
- if (importMap) {
16361
- url = applyImportMap({
16362
- importMap,
16363
- specifier,
16364
- importer,
16365
- createBareSpecifierError,
16366
- onImportMapping,
16359
+ const parseAndTransformCssUrls = async (urlInfo) => {
16360
+ const cssUrls = await parseCssUrls({
16361
+ css: urlInfo.content,
16362
+ url: urlInfo.originalUrl,
16363
+ });
16364
+ const actions = [];
16365
+ const magicSource = createMagicSource(urlInfo.content);
16366
+ for (const cssUrl of cssUrls) {
16367
+ const reference = urlInfo.dependencies.found({
16368
+ type: cssUrl.type,
16369
+ specifier: cssUrl.specifier,
16370
+ specifierStart: cssUrl.start,
16371
+ specifierEnd: cssUrl.end,
16372
+ specifierLine: cssUrl.line,
16373
+ specifierColumn: cssUrl.column,
16374
+ });
16375
+ actions.push(async () => {
16376
+ await reference.readGeneratedSpecifier();
16377
+ const replacement = reference.generatedSpecifier;
16378
+ magicSource.replace({
16379
+ start: cssUrl.start,
16380
+ end: cssUrl.end,
16381
+ replacement,
16382
+ });
16367
16383
  });
16368
- } else {
16369
- url = resolveUrl(specifier, importer);
16370
16384
  }
16371
-
16372
- if (defaultExtension) {
16373
- url = applyDefaultExtension({ url, importer, defaultExtension });
16385
+ if (actions.length > 0) {
16386
+ await Promise.all(actions.map((action) => action()));
16374
16387
  }
16388
+ return magicSource.toContentAndSourcemap();
16389
+ };
16375
16390
 
16376
- return url
16391
+ const jsenvPluginJsReferenceAnalysis = ({ inlineContent }) => {
16392
+ return [
16393
+ {
16394
+ name: "jsenv:js_reference_analysis",
16395
+ appliesDuring: "*",
16396
+ transformUrlContent: {
16397
+ js_classic: (urlInfo) =>
16398
+ parseAndTransformJsReferences(urlInfo, {
16399
+ inlineContent,
16400
+ canUseTemplateLiterals:
16401
+ urlInfo.context.isSupportedOnCurrentClients("template_literals"),
16402
+ }),
16403
+ js_module: (urlInfo) =>
16404
+ parseAndTransformJsReferences(urlInfo, {
16405
+ inlineContent,
16406
+ canUseTemplateLiterals:
16407
+ urlInfo.context.isSupportedOnCurrentClients("template_literals"),
16408
+ }),
16409
+ },
16410
+ },
16411
+ ];
16377
16412
  };
16378
16413
 
16379
- const applyDefaultExtension = ({ url, importer, defaultExtension }) => {
16380
- if (urlToPathname(url).endsWith("/")) {
16381
- return url
16382
- }
16414
+ const parseAndTransformJsReferences = async (
16415
+ urlInfo,
16416
+ { inlineContent, canUseTemplateLiterals },
16417
+ ) => {
16418
+ const magicSource = createMagicSource(urlInfo.content);
16419
+ const parallelActions = [];
16420
+ const sequentialActions = [];
16421
+ const isNodeJs =
16422
+ Object.keys(urlInfo.context.runtimeCompat).toString() === "node";
16383
16423
 
16384
- if (typeof defaultExtension === "string") {
16385
- const extension = pathnameToExtension(url);
16386
- if (extension === "") {
16387
- return `${url}${defaultExtension}`
16424
+ const onInlineReference = (inlineReferenceInfo) => {
16425
+ const inlineUrl = getUrlForContentInsideJs(inlineReferenceInfo, {
16426
+ url: urlInfo.url,
16427
+ });
16428
+ let { quote } = inlineReferenceInfo;
16429
+ if (quote === "`" && !canUseTemplateLiterals) {
16430
+ // if quote is "`" and template literals are not supported
16431
+ // we'll use a regular string (single or double quote)
16432
+ // when rendering the string
16433
+ quote = JS_QUOTES.pickBest(inlineReferenceInfo.content);
16434
+ }
16435
+ const inlineReference = urlInfo.dependencies.foundInline({
16436
+ type: "js_inline_content",
16437
+ subtype: inlineReferenceInfo.type, // "new_blob_first_arg", "new_inline_content_first_arg", "json_parse_first_arg"
16438
+ isOriginalPosition: urlInfo.content === urlInfo.originalContent,
16439
+ specifierLine: inlineReferenceInfo.line,
16440
+ specifierColumn: inlineReferenceInfo.column,
16441
+ specifier: inlineUrl,
16442
+ contentType: inlineReferenceInfo.contentType,
16443
+ content: inlineReferenceInfo.content,
16444
+ });
16445
+ const inlineUrlInfo = inlineReference.urlInfo;
16446
+ inlineUrlInfo.jsQuote = quote;
16447
+ inlineReference.escape = (value) => {
16448
+ return JS_QUOTES.escapeSpecialChars(value.slice(1, -1), { quote });
16449
+ };
16450
+
16451
+ sequentialActions.push(async () => {
16452
+ await inlineUrlInfo.cook();
16453
+ const replacement = JS_QUOTES.escapeSpecialChars(inlineUrlInfo.content, {
16454
+ quote,
16455
+ });
16456
+ magicSource.replace({
16457
+ start: inlineReferenceInfo.start,
16458
+ end: inlineReferenceInfo.end,
16459
+ replacement,
16460
+ });
16461
+ });
16462
+ };
16463
+ const onExternalReference = (externalReferenceInfo) => {
16464
+ if (
16465
+ externalReferenceInfo.subtype === "import_static" ||
16466
+ externalReferenceInfo.subtype === "import_dynamic"
16467
+ ) {
16468
+ urlInfo.data.usesImport = true;
16469
+ }
16470
+ if (
16471
+ isNodeJs &&
16472
+ externalReferenceInfo.type === "js_url" &&
16473
+ externalReferenceInfo.expectedSubtype === "worker" &&
16474
+ externalReferenceInfo.expectedType === "js_classic" &&
16475
+ // TODO: it's true also if closest package.json
16476
+ // is type: module
16477
+ urlToExtension$1(
16478
+ new URL(externalReferenceInfo.specifier, urlInfo.url).href,
16479
+ ) === ".mjs"
16480
+ ) {
16481
+ externalReferenceInfo.expectedType = "js_module";
16482
+ }
16483
+ const reference = urlInfo.dependencies.found({
16484
+ type: externalReferenceInfo.type,
16485
+ subtype: externalReferenceInfo.subtype,
16486
+ expectedType: externalReferenceInfo.expectedType,
16487
+ expectedSubtype: externalReferenceInfo.expectedSubtype || urlInfo.subtype,
16488
+ specifier: externalReferenceInfo.specifier,
16489
+ specifierStart: externalReferenceInfo.start,
16490
+ specifierEnd: externalReferenceInfo.end,
16491
+ specifierLine: externalReferenceInfo.line,
16492
+ specifierColumn: externalReferenceInfo.column,
16493
+ data: externalReferenceInfo.data,
16494
+ baseUrl: {
16495
+ "StringLiteral": externalReferenceInfo.baseUrl,
16496
+ "window.location": urlInfo.url,
16497
+ "window.origin": urlInfo.context.rootDirectoryUrl,
16498
+ "import.meta.url": urlInfo.url,
16499
+ "context.meta.url": urlInfo.url,
16500
+ "document.currentScript.src": urlInfo.url,
16501
+ }[externalReferenceInfo.baseUrlType],
16502
+ importAttributes: externalReferenceInfo.importAttributes,
16503
+ astInfo: externalReferenceInfo.astInfo,
16504
+ });
16505
+ parallelActions.push(async () => {
16506
+ await reference.readGeneratedSpecifier();
16507
+ const replacement = reference.generatedSpecifier;
16508
+ magicSource.replace({
16509
+ start: externalReferenceInfo.start,
16510
+ end: externalReferenceInfo.end,
16511
+ replacement,
16512
+ });
16513
+ if (reference.mutation) {
16514
+ reference.mutation(magicSource, urlInfo);
16515
+ }
16516
+ });
16517
+ };
16518
+ const jsReferenceInfos = parseJsUrls({
16519
+ js: urlInfo.content,
16520
+ url: urlInfo.originalUrl,
16521
+ ast: urlInfo.contentAst,
16522
+ isJsModule: urlInfo.type === "js_module",
16523
+ isWebWorker: isWebWorkerUrlInfo(urlInfo),
16524
+ inlineContent,
16525
+ isNodeJs,
16526
+ });
16527
+ for (const jsReferenceInfo of jsReferenceInfos) {
16528
+ if (jsReferenceInfo.isInline) {
16529
+ onInlineReference(jsReferenceInfo);
16530
+ } else {
16531
+ onExternalReference(jsReferenceInfo);
16388
16532
  }
16389
- return url
16390
16533
  }
16391
-
16392
- if (defaultExtension === true) {
16393
- const extension = pathnameToExtension(url);
16394
- if (extension === "" && importer) {
16395
- const importerPathname = urlToPathname(importer);
16396
- const importerExtension = pathnameToExtension(importerPathname);
16397
- return `${url}${importerExtension}`
16398
- }
16534
+ if (parallelActions.length > 0) {
16535
+ await Promise.all(parallelActions.map((action) => action()));
16536
+ }
16537
+ if (sequentialActions.length > 0) {
16538
+ await sequentialActions.reduce(async (previous, action) => {
16539
+ await previous;
16540
+ await action();
16541
+ }, Promise.resolve());
16399
16542
  }
16400
16543
 
16401
- return url
16544
+ const { content, sourcemap } = magicSource.toContentAndSourcemap();
16545
+ return { content, sourcemap };
16402
16546
  };
16403
16547
 
16404
- /*
16405
- * Plugin to read and apply importmap files found in html files.
16406
- * - feeds importmap files to jsenv kitchen
16407
- * - use importmap to resolve import (when there is one + fallback to other resolution mecanism)
16408
- * - inline importmap with [src=""]
16409
- *
16410
- * A correct importmap resolution should scope importmap resolution per html file.
16411
- * It would be doable by adding ?html_id to each js file in order to track
16412
- * the html file importing it.
16413
- * Considering it happens only when all the following conditions are met:
16414
- * - 2+ html files are using an importmap
16415
- * - the importmap used is not the same
16416
- * - the importmap contain conflicting mappings
16417
- * - these html files are both executed during the same scenario (dev, test, build)
16418
- * And that it would be ugly to see ?html_id all over the place
16419
- * -> The importmap resolution implemented here takes a shortcut and does the following:
16420
- * - All importmap found are merged into a single one that is applied to every import specifiers
16421
- */
16422
-
16423
-
16424
- const jsenvPluginImportmap = () => {
16425
- let finalImportmap = null;
16426
- const importmaps = {};
16427
- const onHtmlImportmapParsed = (importmap, htmlUrl) => {
16428
- importmaps[htmlUrl] = importmap
16429
- ? normalizeImportMap(importmap, htmlUrl)
16430
- : null;
16431
- finalImportmap = Object.keys(importmaps).reduce((previous, url) => {
16432
- const importmap = importmaps[url];
16433
- if (!previous) {
16434
- return importmap;
16435
- }
16436
- if (!importmap) {
16437
- return previous;
16438
- }
16439
- return composeTwoImportMaps(previous, importmap);
16440
- }, null);
16441
- };
16548
+ const jsenvPluginReferenceAnalysis = ({
16549
+ inlineContent = true,
16550
+ inlineConvertedScript = false,
16551
+ fetchInlineUrls = true,
16552
+ }) => {
16553
+ return [
16554
+ jsenvPluginDirectoryReferenceAnalysis(),
16555
+ jsenvPluginHtmlReferenceAnalysis({
16556
+ inlineContent,
16557
+ inlineConvertedScript,
16558
+ }),
16559
+ jsenvPluginWebmanifestReferenceAnalysis(),
16560
+ jsenvPluginCssReferenceAnalysis(),
16561
+ jsenvPluginJsReferenceAnalysis({
16562
+ inlineContent,
16563
+ }),
16564
+ ...(inlineContent ? [jsenvPluginDataUrlsAnalysis()] : []),
16565
+ ...(inlineContent && fetchInlineUrls
16566
+ ? [jsenvPluginInlineContentFetcher()]
16567
+ : []),
16568
+ jsenvPluginReferenceExpectedTypes(),
16569
+ ];
16570
+ };
16442
16571
 
16572
+ const jsenvPluginInlineContentFetcher = () => {
16443
16573
  return {
16444
- name: "jsenv:importmap",
16574
+ name: "jsenv:inline_content_fetcher",
16445
16575
  appliesDuring: "*",
16446
- resolveReference: {
16447
- js_import: (reference) => {
16448
- if (!finalImportmap) {
16449
- return null;
16450
- }
16451
- try {
16452
- let fromMapping = false;
16453
- const result = resolveImport({
16454
- specifier: reference.specifier,
16455
- importer: reference.ownerUrlInfo.url,
16456
- importMap: finalImportmap,
16457
- onImportMapping: () => {
16458
- fromMapping = true;
16459
- },
16460
- });
16461
- if (fromMapping) {
16462
- return result;
16463
- }
16464
- return null;
16465
- } catch (e) {
16466
- if (e.message.includes("bare specifier")) {
16467
- // in theory we should throw to be compliant with web behaviour
16468
- // but for now it's simpler to return null
16469
- // and let a chance to other plugins to handle the bare specifier
16470
- // (node esm resolution)
16471
- // and we want importmap to be prio over node esm so we cannot put this plugin after
16472
- return null;
16473
- }
16474
- throw e;
16475
- }
16476
- },
16477
- },
16478
- transformUrlContent: {
16479
- html: async (htmlUrlInfo) => {
16480
- const htmlAst = parseHtmlString(htmlUrlInfo.content);
16481
- const importmap = findHtmlNode(htmlAst, (node) => {
16482
- if (node.nodeName !== "script") {
16483
- return false;
16484
- }
16485
- const type = getHtmlNodeAttribute(node, "type");
16486
- if (type === undefined || type !== "importmap") {
16487
- return false;
16488
- }
16489
- return true;
16490
- });
16491
- if (!importmap) {
16492
- onHtmlImportmapParsed(null, htmlUrlInfo.url);
16493
- return null;
16494
- }
16495
- const handleInlineImportmap = async (importmap, htmlNodeText) => {
16496
- const { line, column, isOriginal } = getHtmlNodePosition(importmap, {
16497
- preferOriginal: true,
16498
- });
16499
- const inlineImportmapUrl = getUrlForContentInsideHtml(importmap, {
16500
- htmlUrl: htmlUrlInfo.url,
16501
- });
16502
- const inlineImportmapReference = htmlUrlInfo.dependencies.foundInline(
16503
- {
16504
- type: "script",
16505
- isOriginalPosition: isOriginal,
16506
- specifierLine: line - 1,
16507
- specifierColumn: column,
16508
- specifier: inlineImportmapUrl,
16509
- contentType: "application/importmap+json",
16510
- content: htmlNodeText,
16511
- },
16512
- );
16513
- const inlineImportmapUrlInfo = inlineImportmapReference.urlInfo;
16514
- await inlineImportmapUrlInfo.cook();
16515
- setHtmlNodeText(importmap, inlineImportmapUrlInfo.content, {
16516
- indentation: "auto",
16517
- });
16518
- setHtmlNodeAttributes(importmap, {
16519
- "jsenv-cooked-by": "jsenv:importmap",
16520
- });
16521
- onHtmlImportmapParsed(
16522
- JSON.parse(inlineImportmapUrlInfo.content),
16523
- htmlUrlInfo.url,
16524
- );
16525
- };
16526
- const handleImportmapWithSrc = async (importmap, src) => {
16527
- // Browser would throw on remote importmap
16528
- // and won't sent a request to the server for it
16529
- // We must precook the importmap to know its content and inline it into the HTML
16530
- // In this situation the ref to the importmap was already discovered
16531
- // when parsing the HTML
16532
- let importmapReference = null;
16533
- for (const referenceToOther of htmlUrlInfo.referenceToOthersSet) {
16534
- if (referenceToOther.generatedSpecifier === src) {
16535
- importmapReference = referenceToOther;
16536
- break;
16537
- }
16538
- }
16539
- const { line, column, isOriginal } = getHtmlNodePosition(importmap, {
16540
- preferOriginal: true,
16541
- });
16542
- const importmapInlineUrl = getUrlForContentInsideHtml(importmap, {
16543
- htmlUrl: htmlUrlInfo.url,
16544
- });
16545
- const importmapReferenceInlined = importmapReference.inline({
16546
- line: line - 1,
16547
- column,
16548
- isOriginal,
16549
- specifier: importmapInlineUrl,
16550
- contentType: "application/importmap+json",
16551
- });
16552
- const importmapInlineUrlInfo = importmapReferenceInlined.urlInfo;
16553
- await importmapInlineUrlInfo.cook();
16554
- onHtmlImportmapParsed(
16555
- JSON.parse(importmapInlineUrlInfo.content),
16556
- htmlUrlInfo.url,
16557
- );
16558
- setHtmlNodeText(importmap, importmapInlineUrlInfo.content, {
16559
- indentation: "auto",
16560
- });
16561
- setHtmlNodeAttributes(importmap, {
16562
- "src": undefined,
16563
- "jsenv-inlined-by": "jsenv:importmap",
16564
- "inlined-from-src": src,
16565
- });
16566
- };
16567
-
16568
- const src = getHtmlNodeAttribute(importmap, "src");
16569
- if (src) {
16570
- await handleImportmapWithSrc(importmap, src);
16571
- } else {
16572
- const htmlNodeText = getHtmlNodeText(importmap);
16573
- if (htmlNodeText) {
16574
- await handleInlineImportmap(importmap, htmlNodeText);
16575
- }
16576
+ fetchUrlContent: async (urlInfo) => {
16577
+ if (!urlInfo.isInline) {
16578
+ return null;
16579
+ }
16580
+ // - we must use last reference because
16581
+ // when updating the file, first reference is the previous version
16582
+ // - we cannot use urlInfo.lastReference because it can be the reference created by "http_request"
16583
+ let lastInlineReference;
16584
+ for (const reference of urlInfo.referenceFromOthersSet) {
16585
+ if (reference.isInline) {
16586
+ lastInlineReference = reference;
16576
16587
  }
16577
- // once this plugin knows the importmap, it will use it
16578
- // to map imports. These import specifiers will be normalized
16579
- // by "formatReferencedUrl" making the importmap presence useless.
16580
- // In dev/test we keep importmap into the HTML to see it even if useless
16581
- // Duing build we get rid of it
16582
- if (htmlUrlInfo.context.build) {
16583
- removeHtmlNode(importmap);
16588
+ }
16589
+ const { prev } = lastInlineReference;
16590
+ if (prev && !prev.isInline) {
16591
+ // got inlined, cook original url
16592
+ if (lastInlineReference.content === undefined) {
16593
+ const originalUrlInfo = prev.urlInfo;
16594
+ await originalUrlInfo.cook();
16595
+ lastInlineReference.content = originalUrlInfo.content;
16596
+ lastInlineReference.contentType = originalUrlInfo.contentType;
16584
16597
  }
16585
- return {
16586
- content: stringifyHtmlAst(htmlAst),
16587
- };
16588
- },
16598
+ }
16599
+ return {
16600
+ originalContent: urlInfo.originalContent,
16601
+ content: lastInlineReference.content,
16602
+ contentType: lastInlineReference.contentType,
16603
+ };
16589
16604
  },
16590
16605
  };
16591
16606
  };
@@ -18050,6 +18065,7 @@ const jsenvPluginProtocolFile = ({
18050
18065
  reference.leadsToADirectory = stat && stat.isDirectory();
18051
18066
  if (reference.leadsToADirectory) {
18052
18067
  const directoryAllowed =
18068
+ reference.type === "http_request" ||
18053
18069
  reference.type === "filesystem" ||
18054
18070
  (typeof directoryReferenceAllowed === "function" &&
18055
18071
  directoryReferenceAllowed(reference)) ||
@@ -18115,7 +18131,6 @@ const jsenvPluginProtocolFile = ({
18115
18131
  }
18116
18132
  const urlObject = new URL(urlInfo.url);
18117
18133
  if (urlInfo.firstReference.leadsToADirectory) {
18118
- const directoryEntries = readdirSync(urlObject);
18119
18134
  if (!urlInfo.filenameHint) {
18120
18135
  if (urlInfo.firstReference.type === "filesystem") {
18121
18136
  urlInfo.filenameHint = `${
@@ -18125,10 +18140,17 @@ const jsenvPluginProtocolFile = ({
18125
18140
  urlInfo.filenameHint = `${urlToFilename$1(urlInfo.url)}/`;
18126
18141
  }
18127
18142
  }
18143
+ const { headers, body } = serveDirectory(urlObject.href, {
18144
+ headers: urlInfo.context.request
18145
+ ? urlInfo.context.request.headers
18146
+ : {},
18147
+ rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
18148
+ });
18128
18149
  return {
18129
18150
  type: "directory",
18130
- contentType: "application/json",
18131
- content: JSON.stringify(directoryEntries, null, " "),
18151
+ contentType: headers["content-type"],
18152
+ contentLength: headers["content-length"],
18153
+ content: body,
18132
18154
  };
18133
18155
  }
18134
18156
  if (
@@ -19254,8 +19276,16 @@ const jsenvPluginHotSearchParam = () => {
19254
19276
  // At this stage the parent is using ?hot and we are going to decide if
19255
19277
  // we propagate the search param to child.
19256
19278
  const referencedUrlInfo = reference.urlInfo;
19257
- const { modifiedTimestamp, dereferencedTimestamp } = referencedUrlInfo;
19258
- if (!modifiedTimestamp && !dereferencedTimestamp) {
19279
+ const {
19280
+ modifiedTimestamp,
19281
+ descendantModifiedTimestamp,
19282
+ dereferencedTimestamp,
19283
+ } = referencedUrlInfo;
19284
+ if (
19285
+ !modifiedTimestamp &&
19286
+ !descendantModifiedTimestamp &&
19287
+ !dereferencedTimestamp
19288
+ ) {
19259
19289
  return null;
19260
19290
  }
19261
19291
  // The goal is to send an url that will bypass client (the browser) cache
@@ -19268,10 +19298,11 @@ const jsenvPluginHotSearchParam = () => {
19268
19298
  // We use the latest timestamp to ensure it's fresh
19269
19299
  // The dereferencedTimestamp is needed because when a js module is re-referenced
19270
19300
  // browser must re-execute it, even if the code is not modified
19271
- const latestTimestamp =
19272
- dereferencedTimestamp && modifiedTimestamp
19273
- ? Math.max(dereferencedTimestamp, modifiedTimestamp)
19274
- : dereferencedTimestamp || modifiedTimestamp;
19301
+ const latestTimestamp = Math.max(
19302
+ modifiedTimestamp,
19303
+ descendantModifiedTimestamp,
19304
+ dereferencedTimestamp,
19305
+ );
19275
19306
  return {
19276
19307
  hot: latestTimestamp,
19277
19308
  };
@@ -19333,89 +19364,157 @@ const jsenvPluginAutoreloadServer = ({
19333
19364
  }
19334
19365
  return url;
19335
19366
  };
19336
- const propagateUpdate = (firstUrlInfo) => {
19337
- const iterate = (urlInfo, seen) => {
19338
- if (urlInfo.data.hotAcceptSelf) {
19339
- return {
19340
- accepted: true,
19341
- reason:
19342
- urlInfo === firstUrlInfo
19343
- ? `file accepts hot reload`
19344
- : `a dependent file accepts hot reload`,
19345
- instructions: [
19346
- {
19347
- type: urlInfo.type,
19348
- boundary: formatUrlForClient(urlInfo.url),
19367
+ const update = (firstUrlInfo) => {
19368
+ const boundaries = new Set();
19369
+ const instructions = [];
19370
+ const propagateUpdate = (firstUrlInfo) => {
19371
+ const iterate = (urlInfo, chain) => {
19372
+ if (urlInfo.data.hotAcceptSelf) {
19373
+ boundaries.add(urlInfo);
19374
+ instructions.push({
19375
+ type: urlInfo.type,
19376
+ boundary: formatUrlForClient(urlInfo.url),
19377
+ acceptedBy: formatUrlForClient(urlInfo.url),
19378
+ });
19379
+ return {
19380
+ accepted: true,
19381
+ reason:
19382
+ urlInfo === firstUrlInfo
19383
+ ? `file accepts hot reload`
19384
+ : `a dependent file accepts hot reload`,
19385
+ };
19386
+ }
19387
+ if (urlInfo.data.hotDecline) {
19388
+ return {
19389
+ declined: true,
19390
+ reason: `file declines hot reload`,
19391
+ declinedBy: formatUrlForClient(urlInfo.url),
19392
+ };
19393
+ }
19394
+ let instructionCountBefore = instructions.length;
19395
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19396
+ if (
19397
+ referenceFromOther.isImplicit &&
19398
+ referenceFromOther.isWeak
19399
+ ) {
19400
+ if (!referenceFromOther.original) {
19401
+ continue;
19402
+ }
19403
+ if (referenceFromOther.original.isWeak) {
19404
+ continue;
19405
+ }
19406
+ }
19407
+ const urlInfoReferencingThisOne =
19408
+ referenceFromOther.ownerUrlInfo;
19409
+ if (urlInfoReferencingThisOne.data.hotDecline) {
19410
+ return {
19411
+ declined: true,
19412
+ reason: `a dependent file declines hot reload`,
19413
+ declinedBy: formatUrlForClient(
19414
+ urlInfoReferencingThisOne.url,
19415
+ ),
19416
+ };
19417
+ }
19418
+ const { hotAcceptDependencies = [] } =
19419
+ urlInfoReferencingThisOne.data;
19420
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
19421
+ boundaries.add(urlInfoReferencingThisOne);
19422
+ instructions.push({
19423
+ type: urlInfoReferencingThisOne.type,
19424
+ boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
19349
19425
  acceptedBy: formatUrlForClient(urlInfo.url),
19350
- },
19351
- ],
19352
- };
19353
- }
19354
- const instructions = [];
19355
- for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19356
- if (referenceFromOther.isImplicit && referenceFromOther.isWeak) {
19357
- if (!referenceFromOther.original) {
19426
+ });
19358
19427
  continue;
19359
19428
  }
19360
- if (referenceFromOther.original.isWeak) {
19429
+ if (chain.includes(urlInfoReferencingThisOne.url)) {
19430
+ return {
19431
+ declined: true,
19432
+ reason: "dead end",
19433
+ declinedBy: formatUrlForClient(
19434
+ urlInfoReferencingThisOne.url,
19435
+ ),
19436
+ };
19437
+ }
19438
+ const dependentPropagationResult = iterateMemoized(
19439
+ urlInfoReferencingThisOne,
19440
+ [...chain, urlInfoReferencingThisOne.url],
19441
+ );
19442
+ if (dependentPropagationResult.accepted) {
19361
19443
  continue;
19362
19444
  }
19445
+ if (
19446
+ // declined explicitely by an other file, it must decline the whole update
19447
+ dependentPropagationResult.declinedBy
19448
+ ) {
19449
+ return dependentPropagationResult;
19450
+ }
19451
+ // declined by absence of boundary, we can keep searching
19363
19452
  }
19364
- const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
19365
- if (urlInfoReferencingThisOne.data.hotDecline) {
19453
+ if (instructionCountBefore === instructions.length) {
19366
19454
  return {
19367
19455
  declined: true,
19368
- reason: `a dependent file declines hot reload`,
19369
- declinedBy: urlInfoReferencingThisOne.url,
19456
+ reason: `there is no file accepting hot reload while propagating update`,
19370
19457
  };
19371
19458
  }
19372
- const { hotAcceptDependencies = [] } =
19373
- urlInfoReferencingThisOne.data;
19374
- if (hotAcceptDependencies.includes(urlInfo.url)) {
19375
- instructions.push({
19376
- type: urlInfoReferencingThisOne.type,
19377
- boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
19378
- acceptedBy: formatUrlForClient(urlInfo.url),
19379
- });
19380
- continue;
19459
+ return {
19460
+ accepted: true,
19461
+ reason: `${instructions.length} dependent file(s) accepts hot reload`,
19462
+ };
19463
+ };
19464
+
19465
+ const map = new Map();
19466
+ const iterateMemoized = (urlInfo, chain) => {
19467
+ const resultFromCache = map.get(urlInfo.url);
19468
+ if (resultFromCache) {
19469
+ return resultFromCache;
19381
19470
  }
19382
- if (seen.includes(urlInfoReferencingThisOne.url)) {
19383
- return {
19471
+ const result = iterate(urlInfo, chain);
19472
+ map.set(urlInfo.url, result);
19473
+ return result;
19474
+ };
19475
+ map.clear();
19476
+ return iterateMemoized(firstUrlInfo, []);
19477
+ };
19478
+
19479
+ let propagationResult = propagateUpdate(firstUrlInfo);
19480
+ const seen = new Set();
19481
+ const invalidateImporters = (urlInfo) => {
19482
+ // to indicate this urlInfo should be modified
19483
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19484
+ const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
19485
+ const { hotDecline, hotAcceptDependencies = [] } =
19486
+ urlInfoReferencingThisOne.data;
19487
+ if (hotDecline) {
19488
+ propagationResult = {
19384
19489
  declined: true,
19385
- reason: "circular dependency",
19490
+ reason: `file declines hot reload`,
19386
19491
  declinedBy: formatUrlForClient(urlInfoReferencingThisOne.url),
19387
19492
  };
19493
+ return;
19388
19494
  }
19389
- const dependentPropagationResult = iterate(
19390
- urlInfoReferencingThisOne,
19391
- [...seen, urlInfoReferencingThisOne.url],
19392
- );
19393
- if (dependentPropagationResult.accepted) {
19394
- instructions.push(...dependentPropagationResult.instructions);
19495
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
19395
19496
  continue;
19396
19497
  }
19397
- if (
19398
- // declined explicitely by an other file, it must decline the whole update
19399
- dependentPropagationResult.declinedBy
19400
- ) {
19401
- return dependentPropagationResult;
19498
+ if (seen.has(urlInfoReferencingThisOne)) {
19499
+ continue;
19402
19500
  }
19403
- // declined by absence of boundary, we can keep searching
19404
- }
19405
- if (instructions.length === 0) {
19406
- return {
19407
- declined: true,
19408
- reason: `there is no file accepting hot reload while propagating update`,
19409
- };
19501
+ seen.add(urlInfoReferencingThisOne);
19502
+ // see https://github.com/vitejs/vite/blob/ab5bb40942c7023046fa6f6d0b49cabc105b6073/packages/vite/src/node/server/moduleGraph.ts#L205C5-L207C6
19503
+ if (boundaries.has(urlInfoReferencingThisOne)) {
19504
+ return;
19505
+ }
19506
+ urlInfoReferencingThisOne.descendantModifiedTimestamp =
19507
+ Date.now();
19508
+ invalidateImporters(urlInfoReferencingThisOne);
19410
19509
  }
19411
- return {
19412
- accepted: true,
19413
- reason: `${instructions.length} dependent file(s) accepts hot reload`,
19414
- instructions,
19415
- };
19416
19510
  };
19417
- const seen = [];
19418
- return iterate(firstUrlInfo, seen);
19511
+ invalidateImporters(firstUrlInfo);
19512
+ boundaries.clear();
19513
+ seen.clear();
19514
+ return {
19515
+ ...propagationResult,
19516
+ instructions,
19517
+ };
19419
19518
  };
19420
19519
 
19421
19520
  // We are delaying the moment we tell client how to reload because:
@@ -19449,7 +19548,7 @@ const jsenvPluginAutoreloadServer = ({
19449
19548
  if (!changedUrlInfo.isUsed()) {
19450
19549
  continue;
19451
19550
  }
19452
- const hotUpdate = propagateUpdate(changedUrlInfo);
19551
+ const hotUpdate = update(changedUrlInfo);
19453
19552
  const relativeUrl = formatUrlForClient(changedUrlInfo.url);
19454
19553
  if (hotUpdate.declined) {
19455
19554
  reloadMessage = {
@@ -19486,7 +19585,7 @@ const jsenvPluginAutoreloadServer = ({
19486
19585
  if (!ownerUrlInfo.isUsed()) {
19487
19586
  continue;
19488
19587
  }
19489
- const ownerHotUpdate = propagateUpdate(ownerUrlInfo);
19588
+ const ownerHotUpdate = update(ownerUrlInfo);
19490
19589
  const cause = `${formatUrlForClient(
19491
19590
  prunedUrlInfo.url,
19492
19591
  )} is no longer referenced`;
@@ -19671,7 +19770,7 @@ const jsenvPluginRibbon = ({
19671
19770
  null,
19672
19771
  " ",
19673
19772
  );
19674
- injectHtmlNode(
19773
+ injectHtmlNodeAsEarlyAsPossible(
19675
19774
  htmlAst,
19676
19775
  createHtmlNode({
19677
19776
  tagName: "script",
@@ -19736,7 +19835,6 @@ const getCorePlugins = ({
19736
19835
  jsenvPluginReferenceAnalysis(referenceAnalysis),
19737
19836
  ...(injections ? [jsenvPluginInjections(injections)] : []),
19738
19837
  jsenvPluginTranspilation(transpilation),
19739
- jsenvPluginImportmap(),
19740
19838
  ...(inlining ? [jsenvPluginInlining()] : []),
19741
19839
  ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
19742
19840
 
@@ -22186,7 +22284,7 @@ const createFileService = ({
22186
22284
  associations: watchAssociations,
22187
22285
  });
22188
22286
  urlInfoCreated.isWatched = watch;
22189
- // wehn an url depends on many others, we check all these (like package.json)
22287
+ // when an url depends on many others, we check all these (like package.json)
22190
22288
  urlInfoCreated.isValid = () => {
22191
22289
  if (!urlInfoCreated.url.startsWith("file:")) {
22192
22290
  return false;