Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/package-extractor-test-02](./build-tests/package-extractor-test-02/) | This project is used by tests in the @rushstack/package-extractor package. |
| [/build-tests/package-extractor-test-03](./build-tests/package-extractor-test-03/) | This project is used by tests in the @rushstack/package-extractor package. |
| [/build-tests/package-extractor-test-04](./build-tests/package-extractor-test-04/) | This project is used by tests in the @rushstack/package-extractor package. |
| [/build-tests/package-extractor-test-05](./build-tests/package-extractor-test-05/) | This project is used by tests in the @rushstack/package-extractor package. |
| [/build-tests/run-scenarios-helpers](./build-tests/run-scenarios-helpers/) | Helpers for the *-scenarios test projects. |
| [/build-tests/rush-amazon-s3-build-cache-plugin-integration-test](./build-tests/rush-amazon-s3-build-cache-plugin-integration-test/) | Tests connecting to an amazon S3 endpoint |
| [/build-tests/rush-lib-declaration-paths-test](./build-tests/rush-lib-declaration-paths-test/) | This project ensures all of the paths in rush-lib/lib/... have imports that resolve correctly. If this project builds, all `lib/**/*.d.ts` files in the `@microsoft/rush-lib` package are valid. |
Expand Down
16 changes: 16 additions & 0 deletions build-tests/package-extractor-test-05/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "package-extractor-test-05",
"description": "This project is used by tests in the @rushstack/package-extractor package.",
"version": "1.0.0",
"private": true,
"main": "./dist/index.js",
"files": [
"dist"
],
"scripts": {
"_phase:build": ""
},
"dependencies": {
"@rushstack/node-core-library": "workspace:*"
}
}
1 change: 1 addition & 0 deletions build-tests/package-extractor-test-05/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/package-extractor",
"comment": "preventing duplicate-copy conflicts across npm-packlist versions",
"type": "patch"
}
],
"packageName": "@rushstack/package-extractor"
}
6 changes: 6 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

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

9 changes: 8 additions & 1 deletion libraries/package-extractor/src/PackageExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,14 @@ export class PackageExtractor {
}
);
const npmPackFiles: string[] = await walkerPromise;
return npmPackFiles;

// npm-packlist@v5 (uses glob@v8) returns "./dist/index.js" for pattern: "./dist/index.js",
// whereas npm-packlist@v2 (uses glob@v7) returns an empty array.
// This may cause duplicate files in the result list, leading to copying conflicts
// in the AssetHandler.ts includeAssetAsync method.
// To avoid this issue, we will normalize the file paths to be relative to the package root and remove duplicates.
const normalizedFiles: string[] = Array.from(new Set(npmPackFiles.map((file) => path.normalize(file))));
return normalizedFiles;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key questions for this change:

  1. Is this a perf concern? We are synchronously calling path.normalize() potentially thousands of times, and allocating multiple data structures containing thousands of elements. How many thousands? It is not the full --create-archive list from rush deploy, but more like the total number of files under a single Rush project. An imperative loop here could be more efficient for time/memory, but the costs are probably already tiny compared to the preceding work required to construct npmPackFiles.

  2. Is it the right fix? The problem did not exist with npm-packlist@2, it is apparently npm-packlist@5 that started returning duplicate paths. Why should a packlist contain duplicates? Aren't duplicate paths a bug? If so, then this "fix" is reversing the effects of a bug, which wouldn't be necessary if we simply fix the bug. (More evidence: npm-packlist is maintained by NPM, Inc., who are known for "how can this have no unit test???" type regressions.)

    If it's a bug, then other tools that use npm-packlist should have the same bug. And here we go, PNPM seems to have encountered the same issue:

    fix: packing the same file twice during publish pnpm/pnpm#7250

    That diff interestingly has a similar deduplication line:

    const files = Array.from(new Set((await packlist(src)).map((f) => path.join(f))))

    ...but if you look closely, it is not calling path.normalize(). It seems to be deduplicating for some other reason. PNPM's real fix was to upgrade npm-packlist:

    The fix was to update npm-packlist to the latest version.

    ...which at the time, they accomplished by introducing a wrapper package @pnpm/fs.packlist.

Copy link
Copy Markdown
Collaborator

@octogonz octogonz Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't downgrade, because that would undo the benefit of PR #5575.

So, one alternative fix is to upgrade to npm-packlist (whose latest version is now already 10.x).

BUT, consider that PNPM's @pnpm/fs.packlist has eliminated its dependency on arborist, so today it is merely:

  • A hardwired dependency on known-good "npm-packlist": "5.1.3"
  • A small amount of normalizing/deduplicating logic in index.ts.

This seems like exactly what rush deploy needs. And Rush primarily uses PNPM, I think we would get more consistent/stable results by replacing our npm-packlist with @pnpm/fs.packlist. That seems like the best solution.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said:

So my recommendation is to merge+publish PR #5720, then do a follow up PR to implement the @pnpm/fs.packlist approach.

}

Comment thread
octogonz marked this conversation as resolved.
/**
Expand Down
12 changes: 12 additions & 0 deletions libraries/package-extractor/src/test/PackageExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ const project1PackageName: string = 'package-extractor-test-01';
const project2PackageName: string = 'package-extractor-test-02';
const project3PackageName: string = 'package-extractor-test-03';
const project4PackageName: string = 'package-extractor-test-04';
const project5PackageName: string = 'package-extractor-test-05';
const project1RelativePath: string = path.join('build-tests', project1PackageName);
const project2RelativePath: string = path.join('build-tests', project2PackageName);
const project3RelativePath: string = path.join('build-tests', project3PackageName);
const project4RelativePath: string = path.join('build-tests', project4PackageName);
const project5RelativePath: string = path.join('build-tests', project5PackageName);
const project1Path: string = path.join(repoRoot, project1RelativePath);
const project2Path: string = path.resolve(repoRoot, project2RelativePath);
const project3Path: string = path.resolve(repoRoot, project3RelativePath);
const project4Path: string = path.resolve(repoRoot, project4RelativePath);
const project5Path: string = path.resolve(repoRoot, project5RelativePath);

function getDefaultProjectConfigurations(): IExtractorProjectConfiguration[] {
return [
Expand Down Expand Up @@ -619,4 +622,13 @@ describe(PackageExtractor.name, () => {
Sort.sortBy(metadata.projects, (x) => x.path);
expect(metadata).toMatchSnapshot();
});

it('should normalize and remove duplicate file paths', async () => {
await FileSystem.writeFileAsync(path.join(project5Path, 'dist', 'index.js'), '', {
ensureFolderExists: true
});
const result = await PackageExtractor.getPackageIncludedFilesAsync(project5Path);
// To make the test work on both Windows and *nix, need to normalize the paths to posix style
expect(result.map(path.posix.normalize)).toEqual(['dist/index.js', 'package.json']);
});
});
6 changes: 6 additions & 0 deletions rush.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,12 @@
"reviewCategory": "tests",
"shouldPublish": false
},
{
"packageName": "package-extractor-test-05",
"projectFolder": "build-tests/package-extractor-test-05",
"reviewCategory": "tests",
"shouldPublish": false
},
{
"packageName": "rush-mcp-example-plugin",
"projectFolder": "build-tests/rush-mcp-example-plugin",
Expand Down
Loading