Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enable-global-virtual-store=true
Comment thread
SoonIter marked this conversation as resolved.
2 changes: 1 addition & 1 deletion e2e/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

exports[`should serialize file content 2`] = `
"\\"use strict\\";
const a = \\"<ROOT>/node_modules/<PNPM_INNER>/@rslib/core/dist/index.js\\";
const a = \\"<PNPM_INNER>/@rslib/core/dist/index.js\\";
"
`;
6 changes: 3 additions & 3 deletions e2e/features.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import { createSnapshotSerializer } from 'path-serializer';
import { expect } from 'vitest';
import { test } from 'vitest';
import { createSnapshotSerializer } from 'path-serializer';

expect.addSnapshotSerializer(
createSnapshotSerializer({
Expand All @@ -20,7 +20,7 @@ const a = "${require.resolve('@rslib/core')}";

expect(fileContent).toMatchInlineSnapshot(`
"use strict";
const a = "<ROOT>/node_modules/<PNPM_INNER>/@rslib/core/dist/index.js";
const a = "<PNPM_INNER>/@rslib/core/dist/index.js";
`);
Comment thread
SoonIter marked this conversation as resolved.
});

Expand All @@ -34,7 +34,7 @@ function _class_private_method_get(receiver, privateSet, fn) {
`;

expect(fileContent).toMatchInlineSnapshot(`
;// CONCATENATED MODULE: ../../../../node_modules/<PNPM_INNER>/@swc/helpers/esm/_class_private_method_get.js
;// CONCATENATED MODULE: <PNPM_INNER>/@swc/helpers/esm/_class_private_method_get.js
function _class_private_method_get(receiver, privateSet, fn) {
if (!privateSet.has(receiver)) throw new TypeError("attempted to get private field on non-instance");

Expand Down
8 changes: 4 additions & 4 deletions e2e/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { expect, test } from 'vitest';
import { createSnapshotSerializer } from 'path-serializer';
import { expect, test } from 'vitest';

const require = createRequire(import.meta.url);

Expand All @@ -23,7 +23,7 @@ test('should serialize <ROOT> and <WORKSPACE>', () => {

test('should serialize <PNPM_INNER>', () => {
expect(require.resolve('@rslib/core')).toMatchInlineSnapshot(
`"<ROOT>/node_modules/<PNPM_INNER>/@rslib/core/dist/index.js"`,
`"<PNPM_INNER>/@rslib/core/dist/index.js"`,
);
Comment thread
SoonIter marked this conversation as resolved.
});

Expand All @@ -38,7 +38,7 @@ test('should serialize Object', () => {
const obj = new MyObj(require.resolve('@rslib/core'));
expect(obj).toMatchInlineSnapshot(`
MyObj {
"attr": "<ROOT>/node_modules/<PNPM_INNER>/@rslib/core/dist/index.js",
"attr": "<PNPM_INNER>/@rslib/core/dist/index.js",
}
`);
Comment thread
SoonIter marked this conversation as resolved.
});
Expand All @@ -50,7 +50,7 @@ const a = "${require.resolve('@rslib/core')}";

expect(fileContent).toMatchInlineSnapshot(`
"\\"use strict\\";
const a = \\"<ROOT>/node_modules/<PNPM_INNER>/@rslib/core/dist/index.js\\";
const a = \\"<PNPM_INNER>/@rslib/core/dist/index.js\\";
"
`);
Comment thread
SoonIter marked this conversation as resolved.

Expand Down
49 changes: 49 additions & 0 deletions e2e/pnpmGlobalVirtualStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createSnapshotSerializer } from 'path-serializer';
import { expect, test } from 'vitest';

expect.addSnapshotSerializer(
createSnapshotSerializer({
root: '/Users/user/project',
}),
);

test('should serialize pnpm global virtual store path (posix)', () => {
const filePath =
'/Users/user/project/node_modules/../../../../../../Library/pnpm/store/v10/links/react/19.2.4/5c6e83be0e5f1f15e83462f0b7655d9d9338d33338bad7cc29bc12f2daa11aa3/node_modules/react/index.js';

expect(filePath).toMatchInlineSnapshot(`"<PNPM_INNER>/react/index.js"`);
});

test('should serialize pnpm global virtual store path with cjs (posix)', () => {
const filePath =
'/Users/user/project/node_modules/../../../../../../Library/pnpm/store/v10/links/react/19.2.4/5c6e83be0e5f1f15e83462f0b7655d9d9338d33338bad7cc29bc12f2daa11aa3/node_modules/react/cjs/react.production.js';

expect(filePath).toMatchInlineSnapshot(
`"<PNPM_INNER>/react/cjs/react.production.js"`,
);
});

test('should serialize pnpm global virtual store path with scoped package', () => {
const filePath =
'/Users/user/project/node_modules/../../../../../../Library/pnpm/store/v10/links/@babel/core/7.25.0/abc123def456/node_modules/@babel/core/lib/index.js';

expect(filePath).toMatchInlineSnapshot(
`"<PNPM_INNER>/@babel/core/lib/index.js"`,
);
});

test('should serialize pnpm global virtual store path (win32)', () => {
const serializer = createSnapshotSerializer({
root: 'D:\\user\\project',
features: {
transformWin32Path: true,
},
});

const filePath =
'D:\\user\\project\\node_modules\\..\\..\\..\\..\\..\\..\\Library\\pnpm\\store\\v10\\links\\react\\19.2.4\\5c6e83be0e5f1f15e83462f0b7655d9d9338d33338bad7cc29bc12f2daa11aa3\\node_modules\\react\\index.js';

expect(serializer.serialize(filePath)).toMatchInlineSnapshot(
`"\\"<PNPM_INNER>/react/index.js\\""`,
);
});
6 changes: 3 additions & 3 deletions e2e/pnpmInnerWin32.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
import { createSnapshotSerializer } from 'path-serializer';
import { expect, test } from 'vitest';

expect.addSnapshotSerializer(
createSnapshotSerializer({
Expand All @@ -21,7 +21,7 @@ test('should serialize <PNPM_INNER> in win32', () => {

expect(fileContent).toMatchInlineSnapshot(`
{
loader: <ROOT>/node_modules/<PNPM_INNER>/css-loader/utils.ts,
loader: <PNPM_INNER>/css-loader/utils.ts,
}
`);
});
Expand All @@ -34,7 +34,7 @@ test('should serialize <PNPM_INNER> with webpack path', () => {

expect(fileContent).toMatchInlineSnapshot(`
{
moduleIdentifier: <ROOT>/node_modules/<PNPM_INNER>/css-loader/dist/cjs.js!<WORKSPACE>/tests/fixtures/css/style.css,
moduleIdentifier: <PNPM_INNER>/css-loader/dist/cjs.js!<WORKSPACE>/tests/fixtures/css/style.css,
}
`);
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"fs-extra": "11.2.0",
"semver": "7.6.3"
},
"packageManager": "pnpm@9.14.4",
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
14 changes: 6 additions & 8 deletions src/createSnapshotSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { pathToFileURL } from 'node:url';
import { applyMatcherReplacement } from './applyMatcherReplacement';
import {
createHomeDirMatchers,
createPnpmInnerMatchers,
createTmpDirMatchers,
} from './matchers';
import { createHomeDirMatchers, createTmpDirMatchers } from './matchers';
import {
normalizeCLR,
normalizeCodeToPosix,
normalizePathToPosix,
} from './normalize';
import { replacePnpmInnerPath } from './pnpm';
import type { PathMatcher, SnapshotSerializerOptions } from './types';

export interface SnapshotSerializer {
Expand Down Expand Up @@ -54,9 +51,6 @@ export function createSnapshotSerializer(
if (replaceRoot && root) {
pathMatchers.push({ mark: 'root', match: root });
}
if (replacePnpmInner) {
pathMatchers.push(...createPnpmInnerMatchers());
}
if (replaceTmpDir) {
pathMatchers.push(...createTmpDirMatchers());
}
Expand Down Expand Up @@ -106,6 +100,10 @@ export function createSnapshotSerializer(

replaced = applyMatcherReplacement(pathMatchers, replaced);

if (replacePnpmInner) {
replaced = replacePnpmInnerPath(replaced);
}

if (transformCLR) {
replaced = normalizeCLR(replaced);
}
Expand Down
15 changes: 0 additions & 15 deletions src/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,3 @@ export const createHomeDirMatchers = (): PathMatcher[] => {
homedir && ret.push({ match: homedir, mark: 'home' });
return ret;
};

export const createPnpmInnerMatchers = (): PathMatcher[] => {
return [
// posix
{
match: /(?<=\/)(\.pnpm\/.+?\/node_modules)(?=\/)/g,
mark: 'pnpmInner',
},
// win32
{
match: /(?<=\\)(\.pnpm\\.+?\\node_modules)(?=\\)/g,
mark: 'pnpmInner',
},
];
};
1 change: 1 addition & 0 deletions src/pnpm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { replacePnpmInnerPath } from './replacePnpmInnerPath';
38 changes: 38 additions & 0 deletions src/pnpm/replacePnpmInnerPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const PNPM_INNER_TOKEN = '<PNPM_INNER>';
const PNPM_INNER_DELIMITERS = /[\s!"']/;

// local virtual store: .pnpm/<pkg>@<ver>/node_modules
const LOCAL_PNPM_POSIX = /(?<=\/)(\.pnpm\/.+?\/node_modules)(?=\/)/g;
const LOCAL_PNPM_WIN32 = /(?<=\\)(\.pnpm\\.+?\\node_modules)(?=\\)/g;
// global virtual store: pnpm/store/<versions>/links/<pkg>/<ver>/<hash>/node_modules
const GLOBAL_PNPM_POSIX =
/(?<=\/)pnpm\/store\/.+?\/links\/.+?\/node_modules(?=\/)/g;

/**
* Replace pnpm virtual store paths with `<PNPM_INNER>` and strip
* the environment-dependent prefix so that local virtual store
* and global virtual store both normalize to `<PNPM_INNER>/pkg/...`.
*/
export function replacePnpmInnerPath(str: string): string {
const replaced = str
.replace(LOCAL_PNPM_POSIX, PNPM_INNER_TOKEN)
.replace(LOCAL_PNPM_WIN32, PNPM_INNER_TOKEN)
.replace(GLOBAL_PNPM_POSIX, PNPM_INNER_TOKEN);

// Strip environment-dependent prefix before <PNPM_INNER>
// e.g. <ROOT>/node_modules/<PNPM_INNER> → <PNPM_INNER>
// Uses string splitting instead of regex to avoid ReDoS.
const parts = replaced.split(PNPM_INNER_TOKEN);
if (parts.length <= 1) return replaced;

for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
let j = part.length - 1;
while (j >= 0 && !PNPM_INNER_DELIMITERS.test(part[j])) {
j--;
}
parts[i] = part.substring(0, j + 1);
}

return parts.join(PNPM_INNER_TOKEN);
}
Loading