@jsenv/core 27.0.0-alpha.51 → 27.0.0-alpha.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/build/build.js +39 -27
- package/src/build/build_urls_generator.js +1 -1
- package/src/execute/runtimes/browsers/from_playwright.js +3 -0
- package/src/omega/kitchen.js +17 -24
- package/src/omega/url_graph/url_graph_load.js +7 -0
- package/src/plugins/autoreload/jsenv_plugin_hmr.js +1 -1
- package/src/plugins/bundling/css/bundle_css.js +127 -8
- package/src/plugins/bundling/js_module/bundle_js_module.js +1 -12
- package/src/plugins/filesystem_magic/jsenv_plugin_filesystem_magic.js +1 -1
- package/src/plugins/inline/jsenv_plugin_js_inline_content.js +3 -2
- package/src/plugins/plugin_controller.js +2 -2
- package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +40 -24
- package/src/plugins/transpilation/as_js_classic/jsenv_plugin_script_type_module_as_classic.js +24 -10
- package/src/plugins/transpilation/as_js_classic/jsenv_plugin_workers_type_module_as_classic.js +1 -1
- package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +3 -3
- package/src/plugins/url_analysis/css/css_urls.js +14 -7
- package/src/plugins/url_analysis/html/html_urls.js +24 -28
- package/src/plugins/url_version/jsenv_plugin_url_version.js +1 -1
- package/src/omega/url_graph/url_graph_sort.js +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "27.0.0-alpha.
|
|
3
|
+
"version": "27.0.0-alpha.54",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@jsenv/node-esm-resolution": "0.0.6",
|
|
69
69
|
"@jsenv/server": "12.6.2",
|
|
70
70
|
"@jsenv/uneval": "1.6.0",
|
|
71
|
-
"@jsenv/utils": "1.7.
|
|
71
|
+
"@jsenv/utils": "1.7.3",
|
|
72
72
|
"construct-style-sheets-polyfill": "3.1.0",
|
|
73
73
|
"cssnano": "5.1.7",
|
|
74
74
|
"cssnano-preset-default": "5.2.7",
|
package/src/build/build.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
injectQueryParams,
|
|
23
23
|
setUrlFilename,
|
|
24
24
|
asUrlUntilPathname,
|
|
25
|
+
normalizeUrl,
|
|
25
26
|
} from "@jsenv/utils/urls/url_utils.js"
|
|
26
27
|
import { createVersionGenerator } from "@jsenv/utils/versioning/version_generator.js"
|
|
27
28
|
import { generateSourcemapUrl } from "@jsenv/utils/sourcemap/sourcemap_utils.js"
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
parseHtmlString,
|
|
30
31
|
stringifyHtmlAst,
|
|
31
32
|
} from "@jsenv/utils/html_ast/html_ast.js"
|
|
33
|
+
import { sortByDependencies } from "@jsenv/utils/graph/sort_by_dependencies.js"
|
|
32
34
|
|
|
33
35
|
import { jsenvPluginUrlAnalysis } from "../plugins/url_analysis/jsenv_plugin_url_analysis.js"
|
|
34
36
|
import { jsenvPluginInline } from "../plugins/inline/jsenv_plugin_inline.js"
|
|
@@ -38,7 +40,6 @@ import { getCorePlugins } from "../plugins/plugins.js"
|
|
|
38
40
|
import { createKitchen } from "../omega/kitchen.js"
|
|
39
41
|
import { loadUrlGraph } from "../omega/url_graph/url_graph_load.js"
|
|
40
42
|
import { createUrlGraphSummary } from "../omega/url_graph/url_graph_report.js"
|
|
41
|
-
import { sortUrlGraphByDependencies } from "../omega/url_graph/url_graph_sort.js"
|
|
42
43
|
import { isWebWorkerEntryPointReference } from "../omega/web_workers.js"
|
|
43
44
|
|
|
44
45
|
import { GRAPH } from "./graph_utils.js"
|
|
@@ -223,9 +224,16 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
|
|
|
223
224
|
})
|
|
224
225
|
})
|
|
225
226
|
const addToBundlerIfAny = (rawUrlInfo) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
if (
|
|
228
|
+
// entry point must be given to the bundler (rollup)
|
|
229
|
+
// so that the bundler know it's an entry point, even if it has no dependency
|
|
230
|
+
// this way the bundler can share code properly and avoid inlining an entry point
|
|
231
|
+
// if it's used by an other entry point
|
|
232
|
+
!rawUrlInfo.data.isEntryPoint &&
|
|
233
|
+
rawUrlInfo.dependencies.size === 0
|
|
234
|
+
) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
229
237
|
const bundler = bundlers[rawUrlInfo.type]
|
|
230
238
|
if (bundler) {
|
|
231
239
|
bundler.urlInfos.push(rawUrlInfo)
|
|
@@ -253,6 +261,21 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
|
|
|
253
261
|
}
|
|
254
262
|
addToBundlerIfAny(dependencyUrlInfo)
|
|
255
263
|
})
|
|
264
|
+
rawUrlInfo.references.forEach((reference) => {
|
|
265
|
+
if (
|
|
266
|
+
reference.isRessourceHint &&
|
|
267
|
+
reference.expectedType === "js_module"
|
|
268
|
+
) {
|
|
269
|
+
const referencedUrlInfo = rawGraph.getUrlInfo(reference.url)
|
|
270
|
+
if (
|
|
271
|
+
referencedUrlInfo &&
|
|
272
|
+
// something else than the ressource hint is using this url
|
|
273
|
+
referencedUrlInfo.dependents.size > 0
|
|
274
|
+
) {
|
|
275
|
+
addToBundlerIfAny(referencedUrlInfo)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
256
279
|
return
|
|
257
280
|
}
|
|
258
281
|
}
|
|
@@ -361,25 +384,10 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
|
|
|
361
384
|
if (urlRedirectedByBundle) {
|
|
362
385
|
return urlRedirectedByBundle
|
|
363
386
|
}
|
|
364
|
-
const parentIsFromBundle = Boolean(
|
|
365
|
-
bundleUrlInfos[reference.parentUrl],
|
|
366
|
-
)
|
|
367
|
-
// urls inside css bundled by parcel
|
|
368
|
-
// contains url relative to the bundle file (which is considered inside build directory)
|
|
369
|
-
// if the file is not itself a bundle file it must be resolved against
|
|
370
|
-
// the original css url
|
|
371
|
-
if (
|
|
372
|
-
parentIsFromBundle &&
|
|
373
|
-
!bundleUrlInfos[url] &&
|
|
374
|
-
urlIsInsideOf(url, buildDirectoryUrl)
|
|
375
|
-
) {
|
|
376
|
-
const parentRawUrl = rawUrls[reference.parentUrl]
|
|
377
|
-
url = new URL(reference.specifier, parentRawUrl).href
|
|
378
|
-
}
|
|
379
387
|
const urlRedirected = rawUrlRedirections[url]
|
|
380
388
|
return urlRedirected || url
|
|
381
389
|
},
|
|
382
|
-
|
|
390
|
+
redirectUrl: (reference) => {
|
|
383
391
|
if (!reference.url.startsWith("file:")) {
|
|
384
392
|
return null
|
|
385
393
|
}
|
|
@@ -618,6 +626,7 @@ ${Object.keys(rawGraph.urlInfos).join("\n")}`,
|
|
|
618
626
|
urlGraph: finalGraph,
|
|
619
627
|
kitchen: finalGraphKitchen,
|
|
620
628
|
outDirectoryUrl: new URL(".jsenv/postbuild/", rootDirectoryUrl),
|
|
629
|
+
skipRessourceHint: true,
|
|
621
630
|
startLoading: (cookEntryFile) => {
|
|
622
631
|
entryUrls.forEach((entryUrl) => {
|
|
623
632
|
const [, postBuildEntryUrlInfo] = cookEntryFile({
|
|
@@ -698,7 +707,7 @@ ${Object.keys(finalGraph.urlInfos).join("\n")}`,
|
|
|
698
707
|
urlInfo.dependents.size === 0
|
|
699
708
|
) {
|
|
700
709
|
cleanupActions.push(() => {
|
|
701
|
-
|
|
710
|
+
finalGraph.deleteUrlInfo(urlInfo.url)
|
|
702
711
|
})
|
|
703
712
|
}
|
|
704
713
|
})
|
|
@@ -783,7 +792,7 @@ const applyUrlVersioning = async ({
|
|
|
783
792
|
}) => {
|
|
784
793
|
const versioningTask = createTaskLog(logger, "inject version in urls")
|
|
785
794
|
try {
|
|
786
|
-
const urlsSorted =
|
|
795
|
+
const urlsSorted = sortByDependencies(finalGraph.urlInfos)
|
|
787
796
|
urlsSorted.forEach((url) => {
|
|
788
797
|
if (url.startsWith("data:")) {
|
|
789
798
|
return
|
|
@@ -859,11 +868,13 @@ const applyUrlVersioning = async ({
|
|
|
859
868
|
})
|
|
860
869
|
urlInfo.data.version = versionGenerator.generate()
|
|
861
870
|
|
|
862
|
-
urlInfo.data.versionedUrl =
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
871
|
+
urlInfo.data.versionedUrl = normalizeUrl(
|
|
872
|
+
injectVersionIntoBuildUrl({
|
|
873
|
+
buildUrl: urlInfo.url,
|
|
874
|
+
version: urlInfo.data.version,
|
|
875
|
+
versioningMethod,
|
|
876
|
+
}),
|
|
877
|
+
)
|
|
867
878
|
})
|
|
868
879
|
const versionMappings = {}
|
|
869
880
|
const usedVersionMappings = []
|
|
@@ -969,6 +980,7 @@ const applyUrlVersioning = async ({
|
|
|
969
980
|
await loadUrlGraph({
|
|
970
981
|
urlGraph: finalGraph,
|
|
971
982
|
kitchen: versioningKitchen,
|
|
983
|
+
skipRessourceHint: true,
|
|
972
984
|
startLoading: (cookEntryFile) => {
|
|
973
985
|
postBuildEntryUrls.forEach((postBuildEntryUrl) => {
|
|
974
986
|
cookEntryFile({
|
|
@@ -56,8 +56,8 @@ export const createBuilUrlsGenerator = ({ buildDirectoryUrl }) => {
|
|
|
56
56
|
// To keep in mind: if you have "user.jsx" and "user.js" AND both file are not bundled
|
|
57
57
|
// you end up with "dist/js/user.js" and "dist/js/user2.js"
|
|
58
58
|
const extensionMappings = {
|
|
59
|
-
".ts": ".js",
|
|
60
59
|
".jsx": ".js",
|
|
60
|
+
".ts": ".js",
|
|
61
61
|
".tsx": ".js",
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -280,6 +280,9 @@ export const createRuntimeFromPlaywright = ({
|
|
|
280
280
|
/* eslint-disable no-undef */
|
|
281
281
|
/* istanbul ignore next */
|
|
282
282
|
() => {
|
|
283
|
+
if (!window.__html_supervisor__) {
|
|
284
|
+
throw new Error(`window.__html_supervisor__ not found`)
|
|
285
|
+
}
|
|
283
286
|
return window.__html_supervisor__.getScriptExecutionResults()
|
|
284
287
|
},
|
|
285
288
|
/* eslint-enable no-undef */
|
package/src/omega/kitchen.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createDetailedMessage } from "@jsenv/logger"
|
|
|
10
10
|
|
|
11
11
|
import { stringifyUrlSite } from "@jsenv/utils/urls/url_trace.js"
|
|
12
12
|
import { CONTENT_TYPE } from "@jsenv/utils/content_type/content_type.js"
|
|
13
|
-
import { setUrlFilename } from "@jsenv/utils/urls/url_utils.js"
|
|
13
|
+
import { normalizeUrl, setUrlFilename } from "@jsenv/utils/urls/url_utils.js"
|
|
14
14
|
|
|
15
15
|
import { createPluginController } from "../plugins/plugin_controller.js"
|
|
16
16
|
import { createUrlInfoTransformer } from "./url_graph/url_info_transformations.js"
|
|
@@ -142,7 +142,7 @@ export const createKitchen = ({
|
|
|
142
142
|
}
|
|
143
143
|
const resolveReference = (reference) => {
|
|
144
144
|
try {
|
|
145
|
-
|
|
145
|
+
let resolvedUrl = pluginController.callHooksUntil(
|
|
146
146
|
"resolveUrl",
|
|
147
147
|
reference,
|
|
148
148
|
baseContext,
|
|
@@ -150,6 +150,7 @@ export const createKitchen = ({
|
|
|
150
150
|
if (!resolvedUrl) {
|
|
151
151
|
throw new Error(`NO_RESOLVE`)
|
|
152
152
|
}
|
|
153
|
+
resolvedUrl = normalizeUrl(resolvedUrl)
|
|
153
154
|
reference.url = resolvedUrl
|
|
154
155
|
if (reference.external) {
|
|
155
156
|
reference.generatedUrl = resolvedUrl
|
|
@@ -157,29 +158,20 @@ export const createKitchen = ({
|
|
|
157
158
|
return urlGraph.reuseOrCreateUrlInfo(reference.url)
|
|
158
159
|
}
|
|
159
160
|
pluginController.callHooks(
|
|
160
|
-
"
|
|
161
|
+
"redirectUrl",
|
|
161
162
|
reference,
|
|
162
163
|
baseContext,
|
|
163
164
|
(returnValue) => {
|
|
164
|
-
|
|
165
|
+
const normalizedReturnValue = normalizeUrl(returnValue)
|
|
166
|
+
if (normalizedReturnValue === reference.url) {
|
|
165
167
|
return
|
|
166
168
|
}
|
|
167
169
|
const previousReference = { ...reference }
|
|
168
|
-
reference.url =
|
|
170
|
+
reference.url = normalizedReturnValue
|
|
169
171
|
mutateReference(previousReference, reference)
|
|
170
172
|
},
|
|
171
173
|
)
|
|
172
|
-
|
|
173
|
-
// some plugin use URLSearchParams to alter the url search params
|
|
174
|
-
// which can result into "file:///file.css?css_module"
|
|
175
|
-
// becoming "file:///file.css?css_module="
|
|
176
|
-
// we want to get rid of the "=" and consider it's the same url
|
|
177
|
-
if (
|
|
178
|
-
// disable on data urls (would mess up base64 encoding)
|
|
179
|
-
!reference.url.startsWith("data:")
|
|
180
|
-
) {
|
|
181
|
-
reference.url = reference.url.replace(/[=](?=&|$)/g, "")
|
|
182
|
-
}
|
|
174
|
+
|
|
183
175
|
const urlInfo = urlGraph.reuseOrCreateUrlInfo(reference.url)
|
|
184
176
|
applyReferenceEffectsOnUrlInfo(reference, urlInfo, baseContext)
|
|
185
177
|
|
|
@@ -275,12 +267,13 @@ export const createKitchen = ({
|
|
|
275
267
|
return
|
|
276
268
|
}
|
|
277
269
|
try {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
270
|
+
const fetchUrlContentReturnValue =
|
|
271
|
+
await pluginController.callAsyncHooksUntil(
|
|
272
|
+
"fetchUrlContent",
|
|
273
|
+
urlInfo,
|
|
274
|
+
context,
|
|
275
|
+
)
|
|
276
|
+
if (!fetchUrlContentReturnValue) {
|
|
284
277
|
logger.warn(
|
|
285
278
|
createDetailedMessage(
|
|
286
279
|
`no plugin has handled the url during "fetchUrlContent" hook -> consider url as external (ignore it)`,
|
|
@@ -293,7 +286,7 @@ export const createKitchen = ({
|
|
|
293
286
|
urlInfo.external = true
|
|
294
287
|
return
|
|
295
288
|
}
|
|
296
|
-
if (
|
|
289
|
+
if (fetchUrlContentReturnValue.external) {
|
|
297
290
|
urlInfo.external = true
|
|
298
291
|
return
|
|
299
292
|
}
|
|
@@ -306,7 +299,7 @@ export const createKitchen = ({
|
|
|
306
299
|
content,
|
|
307
300
|
sourcemap,
|
|
308
301
|
filename,
|
|
309
|
-
} =
|
|
302
|
+
} = fetchUrlContentReturnValue
|
|
310
303
|
urlInfo.type =
|
|
311
304
|
type ||
|
|
312
305
|
reference.expectedType ||
|
|
@@ -33,6 +33,13 @@ export const loadUrlGraph = async ({
|
|
|
33
33
|
})
|
|
34
34
|
const { references } = urlInfo
|
|
35
35
|
references.forEach((reference) => {
|
|
36
|
+
// we don't cook ressource hints
|
|
37
|
+
// because they might refer to ressource that will be modified during build
|
|
38
|
+
// It also means something else have to reference that url in order to cook it
|
|
39
|
+
// so that the preload is deleted by "resync_ressource_hints.js" otherwise
|
|
40
|
+
if (reference.isRessourceHint) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
36
43
|
// we use reference.generatedUrl to mimic what a browser would do:
|
|
37
44
|
// do a fetch to the specifier as found in the file
|
|
38
45
|
const referencedUrlInfo = urlGraph.reuseOrCreateUrlInfo(
|
|
@@ -2,7 +2,7 @@ export const jsenvPluginHmr = () => {
|
|
|
2
2
|
return {
|
|
3
3
|
name: "jsenv:hmr",
|
|
4
4
|
appliesDuring: { dev: true },
|
|
5
|
-
|
|
5
|
+
redirectUrl: (reference) => {
|
|
6
6
|
const urlObject = new URL(reference.url)
|
|
7
7
|
if (!urlObject.searchParams.has("hmr")) {
|
|
8
8
|
reference.data.hmr = false
|
|
@@ -1,21 +1,140 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Each @import found in css is replaced by the file content
|
|
3
|
+
* - There is no need to worry about urls (such as background-image: url())
|
|
4
|
+
* because they are absolute (file://*) and will be made relative again by jsenv build
|
|
5
|
+
* - The sourcemap are not generated but ideally they should be
|
|
6
|
+
* It can be quite challenging, see "bundle_sourcemap.js"
|
|
7
|
+
*/
|
|
2
8
|
|
|
9
|
+
import { applyPostCss } from "@jsenv/utils/css_ast/apply_post_css.js"
|
|
10
|
+
import { postCssPluginUrlVisitor } from "@jsenv/utils/css_ast/postcss_plugin_url_visitor.js"
|
|
11
|
+
import { createMagicSource } from "@jsenv/utils/sourcemap/magic_source.js"
|
|
12
|
+
import { sortByDependencies } from "@jsenv/utils/graph/sort_by_dependencies.js"
|
|
13
|
+
|
|
14
|
+
// Do not use until https://github.com/parcel-bundler/parcel-css/issues/181
|
|
3
15
|
export const bundleCss = async ({ cssUrlInfos, context }) => {
|
|
4
16
|
const bundledCssUrlInfos = {}
|
|
17
|
+
const cssBundleInfos = await performCssBundling({
|
|
18
|
+
cssEntryUrlInfos: cssUrlInfos,
|
|
19
|
+
context,
|
|
20
|
+
})
|
|
5
21
|
cssUrlInfos.forEach((cssUrlInfo) => {
|
|
6
|
-
const { code, map } = bundleWithParcel(cssUrlInfo, context)
|
|
7
|
-
const content = String(code)
|
|
8
|
-
const sourcemap = map
|
|
9
|
-
// here we need to replace css urls to ensure
|
|
10
|
-
// all urls targets the correct stuff
|
|
11
22
|
bundledCssUrlInfos[cssUrlInfo.url] = {
|
|
12
23
|
data: {
|
|
13
24
|
generatedBy: "parcel",
|
|
14
25
|
},
|
|
15
26
|
contentType: "text/css",
|
|
16
|
-
content,
|
|
17
|
-
sourcemap,
|
|
27
|
+
content: cssBundleInfos[cssUrlInfo.url].bundleContent,
|
|
18
28
|
}
|
|
19
29
|
})
|
|
20
30
|
return bundledCssUrlInfos
|
|
21
31
|
}
|
|
32
|
+
|
|
33
|
+
const performCssBundling = async ({ cssEntryUrlInfos, context }) => {
|
|
34
|
+
const cssBundleInfos = await loadCssUrls({
|
|
35
|
+
cssEntryUrlInfos,
|
|
36
|
+
context,
|
|
37
|
+
})
|
|
38
|
+
const cssUrlsSorted = sortByDependencies(cssBundleInfos)
|
|
39
|
+
cssUrlsSorted.forEach((cssUrl) => {
|
|
40
|
+
const cssBundleInfo = cssBundleInfos[cssUrl]
|
|
41
|
+
const magicSource = createMagicSource(cssBundleInfo.content)
|
|
42
|
+
cssBundleInfo.cssUrls.forEach((cssUrl) => {
|
|
43
|
+
if (cssUrl.type === "@import") {
|
|
44
|
+
magicSource.replace({
|
|
45
|
+
start: cssUrl.atRuleStart,
|
|
46
|
+
end: cssUrl.atRuleEnd,
|
|
47
|
+
replacement: cssBundleInfos[cssUrl.url].bundleContent,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
const { content } = magicSource.toContentAndSourcemap()
|
|
52
|
+
cssBundleInfo.bundleContent = content.trim()
|
|
53
|
+
})
|
|
54
|
+
return cssBundleInfos
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parseCssUrls = async ({ css, url }) => {
|
|
58
|
+
const cssUrls = []
|
|
59
|
+
await applyPostCss({
|
|
60
|
+
sourcemaps: false,
|
|
61
|
+
plugins: [
|
|
62
|
+
postCssPluginUrlVisitor({
|
|
63
|
+
urlVisitor: ({
|
|
64
|
+
type,
|
|
65
|
+
specifier,
|
|
66
|
+
specifierStart,
|
|
67
|
+
specifierEnd,
|
|
68
|
+
atRuleStart,
|
|
69
|
+
atRuleEnd,
|
|
70
|
+
}) => {
|
|
71
|
+
cssUrls.push({
|
|
72
|
+
type,
|
|
73
|
+
url: new URL(specifier, url).href,
|
|
74
|
+
specifierStart,
|
|
75
|
+
specifierEnd,
|
|
76
|
+
atRuleStart,
|
|
77
|
+
atRuleEnd,
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
url,
|
|
83
|
+
content: css,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return cssUrls
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const loadCssUrls = async ({ cssEntryUrlInfos, context }) => {
|
|
90
|
+
const cssBundleInfos = {}
|
|
91
|
+
const promises = []
|
|
92
|
+
const promiseMap = new Map()
|
|
93
|
+
|
|
94
|
+
const load = (cssUrlInfo) => {
|
|
95
|
+
const promiseFromData = promiseMap.get(cssUrlInfo.url)
|
|
96
|
+
if (promiseFromData) return promiseFromData
|
|
97
|
+
const promise = _load(cssUrlInfo)
|
|
98
|
+
promises.push(promise)
|
|
99
|
+
promiseMap.set(cssUrlInfo.url, promise)
|
|
100
|
+
return promise
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const _load = async (cssUrlInfo) => {
|
|
104
|
+
const cssUrls = await parseCssUrls({
|
|
105
|
+
css: cssUrlInfo.content,
|
|
106
|
+
url: cssUrlInfo.url,
|
|
107
|
+
})
|
|
108
|
+
const cssBundleInfo = {
|
|
109
|
+
content: cssUrlInfo.content,
|
|
110
|
+
cssUrls,
|
|
111
|
+
dependencies: [],
|
|
112
|
+
}
|
|
113
|
+
cssBundleInfos[cssUrlInfo.url] = cssBundleInfo
|
|
114
|
+
cssUrls.forEach((cssUrl) => {
|
|
115
|
+
if (cssUrl.type === "@import") {
|
|
116
|
+
cssBundleInfo.dependencies.push(cssUrl.url)
|
|
117
|
+
const importedCssUrlInfo = context.urlGraph.getUrlInfo(cssUrl.url)
|
|
118
|
+
load(importedCssUrlInfo)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cssEntryUrlInfos.forEach((cssEntryUrlInfo) => {
|
|
124
|
+
load(cssEntryUrlInfo)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const waitAll = async () => {
|
|
128
|
+
if (promises.length === 0) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const promisesToWait = promises.slice()
|
|
132
|
+
promises.length = 0
|
|
133
|
+
await Promise.all(promisesToWait)
|
|
134
|
+
await waitAll()
|
|
135
|
+
}
|
|
136
|
+
await waitAll()
|
|
137
|
+
promiseMap.clear()
|
|
138
|
+
|
|
139
|
+
return cssBundleInfos
|
|
140
|
+
}
|
|
@@ -247,20 +247,9 @@ const rollupPluginJsenv = ({
|
|
|
247
247
|
code: urlInfo.content,
|
|
248
248
|
map: urlInfo.sourcemap
|
|
249
249
|
? sourcemapConverter.toFilePaths(urlInfo.sourcemap)
|
|
250
|
-
:
|
|
250
|
+
: null,
|
|
251
251
|
}
|
|
252
252
|
},
|
|
253
|
-
// resolveFileUrl: ({ moduleId }) => {
|
|
254
|
-
// return `${fileUrlConverter.asFileUrl(moduleId)}`
|
|
255
|
-
// },
|
|
256
|
-
renderChunk: (code, chunkInfo) => {
|
|
257
|
-
const { facadeModuleId } = chunkInfo
|
|
258
|
-
if (!facadeModuleId) {
|
|
259
|
-
// happens for inline module scripts for instance
|
|
260
|
-
return null
|
|
261
|
-
}
|
|
262
|
-
return null
|
|
263
|
-
},
|
|
264
253
|
}
|
|
265
254
|
}
|
|
266
255
|
|
|
@@ -13,7 +13,7 @@ export const jsenvPluginFileSystemMagic = ({
|
|
|
13
13
|
return {
|
|
14
14
|
name: "jsenv:filesystem_magic",
|
|
15
15
|
appliesDuring: "*",
|
|
16
|
-
|
|
16
|
+
redirectUrl: (reference) => {
|
|
17
17
|
// http, https, data, about, etc
|
|
18
18
|
if (!reference.url.startsWith("file:")) {
|
|
19
19
|
return null
|
|
@@ -274,8 +274,9 @@ const getOriginalName = (path, name) => {
|
|
|
274
274
|
return getOriginalName(path, importedName)
|
|
275
275
|
}
|
|
276
276
|
if (binding.path.type === "VariableDeclarator") {
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
const { init } = binding.path.node
|
|
278
|
+
if (init && init.type === "Identifier") {
|
|
279
|
+
const previousName = init.name
|
|
279
280
|
return getOriginalName(path, previousName)
|
|
280
281
|
}
|
|
281
282
|
}
|
|
@@ -5,7 +5,7 @@ export const createPluginController = ({
|
|
|
5
5
|
scenario,
|
|
6
6
|
hooks = [
|
|
7
7
|
"resolveUrl",
|
|
8
|
-
"
|
|
8
|
+
"redirectUrl",
|
|
9
9
|
"fetchUrlContent",
|
|
10
10
|
"transformUrlContent",
|
|
11
11
|
"transformUrlSearchParams",
|
|
@@ -226,7 +226,7 @@ const assertAndNormalizeReturnValue = (hookName, returnValue) => {
|
|
|
226
226
|
const returnValueAssertions = [
|
|
227
227
|
{
|
|
228
228
|
name: "url_assertion",
|
|
229
|
-
appliesTo: ["resolveUrl", "
|
|
229
|
+
appliesTo: ["resolveUrl", "redirectUrl"],
|
|
230
230
|
assertion: (valueReturned) => {
|
|
231
231
|
if (valueReturned instanceof URL) {
|
|
232
232
|
return valueReturned.href
|
|
@@ -44,34 +44,42 @@ export const jsenvPluginAsJsClassic = ({ systemJsInjection }) => {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const asJsClassic = ({ systemJsInjection, systemJsClientFileUrl }) => {
|
|
47
|
+
const propagateJsClassicSearchParam = (reference, context) => {
|
|
48
|
+
const parentUrlInfo = context.urlGraph.getUrlInfo(reference.parentUrl)
|
|
49
|
+
if (
|
|
50
|
+
!parentUrlInfo ||
|
|
51
|
+
!new URL(parentUrlInfo.url).searchParams.has("as_js_classic")
|
|
52
|
+
) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
const urlTransformed = injectQueryParams(reference.url, {
|
|
56
|
+
as_js_classic: "",
|
|
57
|
+
})
|
|
58
|
+
reference.filename = generateJsClassicFilename(reference.url)
|
|
59
|
+
return urlTransformed
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
return {
|
|
48
63
|
name: "jsenv:as_js_classic",
|
|
49
64
|
appliesDuring: "*",
|
|
50
65
|
// forward ?as_js_classic to referenced urls
|
|
51
|
-
|
|
52
|
-
// We want to propagate transformation of js module to js classic
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
!new URL(parentUrlInfo.url).searchParams.has("as_js_classic")
|
|
67
|
-
) {
|
|
66
|
+
redirectUrl: {
|
|
67
|
+
// We want to propagate transformation of js module to js classic to:
|
|
68
|
+
// - import specifier (static/dynamic import + re-export)
|
|
69
|
+
// - url specifier when inside System.register/_context.import()
|
|
70
|
+
// (because it's the transpiled equivalent of static and dynamic imports)
|
|
71
|
+
// And not other references otherwise we could try to transform inline ressources
|
|
72
|
+
// or specifiers inside new URL()...
|
|
73
|
+
js_import_export: propagateJsClassicSearchParam,
|
|
74
|
+
js_url_specifier: (reference, context) => {
|
|
75
|
+
if (
|
|
76
|
+
reference.subtype === "system_register_arg" ||
|
|
77
|
+
reference.subtype === "system_import_arg"
|
|
78
|
+
) {
|
|
79
|
+
return propagateJsClassicSearchParam(reference, context)
|
|
80
|
+
}
|
|
68
81
|
return null
|
|
69
|
-
}
|
|
70
|
-
const urlTransformed = injectQueryParams(reference.url, {
|
|
71
|
-
as_js_classic: "",
|
|
72
|
-
})
|
|
73
|
-
reference.filename = generateJsClassicFilename(reference.url)
|
|
74
|
-
return urlTransformed
|
|
82
|
+
},
|
|
75
83
|
},
|
|
76
84
|
fetchUrlContent: async (urlInfo, context) => {
|
|
77
85
|
const originalUrlInfo = await fetchOriginalUrlInfo({
|
|
@@ -119,7 +127,15 @@ const asJsClassic = ({ systemJsInjection, systemJsClientFileUrl }) => {
|
|
|
119
127
|
|
|
120
128
|
const generateJsClassicFilename = (url) => {
|
|
121
129
|
const filename = urlToFilename(url)
|
|
122
|
-
|
|
130
|
+
let [basename, extension] = splitFileExtension(filename)
|
|
131
|
+
const { searchParams } = new URL(url)
|
|
132
|
+
if (
|
|
133
|
+
searchParams.has("as_json_module") ||
|
|
134
|
+
searchParams.has("as_css_module") ||
|
|
135
|
+
searchParams.has("as_text_module")
|
|
136
|
+
) {
|
|
137
|
+
extension = ".js"
|
|
138
|
+
}
|
|
123
139
|
return `${basename}.es5${extension}`
|
|
124
140
|
}
|
|
125
141
|
|
package/src/plugins/transpilation/as_js_classic/jsenv_plugin_script_type_module_as_classic.js
CHANGED
|
@@ -98,7 +98,16 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
const jsModuleUrls = []
|
|
101
|
-
const getReferenceAsJsClassic = async (
|
|
101
|
+
const getReferenceAsJsClassic = async (
|
|
102
|
+
reference,
|
|
103
|
+
{
|
|
104
|
+
// we don't cook ressource hints
|
|
105
|
+
// because they might refer to ressource that will be modified during build
|
|
106
|
+
// It also means something else HAVE to reference that url in order to cook it
|
|
107
|
+
// so that the preload is deleted by "resync_ressource_hints.js" otherwise
|
|
108
|
+
cookIt = false,
|
|
109
|
+
} = {},
|
|
110
|
+
) => {
|
|
102
111
|
const [newReference, newUrlInfo] = context.referenceUtils.update(
|
|
103
112
|
reference,
|
|
104
113
|
{
|
|
@@ -112,6 +121,8 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
112
121
|
const jsModuleUrl = newUrlInfo.url
|
|
113
122
|
if (!jsModuleUrls.includes(jsModuleUrl)) {
|
|
114
123
|
jsModuleUrls.push(newUrlInfo.url)
|
|
124
|
+
}
|
|
125
|
+
if (cookIt) {
|
|
115
126
|
// during dev it means js modules will be cooked before server sends the HTML
|
|
116
127
|
// it's ok because:
|
|
117
128
|
// - during dev script_type_module are supported (dev use a recent browser)
|
|
@@ -141,9 +152,9 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
141
152
|
// but it's unlikely to happen and people should use "modulepreload" in that case anyway
|
|
142
153
|
if (expectedScriptType === "module") {
|
|
143
154
|
actions.push(async () => {
|
|
144
|
-
const
|
|
145
|
-
context.referenceUtils.findByGeneratedSpecifier(href)
|
|
146
|
-
)
|
|
155
|
+
const reference =
|
|
156
|
+
context.referenceUtils.findByGeneratedSpecifier(href)
|
|
157
|
+
const [newReference] = await getReferenceAsJsClassic(reference)
|
|
147
158
|
assignHtmlNodeAttributes(preloadAsScriptNode, {
|
|
148
159
|
href: newReference.generatedSpecifier,
|
|
149
160
|
})
|
|
@@ -158,9 +169,9 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
158
169
|
)
|
|
159
170
|
const href = hrefAttribute.value
|
|
160
171
|
actions.push(async () => {
|
|
161
|
-
const
|
|
162
|
-
context.referenceUtils.findByGeneratedSpecifier(href)
|
|
163
|
-
)
|
|
172
|
+
const reference =
|
|
173
|
+
context.referenceUtils.findByGeneratedSpecifier(href)
|
|
174
|
+
const [newReference] = await getReferenceAsJsClassic(reference)
|
|
164
175
|
assignHtmlNodeAttributes(modulePreloadNode, {
|
|
165
176
|
rel: "preload",
|
|
166
177
|
as: "script",
|
|
@@ -176,9 +187,11 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
176
187
|
if (srcAttribute) {
|
|
177
188
|
actions.push(async () => {
|
|
178
189
|
const specifier = srcAttribute.value
|
|
179
|
-
const
|
|
180
|
-
context.referenceUtils.findByGeneratedSpecifier(specifier)
|
|
181
|
-
|
|
190
|
+
const reference =
|
|
191
|
+
context.referenceUtils.findByGeneratedSpecifier(specifier)
|
|
192
|
+
const [newReference] = await getReferenceAsJsClassic(reference, {
|
|
193
|
+
cookIt: true,
|
|
194
|
+
})
|
|
182
195
|
removeHtmlNodeAttributeByName(moduleScriptNode, "type")
|
|
183
196
|
srcAttribute.value = newReference.generatedSpecifier
|
|
184
197
|
})
|
|
@@ -214,6 +227,7 @@ export const jsenvPluginScriptTypeModuleAsClassic = ({
|
|
|
214
227
|
})
|
|
215
228
|
const [, newUrlInfo] = await getReferenceAsJsClassic(
|
|
216
229
|
inlineReference,
|
|
230
|
+
{ cookIt: true },
|
|
217
231
|
)
|
|
218
232
|
removeHtmlNodeAttributeByName(moduleScriptNode, "type")
|
|
219
233
|
setHtmlNodeGeneratedText(moduleScriptNode, {
|
package/src/plugins/transpilation/as_js_classic/jsenv_plugin_workers_type_module_as_classic.js
CHANGED
|
@@ -21,7 +21,7 @@ export const jsenvPluginWorkersTypeModuleAsClassic = ({
|
|
|
21
21
|
return {
|
|
22
22
|
name: "jsenv:workers_type_module_as_classic",
|
|
23
23
|
appliesDuring: "*",
|
|
24
|
-
|
|
24
|
+
redirectUrl: {
|
|
25
25
|
js_url_specifier: (reference, context) => {
|
|
26
26
|
if (reference.expectedType !== "js_module") {
|
|
27
27
|
return null
|
|
@@ -27,16 +27,16 @@ export const jsenvPluginImportAssertions = () => {
|
|
|
27
27
|
end: reference.assertNode.end,
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
return injectQueryParams(reference.url, {
|
|
30
|
+
const newUrl = injectQueryParams(reference.url, {
|
|
32
31
|
[searchParam]: "",
|
|
33
32
|
})
|
|
33
|
+
return newUrl
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const importAssertions = {
|
|
37
37
|
name: "jsenv:import_assertions",
|
|
38
38
|
appliesDuring: "*",
|
|
39
|
-
|
|
39
|
+
redirectUrl: {
|
|
40
40
|
js_import_export: (reference, context) => {
|
|
41
41
|
if (!reference.assert) {
|
|
42
42
|
return null
|
|
@@ -13,19 +13,26 @@ export const parseAndTransformCssUrls = async (urlInfo, context) => {
|
|
|
13
13
|
sourcemaps: false,
|
|
14
14
|
plugins: [
|
|
15
15
|
postCssPluginUrlVisitor({
|
|
16
|
-
urlVisitor: ({
|
|
16
|
+
urlVisitor: ({
|
|
17
|
+
type,
|
|
18
|
+
specifier,
|
|
19
|
+
specifierStart,
|
|
20
|
+
specifierEnd,
|
|
21
|
+
specifierLine,
|
|
22
|
+
specifierColumn,
|
|
23
|
+
}) => {
|
|
17
24
|
const [reference] = context.referenceUtils.found({
|
|
18
25
|
type: `css_${type}`,
|
|
19
26
|
specifier,
|
|
20
|
-
specifierStart
|
|
21
|
-
specifierEnd
|
|
22
|
-
specifierLine
|
|
23
|
-
specifierColumn
|
|
27
|
+
specifierStart,
|
|
28
|
+
specifierEnd,
|
|
29
|
+
specifierLine,
|
|
30
|
+
specifierColumn,
|
|
24
31
|
})
|
|
25
32
|
actions.push(async () => {
|
|
26
33
|
magicSource.replace({
|
|
27
|
-
start,
|
|
28
|
-
end,
|
|
34
|
+
start: specifierStart,
|
|
35
|
+
end: specifierEnd,
|
|
29
36
|
replacement: await context.referenceUtils.readGeneratedSpecifier(
|
|
30
37
|
reference,
|
|
31
38
|
),
|
|
@@ -96,8 +96,8 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
96
96
|
...readFetchMetas(node),
|
|
97
97
|
})
|
|
98
98
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
99
|
+
const visitors = {
|
|
100
|
+
link: (node) => {
|
|
101
101
|
const relAttribute = getHtmlNodeAttributeByName(node, "rel")
|
|
102
102
|
const rel = relAttribute ? relAttribute.value : undefined
|
|
103
103
|
const typeAttribute = getHtmlNodeAttributeByName(node, "type")
|
|
@@ -114,13 +114,9 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
114
114
|
stylesheet: "css",
|
|
115
115
|
}[rel],
|
|
116
116
|
})
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// // styles.push(node)
|
|
121
|
-
// return
|
|
122
|
-
// }
|
|
123
|
-
if (node.nodeName === "script") {
|
|
117
|
+
},
|
|
118
|
+
// style: () => {},
|
|
119
|
+
script: (node) => {
|
|
124
120
|
const typeAttributeNode = getHtmlNodeAttributeByName(node, "type")
|
|
125
121
|
visitAttributeAsUrlSpecifier({
|
|
126
122
|
type: "script_src",
|
|
@@ -133,23 +129,22 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
133
129
|
node,
|
|
134
130
|
attributeName: "src",
|
|
135
131
|
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (node.nodeName === "a") {
|
|
132
|
+
},
|
|
133
|
+
a: (node) => {
|
|
139
134
|
visitAttributeAsUrlSpecifier({
|
|
140
135
|
type: "a_href",
|
|
141
136
|
node,
|
|
142
137
|
attributeName: "href",
|
|
143
138
|
})
|
|
144
|
-
}
|
|
145
|
-
|
|
139
|
+
},
|
|
140
|
+
iframe: (node) => {
|
|
146
141
|
visitAttributeAsUrlSpecifier({
|
|
147
142
|
type: "iframe_src",
|
|
148
143
|
node,
|
|
149
144
|
attributeName: "src",
|
|
150
145
|
})
|
|
151
|
-
}
|
|
152
|
-
|
|
146
|
+
},
|
|
147
|
+
img: (node) => {
|
|
153
148
|
visitAttributeAsUrlSpecifier({
|
|
154
149
|
type: "img_src",
|
|
155
150
|
node,
|
|
@@ -159,9 +154,8 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
159
154
|
type: "img_srcset",
|
|
160
155
|
node,
|
|
161
156
|
})
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (node.nodeName === "source") {
|
|
157
|
+
},
|
|
158
|
+
souce: (node) => {
|
|
165
159
|
visitAttributeAsUrlSpecifier({
|
|
166
160
|
type: "source_src",
|
|
167
161
|
node,
|
|
@@ -171,25 +165,22 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
171
165
|
type: "source_srcset",
|
|
172
166
|
node,
|
|
173
167
|
})
|
|
174
|
-
|
|
175
|
-
}
|
|
168
|
+
},
|
|
176
169
|
// svg <image> tag
|
|
177
|
-
|
|
170
|
+
image: (node) => {
|
|
178
171
|
visitAttributeAsUrlSpecifier({
|
|
179
172
|
type: "image_href",
|
|
180
173
|
node,
|
|
181
174
|
attributeName: "href",
|
|
182
175
|
})
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (node.nodeName === "use") {
|
|
176
|
+
},
|
|
177
|
+
use: (node) => {
|
|
186
178
|
visitAttributeAsUrlSpecifier({
|
|
187
179
|
type: "use_href",
|
|
188
180
|
node,
|
|
189
181
|
attributeName: "href",
|
|
190
182
|
})
|
|
191
|
-
|
|
192
|
-
}
|
|
183
|
+
},
|
|
193
184
|
}
|
|
194
185
|
const visitAttributeAsUrlSpecifier = ({
|
|
195
186
|
type,
|
|
@@ -252,7 +243,12 @@ const visitHtmlUrls = ({ url, htmlAst, onUrl }) => {
|
|
|
252
243
|
})
|
|
253
244
|
}
|
|
254
245
|
}
|
|
255
|
-
visitHtmlAst(htmlAst,
|
|
246
|
+
visitHtmlAst(htmlAst, (node) => {
|
|
247
|
+
const visitor = visitors[node.nodeName]
|
|
248
|
+
if (visitor) {
|
|
249
|
+
visitor(node)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
256
252
|
}
|
|
257
253
|
|
|
258
254
|
const crossOriginCompatibleTagNames = ["script", "link", "img", "source"]
|
|
@@ -2,7 +2,7 @@ export const jsenvPluginUrlVersion = ({ longTermCache = true } = {}) => {
|
|
|
2
2
|
return {
|
|
3
3
|
name: "jsenv:url_version",
|
|
4
4
|
appliesDuring: "*", // maybe only during dev?
|
|
5
|
-
|
|
5
|
+
redirectUrl: (reference) => {
|
|
6
6
|
// "v" search param goal is to enable long-term cache
|
|
7
7
|
// for server response headers
|
|
8
8
|
// it is also used by hmr to bypass browser cache
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export const sortUrlGraphByDependencies = (urlGraph) => {
|
|
2
|
-
const { urlInfos } = urlGraph
|
|
3
|
-
|
|
4
|
-
const visited = []
|
|
5
|
-
const sorted = []
|
|
6
|
-
const circular = []
|
|
7
|
-
const visit = (url) => {
|
|
8
|
-
const isSorted = sorted.includes(url)
|
|
9
|
-
if (isSorted) {
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
const isVisited = visited.includes(url)
|
|
13
|
-
if (isVisited) {
|
|
14
|
-
circular.push(url)
|
|
15
|
-
sorted.push(url)
|
|
16
|
-
} else {
|
|
17
|
-
visited.push(url)
|
|
18
|
-
urlInfos[url].dependencies.forEach((dependencyUrl) => {
|
|
19
|
-
visit(dependencyUrl, url)
|
|
20
|
-
})
|
|
21
|
-
sorted.push(url)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
Object.keys(urlInfos).forEach((url) => {
|
|
25
|
-
visit(url)
|
|
26
|
-
})
|
|
27
|
-
sorted.circular = circular
|
|
28
|
-
return sorted
|
|
29
|
-
}
|