@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 ADDED
@@ -0,0 +1,2 @@
1
+ export { jsenvPluginCommonJs } from "./src/jsenv_plugin_commonjs.js"
2
+ export { commonJsToJsModule } from "./src/cjs_to_esm.js"
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
+ }