Skip to content

Commit

Permalink
fix: nyc coverage compatibility (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Jun 21, 2024
1 parent f714d74 commit 26d633c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 15 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@types/cross-spawn": "^6.0.6",
"@types/node": "^20.14.1",
"@types/split2": "^4.2.3",
"append-transform": "^2.0.0",
"cachedir": "^2.4.0",
"chokidar": "^3.6.0",
"clean-pkg-json": "^1.2.0",
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 69 additions & 15 deletions src/cjs/api/module-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,70 @@ const transformExtensions = [
'.mjs',
] as const;

const cloneExtensions = <ObjectType extends object>(
extensions: ObjectType,
) => {
const cloneTo: ObjectType = Object.create(Object.getPrototypeOf(extensions));

// Preserves setters if they exist (e.g. nyc via append-transform)
const descriptors = Object.getOwnPropertyDescriptors(extensions);
for (const property in descriptors) {
if (Object.hasOwn(descriptors, property)) {
Object.defineProperty(cloneTo, property, descriptors[property]);
}
}

return cloneTo;
};

const safeSet = <T extends Record<string, unknown>>(
object: T,
property: keyof T,
value: T[keyof T],
descriptor?: {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
},
) => {
const existingDescriptor = Object.getOwnPropertyDescriptor(object, property);

// If setter is provided, use it
if (existingDescriptor?.set) {
object[property] = value;
} else if (
!existingDescriptor
|| existingDescriptor.configurable
) {
Object.defineProperty(object, property, {
value,
enumerable: existingDescriptor?.enumerable || descriptor?.enumerable,
writable: (
descriptor?.writable
?? (
existingDescriptor
? existingDescriptor.writable
: true
)
),
configurable: (
descriptor?.configurable
?? (
existingDescriptor
? existingDescriptor.configurable
: true
)
),
});
}
};

export const createExtensions = (
extendExtensions: NodeJS.RequireExtensions,
namespace?: string,
) => {
// Clone Module._extensions with null prototype
const extensions: NodeJS.RequireExtensions = Object.assign(
Object.create(null),
extendExtensions,
);
const extensions = cloneExtensions(extendExtensions);

const defaultLoader = extensions['.js'];

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

for (const extension of implicitlyResolvableExtensions) {
const descriptor = Object.getOwnPropertyDescriptor(extensions, extension);
Object.defineProperty(extensions, extension, {
value: transformer,

safeSet(extensions, extension, transformer, {
/**
* Registeration needs to be enumerable for some 3rd party libraries
* https://github.com/gulpjs/rechoir/blob/v0.8.0/index.js#L21 (used by Webpack CLI)
*
* If the extension already exists, inherit its enumerable property
* If not, only expose if it's not namespaced
*/
enumerable: descriptor?.enumerable || !namespace,
enumerable: !namespace,
writable: true,
configurable: true,
});
}

Expand All @@ -133,15 +186,16 @@ export const createExtensions = (
* That said, it's actually ".js" and ".mjs" that get special treatment
* rather than ".cjs" (it might as well be ".random-ext")
*/
Object.defineProperty(extensions, '.mjs', {
value: transformer,

safeSet(extensions, '.mjs', transformer, {
/**
* Prevent Object.keys from detecting these extensions
* enumerable defaults to whatever is already set, but if not set, it's false
*
* This prevent Object.keys from detecting these extensions
* when CJS loader iterates over the possible extensions
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609
*/
enumerable: false,
writable: true,
configurable: true,
});

return extensions;
Expand Down
41 changes: 41 additions & 0 deletions tests/specs/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'node:path';
import { execaNode } from 'execa';
import { testSuite, expect } from 'manten';
import { createFixture } from 'fs-fixture';
Expand Down Expand Up @@ -94,6 +95,46 @@ export default testSuite(({ describe }, node: NodeApis) => {
});
});

test('works with append-transform (nyc)', async () => {
await using fixture = await createFixture({
'index.js': `
import path from 'node:path';
import './ts.ts'
`,
'ts.ts': 'export const ts = "ts" as string',
'hook.js': `
const path = require('path');
const appendTransform = require('append-transform')
appendTransform((code, filename) => {
if (filename.endsWith(path.sep + 'index.js')) {
console.log('js working');
}
return code;
});
appendTransform((code, filename) => {
if (filename.endsWith(path.sep + 'ts.ts')) {
console.log('ts working');
}
return code;
}, '.ts');
`,
node_modules: ({ symlink }) => symlink(path.resolve('node_modules'), 'junction'),
});

const { stdout } = await execaNode('./index.js', {
cwd: fixture.path,
nodePath: node.path,
nodeOptions: [
'--require',
'./hook.js',
'--require',
tsxCjsPath,
],
});

expect(stdout).toBe('js working\nts working');
});

test('register / unregister', async () => {
await using fixture = await createFixture({
'register.cjs': `
Expand Down

0 comments on commit 26d633c

Please sign in to comment.