import-in-the-middle 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,14 @@
1
+ # http://editorconfig.org
2
+
3
+ root = true
4
+
5
+ [*]
6
+ indent_style = space
7
+ indent_size = 2
8
+ end_of_line = lf
9
+ charset = utf-8
10
+ trim_trailing_whitespace = true
11
+ insert_final_newline = true
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
package/hook.js ADDED
@@ -0,0 +1,182 @@
1
+ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
2
+ //
3
+ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
+
5
+ const specifiers = new Map()
6
+ const isWin = process.platform === "win32"
7
+
8
+ // FIXME: Typescript extensions are added temporarily until we find a better
9
+ // way of supporting arbitrary extensions
10
+ const EXTENSION_RE = /\.(js|mjs|cjs|ts|mts|cts)$/
11
+ const NODE_VERSION = process.versions.node.split('.')
12
+ const NODE_MAJOR = Number(NODE_VERSION[0])
13
+ const NODE_MINOR = Number(NODE_VERSION[1])
14
+
15
+ let entrypoint
16
+
17
+ if (NODE_MAJOR >= 20) {
18
+ getExports = require('./lib/get-exports.js')
19
+ } else {
20
+ getExports = (url) => import(url).then(Object.keys)
21
+ }
22
+
23
+ function hasIitm (url) {
24
+ try {
25
+ return new URL(url).searchParams.has('iitm')
26
+ } catch {
27
+ return false
28
+ }
29
+ }
30
+
31
+ function isIitm (url, meta) {
32
+ return url === meta.url || url === meta.url.replace('hook.mjs', 'hook.js')
33
+ }
34
+
35
+ function deleteIitm (url) {
36
+ let resultUrl
37
+ try {
38
+ const urlObj = new URL(url)
39
+ if (urlObj.searchParams.has('iitm')) {
40
+ urlObj.searchParams.delete('iitm')
41
+ resultUrl = urlObj.href
42
+ if (resultUrl.startsWith('file:node:')) {
43
+ resultUrl = resultUrl.replace('file:', '')
44
+ }
45
+ if (resultUrl.startsWith('file:///node:')) {
46
+ resultUrl = resultUrl.replace('file:///', '')
47
+ }
48
+ } else {
49
+ resultUrl = urlObj.href
50
+ }
51
+ } catch {
52
+ resultUrl = url
53
+ }
54
+ return resultUrl
55
+ }
56
+
57
+ function isNode16AndBiggerOrEqualsThan16_17_0() {
58
+ return NODE_MAJOR === 16 && NODE_MINOR >= 17
59
+ }
60
+
61
+ function isFileProtocol (urlObj) {
62
+ return urlObj.protocol === 'file:'
63
+ }
64
+
65
+ function isNodeProtocol (urlObj) {
66
+ return urlObj.protocol === 'node:'
67
+ }
68
+
69
+ function needsToAddFileProtocol(urlObj) {
70
+ if (NODE_MAJOR === 17) {
71
+ return !isFileProtocol(urlObj)
72
+ }
73
+ if (isNode16AndBiggerOrEqualsThan16_17_0()) {
74
+ return !isFileProtocol(urlObj) && !isNodeProtocol(urlObj)
75
+ }
76
+ return !isFileProtocol(urlObj) && NODE_MAJOR < 18
77
+ }
78
+
79
+
80
+ function addIitm (url) {
81
+ const urlObj = new URL(url)
82
+ urlObj.searchParams.set('iitm', 'true')
83
+ return needsToAddFileProtocol(urlObj) ? 'file:' + urlObj.href : urlObj.href
84
+ }
85
+
86
+ function createHook (meta) {
87
+ async function resolve (specifier, context, parentResolve) {
88
+ const { parentURL = '' } = context
89
+ const newSpecifier = deleteIitm(specifier)
90
+ if (isWin && parentURL.indexOf('file:node') === 0) {
91
+ context.parentURL = ''
92
+ }
93
+ const url = await parentResolve(newSpecifier, context, parentResolve)
94
+ if (parentURL === '' && !EXTENSION_RE.test(url.url)) {
95
+ entrypoint = url.url
96
+ return { url: url.url, format: 'commonjs' }
97
+ }
98
+
99
+ if (isIitm(parentURL, meta) || hasIitm(parentURL)) {
100
+ return url
101
+ }
102
+
103
+ if (context.importAssertions && context.importAssertions.type === 'json') {
104
+ return url
105
+ }
106
+
107
+
108
+ specifiers.set(url.url, specifier)
109
+
110
+ return {
111
+ url: addIitm(url.url),
112
+ shortCircuit: true,
113
+ format: url.format
114
+ }
115
+ }
116
+
117
+ const iitmURL = new URL('lib/register.js', meta.url).toString()
118
+ async function getSource (url, context, parentGetSource) {
119
+ if (hasIitm(url)) {
120
+ const realUrl = deleteIitm(url)
121
+ const exportNames = await getExports(realUrl, context, parentGetSource)
122
+ return {
123
+ source: `
124
+ import { register } from '${iitmURL}'
125
+ import * as namespace from '${url}'
126
+ const set = {}
127
+ ${exportNames.map((n) => `
128
+ let $${n} = namespace.${n}
129
+ export { $${n} as ${n} }
130
+ set.${n} = (v) => {
131
+ $${n} = v
132
+ return true
133
+ }
134
+ `).join('\n')}
135
+ register('${realUrl}', namespace, set, '${specifiers.get(realUrl)}')
136
+ `
137
+ }
138
+ }
139
+
140
+ return parentGetSource(url, context, parentGetSource)
141
+ }
142
+
143
+ // For Node.js 16.12.0 and higher.
144
+ async function load (url, context, parentLoad) {
145
+ if (hasIitm(url)) {
146
+ const { source } = await getSource(url, context, parentLoad)
147
+ return {
148
+ source,
149
+ shortCircuit: true,
150
+ format: 'module'
151
+ }
152
+ }
153
+
154
+ return parentLoad(url, context, parentLoad)
155
+ }
156
+
157
+ if (NODE_MAJOR >= 17 || (NODE_MAJOR === 16 && NODE_MINOR >= 12)) {
158
+ return { load, resolve }
159
+ } else {
160
+ return {
161
+ load,
162
+ resolve,
163
+ getSource,
164
+ getFormat (url, context, parentGetFormat) {
165
+ if (hasIitm(url)) {
166
+ return {
167
+ format: 'module'
168
+ }
169
+ }
170
+ if (url === entrypoint) {
171
+ return {
172
+ format: 'commonjs'
173
+ }
174
+ }
175
+
176
+ return parentGetFormat(url, context, parentGetFormat)
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ module.exports = { createHook }
package/hook.mjs CHANGED
@@ -2,160 +2,8 @@
2
2
  //
3
3
  // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
4
 
5
- const specifiers = new Map()
6
- const isWin = process.platform === "win32"
5
+ import { createHook } from './hook.js'
7
6
 
7
+ const { load, resolve, getFormat, getSource } = createHook(import.meta)
8
8
 
9
- // FIXME: Typescript extensions are added temporarily until we find a better
10
- // way of supporting arbitrary extensions
11
- const EXTENSION_RE = /\.(js|mjs|cjs|ts|mts|cts)$/
12
- const NODE_VERSION = process.versions.node.split('.')
13
- const NODE_MAJOR = Number(NODE_VERSION[0])
14
- const NODE_MINOR = Number(NODE_VERSION[1])
15
-
16
- let entrypoint
17
-
18
- function hasIitm (url) {
19
- try {
20
- return new URL(url).searchParams.has('iitm')
21
- } catch {
22
- return false
23
- }
24
- }
25
-
26
- function deleteIitm (url) {
27
- let resultUrl
28
- try {
29
- const urlObj = new URL(url)
30
- if (urlObj.searchParams.has('iitm')) {
31
- urlObj.searchParams.delete('iitm')
32
- resultUrl = urlObj.href
33
- if (resultUrl.startsWith('file:node:')) {
34
- resultUrl = resultUrl.replace('file:', '')
35
- }
36
- if (resultUrl.startsWith('file:///node:')) {
37
- resultUrl = resultUrl.replace('file:///', '')
38
- }
39
- } else {
40
- resultUrl = urlObj.href
41
- }
42
- } catch {
43
- resultUrl = url
44
- }
45
- return resultUrl
46
- }
47
-
48
- function isNode16AndBiggerOrEqualsThan16_17_0() {
49
- return NODE_MAJOR === 16 && NODE_MINOR >= 17
50
- }
51
-
52
- function isFileProtocol (urlObj) {
53
- return urlObj.protocol === 'file:'
54
- }
55
-
56
- function isNodeProtocol (urlObj) {
57
- return urlObj.protocol === 'node:'
58
- }
59
-
60
- function needsToAddFileProtocol(urlObj) {
61
- if (NODE_MAJOR === 17) {
62
- return !isFileProtocol(urlObj)
63
- }
64
- if (isNode16AndBiggerOrEqualsThan16_17_0()) {
65
- return !isFileProtocol(urlObj) && !isNodeProtocol(urlObj)
66
- }
67
- return !isFileProtocol(urlObj) && NODE_MAJOR < 18
68
- }
69
-
70
-
71
- function addIitm (url) {
72
- const urlObj = new URL(url)
73
- urlObj.searchParams.set('iitm', 'true')
74
- return needsToAddFileProtocol(urlObj) ? 'file:' + urlObj.href : urlObj.href
75
- }
76
-
77
- export async function resolve (specifier, context, parentResolve) {
78
- const { parentURL = '' } = context
79
- const newSpecifier = deleteIitm(specifier)
80
- if (isWin && parentURL.indexOf('file:node') === 0) {
81
- context.parentURL = ''
82
- }
83
- const url = await parentResolve(newSpecifier, context, parentResolve)
84
- if (parentURL === '' && !EXTENSION_RE.test(url.url)) {
85
- entrypoint = url.url
86
- return { url: url.url, format: 'commonjs' }
87
- }
88
-
89
- if (parentURL === import.meta.url || hasIitm(parentURL)) {
90
- return url
91
- }
92
-
93
- if (context.importAssertions && context.importAssertions.type === 'json') {
94
- return url
95
- }
96
-
97
-
98
- specifiers.set(url.url, specifier)
99
-
100
- return {
101
- url: addIitm(url.url),
102
- shortCircuit: true
103
- }
104
- }
105
-
106
- export function getFormat (url, context, parentGetFormat) {
107
- if (hasIitm(url)) {
108
- return {
109
- format: 'module'
110
- }
111
- }
112
- if (url === entrypoint) {
113
- return {
114
- format: 'commonjs'
115
- }
116
- }
117
-
118
- return parentGetFormat(url, context, parentGetFormat)
119
- }
120
-
121
- const iitmURL = new URL('lib/register.js', import.meta.url).toString()
122
- export async function getSource (url, context, parentGetSource) {
123
- if (hasIitm(url)) {
124
- const realUrl = deleteIitm(url)
125
- const realModule = await import(realUrl)
126
- const exportNames = Object.keys(realModule)
127
- return {
128
- source: `
129
- import { register } from '${iitmURL}'
130
- import * as namespace from '${url}'
131
- const set = {}
132
- ${exportNames.map((n) => `
133
- let $${n} = namespace.${n}
134
- export { $${n} as ${n} }
135
- set.${n} = (v) => {
136
- $${n} = v
137
- return true
138
- }
139
- `).join('\n')}
140
- register('${realUrl}', namespace, set, '${specifiers.get(realUrl)}')
141
- `
142
- }
143
- }
144
-
145
- return parentGetSource(url, context, parentGetSource)
146
- }
147
-
148
- // For Node.js 16.12.0 and higher.
149
- export async function load (url, context, parentLoad) {
150
- if (hasIitm(url)) {
151
- const { source } = await getSource(url, context)
152
- return {
153
- source,
154
- shortCircuit: true,
155
- format: 'module'
156
- }
157
- }
158
-
159
- return parentLoad(url, context, parentLoad)
160
- }
161
-
9
+ export { load, resolve, getFormat, getSource }
@@ -0,0 +1,97 @@
1
+ 'use strict'
2
+
3
+ const { Parser } = require('acorn')
4
+ const { importAssertions } = require('acorn-import-assertions');
5
+
6
+ const acornOpts = {
7
+ ecmaVersion: 'latest',
8
+ sourceType: 'module'
9
+ }
10
+
11
+ const parser = Parser.extend(importAssertions)
12
+
13
+ function warn (txt) {
14
+ process.emitWarning(txt, 'get-esm-exports')
15
+ }
16
+
17
+ function getEsmExports (moduleStr) {
18
+ const exportedNames = new Set()
19
+ const tree = parser.parse(moduleStr, acornOpts)
20
+ for (const node of tree.body) {
21
+ if (!node.type.startsWith('Export')) continue
22
+ switch (node.type) {
23
+ case 'ExportNamedDeclaration':
24
+ if (node.declaration) {
25
+ parseDeclaration(node, exportedNames)
26
+ } else {
27
+ parseSpecifiers(node, exportedNames)
28
+ }
29
+ break
30
+ case 'ExportDefaultDeclaration':
31
+ exportedNames.add('default')
32
+ break
33
+ case 'ExportAllDeclaration':
34
+ if (node.exported) {
35
+ exportedNames.add(node.exported.name)
36
+ } else {
37
+ exportedNames.add('*')
38
+ }
39
+ break
40
+ default:
41
+ warn('unrecognized export type: ' + node.type)
42
+ }
43
+ }
44
+ return Array.from(exportedNames)
45
+ }
46
+
47
+ function parseDeclaration (node, exportedNames) {
48
+ switch (node.declaration.type) {
49
+ case 'FunctionDeclaration':
50
+ exportedNames.add(node.declaration.id.name)
51
+ break
52
+ case 'VariableDeclaration':
53
+ for (const varDecl of node.declaration.declarations) {
54
+ parseVariableDeclaration(varDecl, exportedNames)
55
+ }
56
+ break
57
+ case 'ClassDeclaration':
58
+ exportedNames.add(node.declaration.id.name)
59
+ break
60
+ default:
61
+ warn('unknown declaration type: ' + node.delcaration.type)
62
+ }
63
+ }
64
+
65
+ function parseVariableDeclaration (node, exportedNames) {
66
+ switch (node.id.type) {
67
+ case 'Identifier':
68
+ exportedNames.add(node.id.name)
69
+ break
70
+ case 'ObjectPattern':
71
+ for (const prop of node.id.properties) {
72
+ exportedNames.add(prop.value.name)
73
+ }
74
+ break
75
+ case 'ArrayPattern':
76
+ for (const elem of node.id.elements) {
77
+ exportedNames.add(elem.name)
78
+ }
79
+ break
80
+ default:
81
+ warn('unknown variable declaration type: ' + node.id.type)
82
+ }
83
+ }
84
+
85
+ function parseSpecifiers (node, exportedNames) {
86
+ for (const specifier of node.specifiers) {
87
+ if (specifier.exported.type === 'Identifier') {
88
+ exportedNames.add(specifier.exported.name)
89
+ } else if (specifier.exported.type === 'Literal') {
90
+ exportedNames.add(specifier.exported.value)
91
+ } else {
92
+ warn('unrecognized specifier type: ' + specifier.exported.type)
93
+ }
94
+ }
95
+ }
96
+
97
+ module.exports = getEsmExports
@@ -0,0 +1,51 @@
1
+ 'use strict'
2
+
3
+ const getEsmExports = require('./get-esm-exports.js')
4
+ const { parse: getCjsExports } = require('cjs-module-lexer')
5
+ const fs = require('fs')
6
+ const { fileURLToPath } = require('url')
7
+
8
+ function addDefault(arr) {
9
+ return Array.from(new Set(['default', ...arr]))
10
+ }
11
+
12
+ async function getExports (url, context, parentLoad) {
13
+ // `parentLoad` gives us the possibility of getting the source
14
+ // from an upstream loader. This doesn't always work though,
15
+ // so later on we fall back to reading it from disk.
16
+ const parentCtx = await parentLoad(url, context)
17
+ let source = parentCtx.source
18
+ const format = parentCtx.format
19
+
20
+ // TODO support non-node/file urls somehow?
21
+ if (format === 'builtin') {
22
+ // Builtins don't give us the source property, so we're stuck
23
+ // just requiring it to get the exports.
24
+ return addDefault(Object.keys(require(url)))
25
+ }
26
+
27
+ if (!source) {
28
+ // Sometimes source is retrieved by parentLoad, sometimes it isn't.
29
+ source = fs.readFileSync(fileURLToPath(url), 'utf8')
30
+ }
31
+
32
+ if (format === 'module') {
33
+ return getEsmExports(source)
34
+ }
35
+ if (format === 'commonjs') {
36
+ return addDefault(getCjsExports(source).exports)
37
+ }
38
+
39
+ // At this point our `format` is either undefined or not known by us. Fall
40
+ // back to parsing as ESM/CJS.
41
+ const esmExports = getEsmExports(source)
42
+ if (!esmExports.length) {
43
+ // TODO(bengl) it's might be possible to get here if somehow the format
44
+ // isn't set at first and yet we have an ESM module with no exports.
45
+ // I couldn't construct an example that would do this, so maybe it's
46
+ // impossible?
47
+ return addDefault(getCjsExports(source).exports)
48
+ }
49
+ }
50
+
51
+ module.exports = getExports
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "c8 --check-coverage --lines 88 imhotap --runner test/runtest --files test/{hook,low-level,other}/*",
8
- "test-win": "c8 --check-coverage --lines 88 imhotap --runner test\\runtest.bat --files test/{hook,low-level,other}/*",
7
+ "test": "c8 --check-coverage --lines 85 imhotap --runner test/runtest --files test/{hook,low-level,other,get-esm-exports}/*",
8
+ "test-win": "c8 --check-coverage --lines 85 imhotap --runner test\\runtest.bat --files test/{hook,low-level,other,get-esm-exports}/*",
9
9
  "test:ts": "c8 imhotap --runner test/runtest --files test/typescript/*.test.mts",
10
10
  "test-win:ts": "c8 imhotap --runner test\\runtest.bat --files test/typescript/*.test.mts",
11
- "coverage": "c8 --reporter html imhotap --runner test/runtest --files test/{hook,low-level,other}/* && echo '\nNow open coverage/index.html\n'"
11
+ "coverage": "c8 --reporter html imhotap --runner test/runtest --files test/{hook,low-level,other,get-esm-exports}/* && echo '\nNow open coverage/index.html\n'"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
@@ -36,6 +36,9 @@
36
36
  "typescript": "^4.7.4"
37
37
  },
38
38
  "dependencies": {
39
+ "acorn": "^8.8.2",
40
+ "acorn-import-assertions": "^1.9.0",
41
+ "cjs-module-lexer": "^1.2.2",
39
42
  "module-details-from-path": "^1.0.3"
40
43
  }
41
44
  }
@@ -0,0 +1,32 @@
1
+ // Exporting declarations
2
+ export let name1, name2/*, … */; // also var //| name1,name2
3
+ export const name1 = 1, name2 = 2/*, … */; // also var, let //| name1,name2
4
+ export function functionName() { /* … */ } //| functionName
5
+ export class ClassName { /* … */ } //| ClassName
6
+ export function* generatorFunctionName() { /* … */ } //| generatorFunctionName
7
+ export const { name1, name2: bar } = o; //| name1,bar
8
+ export const [ name1, name2 ] = array; //| name1,name2
9
+
10
+ // Export list
11
+ let name1, nameN; export { name1, /* …, */ nameN }; //| name1,nameN
12
+ let variable1, variable2, nameN; export { variable1 as name1, variable2 as name2, /* …, */ nameN }; //| name1,name2,nameN
13
+ let variable1; export { variable1 as "string name" }; //| string name
14
+ let name1; export { name1 as default /*, … */ }; //| default
15
+
16
+ // Default exports
17
+ export default expression; //| default
18
+ export default function functionName() { /* … */ } //| default
19
+ export default class ClassName { /* … */ } //| default
20
+ export default function* generatorFunctionName() { /* … */ } //| default
21
+ export default function () { /* … */ } //| default
22
+ export default class { /* … */ } //| default
23
+ export default function* () { /* … */ } //| default
24
+
25
+ // Aggregating modules
26
+ export * from "module-name"; //| *
27
+ export * as name1 from "module-name"; //| name1
28
+ export { name1, /* …, */ nameN } from "module-name"; //| name1,nameN
29
+ export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name"; //| name1,name2,nameN
30
+ export { default, /* …, */ } from "module-name"; //| default
31
+ export { default as name1 } from "module-name"; //| name1
32
+
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ const getEsmExports = require('../../lib/get-esm-exports.js')
4
+ const fs = require('fs')
5
+ const assert = require('assert')
6
+ const path = require('path')
7
+
8
+ const fixturePath = path.join(__dirname, '../fixtures/esm-exports.txt')
9
+ const fixture = fs.readFileSync(fixturePath, 'utf8')
10
+
11
+ fixture.split('\n').forEach(line => {
12
+ if (!line.includes(' //| ')) return
13
+ const [mod, testStr] = line.split(' //| ')
14
+ const expectedNames = testStr.split(',').map(x => x.trim())
15
+ if (expectedNames[0] === '') {
16
+ expectedNames.length = 0
17
+ }
18
+ const names = getEsmExports(mod)
19
+ assert.deepEqual(expectedNames, names)
20
+ console.log(`${mod}\n ✅ contains exports: ${testStr}`)
21
+ })
22
+
23
+ // // Generate fixture data
24
+ // fixture.split('\n').forEach(line => {
25
+ // if (!line.includes('export ')) {
26
+ // console.log(line)
27
+ // return
28
+ // }
29
+ // const names = getEsmExports(line)
30
+ // console.log(line, '//|', names.join(','))
31
+ // })
@@ -0,0 +1,5 @@
1
+ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
2
+ //
3
+ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
+
5
+ // Empty file just to validate the loader is not crashing.