@pnpm/releasing.exportable-manifest 1100.3.3 → 1100.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pnpm/exportable-manifest
2
2
 
3
+ ## 1100.3.5
4
+
5
+ ### Patch Changes
6
+
7
+ - `pnpm publish` now includes bare `README` files and README files with Markdown extensions such as `readme.markdown` in registry metadata [#12704](https://github.com/pnpm/pnpm/issues/12704).
8
+
9
+ ## 1100.3.4
10
+
11
+ ### Patch Changes
12
+
13
+ - `pnpm install` now works in StackBlitz WebContainers on projects without a lockfile. It used to fail there with `ENOENT ... pnpm-lock.yaml`, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls [#15649](https://github.com/pnpm/pnpm/issues/15649).
14
+
15
+ - Updated dependencies:
16
+ - @pnpm/catalogs.resolver@1100.1.1
17
+ - @pnpm/error@1100.2.1
18
+ - @pnpm/resolving.jsr-specifier-parser@1100.0.9
19
+ - @pnpm/workspace.project-manifest-reader@1100.1.1
20
+
3
21
  ## 1100.3.3
4
22
 
5
23
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -3,6 +3,29 @@ import type { Hooks } from '@pnpm/hooks.pnpmfile';
3
3
  import type { ProjectManifest } from '@pnpm/types';
4
4
  import { type ExportedManifest } from './transform/index.js';
5
5
  export { type ExportedManifest };
6
+ declare const README_RANK: {
7
+ readonly bare: 1;
8
+ readonly markdown: 2;
9
+ readonly readmeMd: 3;
10
+ };
11
+ export interface ReadmeCandidate {
12
+ fileName: string;
13
+ rank: ReadmeRank;
14
+ }
15
+ /** How npm ranks a package-root file as the package's README. A higher rank wins. */
16
+ export type ReadmeRank = typeof README_RANK[keyof typeof README_RANK];
17
+ /**
18
+ * Rank a filename as a README candidate, or return `undefined` if npm would not use it as the
19
+ * package's README. npm matches the bare `README` case-sensitively and accepts `README.*` whose
20
+ * extension matches `/.m?a?r?k?d?o?w?n?$/i`.
21
+ */
22
+ export declare function getReadmeRank(fileName: string): ReadmeRank | undefined;
23
+ /**
24
+ * Whether README `candidate` should replace the `current` selection. A higher rank wins. Equal
25
+ * ranks keep the lower filename, so the choice does not depend on directory or archive order, and
26
+ * an equal filename replaces, so the last duplicate archive entry wins as it would on extraction.
27
+ */
28
+ export declare function isPreferredReadme(candidate: ReadmeCandidate, current: ReadmeCandidate | undefined): boolean;
6
29
  export type WorkspacePackageLookup = Array<{
7
30
  manifest: ProjectManifest;
8
31
  rootDir?: string;
package/lib/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import util from 'node:util';
4
3
  import { resolveFromCatalog } from '@pnpm/catalogs.resolver';
5
- import { PnpmError } from '@pnpm/error';
4
+ import { isError, PnpmError } from '@pnpm/error';
6
5
  import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
7
6
  import { tryReadProjectManifest } from '@pnpm/workspace.project-manifest-reader';
8
7
  import { WorkspaceSpec } from '@pnpm/workspace.spec-parser';
@@ -10,6 +9,35 @@ import { clone, omit } from 'ramda';
10
9
  import { overridePublishConfig } from './overridePublishConfig.js';
11
10
  import { transform } from './transform/index.js';
12
11
  export {};
12
+ const README_RANK = {
13
+ bare: 1,
14
+ markdown: 2,
15
+ readmeMd: 3,
16
+ };
17
+ /**
18
+ * Rank a filename as a README candidate, or return `undefined` if npm would not use it as the
19
+ * package's README. npm matches the bare `README` case-sensitively and accepts `README.*` whose
20
+ * extension matches `/.m?a?r?k?d?o?w?n?$/i`.
21
+ */
22
+ export function getReadmeRank(fileName) {
23
+ if (/^readme\.md$/i.test(fileName))
24
+ return README_RANK.readmeMd;
25
+ if (fileName === 'README')
26
+ return README_RANK.bare;
27
+ if (/^readme\./i.test(fileName) && /\.m?a?r?k?d?o?w?n?$/i.test(fileName))
28
+ return README_RANK.markdown;
29
+ return undefined;
30
+ }
31
+ /**
32
+ * Whether README `candidate` should replace the `current` selection. A higher rank wins. Equal
33
+ * ranks keep the lower filename, so the choice does not depend on directory or archive order, and
34
+ * an equal filename replaces, so the last duplicate archive entry wins as it would on extraction.
35
+ */
36
+ export function isPreferredReadme(candidate, current) {
37
+ if (current == null || candidate.rank > current.rank)
38
+ return true;
39
+ return candidate.rank === current.rank && candidate.fileName <= current.fileName;
40
+ }
13
41
  const PREPUBLISH_SCRIPTS = [
14
42
  'prepublishOnly',
15
43
  'prepack',
@@ -60,31 +88,38 @@ export async function createExportableManifest(dir, originalManifest, opts) {
60
88
  }
61
89
  }
62
90
  for (const hook of opts?.hooks?.beforePacking ?? []) {
63
- // eslint-disable-next-line no-await-in-loop
91
+ // eslint-disable-next-line no-await-in-loop -- each hook receives the manifest returned by the previous one
64
92
  publishManifest = await hook(publishManifest, dir) ?? publishManifest;
65
93
  }
66
94
  return transform(publishManifest);
67
95
  }
68
96
  // `O_NOFOLLOW` makes the open itself refuse a symlink at the final path component, closing the
69
- // TOCTOU window between the `readdir` type check and the read: a symlink swapped in for README.md
70
- // after the check can't redirect the read outside the project and leak its target into the
71
- // published manifest. Windows lacks the flag (and requires privileges to create symlinks), so it
72
- // falls back to a plain read.
97
+ // TOCTOU window between the `readdir` type check and the read: a symlink swapped in for the
98
+ // selected README after the check can't redirect the read outside the project and leak its target
99
+ // into the published manifest. Windows lacks the flag (and requires privileges to create symlinks),
100
+ // so it falls back to a plain read.
73
101
  const README_READ_FLAGS = fs.constants.O_RDONLY | (process.platform === 'win32' ? 0 : fs.constants.O_NOFOLLOW);
74
102
  export async function readReadmeFile(projectDir) {
75
103
  const entries = await fs.promises.readdir(projectDir, { withFileTypes: true });
76
- // Only embed a regular README.md file — a symlink is skipped (see README_READ_FLAGS).
77
- const readmeEntry = entries.find((entry) => entry.isFile() && /^readme\.md$/i.test(entry.name));
78
- if (readmeEntry == null)
104
+ let readme;
105
+ for (const entry of entries) {
106
+ const rank = getReadmeRank(entry.name);
107
+ if (rank == null || !entry.isFile())
108
+ continue;
109
+ const candidate = { fileName: entry.name, rank };
110
+ if (isPreferredReadme(candidate, readme))
111
+ readme = candidate;
112
+ }
113
+ if (readme == null)
79
114
  return undefined;
80
115
  let handle;
81
116
  try {
82
- handle = await fs.promises.open(path.join(projectDir, readmeEntry.name), README_READ_FLAGS);
117
+ handle = await fs.promises.open(path.join(projectDir, readme.fileName), README_READ_FLAGS);
83
118
  return await handle.readFile('utf8');
84
119
  }
85
120
  catch (err) {
86
121
  // ELOOP: the entry is a symlink after all (a concurrent swap) — skip it, as the isFile() check intended.
87
- if (util.types.isNativeError(err) && 'code' in err && err.code === 'ELOOP')
122
+ if (isError(err) && 'code' in err && err.code === 'ELOOP')
88
123
  return undefined;
89
124
  throw err;
90
125
  }
@@ -96,7 +131,7 @@ function combineConverters(...converters) {
96
131
  return async (depName, depSpec, context) => {
97
132
  let bareSpecifier = depSpec;
98
133
  for (const converter of converters) {
99
- // eslint-disable-next-line no-await-in-loop
134
+ // eslint-disable-next-line no-await-in-loop -- each converter receives the specifier returned by the previous one
100
135
  bareSpecifier = await converter(depName, bareSpecifier, context);
101
136
  }
102
137
  return bareSpecifier;
@@ -33,11 +33,11 @@ export function overridePublishConfig(publishManifest) {
33
33
  if (!publishManifest.publishConfig)
34
34
  return;
35
35
  const publishConfig = { ...publishManifest.publishConfig };
36
+ const manifestFields = publishManifest;
36
37
  for (const key in publishConfig) {
37
38
  if (!PUBLISH_CONFIG_WHITELIST.has(key))
38
39
  continue;
39
- const value = publishConfig[key];
40
- publishManifest[key] = value; // eslint-disable-line @typescript-eslint/no-explicit-any
40
+ manifestFields[key] = publishConfig[key];
41
41
  delete publishConfig[key];
42
42
  }
43
43
  if (isEmpty(publishConfig)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/releasing.exportable-manifest",
3
- "version": "1100.3.3",
3
+ "version": "1100.3.5",
4
4
  "description": "Creates an exportable manifest",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,21 +29,21 @@
29
29
  "dependencies": {
30
30
  "@npm/types": "^2.1.0",
31
31
  "@pnpm/bins.resolver": "1100.0.17",
32
- "@pnpm/catalogs.resolver": "1100.1.0",
33
- "@pnpm/error": "1100.2.0",
34
- "@pnpm/resolving.jsr-specifier-parser": "1100.0.8",
32
+ "@pnpm/catalogs.resolver": "1100.1.1",
33
+ "@pnpm/error": "1100.2.1",
34
+ "@pnpm/resolving.jsr-specifier-parser": "1100.0.9",
35
35
  "@pnpm/types": "1102.1.1",
36
- "@pnpm/workspace.project-manifest-reader": "1100.1.0",
36
+ "@pnpm/workspace.project-manifest-reader": "1100.1.1",
37
37
  "@pnpm/workspace.spec-parser": "1100.0.1",
38
38
  "ramda": "npm:@pnpm/ramda@0.28.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@jest/globals": "30.4.1",
42
- "@pnpm/catalogs.config": "1100.0.8",
42
+ "@pnpm/catalogs.config": "1100.0.9",
43
43
  "@pnpm/catalogs.types": "1100.0.1",
44
- "@pnpm/hooks.pnpmfile": "1100.0.33",
45
- "@pnpm/prepare": "1100.0.32",
46
- "@pnpm/releasing.exportable-manifest": "1100.3.3",
44
+ "@pnpm/hooks.pnpmfile": "1100.0.34",
45
+ "@pnpm/prepare": "1100.0.34",
46
+ "@pnpm/releasing.exportable-manifest": "1100.3.5",
47
47
  "@types/cross-spawn": "^6.0.6",
48
48
  "@types/ramda": "0.32.0",
49
49
  "cross-spawn": "^7.0.6",