@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.
- package/README.md +455 -28
- package/dist/esm/CLIApp.js +6 -0
- package/dist/esm/Config.js +53 -0
- package/dist/esm/controllers/CommandLineController.js +180 -0
- package/dist/esm/controllers/I18nController.js +67 -0
- package/dist/esm/controllers/MigrationController.js +66 -0
- package/dist/esm/controllers/NativeController.js +41 -0
- package/dist/esm/controllers/PackController.js +88 -0
- package/dist/esm/controllers/WorkspaceController.js +91 -0
- package/dist/esm/dtos/AddPackageDTO.js +25 -0
- package/dist/esm/dtos/BundleDTO.js +29 -0
- package/dist/esm/dtos/CleanDTO.js +17 -0
- package/dist/esm/dtos/CreateProjectDTO.js +61 -0
- package/dist/esm/dtos/ExecutableDTO.js +37 -0
- package/dist/esm/dtos/I18nDTO.js +41 -0
- package/dist/esm/dtos/InstallerDTO.js +33 -0
- package/dist/{options/ListTemplatesOptions.js → esm/dtos/ListTemplatesDTO.js} +5 -14
- package/dist/esm/dtos/MigrationDTO.js +45 -0
- package/dist/esm/dtos/NativeDTO.js +41 -0
- package/dist/esm/dtos/RunScriptDTO.js +33 -0
- package/dist/esm/dtos/VersionDTO.js +29 -0
- package/dist/esm/lib/AddPackage.js +487 -0
- package/dist/esm/lib/Builder.js +21 -0
- package/dist/esm/lib/Clean.js +146 -0
- package/dist/esm/lib/Environment.js +11 -0
- package/dist/esm/lib/I18n.js +67 -0
- package/dist/esm/lib/Language.js +24 -0
- package/dist/esm/lib/Migration.js +94 -0
- package/dist/esm/lib/Output.js +40 -0
- package/dist/esm/lib/Processes.js +54 -0
- package/dist/esm/lib/ProjectCreator.js +191 -0
- package/dist/esm/lib/Registry.js +152 -0
- package/dist/esm/lib/Settings.js +25 -0
- package/dist/esm/lib/Tar.js +145 -0
- package/dist/esm/lib/TemplateCatalog.js +195 -0
- package/dist/esm/lib/TemplateFeatures.js +141 -0
- package/dist/esm/lib/Versions.js +58 -0
- package/dist/esm/lib/native/CMake.js +357 -0
- package/dist/esm/lib/native/Gyp.js +398 -0
- package/dist/esm/lib/native/NativeProject.js +370 -0
- package/dist/esm/lib/native/TextEdits.js +68 -0
- package/dist/esm/lib/native/Toml.js +325 -0
- package/dist/esm/lib/workspace/Run.js +111 -0
- package/dist/esm/lib/workspace/Version.js +144 -0
- package/dist/esm/lib/workspace/Workspace.js +231 -0
- package/dist/esm/locales/zh-CN.json +194 -0
- package/dist/esm/locales/zh-TW.json +194 -0
- package/dist/esm/types/ProcessResult.js +1 -0
- package/package.json +32 -43
- package/CHANGELOG.md +0 -452
- package/dist/CLIApp.d.ts +0 -3
- package/dist/CLIApp.d.ts.map +0 -1
- package/dist/CLIApp.js +0 -45
- package/dist/config/Config.d.ts +0 -3
- package/dist/config/Config.d.ts.map +0 -1
- package/dist/config/Config.js +0 -63
- package/dist/config/SetupCLIEntrypoint.d.ts +0 -3
- package/dist/config/SetupCLIEntrypoint.d.ts.map +0 -1
- package/dist/config/SetupCLIEntrypoint.js +0 -31
- package/dist/controllers/CommandLineController.d.ts +0 -25
- package/dist/controllers/CommandLineController.d.ts.map +0 -1
- package/dist/controllers/CommandLineController.js +0 -78
- package/dist/lib/ConvertDTO2Inquirer.d.ts +0 -3
- package/dist/lib/ConvertDTO2Inquirer.d.ts.map +0 -1
- package/dist/lib/ConvertDTO2Inquirer.js +0 -61
- package/dist/lib/GetDataDirectory.d.ts +0 -2
- package/dist/lib/GetDataDirectory.d.ts.map +0 -1
- package/dist/lib/GetDataDirectory.js +0 -11
- package/dist/lib/GetLocalDataFilename.d.ts +0 -2
- package/dist/lib/GetLocalDataFilename.d.ts.map +0 -1
- package/dist/lib/GetLocalDataFilename.js +0 -12
- package/dist/lib/ListTemplateNames.d.ts +0 -2
- package/dist/lib/ListTemplateNames.d.ts.map +0 -1
- package/dist/lib/ListTemplateNames.js +0 -44
- package/dist/lib/SpecialChar.d.ts +0 -9
- package/dist/lib/SpecialChar.d.ts.map +0 -1
- package/dist/lib/SpecialChar.js +0 -11
- package/dist/lib/components/DeGitPuller.d.ts +0 -19
- package/dist/lib/components/DeGitPuller.d.ts.map +0 -1
- package/dist/lib/components/DeGitPuller.js +0 -65
- package/dist/lib/components/Spinner.d.ts +0 -23
- package/dist/lib/components/Spinner.d.ts.map +0 -1
- package/dist/lib/components/Spinner.js +0 -61
- package/dist/lib/providers/Creator.d.ts +0 -46
- package/dist/lib/providers/Creator.d.ts.map +0 -1
- package/dist/lib/providers/Creator.js +0 -179
- package/dist/lib/providers/Information.d.ts +0 -91
- package/dist/lib/providers/Information.d.ts.map +0 -1
- package/dist/lib/providers/Information.js +0 -225
- package/dist/lib/providers/OnlineLatestVersion.d.ts +0 -14
- package/dist/lib/providers/OnlineLatestVersion.d.ts.map +0 -1
- package/dist/lib/providers/OnlineLatestVersion.js +0 -47
- package/dist/lib/providers/TemplateManager.d.ts +0 -36
- package/dist/lib/providers/TemplateManager.d.ts.map +0 -1
- package/dist/lib/providers/TemplateManager.js +0 -164
- package/dist/options/CreateProjectOptions.d.ts +0 -15
- package/dist/options/CreateProjectOptions.d.ts.map +0 -1
- package/dist/options/CreateProjectOptions.js +0 -94
- package/dist/options/ListTemplatesOptions.d.ts +0 -5
- package/dist/options/ListTemplatesOptions.d.ts.map +0 -1
- 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
|
+
}
|