@pnpm/releasing.exportable-manifest 1100.3.4 → 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,11 @@
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
+
3
9
  ## 1100.3.4
4
10
 
5
11
  ### 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
@@ -9,6 +9,35 @@ import { clone, omit } from 'ramda';
9
9
  import { overridePublishConfig } from './overridePublishConfig.js';
10
10
  import { transform } from './transform/index.js';
11
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
+ }
12
41
  const PREPUBLISH_SCRIPTS = [
13
42
  'prepublishOnly',
14
43
  'prepack',
@@ -59,26 +88,33 @@ export async function createExportableManifest(dir, originalManifest, opts) {
59
88
  }
60
89
  }
61
90
  for (const hook of opts?.hooks?.beforePacking ?? []) {
62
- // 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
63
92
  publishManifest = await hook(publishManifest, dir) ?? publishManifest;
64
93
  }
65
94
  return transform(publishManifest);
66
95
  }
67
96
  // `O_NOFOLLOW` makes the open itself refuse a symlink at the final path component, closing the
68
- // TOCTOU window between the `readdir` type check and the read: a symlink swapped in for README.md
69
- // after the check can't redirect the read outside the project and leak its target into the
70
- // published manifest. Windows lacks the flag (and requires privileges to create symlinks), so it
71
- // 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.
72
101
  const README_READ_FLAGS = fs.constants.O_RDONLY | (process.platform === 'win32' ? 0 : fs.constants.O_NOFOLLOW);
73
102
  export async function readReadmeFile(projectDir) {
74
103
  const entries = await fs.promises.readdir(projectDir, { withFileTypes: true });
75
- // Only embed a regular README.md file — a symlink is skipped (see README_READ_FLAGS).
76
- const readmeEntry = entries.find((entry) => entry.isFile() && /^readme\.md$/i.test(entry.name));
77
- 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)
78
114
  return undefined;
79
115
  let handle;
80
116
  try {
81
- 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);
82
118
  return await handle.readFile('utf8');
83
119
  }
84
120
  catch (err) {
@@ -95,7 +131,7 @@ function combineConverters(...converters) {
95
131
  return async (depName, depSpec, context) => {
96
132
  let bareSpecifier = depSpec;
97
133
  for (const converter of converters) {
98
- // 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
99
135
  bareSpecifier = await converter(depName, bareSpecifier, context);
100
136
  }
101
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.4",
3
+ "version": "1100.3.5",
4
4
  "description": "Creates an exportable manifest",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -42,8 +42,8 @@
42
42
  "@pnpm/catalogs.config": "1100.0.9",
43
43
  "@pnpm/catalogs.types": "1100.0.1",
44
44
  "@pnpm/hooks.pnpmfile": "1100.0.34",
45
- "@pnpm/prepare": "1100.0.33",
46
- "@pnpm/releasing.exportable-manifest": "1100.3.4",
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",