@dominic.mayers/last-of-readme 0.1.21 → 0.1.22

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Last of README
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@dominic.mayers/last-of-readme)](https://www.npmjs.com/package/@dominic.mayers/last-of-readme) <!-- DOC-LINK-START --><a href="https://dominic-mayers.github.io/last-of-readme/readme-resolver.html?mode=last&pkg=%40dominic.mayers%2Flast-of-readme&repo=Dominic-Mayers%2Flast-of-readme&host=github.com&v=0.1.21&urlPath="><img alt="README-last of 0.1.21" src="https://img.shields.io/badge/README-last%20of%200.1.21-blue?logo=github"></a><!-- DOC-LINK-END -->
3
+ [![npm version](https://img.shields.io/npm/v/@dominic.mayers/last-of-readme)](https://www.npmjs.com/package/@dominic.mayers/last-of-readme) <!-- DOC-LINK-START --><a href="https://dominic-mayers.github.io/last-of-readme/readme-resolver.html?mode=last&pkg=%40dominic.mayers%2Flast-of-readme&repo=Dominic-Mayers%2Flast-of-readme&host=github.com&v=0.1.22&urlPath="><img alt="README-last of 0.1.22" src="https://img.shields.io/badge/README-last%20of%200.1.22-blue?logo=github"></a><!-- DOC-LINK-END -->
4
4
 
5
5
  By default, a package version is expected to contain its correct documentation, but in practice there is no guarantee that documentation is complete or even correct at release time.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dominic.mayers/last-of-readme",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Resolve README to last relevant commit based on npm version",
5
5
  "author": "Dominic Mayers",
6
6
  "license": "MIT",
@@ -25,6 +25,8 @@
25
25
  "kind": "github",
26
26
  "host": "github.com",
27
27
  "repository": "Dominic-Mayers/last-of-readme"
28
- }
28
+ },
29
+ "packageFilePath": "README.md",
30
+ "repositoryUrlPath": ""
29
31
  }
30
32
  }
@@ -1,22 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
3
+ const { execFileSync } = require('child_process');
5
4
  const readline = require('readline');
6
5
  const { listRemoteChoices } = require('./install-remote.cjs');
7
6
 
8
- function loadPackageJson() {
9
- const packageJsonPath = path.resolve(process.cwd(), 'package.json');
10
- if (!fs.existsSync(packageJsonPath)) {
11
- return null;
7
+ function runNpmPkg(args, { allowFailure = false } = {}) {
8
+ try {
9
+ return execFileSync('npm', ['pkg', ...args], {
10
+ cwd: process.cwd(),
11
+ encoding: 'utf8',
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ }).trim();
14
+ } catch (error) {
15
+ if (allowFailure) {
16
+ return null;
17
+ }
18
+
19
+ const stderr = String(error.stderr || '').trim();
20
+ const suffix = stderr ? `: ${stderr}` : '';
21
+ throw new Error(`Could not access package.json through npm pkg${suffix}`);
12
22
  }
23
+ }
13
24
 
25
+ function getPackageJsonField(fieldPath) {
26
+ const raw = runNpmPkg(['get', fieldPath, '--json'], { allowFailure: true });
27
+ if (raw === null || raw === '') {
28
+ return undefined;
29
+ }
30
+
31
+ let parsed;
14
32
  try {
15
- const raw = fs.readFileSync(packageJsonPath, 'utf8');
16
- return JSON.parse(raw);
33
+ parsed = JSON.parse(raw);
17
34
  } catch (error) {
18
- throw new Error(`Could not read package.json: ${error.message}`);
35
+ throw new Error(`Could not parse npm pkg output for ${fieldPath}: ${error.message}`);
36
+ }
37
+
38
+ return getNestedValue(parsed, fieldPath);
39
+ }
40
+
41
+ function getNestedValue(value, fieldPath) {
42
+ return fieldPath
43
+ .split('.')
44
+ .reduce(
45
+ (current, key) =>
46
+ current && typeof current === 'object' && key in current ? current[key] : undefined,
47
+ value
48
+ );
49
+ }
50
+
51
+ function loadPackageJson() {
52
+ const versionScript = getPackageJsonField('scripts.version');
53
+
54
+ if (typeof versionScript === 'undefined') {
55
+ return null;
19
56
  }
57
+
58
+ return {
59
+ scripts: {
60
+ version: versionScript,
61
+ },
62
+ };
20
63
  }
21
64
 
22
65
  function parsePackageFilePathFromVersionScript(versionScript) {
@@ -28,7 +71,7 @@ function parsePackageFilePathFromVersionScript(versionScript) {
28
71
  /node\s+scripts\/update-readme-link\.cjs\s+(?:'([^']+)'|"([^"]+)"|([^\s]+))/
29
72
  );
30
73
 
31
- return match ? (match[1] || match[2] || match[3]) : null;
74
+ return match ? match[1] || match[2] || match[3] : null;
32
75
  }
33
76
 
34
77
  function parseRepositoryUrlPathFromVersionScript(versionScript) {
@@ -40,7 +83,7 @@ function parseRepositoryUrlPathFromVersionScript(versionScript) {
40
83
  /node\s+scripts\/update-readme-link\.cjs\s+(?:'[^']+'|"[^"]+"|[^\s]+)\s+(?:'([^']*)'|"([^"]*)"|([^\s]+))/
41
84
  );
42
85
 
43
- return match ? (match[1] || match[2] || match[3] || '') : null;
86
+ return match ? match[1] || match[2] || match[3] || '' : null;
44
87
  }
45
88
 
46
89
  function createInterface() {
@@ -232,4 +275,6 @@ module.exports = {
232
275
  loadPackageJson,
233
276
  parsePackageFilePathFromVersionScript,
234
277
  parseRepositoryUrlPathFromVersionScript,
278
+ runNpmPkg,
279
+ getPackageJsonField,
235
280
  };
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFileSync } = require('child_process');
4
3
  const fs = require('fs');
5
4
  const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
6
 
7
7
  const START_MARKER = '<!-- DOC-LINK-START -->';
8
8
  const END_MARKER = '<!-- DOC-LINK-END -->';
@@ -188,67 +188,41 @@ function installDocLink(config = {}) {
188
188
  };
189
189
  }
