@lakutata/cli 2.17.0 → 3.0.0-beta.0

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.
Files changed (101) hide show
  1. package/README.md +455 -28
  2. package/dist/esm/CLIApp.js +6 -0
  3. package/dist/esm/Config.js +53 -0
  4. package/dist/esm/controllers/CommandLineController.js +180 -0
  5. package/dist/esm/controllers/I18nController.js +67 -0
  6. package/dist/esm/controllers/MigrationController.js +66 -0
  7. package/dist/esm/controllers/NativeController.js +41 -0
  8. package/dist/esm/controllers/PackController.js +88 -0
  9. package/dist/esm/controllers/WorkspaceController.js +91 -0
  10. package/dist/esm/dtos/AddPackageDTO.js +25 -0
  11. package/dist/esm/dtos/BundleDTO.js +29 -0
  12. package/dist/esm/dtos/CleanDTO.js +17 -0
  13. package/dist/esm/dtos/CreateProjectDTO.js +61 -0
  14. package/dist/esm/dtos/ExecutableDTO.js +37 -0
  15. package/dist/esm/dtos/I18nDTO.js +41 -0
  16. package/dist/esm/dtos/InstallerDTO.js +33 -0
  17. package/dist/{options/ListTemplatesOptions.js → esm/dtos/ListTemplatesDTO.js} +5 -14
  18. package/dist/esm/dtos/MigrationDTO.js +45 -0
  19. package/dist/esm/dtos/NativeDTO.js +41 -0
  20. package/dist/esm/dtos/RunScriptDTO.js +33 -0
  21. package/dist/esm/dtos/VersionDTO.js +29 -0
  22. package/dist/esm/lib/AddPackage.js +487 -0
  23. package/dist/esm/lib/Builder.js +21 -0
  24. package/dist/esm/lib/Clean.js +146 -0
  25. package/dist/esm/lib/Environment.js +11 -0
  26. package/dist/esm/lib/I18n.js +67 -0
  27. package/dist/esm/lib/Language.js +24 -0
  28. package/dist/esm/lib/Migration.js +94 -0
  29. package/dist/esm/lib/Output.js +40 -0
  30. package/dist/esm/lib/Processes.js +54 -0
  31. package/dist/esm/lib/ProjectCreator.js +191 -0
  32. package/dist/esm/lib/Registry.js +152 -0
  33. package/dist/esm/lib/Settings.js +25 -0
  34. package/dist/esm/lib/Tar.js +145 -0
  35. package/dist/esm/lib/TemplateCatalog.js +195 -0
  36. package/dist/esm/lib/TemplateFeatures.js +141 -0
  37. package/dist/esm/lib/Versions.js +58 -0
  38. package/dist/esm/lib/native/CMake.js +357 -0
  39. package/dist/esm/lib/native/Gyp.js +398 -0
  40. package/dist/esm/lib/native/NativeProject.js +370 -0
  41. package/dist/esm/lib/native/TextEdits.js +68 -0
  42. package/dist/esm/lib/native/Toml.js +325 -0
  43. package/dist/esm/lib/workspace/Run.js +111 -0
  44. package/dist/esm/lib/workspace/Version.js +144 -0
  45. package/dist/esm/lib/workspace/Workspace.js +231 -0
  46. package/dist/esm/locales/zh-CN.json +194 -0
  47. package/dist/esm/locales/zh-TW.json +194 -0
  48. package/dist/esm/types/ProcessResult.js +1 -0
  49. package/package.json +32 -43
  50. package/CHANGELOG.md +0 -452
  51. package/dist/CLIApp.d.ts +0 -3
  52. package/dist/CLIApp.d.ts.map +0 -1
  53. package/dist/CLIApp.js +0 -45
  54. package/dist/config/Config.d.ts +0 -3
  55. package/dist/config/Config.d.ts.map +0 -1
  56. package/dist/config/Config.js +0 -63
  57. package/dist/config/SetupCLIEntrypoint.d.ts +0 -3
  58. package/dist/config/SetupCLIEntrypoint.d.ts.map +0 -1
  59. package/dist/config/SetupCLIEntrypoint.js +0 -31
  60. package/dist/controllers/CommandLineController.d.ts +0 -25
  61. package/dist/controllers/CommandLineController.d.ts.map +0 -1
  62. package/dist/controllers/CommandLineController.js +0 -78
  63. package/dist/lib/ConvertDTO2Inquirer.d.ts +0 -3
  64. package/dist/lib/ConvertDTO2Inquirer.d.ts.map +0 -1
  65. package/dist/lib/ConvertDTO2Inquirer.js +0 -61
  66. package/dist/lib/GetDataDirectory.d.ts +0 -2
  67. package/dist/lib/GetDataDirectory.d.ts.map +0 -1
  68. package/dist/lib/GetDataDirectory.js +0 -11
  69. package/dist/lib/GetLocalDataFilename.d.ts +0 -2
  70. package/dist/lib/GetLocalDataFilename.d.ts.map +0 -1
  71. package/dist/lib/GetLocalDataFilename.js +0 -12
  72. package/dist/lib/ListTemplateNames.d.ts +0 -2
  73. package/dist/lib/ListTemplateNames.d.ts.map +0 -1
  74. package/dist/lib/ListTemplateNames.js +0 -44
  75. package/dist/lib/SpecialChar.d.ts +0 -9
  76. package/dist/lib/SpecialChar.d.ts.map +0 -1
  77. package/dist/lib/SpecialChar.js +0 -11
  78. package/dist/lib/components/DeGitPuller.d.ts +0 -19
  79. package/dist/lib/components/DeGitPuller.d.ts.map +0 -1
  80. package/dist/lib/components/DeGitPuller.js +0 -65
  81. package/dist/lib/components/Spinner.d.ts +0 -23
  82. package/dist/lib/components/Spinner.d.ts.map +0 -1
  83. package/dist/lib/components/Spinner.js +0 -61
  84. package/dist/lib/providers/Creator.d.ts +0 -46
  85. package/dist/lib/providers/Creator.d.ts.map +0 -1
  86. package/dist/lib/providers/Creator.js +0 -179
  87. package/dist/lib/providers/Information.d.ts +0 -91
  88. package/dist/lib/providers/Information.d.ts.map +0 -1
  89. package/dist/lib/providers/Information.js +0 -225
  90. package/dist/lib/providers/OnlineLatestVersion.d.ts +0 -14
  91. package/dist/lib/providers/OnlineLatestVersion.d.ts.map +0 -1
  92. package/dist/lib/providers/OnlineLatestVersion.js +0 -47
  93. package/dist/lib/providers/TemplateManager.d.ts +0 -36
  94. package/dist/lib/providers/TemplateManager.d.ts.map +0 -1
  95. package/dist/lib/providers/TemplateManager.js +0 -164
  96. package/dist/options/CreateProjectOptions.d.ts +0 -15
  97. package/dist/options/CreateProjectOptions.d.ts.map +0 -1
  98. package/dist/options/CreateProjectOptions.js +0 -94
  99. package/dist/options/ListTemplatesOptions.d.ts +0 -5
  100. package/dist/options/ListTemplatesOptions.d.ts.map +0 -1
  101. package/tsconfig.json +0 -7
