Skip to content

Commit 26d633c

Browse files
committedJun 21, 2024
fix: nyc coverage compatibility (#54)
1 parent f714d74 commit 26d633c

File tree

4 files changed

+136
-15
lines changed

4 files changed

+136
-15
lines changed
 

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@types/cross-spawn": "^6.0.6",
8989
"@types/node": "^20.14.1",
9090
"@types/split2": "^4.2.3",
91+
"append-transform": "^2.0.0",
9192
"cachedir": "^2.4.0",
9293
"chokidar": "^3.6.0",
9394
"clean-pkg-json": "^1.2.0",

‎pnpm-lock.yaml

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cjs/api/module-extensions.ts

+69-15
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,70 @@ const transformExtensions = [
2323
'.mjs',
2424
] as const;
2525

26+
const cloneExtensions = <ObjectType extends object>(
27+
extensions: ObjectType,
28+
) => {
29+
const cloneTo: ObjectType = Object.create(Object.getPrototypeOf(extensions));
30+
31+
// Preserves setters if they exist (e.g. nyc via append-transform)
32+
const descriptors = Object.getOwnPropertyDescriptors(extensions);
33+
for (const property in descriptors) {
34+
if (Object.hasOwn(descriptors, property)) {
35+
Object.defineProperty(cloneTo, property, descriptors[property]);
36+
}
37+
}
38+
39+
return cloneTo;
40+
};
41+
42+
const safeSet = <T extends Record<string, unknown>>(
43+
object: T,
44+
property: keyof T,
45+
value: T[keyof T],
46+
descriptor?: {
47+
enumerable?: boolean;
48+
configurable?: boolean;
49+
writable?: boolean;
50+
},
51+
) => {
52+
const existingDescriptor = Object.getOwnPropertyDescriptor(object, property);
53+
54+
// If setter is provided, use it
55+
if (existingDescriptor?.set) {
56+
object[property] = value;
57+
} else if (
58+
!existingDescriptor
59+
|| existingDescriptor.configurable
60+
) {
61+
Object.defineProperty(object, property, {
62+
value,
63+
enumerable: existingDescriptor?.enumerable || descriptor?.enumerable,
64+
writable: (
65+
descriptor?.writable
66+
?? (
67+
existingDescriptor
68+
? existingDescriptor.writable
69+
: true
70+
)
71+
),
72+
configurable: (
73+
descriptor?.configurable
74+
?? (
75+
existingDescriptor
76+
? existingDescriptor.configurable
77+
: true
78+
)
79+
),
80+
});
81+
}
82+
};
83+
2684
export const createExtensions = (
2785
extendExtensions: NodeJS.RequireExtensions,
2886
namespace?: string,
2987
) => {
3088
// Clone Module._extensions with null prototype
31-
const extensions: NodeJS.RequireExtensions = Object.assign(
32-
Object.create(null),
33-
extendExtensions,
34-
);
89+
const extensions = cloneExtensions(extendExtensions);
3590

3691
const defaultLoader = extensions['.js'];
3792

@@ -105,22 +160,20 @@ export const createExtensions = (
105160
* Any file requested with an explicit extension will be loaded using the .js loader:
106161
* https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430
107162
*/
108-
extensions['.js'] = transformer;
163+
safeSet(extensions, '.js', transformer);
109164

110165
for (const extension of implicitlyResolvableExtensions) {
111-
const descriptor = Object.getOwnPropertyDescriptor(extensions, extension);
112-
Object.defineProperty(extensions, extension, {
113-
value: transformer,
114-
166+
safeSet(extensions, extension, transformer, {
115167
/**
116168
* Registeration needs to be enumerable for some 3rd party libraries
117169
* https://github.com/gulpjs/rechoir/blob/v0.8.0/index.js#L21 (used by Webpack CLI)
118170
*
119171
* If the extension already exists, inherit its enumerable property
120172
* If not, only expose if it's not namespaced
121173
*/
122-
enumerable: descriptor?.enumerable || !namespace,
174+
enumerable: !namespace,
123175
writable: true,
176+
configurable: true,
124177
});
125178
}
126179

@@ -133,15 +186,16 @@ export const createExtensions = (
133186
* That said, it's actually ".js" and ".mjs" that get special treatment
134187
* rather than ".cjs" (it might as well be ".random-ext")
135188
*/
136-
Object.defineProperty(extensions, '.mjs', {
137-
value: transformer,
138-
189+
safeSet(extensions, '.mjs', transformer, {
139190
/**
140-
* Prevent Object.keys from detecting these extensions
191+
* enumerable defaults to whatever is already set, but if not set, it's false
192+
*
193+
* This prevent Object.keys from detecting these extensions
141194
* when CJS loader iterates over the possible extensions
142195
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609
143196
*/
144-
enumerable: false,
197+
writable: true,
198+
configurable: true,
145199
});
146200

147201
return extensions;

‎tests/specs/api.ts

+41
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'node:path';
12
import { execaNode } from 'execa';
23
import { testSuite, expect } from 'manten';
34
import { createFixture } from 'fs-fixture';
@@ -94,6 +95,46 @@ export default testSuite(({ describe }, node: NodeApis) => {
9495
});
9596
});
9697

98+
test('works with append-transform (nyc)', async () => {
99+
await using fixture = await createFixture({
100+
'index.js': `
101+
import path from 'node:path';
102+
import './ts.ts'
103+
`,
104+
'ts.ts': 'export const ts = "ts" as string',
105+
'hook.js': `
106+
const path = require('path');
107+
const appendTransform = require('append-transform')
108+
appendTransform((code, filename) => {
109+
if (filename.endsWith(path.sep + 'index.js')) {
110+
console.log('js working');
111+
}
112+
return code;
113+
});
114+
appendTransform((code, filename) => {
115+
if (filename.endsWith(path.sep + 'ts.ts')) {
116+
console.log('ts working');
117+
}
118+
return code;
119+
}, '.ts');
120+
`,
121+
node_modules: ({ symlink }) => symlink(path.resolve('node_modules'), 'junction'),
122+
});
123+
124+
const { stdout } = await execaNode('./index.js', {
125+
cwd: fixture.path,
126+
nodePath: node.path,
127+
nodeOptions: [
128+
'--require',
129+
'./hook.js',
130+
'--require',
131+
tsxCjsPath,
132+
],
133+
});
134+
135+
expect(stdout).toBe('js working\nts working');
136+
});
137+
97138
test('register / unregister', async () => {
98139
await using fixture = await createFixture({
99140
'register.cjs': `

0 commit comments

Comments
 (0)
Please sign in to comment.