190
190
 
191
- function runNpmPkg(args, errorPrefix) {
192
- try {
193
- return execFileSync('npm', ['pkg', ...args], {
194
- cwd: process.cwd(),
195
- encoding: 'utf8',
196
- stdio: ['ignore', 'pipe', 'pipe'],
197
- }).trim();
198
- } catch (error) {
199
- const details = [error.stderr, error.stdout]
200
- .filter(Boolean)
201
- .map((value) => String(value).trim())
202
- .find(Boolean);
191
+ function runNpmPkg(args, failureMessage) {
192
+ const result = spawnSync('npm', ['pkg', ...args], {
193
+ cwd: process.cwd(),
194
+ encoding: 'utf8',
195
+ });
203
196
 
204
- throw new Error(
205
- details ? `${errorPrefix}: ${details}` : `${errorPrefix}: ${error.message}`
206
- );
197
+ if (result.error) {
198
+ throw new Error(`${failureMessage}: ${result.error.message}`);
207
199
  }
208
- }
209
-
210
- function readPackageJson() {
211
- const raw = runNpmPkg(['get', '--json'], 'Could not read package.json');
212
200
 
213
- try {
214
- return JSON.parse(raw);
215
- } catch (error) {
216
- throw new Error(`Could not parse package.json content from npm pkg get: ${error.message}`);
201
+ if (result.status !== 0) {
202
+ const suffix = (result.stderr || result.stdout || '').trim();
203
+ throw new Error(`${failureMessage}${suffix ? `: ${suffix}` : ''}`);
217
204
  }
205
+
206
+ return (result.stdout || '').trim();
218
207
  }
219
208
 
220
209
  function getPackageJsonField(field) {
221
210
  const raw = runNpmPkg(['get', field, '--json'], `Could not read package.json field "${field}"`);
222
211
 
223
- if (!raw || raw === 'undefined') {
224
- return undefined;
225
- }
226
-
227
- let parsed;
228
212
  try {
229
- parsed = JSON.parse(raw);
213
+ const parsed = JSON.parse(raw);
214
+ return Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
230
215
  } catch (error) {
231
216
  throw new Error(
232
217
  `Could not parse package.json field "${field}" from npm pkg get: ${error.message}`
233
218
  );
234
219
  }
235
-
236
- if (
237
- parsed &&
238
- typeof parsed === 'object' &&
239
- !Array.isArray(parsed) &&
240
- Object.prototype.hasOwnProperty.call(parsed, field)
241
- ) {
242
- return parsed[field];
243
- }
244
-
245
- return parsed;
246
220
  }
247
221
 
248
- function setPackageJsonFields(updates) {
249
- const assignments = Object.entries(updates).map(
250
- ([key, value]) => `${key}=${JSON.stringify(value)}`
251
- );
222
+ function setPackageJsonFields(assignments) {
223
+ if (!Array.isArray(assignments) || assignments.length === 0) {
224
+ return;
225
+ }
252
226
 
253
227
  runNpmPkg(['set', '--json', ...assignments], 'Could not update package.json');
254
228
  }
@@ -274,20 +248,20 @@ function installDocLinkPackageJson(config = {}) {
274
248
  throw new Error('Doc-link package.json installation requires resolved doc-link cycle state');
275
249
  }
276
250
 
277
- const updates = {
278
- 'scripts.version': buildVersionScript(
279
- docLink.packageFilePath,
280
- docLink.repositoryUrlPath
281
- ),
282
- };
251
+ const assignments = [
252
+ `lastOfReadme.packageFilePath=${JSON.stringify(docLink.packageFilePath)}`,
253
+ `lastOfReadme.repositoryUrlPath=${JSON.stringify(docLink.repositoryUrlPath || '')}`,
254
+ `scripts.version=${JSON.stringify(
255
+ buildVersionScript(docLink.packageFilePath, docLink.repositoryUrlPath)
256
+ )}`,
257
+ ];
283
258
 
284
- const files = getPackageJsonField('files');
285
- if (files !== undefined) {
286
- if (!Array.isArray(files)) {
287
- throw new Error('package.json.files must be an array when present');
288
- }
259
+ setPackageJsonFields(assignments);
289
260
 
261
+ const files = getPackageJsonField('files');
262
+ if (Array.isArray(files)) {
290
263
  const nextFiles = [...files];
264
+
291
265
  if (!nextFiles.includes(docLink.packageFilePath)) {
292
266
  nextFiles.push(docLink.packageFilePath);
293
267
  }
@@ -297,16 +271,15 @@ function installDocLinkPackageJson(config = {}) {
297
271
  docLink.previousPackageFilePath &&
298
272
  docLink.previousPackageFilePath !== docLink.packageFilePath
299
273
  ) {
300
- updates.files = nextFiles.filter(
274
+ const filteredFiles = nextFiles.filter(
301
275
  (item) => item !== docLink.previousPackageFilePath
302
276
  );
303
- } else {
304
- updates.files = nextFiles;
277
+ setPackageJsonFields([`files=${JSON.stringify(filteredFiles)}`]);
278
+ } else if (nextFiles.length !== files.length) {
279
+ setPackageJsonFields([`files=${JSON.stringify(nextFiles)}`]);
305
280
  }
306
281
  }
307
282
 
308
- setPackageJsonFields(updates);
309
-
310
283
  return {
311
284
  path: 'package.json',
312
285
  packageFilePath: docLink.packageFilePath,
@@ -325,6 +298,6 @@ module.exports = {
325
298
  installDocLink,
326
299
  checkDocLinkPackageJsonRequirements,
327
300
  installDocLinkPackageJson,
328
- readPackageJson,
301
+ getPackageJsonField,
329
302
  buildVersionScript,
330
303
  };
@@ -21,6 +21,44 @@ function run(command, options = {}) {
21
21
  }).trim();
22
22
  }
23
23
 
24
+ function runNpmPkg(args) {
25
+ const result = cp.spawnSync('npm', ['pkg', ...args], {
26
+ cwd: WORKSPACE_ROOT,
27
+ encoding: 'utf8',
28
+ });
29
+
30
+ if (result.error) {
31
+ fail(`Could not run npm pkg ${args.join(' ')}: ${result.error.message}`);
32
+ }
33
+
34
+ if (result.status !== 0) {
35
+ const detail = (result.stderr || result.stdout || '').trim();
36
+ fail(`npm pkg ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
37
+ }
38
+
39
+ const stdout = (result.stdout || '').trim();
40
+ if (!stdout) {
41
+ fail(`npm pkg ${args.join(' ')} returned no output`);
42
+ }
43
+
44
+ try {
45
+ return JSON.parse(stdout);
46
+ } catch (err) {
47
+ fail(`Could not parse npm pkg ${args.join(' ')} output: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ function getPackageJsonField(field, { allowEmpty = false } = {}) {
52
+ const value = runNpmPkg(['get', field, '--json']);
53
+ const normalized = Array.isArray(value) && value.length === 1 ? value[0] : value;
54
+
55
+ if ((normalized === undefined || normalized === null || normalized === '') && !allowEmpty) {
56
+ fail(`package.json has no ${field}`);
57
+ }
58
+
59
+ return normalized;
60
+ }
61
+
24
62
  function resolveWorkspacePath(relativePath) {
25
63
  if (!relativePath) {
26
64
  fail('Path is required');
@@ -37,7 +75,8 @@ function ensureFile(filePath, label) {
37
75
  function readPackageJson() {
38
76
  ensureFile(PACKAGE_PATH, 'package.json');
39
77
  try {
40
- return JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8'));
78
+ const result = runNpmPkg(['get', '--json']);
79
+ return Array.isArray(result) && result.length === 1 ? result[0] : result;
41
80
  } catch (err) {
42
81
  fail(`Could not read package.json: ${err.message}`);
43
82
  }
@@ -97,45 +136,37 @@ function currentRepoNode() {
97
136
  }
98
137
 
99
138
  function currentPackageVersion() {
100
- const pkg = readPackageJson();
101
- if (!pkg.version) {
102
- fail('package.json has no version');
103
- }
104
- return pkg.version;
139
+ return String(getPackageJsonField('version'));
105
140
  }
106
141
 
107
142
  function packageName() {
108
- const pkg = readPackageJson();
109
- if (!pkg.name) {
110
- fail('package.json has no name');
111
- }
112
- return pkg.name;
143
+ return String(getPackageJsonField('name'));
113
144
  }
114
145
 
115
146
  function remoteConfiguration() {
116
- const pkg = readPackageJson();
117
- const cfg = pkg.lastOfReadme && pkg.lastOfReadme.remote;
147
+ const kind = getPackageJsonField('lastOfReadme.remote.kind', { allowEmpty: true });
148
+ const host = getPackageJsonField('lastOfReadme.remote.host', { allowEmpty: true });
149
+ const repository = getPackageJsonField('lastOfReadme.remote.repository', { allowEmpty: true });
118
150
 
119
- if (cfg && typeof cfg === 'object') {
120
- if (cfg.kind !== 'github') {
151
+ if (kind !== undefined && kind !== null && kind !== '') {
152
+ if (kind !== 'github') {
121
153
  fail('package.json lastOfReadme.remote.kind must be "github"');
122
154
  }
123
- if (!cfg.host || !cfg.repository) {
155
+ if (!host || !repository) {
124
156
  fail('package.json lastOfReadme.remote must include host and repository');
125
157
  }
126
158
  return {
127
159
  kind: 'github',
128
- host: String(cfg.host),
129
- repository: String(cfg.repository),
160
+ host: String(host),
161
+ repository: String(repository),
130
162
  };
131
163
  }
132
164
 
133
- return normalizeRepositoryUrl(pkg.repository);
165
+ return normalizeRepositoryUrl(getPackageJsonField('repository'));
134
166
  }
135
167
 
136
168
  function remoteName() {
137
- const pkg = readPackageJson();
138
- const configuredRemoteName = pkg.lastOfReadme && pkg.lastOfReadme.remoteName;
169
+ const configuredRemoteName = getPackageJsonField('lastOfReadme.remoteName', { allowEmpty: true });
139
170
  if (configuredRemoteName && typeof configuredRemoteName === 'string') {
140
171
  return configuredRemoteName;
141
172
  }
@@ -146,6 +177,22 @@ function remoteRepository() {
146
177
  return remoteConfiguration().repository;
147
178
  }
148
179
 
180
+ function packageFilePath() {
181
+ const value = getPackageJsonField('lastOfReadme.packageFilePath', { allowEmpty: true });
182
+ if (!value || typeof value !== 'string') {
183
+ fail('package.json has no lastOfReadme.packageFilePath');
184
+ }
185
+ return value;
186
+ }
187
+
188
+ function repositoryUrlPath() {
189
+ const value = getPackageJsonField('lastOfReadme.repositoryUrlPath', { allowEmpty: true });
190
+ if (value === undefined || value === null) {
191
+ fail('package.json has no lastOfReadme.repositoryUrlPath');
192
+ }
193
+ return String(value);
194
+ }
195
+
149
196
  function readFile(relativePath) {
150
197
  const filePath = resolveWorkspacePath(relativePath);
151
198
  ensureFile(filePath, relativePath);
@@ -201,4 +248,6 @@ module.exports = {
201
248
  packageName,
202
249
  currentPackageVersion,
203
250
  remoteName,
251
+ packageFilePath,
252
+ repositoryUrlPath,
204
253
  };
@@ -21,6 +21,23 @@ function usage() {
21
21
  process.exit(1);
22
22
  }
23
23
 
24
+ function resolveInputs() {
25
+ const cliDocumentationPath = process.argv[2];
26
+ const cliUrlPath = process.argv[3] || '';
27
+
28
+ if (cliDocumentationPath) {
29
+ return {
30
+ documentationPath: cliDocumentationPath,
31
+ urlPath: cliUrlPath,
32
+ };
33
+ }
34
+
35
+ return {
36
+ documentationPath: workspace.packageFilePath(),
37
+ urlPath: workspace.repositoryUrlPath(),
38
+ };
39
+ }
40
+
24
41
  function buildResolverLink(urlPath = '') {
25
42
  const version = workspace.currentPackageVersion();
26
43
  const packageName = workspace.packageName();
@@ -87,12 +104,7 @@ function replaceManagedBlock(content, replacement) {
87
104
  }
88
105
 
89
106
  function main() {
90
- const documentationPath = process.argv[2];
91
- const urlPath = process.argv[3] || '';
92
-
93
- if (!documentationPath) {
94
- usage();
95
- }
107
+ const { documentationPath, urlPath } = resolveInputs();
96
108
 
97
109
  try {
98
110
  const link = buildResolverLink(urlPath);