@jsenv/plugin-commonjs 0.0.5
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/main.js +2 -0
- package/package.json +31 -0
- package/src/cjs_to_esm.js +129 -0
- package/src/cjs_to_esm_raw.js +167 -0
- package/src/compile_cache/compiled_file_cache.js +176 -0
- package/src/compile_cache/file_lock_registry.js +24 -0
- package/src/compile_cache/update_compile_cache.js +97 -0
- package/src/compile_cache/validate_compile_cache.js +226 -0
- package/src/jsenv_plugin_commonjs.js +63 -0
- package/src/rollup_plugin_commonjs_named_exports.js +186 -0
package/main.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsenv/plugin-commonjs",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/jsenv/jsenv-core",
|
|
8
|
+
"directory": "packages/jsenv-plugin-commonjs"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./main.js",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@jsenv/filesystem": "3.1.0",
|
|
18
|
+
"@jsenv/logger": "4.0.1",
|
|
19
|
+
"@rollup/plugin-commonjs": "22.0.0",
|
|
20
|
+
"@rollup/plugin-json": "4.1.0",
|
|
21
|
+
"@rollup/plugin-node-resolve": "13.3.0",
|
|
22
|
+
"@rollup/plugin-replace": "4.0.0",
|
|
23
|
+
"cjs-module-lexer": "1.2.2",
|
|
24
|
+
"is-valid-identifier": "2.0.2",
|
|
25
|
+
"resolve": "1.22.0",
|
|
26
|
+
"rollup": "2.73.0",
|
|
27
|
+
"rollup-plugin-node-globals": "1.4.0",
|
|
28
|
+
"rollup-plugin-polyfill-node": "0.9.0",
|
|
29
|
+
"vm2": "3.9.9"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import {
|
|
3
|
+
assertAndNormalizeDirectoryUrl,
|
|
4
|
+
urlIsInsideOf,
|
|
5
|
+
moveUrl,
|
|
6
|
+
ensureWindowsDriveLetter,
|
|
7
|
+
urlToExtension,
|
|
8
|
+
urlToBasename,
|
|
9
|
+
} from "@jsenv/filesystem"
|
|
10
|
+
|
|
11
|
+
import { setUrlExtension, setUrlFilename } from "@jsenv/utils/urls/url_utils.js"
|
|
12
|
+
import { commonJsToJsModuleRaw } from "./cjs_to_esm_raw.js"
|
|
13
|
+
import { reuseOrCreateCompiledFile } from "./compile_cache/compiled_file_cache.js"
|
|
14
|
+
|
|
15
|
+
export const commonJsToJsModule = ({
|
|
16
|
+
logLevel,
|
|
17
|
+
filesystemCache = true,
|
|
18
|
+
rootDirectoryUrl,
|
|
19
|
+
sourceFileUrl,
|
|
20
|
+
processEnvNodeEnv,
|
|
21
|
+
...rest
|
|
22
|
+
}) => {
|
|
23
|
+
if (filesystemCache) {
|
|
24
|
+
rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl)
|
|
25
|
+
const compileDirectoryUrl = new URL(
|
|
26
|
+
"./.jsenv/cjs_to_esm/",
|
|
27
|
+
rootDirectoryUrl,
|
|
28
|
+
)
|
|
29
|
+
const compiledFileUrl = determineCompiledFileUrl({
|
|
30
|
+
url: sourceFileUrl,
|
|
31
|
+
rootDirectoryUrl,
|
|
32
|
+
compileDirectoryUrl,
|
|
33
|
+
processEnvNodeEnv,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return reuseOrCreateCompiledFile({
|
|
37
|
+
logLevel,
|
|
38
|
+
compileDirectoryUrl,
|
|
39
|
+
sourceFileUrl,
|
|
40
|
+
compiledFileUrl,
|
|
41
|
+
compile: async () => {
|
|
42
|
+
const { content, sourcemap } = await commonJsToJsModuleRaw({
|
|
43
|
+
logLevel,
|
|
44
|
+
rootDirectoryUrl,
|
|
45
|
+
sourceFileUrl,
|
|
46
|
+
processEnvNodeEnv,
|
|
47
|
+
...rest,
|
|
48
|
+
})
|
|
49
|
+
const assets = extractCompileAssets({
|
|
50
|
+
sourceUrl: sourceFileUrl,
|
|
51
|
+
sourceContent: String(readFileSync(new URL(sourceFileUrl))),
|
|
52
|
+
compiledUrl: compiledFileUrl,
|
|
53
|
+
compiledContent: content,
|
|
54
|
+
sourcemap,
|
|
55
|
+
})
|
|
56
|
+
return {
|
|
57
|
+
content,
|
|
58
|
+
sourcemap,
|
|
59
|
+
assets,
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return commonJsToJsModuleRaw({
|
|
65
|
+
logLevel,
|
|
66
|
+
rootDirectoryUrl,
|
|
67
|
+
sourceFileUrl,
|
|
68
|
+
...rest,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const extractCompileAssets = ({
|
|
73
|
+
sourceUrl,
|
|
74
|
+
sourceContent,
|
|
75
|
+
compiledUrl,
|
|
76
|
+
sourcemap,
|
|
77
|
+
}) => {
|
|
78
|
+
const compileAssets = {}
|
|
79
|
+
|
|
80
|
+
// ensure the source file is part of sources no matter what
|
|
81
|
+
// it covers cases where sourcemap.sources is empty
|
|
82
|
+
// or do not contain the real source file
|
|
83
|
+
// there is a check to prevent duplicate sources in case the sourcemap is correct and contains
|
|
84
|
+
// the source file in sourcemap.sources
|
|
85
|
+
compileAssets[sourceUrl] = { type: "source", content: sourceContent }
|
|
86
|
+
if (sourcemap) {
|
|
87
|
+
sourcemap.sources.forEach((source, index) => {
|
|
88
|
+
if (source.startsWith("file://")) {
|
|
89
|
+
const contentFromSourcemap = sourcemap.sourcesContent
|
|
90
|
+
? sourcemap.sourcesContent[index]
|
|
91
|
+
: null
|
|
92
|
+
const content =
|
|
93
|
+
contentFromSourcemap || String(readFileSync(new URL(source)))
|
|
94
|
+
compileAssets[source] = { type: "source", content }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
const sourcemapUrl = setUrlExtension(compiledUrl, ".map")
|
|
98
|
+
const sourcemapContent = JSON.stringify(sourcemap)
|
|
99
|
+
compileAssets[sourcemapUrl] = {
|
|
100
|
+
type: "sourcemap",
|
|
101
|
+
content: sourcemapContent,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return compileAssets
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const determineCompiledFileUrl = ({
|
|
109
|
+
url,
|
|
110
|
+
rootDirectoryUrl,
|
|
111
|
+
compileDirectoryUrl,
|
|
112
|
+
processEnvNodeEnv,
|
|
113
|
+
}) => {
|
|
114
|
+
if (!urlIsInsideOf(url, rootDirectoryUrl)) {
|
|
115
|
+
const fsRootUrl = ensureWindowsDriveLetter("file:///", url)
|
|
116
|
+
url = `${rootDirectoryUrl}@fs/${url.slice(fsRootUrl.length)}`
|
|
117
|
+
}
|
|
118
|
+
if (processEnvNodeEnv) {
|
|
119
|
+
const basename = urlToBasename(url)
|
|
120
|
+
const extension = urlToExtension(url)
|
|
121
|
+
url = setUrlFilename(url, `${basename}.${processEnvNodeEnv}${extension}`)
|
|
122
|
+
}
|
|
123
|
+
return moveUrl({
|
|
124
|
+
url,
|
|
125
|
+
from: rootDirectoryUrl,
|
|
126
|
+
to: compileDirectoryUrl,
|
|
127
|
+
preferAbsolute: true,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
import { createLogger } from "@jsenv/logger"
|
|
3
|
+
|
|
4
|
+
import { rollupPluginCommonJsNamedExports } from "./rollup_plugin_commonjs_named_exports.js"
|
|
5
|
+
|
|
6
|
+
export const commonJsToJsModuleRaw = async ({
|
|
7
|
+
logLevel,
|
|
8
|
+
sourceFileUrl,
|
|
9
|
+
|
|
10
|
+
replaceGlobalObject = true,
|
|
11
|
+
replaceGlobalFilename = true,
|
|
12
|
+
replaceGlobalDirname = true,
|
|
13
|
+
replaceProcessEnvNodeEnv = true,
|
|
14
|
+
replaceProcess = true,
|
|
15
|
+
replaceBuffer = true,
|
|
16
|
+
processEnvNodeEnv = process.env.NODE_ENV,
|
|
17
|
+
replaceMap = {},
|
|
18
|
+
convertBuiltinsToBrowser = true,
|
|
19
|
+
external = [],
|
|
20
|
+
sourcemapExcludeSources,
|
|
21
|
+
} = {}) => {
|
|
22
|
+
const logger = createLogger({ logLevel })
|
|
23
|
+
if (!sourceFileUrl.startsWith("file:///")) {
|
|
24
|
+
// it's possible to make rollup compatible with http:// for instance
|
|
25
|
+
// however it's an exotic use case for now
|
|
26
|
+
throw new Error(
|
|
27
|
+
`compatible only with file:// protocol, got ${sourceFileUrl}`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
const sourceFilePath = fileURLToPath(sourceFileUrl)
|
|
31
|
+
|
|
32
|
+
const { nodeResolve } = await import("@rollup/plugin-node-resolve")
|
|
33
|
+
const nodeResolveRollupPlugin = nodeResolve({
|
|
34
|
+
mainFields: [
|
|
35
|
+
"browser:module",
|
|
36
|
+
"module",
|
|
37
|
+
"browser",
|
|
38
|
+
"main:esnext",
|
|
39
|
+
"jsnext:main",
|
|
40
|
+
"main",
|
|
41
|
+
],
|
|
42
|
+
extensions: [".mjs", ".cjs", ".js", ".json"],
|
|
43
|
+
preferBuiltins: false,
|
|
44
|
+
exportConditions: [],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { default: createJSONRollupPlugin } = await import(
|
|
48
|
+
"@rollup/plugin-json"
|
|
49
|
+
)
|
|
50
|
+
const jsonRollupPlugin = createJSONRollupPlugin({
|
|
51
|
+
preferConst: true,
|
|
52
|
+
indent: " ",
|
|
53
|
+
compact: false,
|
|
54
|
+
namedExports: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const { default: createReplaceRollupPlugin } = await import(
|
|
58
|
+
"@rollup/plugin-replace"
|
|
59
|
+
)
|
|
60
|
+
const replaceRollupPlugin = createReplaceRollupPlugin({
|
|
61
|
+
preventAssignment: true,
|
|
62
|
+
values: {
|
|
63
|
+
...(replaceProcessEnvNodeEnv
|
|
64
|
+
? { "process.env.NODE_ENV": JSON.stringify(processEnvNodeEnv) }
|
|
65
|
+
: {}),
|
|
66
|
+
...(replaceGlobalObject ? { global: "globalThis" } : {}),
|
|
67
|
+
...(replaceGlobalFilename ? { __filename: __filenameReplacement } : {}),
|
|
68
|
+
...(replaceGlobalDirname ? { __dirname: __dirnameReplacement } : {}),
|
|
69
|
+
...replaceMap,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const { default: commonjs } = await import("@rollup/plugin-commonjs")
|
|
74
|
+
const commonJsRollupPlugin = commonjs({
|
|
75
|
+
extensions: [".js", ".cjs"],
|
|
76
|
+
// esmExternals: true,
|
|
77
|
+
// defaultIsModuleExports: true,
|
|
78
|
+
// requireReturnsDefault: "namespace",
|
|
79
|
+
requireReturnsDefault: "auto",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const { default: createNodeGlobalRollupPlugin } = await import(
|
|
83
|
+
"rollup-plugin-node-globals"
|
|
84
|
+
)
|
|
85
|
+
const nodeGlobalRollupPlugin = createNodeGlobalRollupPlugin({
|
|
86
|
+
global: false, // handled by replaceMap
|
|
87
|
+
dirname: false, // handled by replaceMap
|
|
88
|
+
filename: false, // handled by replaceMap
|
|
89
|
+
process: replaceProcess,
|
|
90
|
+
buffer: replaceBuffer,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const commonJsNamedExportsRollupPlugin = rollupPluginCommonJsNamedExports({
|
|
94
|
+
logger,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const { default: rollupPluginNodePolyfills } = await import(
|
|
98
|
+
"rollup-plugin-polyfill-node"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const { rollup } = await import("rollup")
|
|
102
|
+
const rollupBuild = await rollup({
|
|
103
|
+
input: sourceFilePath,
|
|
104
|
+
inlineDynamicImports: true,
|
|
105
|
+
external,
|
|
106
|
+
plugins: [
|
|
107
|
+
nodeResolveRollupPlugin,
|
|
108
|
+
jsonRollupPlugin,
|
|
109
|
+
replaceRollupPlugin,
|
|
110
|
+
commonJsRollupPlugin,
|
|
111
|
+
commonJsNamedExportsRollupPlugin,
|
|
112
|
+
nodeGlobalRollupPlugin,
|
|
113
|
+
...(convertBuiltinsToBrowser
|
|
114
|
+
? [
|
|
115
|
+
rollupPluginNodePolyfills({
|
|
116
|
+
include: null,
|
|
117
|
+
}),
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
120
|
+
],
|
|
121
|
+
onwarn: (warning) => {
|
|
122
|
+
const { loc, message } = warning
|
|
123
|
+
const logMessage = loc
|
|
124
|
+
? `${loc.file}:${loc.line}:${loc.column} ${message}`
|
|
125
|
+
: message
|
|
126
|
+
|
|
127
|
+
// These warnings are usually harmless in packages, so don't show them by default
|
|
128
|
+
if (
|
|
129
|
+
warning.code === "CIRCULAR_DEPENDENCY" ||
|
|
130
|
+
warning.code === "NAMESPACE_CONFLICT" ||
|
|
131
|
+
warning.code === "THIS_IS_UNDEFINED" ||
|
|
132
|
+
warning.code === "EMPTY_BUNDLE" ||
|
|
133
|
+
warning.code === "UNUSED_EXTERNAL_IMPORT"
|
|
134
|
+
) {
|
|
135
|
+
logger.debug(logMessage)
|
|
136
|
+
} else {
|
|
137
|
+
logger.warn(logMessage)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
const abstractDirUrl = new URL("./dist/", sourceFileUrl) // to help rollup generate property sourcemap paths
|
|
142
|
+
const generateOptions = {
|
|
143
|
+
// https://rollupjs.org/guide/en#output-format
|
|
144
|
+
format: "esm",
|
|
145
|
+
// entryFileNames: `./[name].js`,
|
|
146
|
+
// https://rollupjs.org/guide/en#output-sourcemap
|
|
147
|
+
sourcemap: true,
|
|
148
|
+
sourcemapExcludeSources,
|
|
149
|
+
exports: "named",
|
|
150
|
+
dir: fileURLToPath(abstractDirUrl),
|
|
151
|
+
sourcemapPathTransform: (relativePath) => {
|
|
152
|
+
const sourceUrl = new URL(relativePath, abstractDirUrl).href
|
|
153
|
+
return sourceUrl
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { output } = await rollupBuild.generate(generateOptions)
|
|
158
|
+
const { code, map } = output[0]
|
|
159
|
+
return {
|
|
160
|
+
content: code,
|
|
161
|
+
sourcemap: map,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const __filenameReplacement = `import.meta.url.slice('file:///'.length)`
|
|
166
|
+
|
|
167
|
+
const __dirnameReplacement = `import.meta.url.slice('file:///'.length).replace(/[\\\/\\\\][^\\\/\\\\]*$/, '')`
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { createLogger } from "@jsenv/logger"
|
|
3
|
+
import { UNICODE } from "@jsenv/log"
|
|
4
|
+
import {
|
|
5
|
+
assertAndNormalizeFileUrl,
|
|
6
|
+
ensureEmptyDirectory,
|
|
7
|
+
} from "@jsenv/filesystem"
|
|
8
|
+
|
|
9
|
+
import { validateCompileCache } from "./validate_compile_cache.js"
|
|
10
|
+
import { createLockRegistry } from "./file_lock_registry.js"
|
|
11
|
+
import { updateCompileCache } from "./update_compile_cache.js"
|
|
12
|
+
|
|
13
|
+
const { lockForRessource } = createLockRegistry()
|
|
14
|
+
|
|
15
|
+
export const reuseOrCreateCompiledFile = async ({
|
|
16
|
+
logLevel,
|
|
17
|
+
compileDirectoryUrl,
|
|
18
|
+
sourceFileUrl,
|
|
19
|
+
compiledFileUrl,
|
|
20
|
+
compileCacheStrategy,
|
|
21
|
+
compileCacheAssetsValidation,
|
|
22
|
+
compile,
|
|
23
|
+
}) => {
|
|
24
|
+
const logger = createLogger({ logLevel })
|
|
25
|
+
await initCompileDirectory({ logger, compileDirectoryUrl })
|
|
26
|
+
sourceFileUrl = assertAndNormalizeFileUrl(sourceFileUrl)
|
|
27
|
+
if (typeof compile !== "function") {
|
|
28
|
+
throw new TypeError(`compile must be a function, got ${compile}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return startAsap(
|
|
32
|
+
async () => {
|
|
33
|
+
logger.debug(`check cache for ${compiledFileUrl}`)
|
|
34
|
+
const cacheValidity = validateCompileCache({
|
|
35
|
+
logger,
|
|
36
|
+
compiledFileUrl,
|
|
37
|
+
compileCacheStrategy,
|
|
38
|
+
compileCacheAssetsValidation,
|
|
39
|
+
})
|
|
40
|
+
if (cacheValidity.isValid) {
|
|
41
|
+
logger.debug(`${UNICODE.OK} found a valid cache`)
|
|
42
|
+
const compileInfo = cacheValidity.compileInfo.data
|
|
43
|
+
const content = String(cacheValidity.compiledFile.data.buffer)
|
|
44
|
+
const assets = {}
|
|
45
|
+
let sourcemap = null
|
|
46
|
+
Object.keys(compileInfo.assetInfos).forEach((assetRelativeUrl) => {
|
|
47
|
+
const assetUrl = new URL(assetRelativeUrl, compiledFileUrl).href
|
|
48
|
+
const assetValidity = cacheValidity.assets.data[assetUrl]
|
|
49
|
+
const asset = {
|
|
50
|
+
type: compileInfo.assetInfos[assetRelativeUrl].type,
|
|
51
|
+
etag: compileInfo.assetInfos[assetRelativeUrl].etag,
|
|
52
|
+
content: assetValidity.data.content,
|
|
53
|
+
}
|
|
54
|
+
assets[assetUrl] = asset
|
|
55
|
+
if (asset.type === "sourcemap") {
|
|
56
|
+
sourcemap = assetValidity.data.sourcemap
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
updateCompileCache({
|
|
60
|
+
logger,
|
|
61
|
+
compiledFileUrl,
|
|
62
|
+
content,
|
|
63
|
+
assets,
|
|
64
|
+
compileResultStatus: "cached",
|
|
65
|
+
})
|
|
66
|
+
return {
|
|
67
|
+
content,
|
|
68
|
+
sourcemap,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (cacheValidity.code === "SOURCES_EMPTY") {
|
|
72
|
+
logger.warn(
|
|
73
|
+
`${UNICODE.WARN} meta.sources is empty for ${compiledFileUrl}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
logger.debug(`${UNICODE.INFO} cache not found or invalid`)
|
|
77
|
+
const compileInfoIsValid = cacheValidity.compileInfo
|
|
78
|
+
? cacheValidity.compileInfo.isValid
|
|
79
|
+
: false
|
|
80
|
+
const fileContentAsBuffer = readFileSync(new URL(sourceFileUrl))
|
|
81
|
+
const fileContentAsString = String(fileContentAsBuffer)
|
|
82
|
+
|
|
83
|
+
const compileResult = await compile({
|
|
84
|
+
content: fileContentAsString,
|
|
85
|
+
})
|
|
86
|
+
if (typeof compileResult !== "object" || compileResult === null) {
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
`compile must return an object, got ${compileResult}`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
const { content, sourcemap, assets } = compileResult
|
|
92
|
+
updateCompileCache({
|
|
93
|
+
logger,
|
|
94
|
+
compiledFileUrl,
|
|
95
|
+
content,
|
|
96
|
+
assets,
|
|
97
|
+
compileResultStatus: compileInfoIsValid ? "updated" : "created",
|
|
98
|
+
})
|
|
99
|
+
return {
|
|
100
|
+
content,
|
|
101
|
+
sourcemap,
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
compiledFileUrl,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const initalized = {}
|
|
111
|
+
const initCompileDirectory = async ({ logger, compileDirectoryUrl }) => {
|
|
112
|
+
if (initalized[compileDirectoryUrl]) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
initalized[compileDirectoryUrl] = true
|
|
116
|
+
logger.debug(`check compile directory at ${compileDirectoryUrl}`)
|
|
117
|
+
const compileContextJsonFileUrl = new URL(
|
|
118
|
+
"./__compile_context__.json",
|
|
119
|
+
compileDirectoryUrl,
|
|
120
|
+
)
|
|
121
|
+
const version = JSON.parse(
|
|
122
|
+
readFileSync(new URL("../../package.json", import.meta.url)),
|
|
123
|
+
).version
|
|
124
|
+
const compileContext = readCompileContextFile({
|
|
125
|
+
logger,
|
|
126
|
+
compileContextJsonFileUrl,
|
|
127
|
+
})
|
|
128
|
+
if (compileContext && compileContext.version === version) {
|
|
129
|
+
logger.debug(`${UNICODE.OK} reuse compile directory`)
|
|
130
|
+
} else {
|
|
131
|
+
if (compileContext) {
|
|
132
|
+
logger.debug(`${UNICODE.WARN} clean existing directory`)
|
|
133
|
+
} else {
|
|
134
|
+
logger.debug(`${UNICODE.INFO} create an empty directory`)
|
|
135
|
+
}
|
|
136
|
+
await ensureEmptyDirectory(compileDirectoryUrl)
|
|
137
|
+
writeFileSync(
|
|
138
|
+
compileContextJsonFileUrl,
|
|
139
|
+
JSON.stringify({ version }, null, " "),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const readCompileContextFile = ({ logger, compileContextJsonFileUrl }) => {
|
|
145
|
+
let compileContextFileContent
|
|
146
|
+
try {
|
|
147
|
+
compileContextFileContent = readFileSync(compileContextJsonFileUrl)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
logger.debug(
|
|
150
|
+
`${UNICODE.INFO} cannot read compile context at ${compileContextJsonFileUrl}`,
|
|
151
|
+
)
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const compileContext = JSON.parse(compileContextFileContent)
|
|
156
|
+
return compileContext
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e.name === "SyntaxError") {
|
|
159
|
+
logger.warn(
|
|
160
|
+
`${UNICODE.WARN} syntax error in ${compileContextJsonFileUrl}`,
|
|
161
|
+
)
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
throw e
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const startAsap = async (fn, { compiledFileUrl }) => {
|
|
169
|
+
const unlockLocal = await lockForRessource(compiledFileUrl)
|
|
170
|
+
try {
|
|
171
|
+
return await fn()
|
|
172
|
+
} finally {
|
|
173
|
+
// "finally" we want to unlock in case of error too
|
|
174
|
+
unlockLocal()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const createLockRegistry = () => {
|
|
2
|
+
let lockArray = []
|
|
3
|
+
const lockForRessource = async (ressource) => {
|
|
4
|
+
const currentLock = lockArray.find((lock) => lock.ressource === ressource)
|
|
5
|
+
let unlockResolve
|
|
6
|
+
const unlocked = new Promise((resolve) => {
|
|
7
|
+
unlockResolve = resolve
|
|
8
|
+
})
|
|
9
|
+
const lock = {
|
|
10
|
+
ressource,
|
|
11
|
+
unlocked,
|
|
12
|
+
}
|
|
13
|
+
lockArray = [...lockArray, lock]
|
|
14
|
+
|
|
15
|
+
if (currentLock) await currentLock.unlocked
|
|
16
|
+
|
|
17
|
+
const unlock = () => {
|
|
18
|
+
lockArray = lockArray.filter((lockCandidate) => lockCandidate !== lock)
|
|
19
|
+
unlockResolve()
|
|
20
|
+
}
|
|
21
|
+
return unlock
|
|
22
|
+
}
|
|
23
|
+
return { lockForRessource }
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, utimesSync } from "node:fs"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import {
|
|
4
|
+
urlToRelativeUrl,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
bufferToEtag,
|
|
7
|
+
} from "@jsenv/filesystem"
|
|
8
|
+
|
|
9
|
+
export const updateCompileCache = ({
|
|
10
|
+
logger,
|
|
11
|
+
compiledFileUrl,
|
|
12
|
+
content,
|
|
13
|
+
assets,
|
|
14
|
+
mtime,
|
|
15
|
+
compileResultStatus,
|
|
16
|
+
}) => {
|
|
17
|
+
const isNew = compileResultStatus === "created"
|
|
18
|
+
const isUpdated = compileResultStatus === "updated"
|
|
19
|
+
if (!isNew && !isUpdated) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ensure source that does not leads to files are not capable to invalidate the cache
|
|
24
|
+
const filesRemoved = []
|
|
25
|
+
Object.keys(assets).forEach((assetUrl) => {
|
|
26
|
+
if (
|
|
27
|
+
assetUrl.startsWith("file://") &&
|
|
28
|
+
assets[assetUrl].type === "source" &&
|
|
29
|
+
!existsSync(new URL(assetUrl))
|
|
30
|
+
) {
|
|
31
|
+
delete assets[assetUrl]
|
|
32
|
+
filesRemoved.push(assetUrl)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
const notFoundCount = filesRemoved.length
|
|
36
|
+
if (notFoundCount > 0) {
|
|
37
|
+
logger.warn(`COMPILE_ASSET_FILE_NOT_FOUND: ${notFoundCount} file(s) not found.
|
|
38
|
+
--- consequence ---
|
|
39
|
+
cache will be reused even if one of the source file is modified
|
|
40
|
+
--- files not found ---
|
|
41
|
+
${filesRemoved.join(`\n`)}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.debug(`write compiled file at ${fileURLToPath(compiledFileUrl)}`)
|
|
45
|
+
writeFileSync(compiledFileUrl, content, {
|
|
46
|
+
fileLikelyNotFound: isNew,
|
|
47
|
+
})
|
|
48
|
+
// mtime is passed, it meant the file mtime is important
|
|
49
|
+
// -> we update file mtime
|
|
50
|
+
if (mtime) {
|
|
51
|
+
utimesSync(new URL(compiledFileUrl), new Date(mtime), new Date(mtime))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const assetInfos = {}
|
|
55
|
+
Object.keys(assets).forEach((assetUrl) => {
|
|
56
|
+
logger.debug(`write compiled file asset at ${fileURLToPath(assetUrl)}`)
|
|
57
|
+
const asset = assets[assetUrl]
|
|
58
|
+
writeFileSync(assetUrl, asset.content, {
|
|
59
|
+
fileLikelyNotFound: isNew,
|
|
60
|
+
})
|
|
61
|
+
const assetRelativeUrl = urlToRelativeUrl(assetUrl, compiledFileUrl)
|
|
62
|
+
const assetEtag = asset.etag || bufferToEtag(Buffer.from(asset.content))
|
|
63
|
+
assetInfos[assetRelativeUrl] = {
|
|
64
|
+
type: asset.type,
|
|
65
|
+
etag: assetEtag,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const compileInfoFileUrl = `${compiledFileUrl}__compile_info__.json`
|
|
70
|
+
let latestCompileInfo
|
|
71
|
+
if (isNew) {
|
|
72
|
+
latestCompileInfo = {
|
|
73
|
+
// was used at some point to ensure the compiled file matches browser etag
|
|
74
|
+
// etag: bufferToEtag(Buffer.from(content)),
|
|
75
|
+
assetInfos,
|
|
76
|
+
createdMs: Number(Date.now()),
|
|
77
|
+
lastModifiedMs: Number(Date.now()),
|
|
78
|
+
}
|
|
79
|
+
} else if (isUpdated) {
|
|
80
|
+
latestCompileInfo = {
|
|
81
|
+
// was used at some point to ensure the compiled file matches browser etag
|
|
82
|
+
// etag: bufferToEtag(Buffer.from(content)),
|
|
83
|
+
assetInfos,
|
|
84
|
+
lastModifiedMs: Number(Date.now()),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
logger.debug(
|
|
88
|
+
`write compiled file info at ${fileURLToPath(compileInfoFileUrl)}`,
|
|
89
|
+
)
|
|
90
|
+
writeFileSync(
|
|
91
|
+
compileInfoFileUrl,
|
|
92
|
+
JSON.stringify(latestCompileInfo, null, " "),
|
|
93
|
+
{
|
|
94
|
+
fileLikelyNotFound: isNew,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { bufferToEtag } from "@jsenv/filesystem"
|
|
3
|
+
|
|
4
|
+
export const validateCompileCache = ({
|
|
5
|
+
compiledFileUrl,
|
|
6
|
+
compileCacheStrategy,
|
|
7
|
+
compileCacheAssetsValidation = true,
|
|
8
|
+
}) => {
|
|
9
|
+
const validity = { isValid: true }
|
|
10
|
+
const compileInfoValidity = validateCompileInfoFile({
|
|
11
|
+
compiledFileUrl,
|
|
12
|
+
})
|
|
13
|
+
validity.compileInfo = compileInfoValidity
|
|
14
|
+
mergeValidity(validity, compileInfoValidity)
|
|
15
|
+
if (!validity.isValid) {
|
|
16
|
+
return validity
|
|
17
|
+
}
|
|
18
|
+
const compiledFileValidity = validateCompiledFile({
|
|
19
|
+
compiledFileUrl,
|
|
20
|
+
compileCacheStrategy,
|
|
21
|
+
})
|
|
22
|
+
validity.compiledFile = compiledFileValidity
|
|
23
|
+
mergeValidity(validity, compiledFileValidity)
|
|
24
|
+
if (!validity.isValid) {
|
|
25
|
+
return validity
|
|
26
|
+
}
|
|
27
|
+
const compileInfo = compileInfoValidity.data
|
|
28
|
+
const assetsValidity = compileCacheAssetsValidation
|
|
29
|
+
? validateAssets({
|
|
30
|
+
compiledFileUrl,
|
|
31
|
+
compileInfo,
|
|
32
|
+
})
|
|
33
|
+
: { isValid: true, code: "ASSETS_VALIDATION_DISABLED" }
|
|
34
|
+
validity.assets = assetsValidity
|
|
35
|
+
mergeValidity(validity, assetsValidity)
|
|
36
|
+
if (!validity.isValid) {
|
|
37
|
+
return validity
|
|
38
|
+
}
|
|
39
|
+
return validity
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const validateCompileInfoFile = ({ compiledFileUrl }) => {
|
|
43
|
+
const compileInfoFileUrl = `${compiledFileUrl}__compile_info__.json`
|
|
44
|
+
const validity = { isValid: true, data: {} }
|
|
45
|
+
let compileInfoFileContentAsBuffer
|
|
46
|
+
try {
|
|
47
|
+
compileInfoFileContentAsBuffer = readFileSync(new URL(compileInfoFileUrl))
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error && error.code === "ENOENT") {
|
|
50
|
+
validity.isValid = false
|
|
51
|
+
validity.code = "COMPILE_INFO_FILE_NOT_FOUND"
|
|
52
|
+
return validity
|
|
53
|
+
}
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
const compileInfoFileContentAsString = String(compileInfoFileContentAsBuffer)
|
|
57
|
+
let compileInfo
|
|
58
|
+
try {
|
|
59
|
+
compileInfo = JSON.parse(compileInfoFileContentAsString)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error && error.name === "SyntaxError") {
|
|
62
|
+
validity.isValid = false
|
|
63
|
+
validity.code = "COMPILE_INFO_FILE_SYNTAX_ERROR"
|
|
64
|
+
return validity
|
|
65
|
+
}
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
validity.data = compileInfo
|
|
69
|
+
if (Object.keys(compileInfo.assetInfos).length === 0) {
|
|
70
|
+
validity.isValid = false
|
|
71
|
+
validity.code = "ASSETS_EMPTY"
|
|
72
|
+
return validity
|
|
73
|
+
}
|
|
74
|
+
return validity
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const validateCompiledFile = ({
|
|
78
|
+
compiledFileUrl,
|
|
79
|
+
compileCacheStrategy,
|
|
80
|
+
lastEtag,
|
|
81
|
+
lastModificationTime,
|
|
82
|
+
}) => {
|
|
83
|
+
const validity = { isValid: true, data: {} }
|
|
84
|
+
try {
|
|
85
|
+
const buffer = readFileSync(new URL(compiledFileUrl))
|
|
86
|
+
validity.data.buffer = buffer
|
|
87
|
+
if (compileCacheStrategy === "etag" && lastEtag) {
|
|
88
|
+
const etag = bufferToEtag(buffer)
|
|
89
|
+
validity.data.etag = etag
|
|
90
|
+
if (lastEtag && lastEtag !== etag) {
|
|
91
|
+
validity.isValid = false
|
|
92
|
+
validity.code = "COMPILED_FILE_ETAG_MISMATCH"
|
|
93
|
+
return validity
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (compileCacheStrategy === "mtime" && lastModificationTime) {
|
|
97
|
+
const stats = statSync(new URL(compiledFileUrl))
|
|
98
|
+
const mtime = Math.floor(stats.mtimeMs)
|
|
99
|
+
validity.data.mtime = mtime
|
|
100
|
+
let ifModifiedSinceDate
|
|
101
|
+
try {
|
|
102
|
+
ifModifiedSinceDate = new Date(lastModificationTime)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
ifModifiedSinceDate = null
|
|
105
|
+
// ideally we should rather respond with
|
|
106
|
+
// 400 "if-modified-since header is not a valid date"
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
ifModifiedSinceDate &&
|
|
110
|
+
ifModifiedSinceDate < dateToSecondsPrecision(mtime)
|
|
111
|
+
) {
|
|
112
|
+
validity.isValid = false
|
|
113
|
+
validity.code = "COMPILED_FILE_MTIME_OUTDATED"
|
|
114
|
+
return validity
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return validity
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error && error.code === "ENOENT") {
|
|
120
|
+
validity.isValid = false
|
|
121
|
+
validity.code = "COMPILED_FILE_NOT_FOUND"
|
|
122
|
+
return validity
|
|
123
|
+
}
|
|
124
|
+
throw error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validateAssets = ({ compiledFileUrl, compileInfo }) => {
|
|
129
|
+
const assetsValidity = { isValid: true, data: {} }
|
|
130
|
+
|
|
131
|
+
const assetRelativeUrls = Object.keys(compileInfo.assetInfos)
|
|
132
|
+
for (const assetRelativeUrl of assetRelativeUrls) {
|
|
133
|
+
const assetInfo = compileInfo.assetInfos[assetRelativeUrl]
|
|
134
|
+
const assetUrl = new URL(assetRelativeUrl, compiledFileUrl).href
|
|
135
|
+
const assetValidity = { isValid: true, data: {} }
|
|
136
|
+
if (assetInfo.type === "source") {
|
|
137
|
+
validateSource(assetValidity, {
|
|
138
|
+
sourceFileUrl: assetUrl,
|
|
139
|
+
eTag: assetInfo.etag,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
if (assetInfo.type === "sourcemap") {
|
|
143
|
+
validateSourcemap(assetValidity, {
|
|
144
|
+
sourcemapFileUrl: assetUrl,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
assetsValidity.data[assetUrl] = assetValidity
|
|
148
|
+
mergeValidity(assetsValidity, assetValidity)
|
|
149
|
+
if (!assetsValidity.isValid) {
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return assetsValidity
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const validateSource = (validity, { sourceFileUrl, eTag }) => {
|
|
158
|
+
try {
|
|
159
|
+
const sourceBuffer = readFileSync(new URL(sourceFileUrl))
|
|
160
|
+
const sourceETag = bufferToEtag(sourceBuffer)
|
|
161
|
+
validity.data.content = String(sourceBuffer)
|
|
162
|
+
validity.data.etag = sourceETag
|
|
163
|
+
if (sourceETag !== eTag) {
|
|
164
|
+
validity.isValid = false
|
|
165
|
+
validity.code = "SOURCE_ETAG_MISMATCH"
|
|
166
|
+
return validity
|
|
167
|
+
}
|
|
168
|
+
return validity
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (e && e.code === "ENOENT") {
|
|
171
|
+
// missing source invalidates the cache because
|
|
172
|
+
// we cannot check its validity
|
|
173
|
+
// HOWEVER inside writeMeta we will check if a source can be found
|
|
174
|
+
// when it cannot we will not put it as a dependency
|
|
175
|
+
// to invalidate the cache.
|
|
176
|
+
// It is important because some files are constructed on other files
|
|
177
|
+
// which are not truly on the filesystem
|
|
178
|
+
// (IN theory the above happens only for convertCommonJsWithRollup because jsenv
|
|
179
|
+
// always have a concrete file especially to avoid that kind of thing)
|
|
180
|
+
validity.isValid = false
|
|
181
|
+
validity.code = "SOURCE_NOT_FOUND"
|
|
182
|
+
return validity
|
|
183
|
+
}
|
|
184
|
+
throw e
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const validateSourcemap = (validity, { sourcemapFileUrl }) => {
|
|
189
|
+
let sourcemapFileContentAsBuffer
|
|
190
|
+
try {
|
|
191
|
+
sourcemapFileContentAsBuffer = readFileSync(new URL(sourcemapFileUrl))
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error && error.code === "ENOENT") {
|
|
194
|
+
validity.isValid = false
|
|
195
|
+
validity.code = "SOURCEMAP_FILE_NOT_FOUND"
|
|
196
|
+
return validity
|
|
197
|
+
}
|
|
198
|
+
throw error
|
|
199
|
+
}
|
|
200
|
+
const sourcemapFileContentAsString = String(sourcemapFileContentAsBuffer)
|
|
201
|
+
validity.data.content = sourcemapFileContentAsString
|
|
202
|
+
let sourcemap
|
|
203
|
+
try {
|
|
204
|
+
sourcemap = JSON.parse(sourcemapFileContentAsString)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error && error.name === "SyntaxError") {
|
|
207
|
+
validity.isValid = false
|
|
208
|
+
validity.code = "SOURCEMAP_FILE_SYNTAX_ERROR"
|
|
209
|
+
return validity
|
|
210
|
+
}
|
|
211
|
+
throw error
|
|
212
|
+
}
|
|
213
|
+
validity.data.sourcemap = sourcemap
|
|
214
|
+
return validity
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const mergeValidity = (parentValidity, childValidity) => {
|
|
218
|
+
parentValidity.isValid = childValidity.isValid
|
|
219
|
+
if (childValidity.code) parentValidity.code = childValidity.code
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const dateToSecondsPrecision = (date) => {
|
|
223
|
+
const dateWithSecondsPrecision = new Date(date)
|
|
224
|
+
dateWithSecondsPrecision.setMilliseconds(0)
|
|
225
|
+
return dateWithSecondsPrecision
|
|
226
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { normalizeStructuredMetaMap, urlToMeta } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
import { fetchOriginalUrlInfo } from "@jsenv/utils/graph/fetch_original_url_info.js"
|
|
4
|
+
import { injectQueryParams } from "@jsenv/utils/urls/url_utils.js"
|
|
5
|
+
import { commonJsToJsModule } from "./cjs_to_esm.js"
|
|
6
|
+
|
|
7
|
+
export const jsenvPluginCommonJs = ({ logLevel, include }) => {
|
|
8
|
+
const structuredMetaMap = normalizeStructuredMetaMap(
|
|
9
|
+
{
|
|
10
|
+
commonjs: include,
|
|
11
|
+
},
|
|
12
|
+
"file://",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: "jsenv:commonjs",
|
|
17
|
+
appliesDuring: "*",
|
|
18
|
+
redirectUrl: {
|
|
19
|
+
js_import_export: (reference) => {
|
|
20
|
+
const { commonjs } = urlToMeta({
|
|
21
|
+
url: reference.url,
|
|
22
|
+
structuredMetaMap,
|
|
23
|
+
})
|
|
24
|
+
if (!commonjs) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
reference.data.commonjs = commonjs
|
|
28
|
+
return injectQueryParams(reference.url, {
|
|
29
|
+
cjs_as_js_module: "",
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
fetchUrlContent: async (urlInfo, context) => {
|
|
34
|
+
const originalUrlInfo = await fetchOriginalUrlInfo({
|
|
35
|
+
urlInfo,
|
|
36
|
+
context,
|
|
37
|
+
searchParam: "cjs_as_js_module",
|
|
38
|
+
// during this fetch we don't want to alter the original file
|
|
39
|
+
// so we consider it as text
|
|
40
|
+
expectedType: "text",
|
|
41
|
+
})
|
|
42
|
+
if (!originalUrlInfo) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
const { content, sourcemap } = await commonJsToJsModule({
|
|
46
|
+
logLevel,
|
|
47
|
+
rootDirectoryUrl: context.rootDirectoryUrl,
|
|
48
|
+
sourceFileUrl: originalUrlInfo.url,
|
|
49
|
+
processEnvNodeEnv:
|
|
50
|
+
context.scenario === "dev" || context.scenario === "test"
|
|
51
|
+
? "development"
|
|
52
|
+
: "production",
|
|
53
|
+
...urlInfo.data.commonjs,
|
|
54
|
+
})
|
|
55
|
+
return {
|
|
56
|
+
type: "js_module",
|
|
57
|
+
contentType: "text/javascript",
|
|
58
|
+
content,
|
|
59
|
+
sourcemap,
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// https://github.com/snowpackjs/snowpack/blob/main/esinstall/src/rollup-plugins/rollup-plugin-wrap-install-targets.ts
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs"
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
+
import { VM as VM2 } from "vm2"
|
|
6
|
+
import resolve from "resolve"
|
|
7
|
+
import isValidIdentifier from "is-valid-identifier"
|
|
8
|
+
import { init, parse } from "cjs-module-lexer"
|
|
9
|
+
|
|
10
|
+
export const rollupPluginCommonJsNamedExports = ({ logger }) => {
|
|
11
|
+
const inputSummaries = {}
|
|
12
|
+
const cjsScannedNamedExports = {}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
async buildStart({ input }) {
|
|
16
|
+
await init()
|
|
17
|
+
|
|
18
|
+
Object.keys(input).forEach((key) => {
|
|
19
|
+
const inputFilePath = input[key]
|
|
20
|
+
const inputFileUrl = pathToFileURL(inputFilePath)
|
|
21
|
+
inputSummaries[inputFileUrl] = {
|
|
22
|
+
all: true,
|
|
23
|
+
default: true,
|
|
24
|
+
namespace: true,
|
|
25
|
+
named: [],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cjsExports =
|
|
29
|
+
detectStaticExports({ logger, fileUrl: inputFileUrl }) ||
|
|
30
|
+
detectExportsUsingSandboxedRuntime({ logger, fileUrl: inputFileUrl })
|
|
31
|
+
|
|
32
|
+
if (cjsExports && cjsExports.length) {
|
|
33
|
+
cjsScannedNamedExports[inputFileUrl] = cjsExports
|
|
34
|
+
}
|
|
35
|
+
input[key] = `jsenv:${inputFileUrl}`
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
resolveId(source) {
|
|
39
|
+
if (source.startsWith("jsenv:")) {
|
|
40
|
+
return source
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
},
|
|
45
|
+
load(id) {
|
|
46
|
+
if (!id.startsWith("jsenv:")) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inputFileUrl = id.substring("jsenv:".length)
|
|
51
|
+
const inputSummary = inputSummaries[inputFileUrl]
|
|
52
|
+
let uniqueNamedExports = inputSummary.named
|
|
53
|
+
const scannedNamedExports = cjsScannedNamedExports[inputFileUrl]
|
|
54
|
+
if (scannedNamedExports) {
|
|
55
|
+
uniqueNamedExports = scannedNamedExports || []
|
|
56
|
+
inputSummary.default = true
|
|
57
|
+
}
|
|
58
|
+
const codeForExports = generateCodeForExports({
|
|
59
|
+
uniqueNamedExports,
|
|
60
|
+
inputSummary,
|
|
61
|
+
inputFileUrl,
|
|
62
|
+
})
|
|
63
|
+
return codeForExports
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* Attempt #1: Static analysis: Lower Fidelity, but faster.
|
|
70
|
+
* Do our best job to statically scan a file for named exports. This uses "cjs-module-lexer", the
|
|
71
|
+
* same CJS export scanner that Node.js uses internally. Very fast, but only works on some modules,
|
|
72
|
+
* depending on how they were build/written/compiled.
|
|
73
|
+
*/
|
|
74
|
+
const detectStaticExports = ({ logger, fileUrl, visited = new Set() }) => {
|
|
75
|
+
const isMainEntrypoint = visited.size === 0
|
|
76
|
+
// Prevent infinite loops via circular dependencies.
|
|
77
|
+
if (visited.has(fileUrl)) {
|
|
78
|
+
return []
|
|
79
|
+
}
|
|
80
|
+
visited.add(fileUrl)
|
|
81
|
+
|
|
82
|
+
const fileContents = readFileSync(new URL(fileUrl), "utf8")
|
|
83
|
+
try {
|
|
84
|
+
const { exports, reexports } = parse(fileContents)
|
|
85
|
+
// If re-exports were detected (`exports.foo = require(...)`) then resolve them here.
|
|
86
|
+
let resolvedReexports = []
|
|
87
|
+
if (reexports.length > 0) {
|
|
88
|
+
reexports.forEach((reexport) => {
|
|
89
|
+
const reExportedFilePath = resolve.sync(reexport, {
|
|
90
|
+
basedir: fileURLToPath(new URL("./", fileUrl)),
|
|
91
|
+
})
|
|
92
|
+
const reExportedFileUrl = pathToFileURL(reExportedFilePath)
|
|
93
|
+
const staticExports = detectStaticExports({
|
|
94
|
+
logger,
|
|
95
|
+
fileUrl: reExportedFileUrl,
|
|
96
|
+
visited,
|
|
97
|
+
})
|
|
98
|
+
if (staticExports) {
|
|
99
|
+
resolvedReexports = [...resolvedReexports, ...staticExports]
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
const resolvedExports = Array.from(
|
|
104
|
+
new Set([...exports, ...resolvedReexports]),
|
|
105
|
+
).filter(isValidNamedExport)
|
|
106
|
+
|
|
107
|
+
if (isMainEntrypoint && resolvedExports.length === 0) {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return resolvedExports
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// Safe to ignore, this is usually due to the file not being CJS.
|
|
114
|
+
logger.debug(`detectStaticExports ${fileUrl}: ${err.message}`)
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Attempt #2b - Sandboxed runtime analysis: More powerful, but slower.
|
|
121
|
+
* This will only work on UMD and very simple CJS files (require not supported).
|
|
122
|
+
* Uses VM2 to run safely sandbox untrusted code (no access no Node.js primitives, just JS).
|
|
123
|
+
* If nothing was detected, return undefined.
|
|
124
|
+
*/
|
|
125
|
+
const detectExportsUsingSandboxedRuntime = ({ logger, fileUrl }) => {
|
|
126
|
+
try {
|
|
127
|
+
const fileContents = readFileSync(new URL(fileUrl), "utf8")
|
|
128
|
+
const vm = new VM2({ wasm: false, fixAsync: false })
|
|
129
|
+
const codeToRun = wrapCodeToRunInVm(fileContents)
|
|
130
|
+
const vmResult = vm.run(codeToRun)
|
|
131
|
+
const exportsResult = Object.keys(vmResult)
|
|
132
|
+
logger.debug(
|
|
133
|
+
`detectExportsUsingSandboxedRuntime success ${fileUrl}: ${exportsResult}`,
|
|
134
|
+
)
|
|
135
|
+
return exportsResult.filter((identifier) => isValidIdentifier(identifier))
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logger.debug(
|
|
138
|
+
`detectExportsUsingSandboxedRuntime error ${fileUrl}: ${err.message}`,
|
|
139
|
+
)
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isValidNamedExport = (name) =>
|
|
145
|
+
name !== "default" && name !== "__esModule" && isValidIdentifier(name)
|
|
146
|
+
|
|
147
|
+
const wrapCodeToRunInVm = (code) => {
|
|
148
|
+
return `const exports = {};
|
|
149
|
+
const module = { exports };
|
|
150
|
+
${code};;
|
|
151
|
+
module.exports;`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const generateCodeForExports = ({
|
|
155
|
+
uniqueNamedExports,
|
|
156
|
+
inputSummary,
|
|
157
|
+
inputFileUrl,
|
|
158
|
+
}) => {
|
|
159
|
+
const from =
|
|
160
|
+
process.platform === "win32"
|
|
161
|
+
? inputFileUrl.slice("file:///".length)
|
|
162
|
+
: inputFileUrl.slice("file://".length)
|
|
163
|
+
const lines = [
|
|
164
|
+
...(inputSummary.namespace ? [stringifyNamespaceReExport({ from })] : []),
|
|
165
|
+
...(inputSummary.default ? [stringifyDefaultReExport({ from })] : []),
|
|
166
|
+
stringifyNamedReExports({
|
|
167
|
+
namedExports: uniqueNamedExports,
|
|
168
|
+
from,
|
|
169
|
+
}),
|
|
170
|
+
]
|
|
171
|
+
return lines.join(`
|
|
172
|
+
`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stringifyNamespaceReExport = ({ from }) => {
|
|
176
|
+
return `export * from "${from}";`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stringifyDefaultReExport = ({ from }) => {
|
|
180
|
+
return `import __jsenv_default_import__ from "${from}";
|
|
181
|
+
export default __jsenv_default_import__;`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stringifyNamedReExports = ({ namedExports, from }) => {
|
|
185
|
+
return `export { ${namedExports.join(",")} } from "${from}";`
|
|
186
|
+
}
|