@pnpm/releasing.exportable-manifest 1100.3.4 → 1100.3.6

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,18 @@
1
1
  # @pnpm/exportable-manifest
2
2
 
3
+ ## 1100.3.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies:
8
+ - @pnpm/workspace.project-manifest-reader@1100.1.2
9
+
10
+ ## 1100.3.5
11
+
12
+ ### Patch Changes
13
+
14
+ - `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).
15
+
3
16
  ## 1100.3.4
4
17
 
5
18
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import type { Catalogs } from '@pnpm/catalogs.types';
2
2
  import type { Hooks } from '@pnpm/hooks.pnpmfile';
3
3
  import type { ProjectManifest } from '@pnpm/types';
4
+ import { getReadmeRank, isPreferredReadme, type ReadmeCandidate, type ReadmeRank, readReadmeFile } from './readme.js';
4
5
  import { type ExportedManifest } from './transform/index.js';
6
+ import { type WorkspacePackageContainer, type WorkspacePackageLookup } from './workspaceLookup.js';
5
7
  export { type ExportedManifest };
6
- export type WorkspacePackageLookup = Array<{
7
- manifest: ProjectManifest;
8
- rootDir?: string;
9
- }> | Map<string, unknown> | Record<string, unknown> | ((depName: string) => ProjectManifest | undefined);
8
+ export { getReadmeRank, isPreferredReadme, type ReadmeCandidate, type ReadmeRank, readReadmeFile, type WorkspacePackageContainer, type WorkspacePackageLookup, };
10
9
  export interface MakePublishManifestOptions {
11
10
  catalogs: Catalogs;
12
11
  hooks?: Hooks;
@@ -16,7 +15,6 @@ export interface MakePublishManifestOptions {
16
15
  workspacePackages?: WorkspacePackageLookup;
17
16
  }
18
17
  export declare function createExportableManifest(dir: string, originalManifest: ProjectManifest, opts: MakePublishManifestOptions): Promise<ExportedManifest>;
19
- export declare function readReadmeFile(projectDir: string): Promise<string | undefined>;
20
18
  export type PublishDependencyConverter = (depName: string, depSpec: string, context: PublishDependencyConverterContext) => Promise<string> | string;
21
19
  export interface PublishDependencyConverterContext {
22
20
  dir: string;
package/lib/index.js CHANGED
@@ -1,14 +1,16 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { resolveFromCatalog } from '@pnpm/catalogs.resolver';
4
- import { isError, PnpmError } from '@pnpm/error';
5
- import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
3
+ import { PnpmError } from '@pnpm/error';
6
4
  import { tryReadProjectManifest } from '@pnpm/workspace.project-manifest-reader';
7
5
  import { WorkspaceSpec } from '@pnpm/workspace.spec-parser';
8
6
  import { clone, omit } from 'ramda';
7
+ import { replaceJsrProtocol } from './jsrProtocol.js';
9
8
  import { overridePublishConfig } from './overridePublishConfig.js';
9
+ import { getReadmeRank, isPreferredReadme, readReadmeFile, } from './readme.js';
10
10
  import { transform } from './transform/index.js';
11
+ import { buildWorkspaceManifestGetter, } from './workspaceLookup.js';
11
12
  export {};
13
+ export { getReadmeRank, isPreferredReadme, readReadmeFile, };
12
14
  const PREPUBLISH_SCRIPTS = [
13
15
  'prepublishOnly',
14
16
  'prepack',
@@ -18,24 +20,44 @@ const PREPUBLISH_SCRIPTS = [
18
20
  'postpublish',
19
21
  ];
20
22
  export async function createExportableManifest(dir, originalManifest, opts) {
21
- let publishManifest;
22
- if (opts.skipManifestObfuscation) {
23
- publishManifest = omit(['pnpm'], clone(originalManifest));
24
- }
25
- else {
26
- publishManifest = omit(['scripts', 'packageManager', 'pnpm'], originalManifest);
27
- if (originalManifest.scripts != null) {
28
- publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts);
23
+ const publishManifest = initPublishManifest(originalManifest, opts.skipManifestObfuscation);
24
+ const getWorkspaceManifest = buildWorkspaceManifestGetter(opts.workspacePackages);
25
+ await convertManifestDependencies({
26
+ catalogs: opts.catalogs,
27
+ dir,
28
+ getWorkspaceManifest,
29
+ modulesDir: opts.modulesDir,
30
+ originalManifest,
31
+ publishManifest,
32
+ });
33
+ overridePublishConfig(publishManifest);
34
+ if (publishManifest.readme == null && opts.embedReadme) {
35
+ const readme = await readReadmeFile(dir);
36
+ if (readme != null) {
37
+ publishManifest.readme = readme;
29
38
  }
30
39
  }
31
- const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs);
32
- const replaceCatalogProtocol = resolveCatalogProtocol.bind(null, catalogResolver);
33
- const getWorkspaceManifest = buildWorkspaceManifestGetter(opts.workspacePackages);
40
+ const finalManifest = await applyPublishHooks(publishManifest, dir, opts.hooks?.beforePacking);
41
+ return transform(finalManifest);
42
+ }
43
+ function initPublishManifest(originalManifest, skipObfuscation) {
44
+ if (skipObfuscation) {
45
+ return omit(['pnpm'], clone(originalManifest));
46
+ }
47
+ const publishManifest = omit(['scripts', 'packageManager', 'pnpm'], originalManifest);
48
+ if (originalManifest.scripts != null) {
49
+ publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts);
50
+ }
51
+ return publishManifest;
52
+ }
53
+ async function convertManifestDependencies(params) {
54
+ const { catalogs, dir, getWorkspaceManifest, modulesDir, originalManifest, publishManifest } = params;
55
+ const replaceCatalogProtocol = resolveCatalogProtocol.bind(null, resolveFromCatalog.bind(null, catalogs));
34
56
  const convertDependencyForPublish = combineConverters(replaceCatalogProtocol, replaceWorkspaceProtocol, replaceJsrProtocol);
35
57
  await Promise.all(['dependencies', 'devDependencies', 'optionalDependencies'].map(async (depsField) => {
36
58
  const deps = await makePublishDependencies(dir, originalManifest[depsField], {
37
- modulesDir: opts?.modulesDir,
38
59
  convertDependencyForPublish,
60
+ modulesDir,
39
61
  workspacePackages: getWorkspaceManifest,
40
62
  });
41
63
  if (deps != null) {
@@ -46,56 +68,25 @@ export async function createExportableManifest(dir, originalManifest, opts) {
46
68
  if (peerDependencies) {
47
69
  const convertPeersForPublish = combineConverters(replaceCatalogProtocol, replaceWorkspaceProtocolPeerDependency, replaceJsrProtocol);
48
70
  publishManifest.peerDependencies = await makePublishDependencies(dir, peerDependencies, {
49
- modulesDir: opts?.modulesDir,
50
71
  convertDependencyForPublish: convertPeersForPublish,
72
+ modulesDir,
51
73
  workspacePackages: getWorkspaceManifest,
52
74
  });
53
75
  }
54
- overridePublishConfig(publishManifest);
55
- if (publishManifest.readme == null && opts?.embedReadme) {
56
- const readme = await readReadmeFile(dir);
57
- if (readme != null) {
58
- publishManifest.readme = readme;
59
- }
60
- }
61
- for (const hook of opts?.hooks?.beforePacking ?? []) {
62
- // eslint-disable-next-line no-await-in-loop
63
- publishManifest = await hook(publishManifest, dir) ?? publishManifest;
64
- }
65
- return transform(publishManifest);
66
76
  }
67
- // `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.
72
- const README_READ_FLAGS = fs.constants.O_RDONLY | (process.platform === 'win32' ? 0 : fs.constants.O_NOFOLLOW);
73
- export async function readReadmeFile(projectDir) {
74
- 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)
78
- return undefined;
79
- let handle;
80
- try {
81
- handle = await fs.promises.open(path.join(projectDir, readmeEntry.name), README_READ_FLAGS);
82
- return await handle.readFile('utf8');
83
- }
84
- catch (err) {
85
- // ELOOP: the entry is a symlink after all (a concurrent swap) — skip it, as the isFile() check intended.
86
- if (isError(err) && 'code' in err && err.code === 'ELOOP')
87
- return undefined;
88
- throw err;
89
- }
90
- finally {
91
- await handle?.close();
77
+ async function applyPublishHooks(manifest, dir, hooks) {
78
+ let result = manifest;
79
+ for (const hook of hooks ?? []) {
80
+ // eslint-disable-next-line no-await-in-loop -- each hook receives the manifest returned by the previous one
81
+ result = await hook(result, dir) ?? result;
92
82
  }
83
+ return result;
93
84
  }
94
85
  function combineConverters(...converters) {
95
86
  return async (depName, depSpec, context) => {
96
87
  let bareSpecifier = depSpec;
97
88
  for (const converter of converters) {
98
- // eslint-disable-next-line no-await-in-loop
89
+ // eslint-disable-next-line no-await-in-loop -- each converter receives the specifier returned by the previous one
99
90
  bareSpecifier = await converter(depName, bareSpecifier, context);
100
91
  }
101
92
  return bareSpecifier;
@@ -107,60 +98,6 @@ async function makePublishDependencies(dir, dependencies, { modulesDir, convertD
107
98
  const publishDependencies = await Promise.all(Object.entries(dependencies).map(async ([depName, depSpec]) => [depName, await convertDependencyForPublish(depName, depSpec, { dir, modulesDir, workspacePackages })]));
108
99
  return Object.fromEntries(publishDependencies);
109
100
  }
110
- function extractManifest(val) {
111
- if (!val || typeof val !== 'object')
112
- return undefined;
113
- if ('manifest' in val && val.manifest && typeof val.manifest === 'object') {
114
- return val.manifest;
115
- }
116
- if ('package' in val && val.package && typeof val.package === 'object' && 'manifest' in val.package && val.package.manifest && typeof val.package.manifest === 'object') {
117
- return val.package.manifest;
118
- }
119
- if ('name' in val && typeof val.name === 'string') {
120
- return val;
121
- }
122
- return undefined;
123
- }
124
- function buildWorkspaceManifestGetter(workspacePackages) {
125
- if (!workspacePackages)
126
- return undefined;
127
- if (typeof workspacePackages === 'function') {
128
- return workspacePackages;
129
- }
130
- const byName = new Map();
131
- const addManifest = (val) => {
132
- const manifest = extractManifest(val);
133
- if (!manifest?.name)
134
- return;
135
- const existing = byName.get(manifest.name);
136
- if (!existing || (!existing.version && manifest.version)) {
137
- byName.set(manifest.name, manifest);
138
- }
139
- };
140
- if (Array.isArray(workspacePackages)) {
141
- for (const pkg of workspacePackages) {
142
- addManifest(pkg);
143
- }
144
- }
145
- else if (workspacePackages instanceof Map) {
146
- for (const val of workspacePackages.values()) {
147
- if (val instanceof Map) {
148
- for (const subVal of val.values()) {
149
- addManifest(subVal);
150
- }
151
- }
152
- else {
153
- addManifest(val);
154
- }
155
- }
156
- }
157
- else if (typeof workspacePackages === 'object') {
158
- for (const val of Object.values(workspacePackages)) {
159
- addManifest(val);
160
- }
161
- }
162
- return (depName) => byName.get(depName);
163
- }
164
101
  async function readAndCheckManifest(depName, dependencyDir, getWorkspaceManifest, targetPkgName) {
165
102
  const { manifest: dirManifest } = await tryReadProjectManifest(dependencyDir);
166
103
  if (dirManifest?.name && dirManifest?.version) {
@@ -171,6 +108,9 @@ async function readAndCheckManifest(depName, dependencyDir, getWorkspaceManifest
171
108
  if (workspaceManifest?.name && workspaceManifest?.version) {
172
109
  return workspaceManifest;
173
110
  }
111
+ return handleMissingOrInvalidManifest(depName, dirManifest ?? undefined, workspaceManifest);
112
+ }
113
+ function handleMissingOrInvalidManifest(depName, dirManifest, workspaceManifest) {
174
114
  const found = dirManifest?.name || dirManifest?.version
175
115
  ? dirManifest
176
116
  : workspaceManifest?.name || workspaceManifest?.version
@@ -195,36 +135,39 @@ function resolveCatalogProtocol(catalogResolver, alias, bareSpecifier) {
195
135
  case 'misconfiguration': throw result.error;
196
136
  }
197
137
  }
198
- async function replaceWorkspaceProtocol(depName, depSpec, { dir, modulesDir, workspacePackages }) {
138
+ async function replaceWorkspaceProtocol(depName, depSpec, context) {
199
139
  if (!depSpec.startsWith('workspace:')) {
200
140
  return depSpec;
201
141
  }
202
142
  // Dependencies with bare "*", "^", "~" versions, or no version (workspace:)
203
143
  const versionAliasSpecParts = /^workspace:(?:(.+)@)?([\^~*])?$/.exec(depSpec);
204
144
  if (versionAliasSpecParts != null) {
205
- modulesDir = modulesDir ?? path.join(dir, 'node_modules');
206
- const targetPkgName = versionAliasSpecParts[1];
207
- const manifest = await readAndCheckManifest(depName, path.join(modulesDir, depName), workspacePackages, targetPkgName);
208
- const specifierSuffix = versionAliasSpecParts[2];
209
- const semverRangeToken = specifierSuffix === '^' || specifierSuffix === '~' ? specifierSuffix : '';
210
- if (depName !== manifest.name) {
211
- return `npm:${manifest.name}@${semverRangeToken}${manifest.version}`;
212
- }
213
- return `${semverRangeToken}${manifest.version}`;
145
+ return resolveVersionAliasWorkspaceSpec(depName, versionAliasSpecParts, context);
214
146
  }
215
147
  if (depSpec.startsWith('workspace:./') || depSpec.startsWith('workspace:../')) {
216
- const manifest = await readAndCheckManifest(depName, path.join(dir, depSpec.slice(10)), workspacePackages);
217
- if (manifest.name === depName)
218
- return `${manifest.version}`;
219
- return `npm:${manifest.name}@${manifest.version}`;
220
- }
221
- depSpec = depSpec.slice(10);
222
- if (depSpec.includes('@')) {
223
- return `npm:${depSpec}`;
148
+ return resolveRelativeWorkspaceSpec(depName, depSpec.slice(10), context);
224
149
  }
225
- return depSpec;
150
+ const spec = depSpec.slice(10);
151
+ return spec.includes('@') ? `npm:${spec}` : spec;
152
+ }
153
+ async function resolveVersionAliasWorkspaceSpec(depName, parts, context) {
154
+ const modulesDir = context.modulesDir ?? path.join(context.dir, 'node_modules');
155
+ const targetPkgName = parts[1];
156
+ const manifest = await readAndCheckManifest(depName, path.join(modulesDir, depName), context.workspacePackages, targetPkgName);
157
+ const specifierSuffix = parts[2];
158
+ const semverRangeToken = specifierSuffix === '^' || specifierSuffix === '~' ? specifierSuffix : '';
159
+ if (depName !== manifest.name) {
160
+ return `npm:${manifest.name}@${semverRangeToken}${manifest.version}`;
161
+ }
162
+ return `${semverRangeToken}${manifest.version}`;
226
163
  }
227
- async function replaceWorkspaceProtocolPeerDependency(depName, depSpec, { dir, modulesDir, workspacePackages }) {
164
+ async function resolveRelativeWorkspaceSpec(depName, relPath, context) {
165
+ const manifest = await readAndCheckManifest(depName, path.join(context.dir, relPath), context.workspacePackages);
166
+ if (manifest.name === depName)
167
+ return `${manifest.version}`;
168
+ return `npm:${manifest.name}@${manifest.version}`;
169
+ }
170
+ async function replaceWorkspaceProtocolPeerDependency(depName, depSpec, context) {
228
171
  if (!depSpec.includes('workspace:')) {
229
172
  return depSpec;
230
173
  }
@@ -236,35 +179,23 @@ async function replaceWorkspaceProtocolPeerDependency(depName, depSpec, { dir, m
236
179
  return `npm:${workspaceSpec.alias}@${version}`;
237
180
  }
238
181
  if (workspaceSpec?.version.startsWith('./') || workspaceSpec?.version.startsWith('../')) {
239
- return replaceWorkspaceProtocol(depName, depSpec, { dir, modulesDir, workspacePackages });
182
+ return replaceWorkspaceProtocol(depName, depSpec, context);
240
183
  }
241
- // Dependencies with bare "*", "^", "~",">=",">","<=", "<", version
242
- const workspaceSemverRegex = /workspace:([\^~*]|>=|>|<=|<)?((\d+|[xX*])(\.(\d+|[xX*])){0,2})?/;
243
- const versionAliasSpecParts = workspaceSemverRegex.exec(depSpec);
244
- if (versionAliasSpecParts != null) {
245
- const [, semverRangGroup = '', version] = versionAliasSpecParts;
246
- if (version) {
247
- return depSpec.replace('workspace:', '');
248
- }
249
- modulesDir = modulesDir ?? path.join(dir, 'node_modules');
250
- const manifest = await readAndCheckManifest(depName, path.join(modulesDir, depName), workspacePackages);
251
- const semverRangeToken = semverRangGroup !== '*' ? semverRangGroup : '';
252
- return depSpec.replace(workspaceSemverRegex, `${semverRangeToken}${manifest.version}`);
253
- }
254
- return depSpec.replace('workspace:', '');
184
+ return resolvePeerWorkspaceSemver(depName, depSpec, context);
255
185
  }
256
- async function replaceJsrProtocol(depName, depSpec) {
257
- const spec = parseJsrSpecifier(depSpec, depName);
258
- if (spec == null) {
259
- return depSpec;
260
- }
261
- return createNpmAliasedSpecifier(spec.npmPkgName, spec.versionSelector);
262
- }
263
- function createNpmAliasedSpecifier(npmPkgName, versionSelector) {
264
- const npmPkgSpecifier = `npm:${npmPkgName}`;
265
- if (!versionSelector) {
266
- return npmPkgSpecifier;
267
- }
268
- return `${npmPkgSpecifier}@${versionSelector}`;
186
+ const WORKSPACE_SEMVER_REGEX = /workspace:([\^~*]|>=|>|<=|<)?((\d+|[xX*])(\.(\d+|[xX*])){0,2})?/;
187
+ async function resolvePeerWorkspaceSemver(depName, depSpec, context) {
188
+ const versionAliasSpecParts = WORKSPACE_SEMVER_REGEX.exec(depSpec);
189
+ if (versionAliasSpecParts == null) {
190
+ return depSpec.replace('workspace:', '');
191
+ }
192
+ const [, semverRangeGroup = '', version] = versionAliasSpecParts;
193
+ if (version) {
194
+ return depSpec.replace('workspace:', '');
195
+ }
196
+ const modulesDir = context.modulesDir ?? path.join(context.dir, 'node_modules');
197
+ const manifest = await readAndCheckManifest(depName, path.join(modulesDir, depName), context.workspacePackages);
198
+ const semverRangeToken = semverRangeGroup !== '*' ? semverRangeGroup : '';
199
+ return depSpec.replace(WORKSPACE_SEMVER_REGEX, `${semverRangeToken}${manifest.version}`);
269
200
  }
270
201
  //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ export declare function replaceJsrProtocol(depName: string, depSpec: string): Promise<string>;
@@ -0,0 +1,16 @@
1
+ import { parseJsrSpecifier } from '@pnpm/resolving.jsr-specifier-parser';
2
+ export async function replaceJsrProtocol(depName, depSpec) {
3
+ const spec = parseJsrSpecifier(depSpec, depName);
4
+ if (spec == null) {
5
+ return depSpec;
6
+ }
7
+ return createNpmAliasedSpecifier(spec.npmPkgName, spec.versionSelector);
8
+ }
9
+ function createNpmAliasedSpecifier(npmPkgName, versionSelector) {
10
+ const npmPkgSpecifier = `npm:${npmPkgName}`;
11
+ if (!versionSelector) {
12
+ return npmPkgSpecifier;
13
+ }
14
+ return `${npmPkgSpecifier}@${versionSelector}`;
15
+ }
16
+ //# sourceMappingURL=jsrProtocol.js.map
@@ -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)) {
@@ -0,0 +1,25 @@
1
+ declare const README_RANK: {
2
+ readonly bare: 1;
3
+ readonly markdown: 2;
4
+ readonly readmeMd: 3;
5
+ };
6
+ export interface ReadmeCandidate {
7
+ fileName: string;
8
+ rank: ReadmeRank;
9
+ }
10
+ /** How npm ranks a package-root file as the package's README. A higher rank wins. */
11
+ export type ReadmeRank = typeof README_RANK[keyof typeof README_RANK];
12
+ /**
13
+ * Rank a filename as a README candidate, or return `undefined` if npm would not use it as the
14
+ * package's README. npm matches the bare `README` case-sensitively and accepts `README.*` whose
15
+ * extension matches `/.m?a?r?k?d?o?w?n?$/i`.
16
+ */
17
+ export declare function getReadmeRank(fileName: string): ReadmeRank | undefined;
18
+ /**
19
+ * Whether README `candidate` should replace the `current` selection. A higher rank wins. Equal
20
+ * ranks keep the lower filename, so the choice does not depend on directory or archive order, and
21
+ * an equal filename replaces, so the last duplicate archive entry wins as it would on extraction.
22
+ */
23
+ export declare function isPreferredReadme(candidate: ReadmeCandidate, current: ReadmeCandidate | undefined): boolean;
24
+ export declare function readReadmeFile(projectDir: string): Promise<string | undefined>;
25
+ export {};
package/lib/readme.js ADDED
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isError } from '@pnpm/error';
4
+ const README_RANK = {
5
+ bare: 1,
6
+ markdown: 2,
7
+ readmeMd: 3,
8
+ };
9
+ /**
10
+ * Rank a filename as a README candidate, or return `undefined` if npm would not use it as the
11
+ * package's README. npm matches the bare `README` case-sensitively and accepts `README.*` whose
12
+ * extension matches `/.m?a?r?k?d?o?w?n?$/i`.
13
+ */
14
+ export function getReadmeRank(fileName) {
15
+ if (/^readme\.md$/i.test(fileName))
16
+ return README_RANK.readmeMd;
17
+ if (fileName === 'README')
18
+ return README_RANK.bare;
19
+ if (/^readme\./i.test(fileName) && /\.m?a?r?k?d?o?w?n?$/i.test(fileName))
20
+ return README_RANK.markdown;
21
+ return undefined;
22
+ }
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 function isPreferredReadme(candidate, current) {
29
+ if (current == null || candidate.rank > current.rank)
30
+ return true;
31
+ return candidate.rank === current.rank && candidate.fileName <= current.fileName;
32
+ }
33
+ // `O_NOFOLLOW` makes the open itself refuse a symlink at the final path component, closing the
34
+ // TOCTOU window between the `readdir` type check and the read: a symlink swapped in for the
35
+ // selected README after the check can't redirect the read outside the project and leak its target
36
+ // into the published manifest. Windows lacks the flag (and requires privileges to create symlinks),
37
+ // so it falls back to a plain read.
38
+ const README_READ_FLAGS = fs.constants.O_RDONLY | (process.platform === 'win32' ? 0 : fs.constants.O_NOFOLLOW);
39
+ export async function readReadmeFile(projectDir) {
40
+ const entries = await fs.promises.readdir(projectDir, { withFileTypes: true });
41
+ let readme;
42
+ for (const entry of entries) {
43
+ const rank = getReadmeRank(entry.name);
44
+ if (rank == null || !entry.isFile())
45
+ continue;
46
+ const candidate = { fileName: entry.name, rank };
47
+ if (isPreferredReadme(candidate, readme))
48
+ readme = candidate;
49
+ }
50
+ if (readme == null)
51
+ return undefined;
52
+ return readReadmeContent(projectDir, readme.fileName);
53
+ }
54
+ async function readReadmeContent(projectDir, fileName) {
55
+ let handle;
56
+ try {
57
+ handle = await fs.promises.open(path.join(projectDir, fileName), README_READ_FLAGS);
58
+ return await handle.readFile('utf8');
59
+ }
60
+ catch (err) {
61
+ // ELOOP: the entry is a symlink after all (a concurrent swap) — skip it, as the isFile() check intended.
62
+ if (isError(err) && 'code' in err && err.code === 'ELOOP')
63
+ return undefined;
64
+ throw err;
65
+ }
66
+ finally {
67
+ await handle?.close();
68
+ }
69
+ }
70
+ //# sourceMappingURL=readme.js.map
@@ -6,7 +6,6 @@ type Output<Manifest extends Input> = Omit<Manifest, 'bin'> & Pick<ExportedManif
6
6
  export declare function transformBin<Manifest extends Input>(manifest: Manifest): Output<Manifest>;
7
7
  /**
8
8
  * The property `"bin"` of a `package.json` could be either an object or a string.
9
- * This function normalizes either forms into an object.
10
9
  */
11
10
  export declare function normalizeBinObject(pkgName: string, bin: string | Record<string, string>): Record<string, string>;
12
11
  export declare class InvalidScopedPackageNameError extends PnpmError {
@@ -10,7 +10,6 @@ export function transformBin(manifest) {
10
10
  }
11
11
  /**
12
12
  * The property `"bin"` of a `package.json` could be either an object or a string.
13
- * This function normalizes either forms into an object.
14
13
  */
15
14
  export function normalizeBinObject(pkgName, bin) {
16
15
  if (typeof bin === 'object')
@@ -0,0 +1,7 @@
1
+ import type { ProjectManifest } from '@pnpm/types';
2
+ export type WorkspacePackageContainer = Array<{
3
+ manifest: ProjectManifest;
4
+ rootDir?: string;
5
+ }> | Map<string, unknown> | Record<string, unknown>;
6
+ export type WorkspacePackageLookup = WorkspacePackageContainer | ((depName: string) => ProjectManifest | undefined);
7
+ export declare function buildWorkspaceManifestGetter(workspacePackages?: WorkspacePackageLookup): ((depName: string) => ProjectManifest | undefined) | undefined;
@@ -0,0 +1,59 @@
1
+ export function buildWorkspaceManifestGetter(workspacePackages) {
2
+ if (!workspacePackages)
3
+ return undefined;
4
+ if (typeof workspacePackages === 'function') {
5
+ return workspacePackages;
6
+ }
7
+ const byName = new Map();
8
+ collectWorkspaceManifests(workspacePackages, byName);
9
+ return (depName) => byName.get(depName);
10
+ }
11
+ function collectWorkspaceManifests(workspacePackages, byName) {
12
+ const add = (val) => addManifestEntry(val, byName);
13
+ if (Array.isArray(workspacePackages)) {
14
+ for (const pkg of workspacePackages)
15
+ add(pkg);
16
+ return;
17
+ }
18
+ if (workspacePackages instanceof Map) {
19
+ collectFromMap(workspacePackages, add);
20
+ return;
21
+ }
22
+ for (const val of Object.values(workspacePackages))
23
+ add(val);
24
+ }
25
+ function collectFromMap(map, add) {
26
+ for (const val of map.values()) {
27
+ if (val instanceof Map) {
28
+ for (const subVal of val.values())
29
+ add(subVal);
30
+ }
31
+ else {
32
+ add(val);
33
+ }
34
+ }
35
+ }
36
+ function addManifestEntry(val, byName) {
37
+ const manifest = extractManifest(val);
38
+ if (!manifest?.name)
39
+ return;
40
+ const existing = byName.get(manifest.name);
41
+ if (!existing || (!existing.version && manifest.version)) {
42
+ byName.set(manifest.name, manifest);
43
+ }
44
+ }
45
+ function extractManifest(val) {
46
+ if (!val || typeof val !== 'object')
47
+ return undefined;
48
+ if ('manifest' in val && val.manifest && typeof val.manifest === 'object') {
49
+ return val.manifest;
50
+ }
51
+ if ('package' in val && val.package && typeof val.package === 'object' && 'manifest' in val.package && val.package.manifest && typeof val.package.manifest === 'object') {
52
+ return val.package.manifest;
53
+ }
54
+ if ('name' in val && typeof val.name === 'string') {
55
+ return val;
56
+ }
57
+ return undefined;
58
+ }
59
+ //# sourceMappingURL=workspaceLookup.js.map
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.6",
4
4
  "description": "Creates an exportable manifest",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -33,7 +33,7 @@
33
33
  "@pnpm/error": "1100.2.1",
34
34
  "@pnpm/resolving.jsr-specifier-parser": "1100.0.9",
35
35
  "@pnpm/types": "1102.1.1",
36
- "@pnpm/workspace.project-manifest-reader": "1100.1.1",
36
+ "@pnpm/workspace.project-manifest-reader": "1100.1.2",
37
37
  "@pnpm/workspace.spec-parser": "1100.0.1",
38
38
  "ramda": "npm:@pnpm/ramda@0.28.1"
39
39
  },
@@ -41,9 +41,9 @@
41
41
  "@jest/globals": "30.4.1",
42
42
  "@pnpm/catalogs.config": "1100.0.9",
43
43
  "@pnpm/catalogs.types": "1100.0.1",
44
- "@pnpm/hooks.pnpmfile": "1100.0.34",
45
- "@pnpm/prepare": "1100.0.33",
46
- "@pnpm/releasing.exportable-manifest": "1100.3.4",
44
+ "@pnpm/hooks.pnpmfile": "1100.0.35",
45
+ "@pnpm/prepare": "1100.0.35",
46
+ "@pnpm/releasing.exportable-manifest": "1100.3.6",
47
47
  "@types/cross-spawn": "^6.0.6",
48
48
  "@types/ramda": "0.32.0",
49
49
  "cross-spawn": "^7.0.6",