@@ -0,0 +1,325 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { applyEdits, lineEndOf, lineIndent, NativeSyntaxError } from './TextEdits.js';
4
+ /**
5
+ * The parser of TOML 1.0: the tables and the arrays of tables, the dotted and quoted keys, the basic and literal
6
+ * strings (multiline too), the arrays (their comments and trailing commas), the inline tables, the other values
7
+ * (numbers, booleans, dates) kept as their text
8
+ */
9
+ class TomlParser {
10
+ constructor(file, text) {
11
+ this.file = file;
12
+ this.text = text;
13
+ this.offset = 0;
14
+ }
15
+ error(message, offset = this.offset) {
16
+ throw new NativeSyntaxError(this.file, this.text, offset, message);
17
+ }
18
+ /**
19
+ * Skip the spaces and tabs of a line
20
+ */
21
+ spaces() {
22
+ while (this.text[this.offset] === ' ' || this.text[this.offset] === '\t')
23
+ this.offset++;
24
+ }
25
+ /**
26
+ * Skip the whitespaces, the newlines and the comments (inside of the arrays)
27
+ */
28
+ blank() {
29
+ for (;;) {
30
+ this.spaces();
31
+ const char = this.text[this.offset];
32
+ if (char === '\n' || char === '\r')
33
+ this.offset++;
34
+ else if (char === '#')
35
+ this.comment();
36
+ else
37
+ return;
38
+ }
39
+ }
40
+ comment() {
41
+ while (this.offset < this.text.length && this.text[this.offset] !== '\n')
42
+ this.offset++;
43
+ }
44
+ /**
45
+ * The end of a line: spaces, a comment, a newline (or the end of the text)
46
+ */
47
+ lineEnd() {
48
+ this.spaces();
49
+ if (this.text[this.offset] === '#')
50
+ this.comment();
51
+ if (this.text[this.offset] === '\r')
52
+ this.offset++;
53
+ if (this.offset < this.text.length && this.text[this.offset] !== '\n')
54
+ this.error('expected the end of the line');
55
+ this.offset++;
56
+ }
57
+ parse() {
58
+ const tables = [{ path: [], array: false, header: -1, headerEnd: 0, entries: [] }];
59
+ for (;;) {
60
+ this.blank();
61
+ if (this.offset >= this.text.length)
62
+ return tables;
63
+ if (this.text[this.offset] === '[') {
64
+ const header = this.offset;
65
+ const array = this.text[this.offset + 1] === '[';
66
+ this.offset += array ? 2 : 1;
67
+ this.spaces();
68
+ const path = this.key();
69
+ this.spaces();
70
+ if (!this.text.startsWith(array ? ']]' : ']', this.offset))
71
+ this.error(`expected "${array ? ']]' : ']'}"`);
72
+ this.offset += array ? 2 : 1;
73
+ const headerEnd = this.offset;
74
+ this.lineEnd();
75
+ tables.push({ path: path, array: array, header: header, headerEnd: headerEnd, entries: [] });
76
+ }
77
+ else {
78
+ tables.at(-1).entries.push(this.entry());
79
+ this.lineEnd();
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * A key: its parts, dotted (bare, basic or literal)
85
+ */
86
+ key() {
87
+ const parts = [];
88
+ for (;;) {
89
+ const char = this.text[this.offset];
90
+ if (char === '"' || char === '\'') {
91
+ parts.push(this.string().value);
92
+ }
93
+ else {
94
+ const bare = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.offset));
95
+ if (!bare)
96
+ this.error('expected a key');
97
+ parts.push(bare[0]);
98
+ this.offset += bare[0].length;
99
+ }
100
+ this.spaces();
101
+ if (this.text[this.offset] !== '.')
102
+ return parts;
103
+ this.offset++;
104
+ this.spaces();
105
+ }
106
+ }
107
+ entry() {
108
+ const start = this.offset;
109
+ const key = this.key();
110
+ this.spaces();
111
+ if (this.text[this.offset] !== '=')
112
+ this.error('expected "="');
113
+ this.offset++;
114
+ this.spaces();
115
+ return { key: key, start: start, value: this.value() };
116
+ }
117
+ value() {
118
+ const char = this.text[this.offset];
119
+ if (char === '"' || char === '\'')
120
+ return this.string();
121
+ if (char === '[')
122
+ return this.array();
123
+ if (char === '{')
124
+ return this.inlineTable();
125
+ //The numbers, the booleans and the dates, their text
126
+ const other = /^[^\s,\]}#]+(?: [0-9:.+\-Z]+)?/.exec(this.text.slice(this.offset));
127
+ if (!other)
128
+ this.error(char === undefined ? 'unexpected end of the file' : `unexpected ${JSON.stringify(char)}`);
129
+ const start = this.offset;
130
+ this.offset += other[0].length;
131
+ return { kind: 'other', text: other[0], start: start, end: this.offset };
132
+ }
133
+ string() {
134
+ const start = this.offset;
135
+ const quote = this.text[this.offset];
136
+ const multiline = this.text.startsWith(quote.repeat(3), this.offset);
137
+ const delimiter = multiline ? quote.repeat(3) : quote;
138
+ this.offset += delimiter.length;
139
+ //The newline right after the opening of a multiline string is not part of it
140
+ if (multiline && this.text[this.offset] === '\n')
141
+ this.offset++;
142
+ else if (multiline && this.text.startsWith('\r\n', this.offset))
143
+ this.offset += 2;
144
+ let value = '';
145
+ for (;;) {
146
+ if (this.offset >= this.text.length)
147
+ this.error('unterminated string', start);
148
+ if (this.text.startsWith(delimiter, this.offset)) {
149
+ //Up to two quotes before the closing ones are the end of the content of a multiline string ('''a''''')
150
+ let extra = 0;
151
+ while (multiline && extra < 2 && this.text[this.offset + delimiter.length + extra] === quote)
152
+ extra++;
153
+ value += quote.repeat(extra);
154
+ this.offset += delimiter.length + extra;
155
+ return { kind: 'string', value: value, start: start, end: this.offset };
156
+ }
157
+ const char = this.text[this.offset];
158
+ if (!multiline && char === '\n')
159
+ this.error('unterminated string', start);
160
+ if (char === '\\' && quote === '"') {
161
+ const next = this.text[this.offset + 1] ?? '';
162
+ if (multiline && /[ \t\r\n]/.test(next)) {
163
+ //A line ending backslash: the whitespaces and newlines after it trimmed
164
+ this.offset++;
165
+ while (/[ \t\r\n]/.test(this.text[this.offset] ?? ''))
166
+ this.offset++;
167
+ continue;
168
+ }
169
+ const escapes = { 'b': '\b', 't': '\t', 'n': '\n', 'f': '\f', 'r': '\r', 'e': '\x1b', '"': '"', '\\': '\\' };
170
+ if (next in escapes) {
171
+ value += escapes[next];
172
+ this.offset += 2;
173
+ }
174
+ else if (next === 'u' || next === 'U' || next === 'x') {
175
+ const length = next === 'u' ? 4 : next === 'U' ? 8 : 2;
176
+ value += String.fromCodePoint(parseInt(this.text.slice(this.offset + 2, this.offset + 2 + length), 16));
177
+ this.offset += 2 + length;
178
+ }
179
+ else {
180
+ this.error(`invalid escape "\\${next}"`);
181
+ }
182
+ continue;
183
+ }
184
+ value += char;
185
+ this.offset++;
186
+ }
187
+ }
188
+ array() {
189
+ const start = this.offset;
190
+ this.offset++;
191
+ const items = [];
192
+ let trailingComma = false;
193
+ for (;;) {
194
+ this.blank();
195
+ if (this.text[this.offset] === ']')
196
+ break;
197
+ items.push(this.value());
198
+ trailingComma = false;
199
+ this.blank();
200
+ if (this.text[this.offset] === ',') {
201
+ this.offset++;
202
+ trailingComma = true;
203
+ }
204
+ else if (this.text[this.offset] !== ']') {
205
+ this.error('expected "," or "]"');
206
+ }
207
+ }
208
+ const close = this.offset;
209
+ this.offset++;
210
+ return { kind: 'array', items: items, start: start, end: this.offset, close: close, trailingComma: trailingComma };
211
+ }
212
+ inlineTable() {
213
+ const start = this.offset;
214
+ this.offset++;
215
+ const entries = [];
216
+ for (;;) {
217
+ this.blank();
218
+ if (this.text[this.offset] === '}')
219
+ break;
220
+ entries.push(this.entry());
221
+ this.blank();
222
+ if (this.text[this.offset] === ',')
223
+ this.offset++;
224
+ else if (this.text[this.offset] !== '}')
225
+ this.error('expected "," or "}"');
226
+ }
227
+ this.offset++;
228
+ return { kind: 'inline-table', entries: entries, start: start, end: this.offset };
229
+ }
230
+ }
231
+ /**
232
+ * A key of TOML: bare when it can be, otherwise literal ('…'), or basic ("…") when it has a quote
233
+ * @param key
234
+ */
235
+ export function tomlKey(key) {
236
+ if (/^[A-Za-z0-9_-]+$/.test(key))
237
+ return key;
238
+ return key.includes('\'') ? JSON.stringify(key) : `'${key}'`;
239
+ }
240
+ /**
241
+ * A string of TOML (basic)
242
+ * @param value
243
+ */
244
+ export function tomlStringText(value) {
245
+ return JSON.stringify(value);
246
+ }
247
+ /**
248
+ * A document of TOML (.cargo/config.toml), parsed with the places of its tables and values: it is changed by edits of
249
+ * its text, its comments and its layout kept
250
+ */
251
+ export class TomlDocument {
252
+ constructor(file, text) {
253
+ this.file = file;
254
+ this.text = text;
255
+ this.edits = [];
256
+ this.tables = new TomlParser(file, text).parse();
257
+ }
258
+ /**
259
+ * The document of a file (empty when it does not exist)
260
+ * @param file
261
+ */
262
+ static async read(file) {
263
+ return new TomlDocument(file, existsSync(file) ? await readFile(file, 'utf8') : '');
264
+ }
265
+ get changes() {
266
+ return this.edits;
267
+ }
268
+ applied() {
269
+ return new TomlDocument(this.file, applyEdits(this.text, this.edits));
270
+ }
271
+ /**
272
+ * The table of a path (not an array of tables)
273
+ * @param path
274
+ */
275
+ table(path) {
276
+ return this.tables.find((candidate) => !candidate.array && candidate.path.length === path.length && candidate.path.every((part, index) => part === path[index]));
277
+ }
278
+ /**
279
+ * The string of a key of a table
280
+ * @param path the path of the table
281
+ * @param key
282
+ */
283
+ string(path, key) {
284
+ const value = this.table(path)?.entries.find((entry) => entry.key.length === 1 && entry.key[0] === key)?.value;
285
+ if (value && value.kind !== 'string')
286
+ throw new NativeSyntaxError(this.file, this.text, value.start, `"${key}" is not a string`);
287
+ return value?.value;
288
+ }
289
+ /**
290
+ * Add values to a list in a string of a table, its items separated by a separator (the table and the key created
291
+ * when missing); the values already in it are skipped
292
+ * @param path the path of the table
293
+ * @param key
294
+ * @param values
295
+ * @param separator
296
+ * @return whether the document is changed
297
+ */
298
+ addToStringList(path, key, values, separator) {
299
+ const current = this.string(path, key);
300
+ const items = current === undefined ? [] : current.split(separator).filter((item) => item.trim() !== '');
301
+ const missing = values.filter((value, index) => !items.includes(value) && values.indexOf(value) === index);
302
+ if (!missing.length)
303
+ return false;
304
+ const rendered = tomlStringText([...items, ...missing].join(separator));
305
+ const table = this.table(path);
306
+ if (!table) {
307
+ const blank = !this.text.length ? '' : this.text.endsWith('\n\n') ? '' : this.text.endsWith('\n') ? '\n' : '\n\n';
308
+ this.edits.push({ offset: this.text.length, remove: 0, insert: `${blank}[${path.map(tomlKey).join('.')}]\n${tomlKey(key)} = ${rendered}\n` });
309
+ return true;
310
+ }
311
+ const entry = table.entries.find((candidate) => candidate.key.length === 1 && candidate.key[0] === key);
312
+ if (entry) {
313
+ this.edits.push({ offset: entry.value.start, remove: entry.value.end - entry.value.start, insert: rendered });
314
+ return true;
315
+ }
316
+ //After the last entry of the table, or its header
317
+ const last = table.entries.at(-1);
318
+ const after = lineEndOf(this.text, last ? last.value.end : table.headerEnd);
319
+ const indent = last ? lineIndent(this.text, last.start) : '';
320
+ this.edits.push(after < this.text.length
321
+ ? { offset: after, remove: 0, insert: `\n${indent}${tomlKey(key)} = ${rendered}` }
322
+ : { offset: after, remove: 0, insert: `${this.text.endsWith('\n') ? '' : '\n'}${indent}${tomlKey(key)} = ${rendered}\n` });
323
+ return true;
324
+ }
325
+ }
@@ -0,0 +1,111 @@
1
+ import os from 'node:os';
2
+ import { LaunchEnvironment } from '../Environment.js';
3
+ import { spawnCommand } from '../Processes.js';
4
+ import { styled, warn } from '../Output.js';
5
+ import { matchName } from './Workspace.js';
6
+ const COLORS = ['cyan', 'magenta', 'yellow', 'green', 'blue', 'red'];
7
+ /**
8
+ * Run a script of a package, its output prefixed with the name of the package
9
+ * @param pkg
10
+ * @param script
11
+ * @param args
12
+ * @param prefix
13
+ */
14
+ function runPackageScript(pkg, script, args, prefix) {
15
+ return new Promise((resolve) => {
16
+ const child = spawnCommand('npm', ['run', script, ...(args.length ? ['--', ...args] : [])], { cwd: pkg.directory, env: LaunchEnvironment });
17
+ const forward = (stream, target) => {
18
+ let pending = '';
19
+ stream.on('data', (chunk) => {
20
+ const lines = (pending + chunk.toString()).split('\n');
21
+ pending = lines.pop();
22
+ for (const line of lines)
23
+ target.write(`${prefix} ${line}\n`);
24
+ });
25
+ stream.on('end', () => {
26
+ if (pending)
27
+ target.write(`${prefix} ${pending}\n`);
28
+ });
29
+ };
30
+ //Piped (the default stdio)
31
+ forward(child.stdout, process.stdout);
32
+ forward(child.stderr, process.stderr);
33
+ child.once('error', (error) => {
34
+ process.stderr.write(`${prefix} ${error.message}\n`);
35
+ resolve(1);
36
+ });
37
+ child.once('close', (code) => resolve(code ?? 1));
38
+ });
39
+ }
40
+ /**
41
+ * Run a script in the packages of a workspace having it: a package runs once the packages it depends on ran,
42
+ * the packages independent of each other run at the same time
43
+ * @param workspace
44
+ * @param script
45
+ * @param options
46
+ */
47
+ export async function runScript(workspace, script, options = {}) {
48
+ const inScope = (pkg) => !options.scope?.length || options.scope.some((pattern) => matchName(pkg.name, pattern));
49
+ const selected = workspace.packages.filter((pkg) => inScope(pkg) && !!pkg.scripts[script]);
50
+ const skipped = workspace.packages.filter((pkg) => inScope(pkg) && !pkg.scripts[script]).map((pkg) => pkg.name);
51
+ const selectedNames = new Set(selected.map((pkg) => pkg.name));
52
+ if (!options.parallel)
53
+ for (const cycle of workspace.cycles())
54
+ warn(`The packages depend on each other: ${cycle.join(' → ')}, they run in the order of their names`);
55
+ //The selected packages each one waits for (through the packages without the script too); the cycles are broken by name
56
+ const waitsFor = new Map(selected.map((pkg) => {
57
+ if (options.parallel)
58
+ return [pkg.name, new Set()];
59
+ const dependencies = new Set([...workspace.transitiveDependencies(pkg)].filter((name) => selectedNames.has(name)));
60
+ for (const name of dependencies)
61
+ if (workspace.transitiveDependencies(workspace.get(name)).has(pkg.name) && name > pkg.name)
62
+ dependencies.delete(name);
63
+ return [pkg.name, dependencies];
64
+ }));
65
+ const concurrency = options.parallel ? Math.max(1, selected.length) : Math.max(1, options.concurrency ?? os.availableParallelism());
66
+ const width = Math.max(0, ...selected.map((pkg) => pkg.name.length));
67
+ const done = new Set();
68
+ //The packages depending on a failed package, not run
69
+ const blocked = new Set();
70
+ const failed = [];
71
+ const ran = [];
72
+ const running = new Map();
73
+ let stopped = false;
74
+ while (true) {
75
+ if (!stopped) {
76
+ for (const pkg of selected) {
77
+ if (running.size >= concurrency)
78
+ break;
79
+ if (done.has(pkg.name) || blocked.has(pkg.name) || running.has(pkg.name) || failed.includes(pkg.name))
80
+ continue;
81
+ if (![...waitsFor.get(pkg.name)].every((name) => done.has(name)))
82
+ continue;
83
+ const prefix = styled(COLORS[selected.indexOf(pkg) % COLORS.length], `${pkg.name.padEnd(width)} │`);
84
+ running.set(pkg.name, runPackageScript(pkg, script, options.args ?? [], prefix).then((code) => {
85
+ running.delete(pkg.name);
86
+ if (code === 0) {
87
+ done.add(pkg.name);
88
+ ran.push(pkg.name);
89
+ }
90
+ else {
91
+ failed.push(pkg.name);
92
+ if (options.bail !== false)
93
+ stopped = true;
94
+ //The packages depending on a failed package do not run
95
+ else
96
+ for (const other of selected)
97
+ if (waitsFor.get(other.name).has(pkg.name))
98
+ blocked.add(other.name);
99
+ }
100
+ }));
101
+ }
102
+ }
103
+ if (!running.size)
104
+ break;
105
+ await Promise.race(running.values());
106
+ }
107
+ const notRun = selected.map((pkg) => pkg.name).filter((name) => !ran.includes(name) && !failed.includes(name));
108
+ if (failed.length)
109
+ throw new Error(`${script} failed in ${failed.join(', ')}${notRun.length ? `, not run in ${notRun.join(', ')}` : ''}`);
110
+ return { ran: ran, skipped: skipped, failed: failed };
111
+ }
@@ -0,0 +1,144 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { runCommand } from '../Processes.js';
4
+ import { warn } from '../Output.js';
5
+ import { DEPENDENCY_FIELDS } from './Workspace.js';
6
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
7
+ /**
8
+ * The next version
9
+ * @param current
10
+ * @param bump a bump, or a version
11
+ * @param preid the identifier of the prereleases ("beta"), none by default
12
+ */
13
+ export function nextVersion(current, bump, preid) {
14
+ if (VERSION.test(bump))
15
+ return bump;
16
+ const matched = VERSION.exec(current);
17
+ if (!matched)
18
+ throw new Error(`The current version "${current}" is not a version`);
19
+ const [major, minor, patch] = [Number(matched[1]), Number(matched[2]), Number(matched[3])];
20
+ const prerelease = matched[4];
21
+ const pre = () => preid ? `${preid}.0` : '0';
22
+ switch (bump) {
23
+ case 'major':
24
+ return prerelease && minor === 0 && patch === 0 ? `${major}.0.0` : `${major + 1}.0.0`;
25
+ case 'minor':
26
+ return prerelease && patch === 0 ? `${major}.${minor}.0` : `${major}.${minor + 1}.0`;
27
+ case 'patch':
28
+ return prerelease ? `${major}.${minor}.${patch}` : `${major}.${minor}.${patch + 1}`;
29
+ case 'premajor':
30
+ return `${major + 1}.0.0-${pre()}`;
31
+ case 'preminor':
32
+ return `${major}.${minor + 1}.0-${pre()}`;
33
+ case 'prepatch':
34
+ return `${major}.${minor}.${patch + 1}-${pre()}`;
35
+ case 'prerelease': {
36
+ if (!prerelease)
37
+ return `${major}.${minor}.${patch + 1}-${pre()}`;
38
+ const parts = prerelease.split('.');
39
+ const last = Number(parts.at(-1));
40
+ //1.0.0-beta.1 → 1.0.0-beta.2; another identifier starts again
41
+ if (preid && parts[0] !== preid)
42
+ return `${major}.${minor}.${patch}-${preid}.0`;
43
+ if (Number.isInteger(last))
44
+ parts[parts.length - 1] = String(last + 1);
45
+ else
46
+ parts.push('0');
47
+ return `${major}.${minor}.${patch}-${parts.join('.')}`;
48
+ }
49
+ default:
50
+ throw new Error(`Invalid bump "${bump}": major, minor, patch, premajor, preminor, prepatch, prerelease or a version`);
51
+ }
52
+ }
53
+ /**
54
+ * The range of a dependency on a package of the workspace, for its new version: its prefix is kept ("^0.0.3" → "^0.0.4"),
55
+ * the ranges which are not a version ("*", "workspace:*", "file:…") are kept
56
+ * @param range
57
+ * @param version
58
+ */
59
+ export function updateRange(range, version) {
60
+ const matched = /^(workspace:)?(\^|~|>=|<=|>|<|=)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(range);
61
+ return matched ? `${matched[1] ?? ''}${matched[2] ?? ''}${version}` : range;
62
+ }
63
+ /**
64
+ * The versions a bump gives
65
+ * @param workspace
66
+ * @param bump
67
+ * @param preid
68
+ */
69
+ export function planVersion(workspace, bump, preid) {
70
+ const from = workspace.version ?? '0.0.0';
71
+ return {
72
+ from: from,
73
+ to: nextVersion(from, bump, preid),
74
+ packages: workspace.packages.map((pkg) => ({ name: pkg.name, from: pkg.version }))
75
+ };
76
+ }
77
+ /**
78
+ * Give the packages of the workspace a version (a fixed version: the same for all), update the dependencies between them and the lock file
79
+ * @param workspace
80
+ * @param plan
81
+ * @param options
82
+ */
83
+ export async function applyVersion(workspace, plan, options = {}) {
84
+ const names = new Set(workspace.packages.map((pkg) => pkg.name));
85
+ const changed = [];
86
+ for (const pkg of workspace.packages) {
87
+ const data = pkg.manifest.data;
88
+ data.version = plan.to;
89
+ for (const field of DEPENDENCY_FIELDS) {
90
+ for (const [name, range] of Object.entries(data[field] ?? {})) {
91
+ if (names.has(name))
92
+ data[field][name] = updateRange(range, plan.to);
93
+ }
94
+ }
95
+ changed.push(pkg.manifest);
96
+ }
97
+ //The version of the workspace is kept by lerna.json when it has one, by the root package.json otherwise
98
+ if (workspace.lerna) {
99
+ workspace.lerna.data.version = plan.to;
100
+ changed.push(workspace.lerna);
101
+ }
102
+ else {
103
+ workspace.manifest.data.version = plan.to;
104
+ changed.push(workspace.manifest);
105
+ }
106
+ //The root package.json may depend on the packages too
107
+ for (const field of DEPENDENCY_FIELDS) {
108
+ for (const [name, range] of Object.entries(workspace.manifest.data[field] ?? {})) {
109
+ if (names.has(name))
110
+ workspace.manifest.data[field][name] = updateRange(range, plan.to);
111
+ }
112
+ }
113
+ if (!changed.includes(workspace.manifest))
114
+ changed.push(workspace.manifest);
115
+ for (const file of changed)
116
+ await file.write();
117
+ const files = changed.map((file) => file.file);
118
+ //The lock file follows (npm ci and the frozen installs fail otherwise)
119
+ const lock = [
120
+ { file: 'package-lock.json', command: 'npm', args: ['install', '--package-lock-only', '--ignore-scripts', '--no-audit', '--no-fund'] },
121
+ { file: 'pnpm-lock.yaml', command: 'pnpm', args: ['install', '--lockfile-only', '--ignore-scripts'] }
122
+ ].find((candidate) => existsSync(path.join(workspace.root, candidate.file)));
123
+ if (lock) {
124
+ const result = await runCommand(lock.command, lock.args, workspace.root).catch((error) => ({ code: 1, output: error.message }));
125
+ if (result.code === 0)
126
+ files.push(path.join(workspace.root, lock.file));
127
+ else
128
+ warn(`The lock file ${lock.file} is not updated (run ${lock.command} install): ${result.output.trim().split('\n').slice(-3).join(' ')}`);
129
+ }
130
+ else if (existsSync(path.join(workspace.root, 'yarn.lock'))) {
131
+ warn('yarn.lock is not updated: run yarn install');
132
+ }
133
+ if (options.git) {
134
+ const git = async (args) => {
135
+ const result = await runCommand('git', args, workspace.root);
136
+ if (result.code !== 0)
137
+ throw new Error(`git ${args[0]} failed: ${result.output.trim()}`);
138
+ };
139
+ await git(['add', ...files]);
140
+ await git(['commit', '-m', `v${plan.to}`]);
141
+ await git(['tag', `v${plan.to}`]);
142
+ }
143
+ return files;
144
+ }