depflow 3.0.0-dev.11 → 3.0.0-dev.13
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 +494 -162
- package/dist/cli/Actions.d.ts +194 -0
- package/dist/cli/Actions.js +803 -0
- package/dist/cli/Actions.js.map +1 -0
- package/dist/cli/CLI.d.ts +70 -4
- package/dist/cli/CLI.js +70 -7
- package/dist/cli/CLI.js.map +1 -1
- package/dist/cli/DepflowCLI.d.ts +66 -0
- package/dist/cli/DepflowCLI.js +228 -0
- package/dist/cli/DepflowCLI.js.map +1 -0
- package/dist/cli/bin.d.ts +6 -0
- package/dist/cli/bin.js +8 -32
- package/dist/cli/bin.js.map +1 -1
- package/dist/config/Config.d.ts +1 -1
- package/dist/dependency/DepUtils.d.ts +35 -7
- package/dist/dependency/DepUtils.js +64 -13
- package/dist/dependency/DepUtils.js.map +1 -1
- package/dist/schema/Config/Config.d.ts +1 -1
- package/dist/schema/Config/Dependency.d.ts +3 -3
- package/dist/schema/Config/Dependency.js +3 -3
- package/dist/schema/Config/Dependency.js.map +1 -1
- package/package.json +3 -2
- package/dist/cli/DepFLowCLI.d.ts +0 -93
- package/dist/cli/DepFLowCLI.js +0 -413
- package/dist/cli/DepFLowCLI.js.map +0 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author NetFeez <netfeez.dev@gmail.com>.
|
|
3
|
+
* @description The operations of DepFlow, detached from the console that invokes them: every action runs
|
|
4
|
+
* against an explicit project context, so it can be driven by the CLI or by a tool built on it.
|
|
5
|
+
* @license Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from '@netfeez/vterm';
|
|
8
|
+
import { Path } from '@netfeez/common-node';
|
|
9
|
+
import Group from '../task/Group.js';
|
|
10
|
+
import OutputPolicy from '../task/OutputPolicy.js';
|
|
11
|
+
import DepUtils from '../dependency/DepUtils.js';
|
|
12
|
+
import GitDependency from '../dependency/GitDependency.js';
|
|
13
|
+
import NpmDependency from '../dependency/NpmDependency.js';
|
|
14
|
+
import PathResolver from '../resolve/PathResolver.js';
|
|
15
|
+
import Config from '../config/Config.js';
|
|
16
|
+
import JsonSchema from '../config/JsonSchema.js';
|
|
17
|
+
import schema from '../schema/schema.js';
|
|
18
|
+
import TSConfig from '../config/TSConfig.js';
|
|
19
|
+
import ImportMap from '../config/ImportMap.js';
|
|
20
|
+
import Builder from '../builder/Builder.js';
|
|
21
|
+
export var Actions;
|
|
22
|
+
(function (Actions) {
|
|
23
|
+
/**
|
|
24
|
+
* The policy the run is presented through, built for the mode the options asked for.
|
|
25
|
+
* @param options - The options of the action being run.
|
|
26
|
+
* @returns The policy of the run, never `null`: the evidence is written whether it is asked for or not.
|
|
27
|
+
*
|
|
28
|
+
* @remarks The policy is also what settles the channel the lifecycle of the run is reported through, so
|
|
29
|
+
* every action asks for it first and reports through `policy.report`: a `silent` run that said
|
|
30
|
+
* "Installing dependencies..." would not be silent, and an undecorated run would still draw boxes
|
|
31
|
+
* around lines that carry nothing but what they say.
|
|
32
|
+
*
|
|
33
|
+
* @remarks An action reports through the policy and not through a console, which belongs to the surface
|
|
34
|
+
* DepFlow was built on: the help, the prompt and the verdict of an invocation are drawn elsewhere, and
|
|
35
|
+
* they stay visible in every mode.
|
|
36
|
+
*/
|
|
37
|
+
function policy(options = {}) {
|
|
38
|
+
return new OutputPolicy({
|
|
39
|
+
logger: new Logger({ name: 'depflow', save: true }),
|
|
40
|
+
name: 'depflow',
|
|
41
|
+
verbose: options.verbose ?? false,
|
|
42
|
+
silent: options.silent ?? false,
|
|
43
|
+
decorate: options.decorate ?? true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
Actions.policy = policy;
|
|
47
|
+
/**
|
|
48
|
+
* Adds a git dependency to the configuration, creating the file from the defaults when it does not exist.
|
|
49
|
+
* @param context - The project the action operates on.
|
|
50
|
+
* @param args - The repository URL and, optionally, the name to register the dependency under.
|
|
51
|
+
* @param options - The mode options of the run.
|
|
52
|
+
* @returns A promise resolving when the dependency is persisted.
|
|
53
|
+
*/
|
|
54
|
+
async function add(context, args, options = {}) {
|
|
55
|
+
const policy = Actions.policy(options);
|
|
56
|
+
const out = policy.report;
|
|
57
|
+
const boxed = policy.decoration === 'on';
|
|
58
|
+
try {
|
|
59
|
+
if (boxed)
|
|
60
|
+
out.group(Group.create('#FFB4DC'));
|
|
61
|
+
const config = await Config.load(context.configPath);
|
|
62
|
+
if (options.npm) {
|
|
63
|
+
const [specifier] = args;
|
|
64
|
+
if (!specifier)
|
|
65
|
+
throw new Error('Usage: dep add --npm <name>[@<version>]');
|
|
66
|
+
out.info(`Adding npm dependency...`);
|
|
67
|
+
const separator = specifier.lastIndexOf('@');
|
|
68
|
+
const name = separator > 0 ? specifier.slice(0, separator) : specifier;
|
|
69
|
+
const version = separator > 0 ? specifier.slice(separator + 1) : 'latest';
|
|
70
|
+
const dep = schema.Dependency.NpmDependency.process({ name, version });
|
|
71
|
+
const taken = config.data.npmDependencies.some(existing => existing.name === dep.name);
|
|
72
|
+
if (taken)
|
|
73
|
+
throw new Error(`An npm dependency named "${dep.name}" already exists, remove it first or add this one with another name.`);
|
|
74
|
+
config.data.npmDependencies.push(dep);
|
|
75
|
+
await config.save();
|
|
76
|
+
out.info(`Added npm dependency "${dep.name}".`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let [repo, name] = args;
|
|
80
|
+
out.info(`Adding dependency...`);
|
|
81
|
+
repo = DepUtils.parse(repo).repo;
|
|
82
|
+
if (!name)
|
|
83
|
+
name = DepUtils.getRepoName(repo);
|
|
84
|
+
const dep = schema.Dependency.GitDependency.process(options.tag ? { name, repo, tag: options.tag } : { name, repo });
|
|
85
|
+
const taken = config.data.dependencies.some(existing => existing.name === dep.name);
|
|
86
|
+
if (taken)
|
|
87
|
+
throw new Error(`A dependency named "${dep.name}" already exists, remove it first or add this one with another name.`);
|
|
88
|
+
config.data.dependencies.push(dep);
|
|
89
|
+
await config.save();
|
|
90
|
+
out.info(`Added dependency "${dep.name}".`);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (boxed)
|
|
94
|
+
out.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
Actions.add = add;
|
|
98
|
+
/**
|
|
99
|
+
* Removes a dependency from the configuration and uninstalls its files.
|
|
100
|
+
* @param context - The project the action operates on.
|
|
101
|
+
* @param args - The name (git or npm) or the git repository URL of the dependency to remove.
|
|
102
|
+
* @param options - The mode options of the run.
|
|
103
|
+
* @returns A promise resolving when the dependency is gone.
|
|
104
|
+
*/
|
|
105
|
+
async function remove(context, args, options = {}) {
|
|
106
|
+
const policy = Actions.policy(options);
|
|
107
|
+
const out = policy.report;
|
|
108
|
+
const boxed = policy.decoration === 'on';
|
|
109
|
+
try {
|
|
110
|
+
if (boxed)
|
|
111
|
+
out.group(Group.create('#FFB4DC'));
|
|
112
|
+
const [identifier] = args;
|
|
113
|
+
if (!identifier)
|
|
114
|
+
throw new Error('Usage: dep remove <name> | <repo_url>');
|
|
115
|
+
const config = await Config.load(context.configPath);
|
|
116
|
+
const gitMatches = config.data.dependencies.filter(dep => dep.name === identifier || dep.repo === identifier);
|
|
117
|
+
const npmMatches = config.data.npmDependencies.filter(dep => dep.name === identifier);
|
|
118
|
+
if (gitMatches.length + npmMatches.length === 0)
|
|
119
|
+
throw new Error(`Dependency "${identifier}" not found.`);
|
|
120
|
+
for (const dep of gitMatches)
|
|
121
|
+
await new GitDependency(context.flowDirectory, context.projectRoot, dep, out).uninstall();
|
|
122
|
+
for (const dep of npmMatches)
|
|
123
|
+
await new NpmDependency(context.flowDirectory, context.projectRoot, dep, out).uninstall();
|
|
124
|
+
config.data.dependencies = config.data.dependencies.filter(dep => dep.name !== identifier && dep.repo !== identifier);
|
|
125
|
+
config.data.npmDependencies = config.data.npmDependencies.filter(dep => dep.name !== identifier);
|
|
126
|
+
await config.save();
|
|
127
|
+
out.info(`Removed dependency "${identifier}".`);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (boxed)
|
|
131
|
+
out.end();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
Actions.remove = remove;
|
|
135
|
+
/**
|
|
136
|
+
* Fetches, builds and extracts dependencies, or only the named ones, and synchronizes the aliases afterwards.
|
|
137
|
+
* @param context - The project the action operates on.
|
|
138
|
+
* @param args - The names of the dependencies to install, every one of them when empty.
|
|
139
|
+
* @param options - The mode options of the run, which also settle the mode of the synchronization.
|
|
140
|
+
* @returns A promise resolving when every target is installed and the aliases are synced.
|
|
141
|
+
*
|
|
142
|
+
* @remarks The editor JSON Schema is refreshed before anything is installed, so an editor can validate
|
|
143
|
+
* the configuration and its `$schema` even when the installation itself fails. The refresh is repeated by
|
|
144
|
+
* the synchronization the action ends with, which is harmless because the schema is a pure function of it.
|
|
145
|
+
*/
|
|
146
|
+
async function install(context, args, options = {}) {
|
|
147
|
+
const policy = Actions.policy(options);
|
|
148
|
+
const out = policy.report;
|
|
149
|
+
const boxed = policy.decoration === 'on';
|
|
150
|
+
try {
|
|
151
|
+
if (boxed)
|
|
152
|
+
out.group(Group.create('#FFB4DC'));
|
|
153
|
+
const config = await Config.load(context.configPath);
|
|
154
|
+
const gitDependencies = config.data.dependencies;
|
|
155
|
+
const npmDependencies = config.data.npmDependencies;
|
|
156
|
+
await JsonSchema.write(context.projectRoot);
|
|
157
|
+
if (gitDependencies.length + npmDependencies.length === 0)
|
|
158
|
+
throw new Error(args.length > 0 ? 'Specified dependencies not found.' : 'No dependencies to install.');
|
|
159
|
+
const names = new Set(args);
|
|
160
|
+
const gitTargets = args.length > 0 ? gitDependencies.filter(dep => names.has(dep.name)) : gitDependencies;
|
|
161
|
+
const npmTargets = args.length > 0 ? npmDependencies.filter(dep => names.has(dep.name)) : npmDependencies;
|
|
162
|
+
if (args.length > 0 && gitTargets.length + npmTargets.length === 0)
|
|
163
|
+
throw new Error('Specified dependencies not found.');
|
|
164
|
+
out.info(`&C5Installing dependencies...`);
|
|
165
|
+
for (const dep of gitTargets) {
|
|
166
|
+
try {
|
|
167
|
+
if (boxed)
|
|
168
|
+
out.group(Group.create('#FFB4DC'));
|
|
169
|
+
out.info(`&C5Installing &C6"${dep.name}" &C5from &C6${dep.repo}&C5...`);
|
|
170
|
+
out.line();
|
|
171
|
+
const dependency = new GitDependency(context.flowDirectory, context.projectRoot, dep, out, policy);
|
|
172
|
+
await dependency.install();
|
|
173
|
+
out.line();
|
|
174
|
+
out.info(`&C2Installed &C6${dep.name}.`);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
if (boxed)
|
|
178
|
+
out.end();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const dep of npmTargets) {
|
|
182
|
+
try {
|
|
183
|
+
if (boxed)
|
|
184
|
+
out.group(Group.create('#FFB4DC'));
|
|
185
|
+
out.info(`&C5Installing npm dependency &C6"${dep.name}" &C5version &C6${dep.version}&C5...`);
|
|
186
|
+
out.line();
|
|
187
|
+
const dependency = new NpmDependency(context.flowDirectory, context.projectRoot, dep, out, policy);
|
|
188
|
+
await dependency.install();
|
|
189
|
+
out.line();
|
|
190
|
+
out.info(`&C2Installed npm dependency &C6${dep.name}.`);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (boxed)
|
|
194
|
+
out.end();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await Actions.sync(context, [], options, policy);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (boxed)
|
|
201
|
+
out.end();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
Actions.install = install;
|
|
205
|
+
/**
|
|
206
|
+
* Runs a named action of the configuration, in the project root.
|
|
207
|
+
* @param context - The project the action operates on.
|
|
208
|
+
* @param args - The name of the action declared in the configuration.
|
|
209
|
+
* @param options - The mode options of the run.
|
|
210
|
+
* @returns A promise resolving when the pipeline of the action is over.
|
|
211
|
+
*/
|
|
212
|
+
async function run(context, args, options = {}) {
|
|
213
|
+
const policy = Actions.policy(options);
|
|
214
|
+
const out = policy.report;
|
|
215
|
+
const boxed = policy.decoration === 'on';
|
|
216
|
+
try {
|
|
217
|
+
const [actionName] = args;
|
|
218
|
+
if (!actionName)
|
|
219
|
+
throw new Error('Usage: dep run <action>');
|
|
220
|
+
if (boxed)
|
|
221
|
+
out.group(Group.create('#FFB4DC'));
|
|
222
|
+
out.info(`&C5Running action &C6${actionName}&C5...`);
|
|
223
|
+
const config = await Config.load(context.configPath);
|
|
224
|
+
const action = config.data.actions[actionName];
|
|
225
|
+
if (!action)
|
|
226
|
+
throw new Error(`action "${actionName}" not found in configuration.`);
|
|
227
|
+
const pipeline = Array.isArray(action) ? action : [action];
|
|
228
|
+
const builder = new Builder({
|
|
229
|
+
cwd: context.projectRoot,
|
|
230
|
+
projectRoot: context.projectRoot,
|
|
231
|
+
pipeline,
|
|
232
|
+
logger: out,
|
|
233
|
+
policy
|
|
234
|
+
});
|
|
235
|
+
await builder.run();
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
if (boxed)
|
|
239
|
+
out.end();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
Actions.run = run;
|
|
243
|
+
/**
|
|
244
|
+
* Lists the declared dependencies, marking each one as installed or missing.
|
|
245
|
+
* @param context - The project the action operates on.
|
|
246
|
+
* @param args - Unused; the command takes no positional argument.
|
|
247
|
+
* @param options - The mode options of the run.
|
|
248
|
+
* @returns A promise resolving when every dependency is listed.
|
|
249
|
+
*/
|
|
250
|
+
async function list(context, args, options = {}) {
|
|
251
|
+
const policy = Actions.policy(options);
|
|
252
|
+
const out = policy.report;
|
|
253
|
+
const boxed = policy.decoration === 'on';
|
|
254
|
+
if (boxed)
|
|
255
|
+
out.group(Group.create('#FFB4DC'));
|
|
256
|
+
try {
|
|
257
|
+
const config = await Config.load(context.configPath);
|
|
258
|
+
if (config.data.dependencies.length + config.data.npmDependencies.length === 0) {
|
|
259
|
+
out.info(`No dependencies found.`);
|
|
260
|
+
}
|
|
261
|
+
for (const dep of config.data.dependencies) {
|
|
262
|
+
const state = await new GitDependency(context.flowDirectory, context.projectRoot, dep).isInstalled()
|
|
263
|
+
? `&C2installed&R` : `&C3missing&R`;
|
|
264
|
+
out.info(`&C6${dep.name} &C7from &C2${dep.repo} &C7(${state}&C7)`);
|
|
265
|
+
}
|
|
266
|
+
for (const dep of config.data.npmDependencies) {
|
|
267
|
+
const state = await new NpmDependency(context.flowDirectory, context.projectRoot, dep).isInstalled()
|
|
268
|
+
? `&C2installed&R` : `&C3missing&R`;
|
|
269
|
+
out.info(`&C6${dep.name} &C7v&C2${dep.version} &C7(${state}&C7)`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
if (boxed)
|
|
274
|
+
out.end();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
Actions.list = list;
|
|
278
|
+
/**
|
|
279
|
+
* Rewrites the import specifiers of the built files of `outDir` according to the resolver.
|
|
280
|
+
* @param context - The project the action operates on.
|
|
281
|
+
* @param args - Unused; the command takes no positional argument.
|
|
282
|
+
* @param options - The mode options of the run; `cdn` and `watch` are honored.
|
|
283
|
+
* @returns A promise resolving when every matched file is rewritten, or when the watcher has started.
|
|
284
|
+
*/
|
|
285
|
+
async function rewritePaths(context, args, options = {}) {
|
|
286
|
+
const policy = Actions.policy(options);
|
|
287
|
+
const out = policy.report;
|
|
288
|
+
const boxed = policy.decoration === 'on';
|
|
289
|
+
if (boxed)
|
|
290
|
+
out.group(Group.create('#FFB4DC'));
|
|
291
|
+
try {
|
|
292
|
+
const { watch, cdn } = options;
|
|
293
|
+
const config = await Config.load(context.configPath);
|
|
294
|
+
const mode = cdn ? 'cdn' : 'local';
|
|
295
|
+
out.info(`Mode: &C3${cdn ? 'CDN' : 'Local'}`);
|
|
296
|
+
if (watch)
|
|
297
|
+
out.info(`Watcher: &C2Enabled`);
|
|
298
|
+
const resolver = new PathResolver(config.data, context.projectRoot, { logger: out });
|
|
299
|
+
if (watch)
|
|
300
|
+
await resolver.watch(mode);
|
|
301
|
+
else
|
|
302
|
+
await resolver.rewritePaths(mode);
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (boxed)
|
|
306
|
+
out.end();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
Actions.rewritePaths = rewritePaths;
|
|
310
|
+
/**
|
|
311
|
+
* Writes the resolver aliases into the configured `tsconfig` and `importmap`.
|
|
312
|
+
* @param context - The project the action operates on.
|
|
313
|
+
* @param args - Unused; the command takes no positional argument.
|
|
314
|
+
* @param options - The mode options of the run; `cdn` selects the mode of the import map.
|
|
315
|
+
* @param caller - The policy of the run to report through, when the synchronization belongs to another
|
|
316
|
+
* action. The policy is built from the options when it is omitted.
|
|
317
|
+
* @returns A promise resolving when both files are updated, or skipped when they are not configured.
|
|
318
|
+
*
|
|
319
|
+
* @remarks The editor JSON Schema is refreshed alongside the synchronization, so a build that ends in
|
|
320
|
+
* `sync` leaves the `$schema` target of the configuration up to date for the editor.
|
|
321
|
+
*/
|
|
322
|
+
async function sync(context, args, options = {}, caller) {
|
|
323
|
+
const policy = caller ?? Actions.policy(options);
|
|
324
|
+
const out = policy.report;
|
|
325
|
+
const boxed = policy.decoration === 'on';
|
|
326
|
+
try {
|
|
327
|
+
if (boxed)
|
|
328
|
+
out.group(Group.create('#FFB4DC'));
|
|
329
|
+
out.info(`Synchronizing configurations...`);
|
|
330
|
+
const { cdn } = options;
|
|
331
|
+
const mode = cdn ? 'cdn' : 'local';
|
|
332
|
+
const config = await Config.load(context.configPath);
|
|
333
|
+
const resolver = new PathResolver(config.data, context.projectRoot, { logger: out });
|
|
334
|
+
await JsonSchema.write(context.projectRoot);
|
|
335
|
+
const tsconfigFile = config.data.tsconfig;
|
|
336
|
+
const importMapFile = config.data.importmap;
|
|
337
|
+
const tsconfigPath = Path.join(context.projectRoot, tsconfigFile || 'tsconfig.json');
|
|
338
|
+
const importMapPath = Path.join(context.projectRoot, importMapFile || 'importmap.json');
|
|
339
|
+
if (tsconfigFile) {
|
|
340
|
+
const tsconfig = await TSConfig.load(tsconfigPath, { logger: out });
|
|
341
|
+
tsconfig.updatePaths(resolver.aliases);
|
|
342
|
+
await tsconfig.save();
|
|
343
|
+
}
|
|
344
|
+
else
|
|
345
|
+
out.warn(`No tsconfig file specified in configuration. Skipping tsconfig synchronization.`);
|
|
346
|
+
if (importMapFile) {
|
|
347
|
+
const importmap = await ImportMap.load(importMapPath, { logger: out });
|
|
348
|
+
importmap.updateImports(resolver.aliases, mode);
|
|
349
|
+
await importmap.save();
|
|
350
|
+
}
|
|
351
|
+
else
|
|
352
|
+
out.warn(`No import map file specified in configuration. Skipping import map synchronization.`);
|
|
353
|
+
out.info(`&C2Successfully synced all configurations.`);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (boxed)
|
|
357
|
+
out.end();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
Actions.sync = sync;
|
|
361
|
+
/**
|
|
362
|
+
* Converts the depflow config file between JSON and YAML.
|
|
363
|
+
* @param context - The project the action operates on.
|
|
364
|
+
* @param command - The invoked command name ('json-to-yaml' or 'yaml-to-json').
|
|
365
|
+
* @param args - Optional output file path.
|
|
366
|
+
* @param options - The mode options of the run.
|
|
367
|
+
* @returns A promise resolving when the converted file is written.
|
|
368
|
+
*/
|
|
369
|
+
async function convert(context, command, args, options = {}) {
|
|
370
|
+
const policy = Actions.policy(options);
|
|
371
|
+
const out = policy.report;
|
|
372
|
+
const boxed = policy.decoration === 'on';
|
|
373
|
+
try {
|
|
374
|
+
if (boxed)
|
|
375
|
+
out.group(Group.create('#FFB4DC'));
|
|
376
|
+
const to = command === 'json-to-yaml' ? 'yaml' : 'json';
|
|
377
|
+
const input = context.configPath;
|
|
378
|
+
const inputExt = Path.extName(input).toLowerCase();
|
|
379
|
+
const isJsonInput = inputExt === '.json';
|
|
380
|
+
const isYamlInput = inputExt === '.yaml' || inputExt === '.yml';
|
|
381
|
+
if (!isJsonInput && !isYamlInput)
|
|
382
|
+
throw new Error(`Unsupported config file extension: ${inputExt}`);
|
|
383
|
+
if (isYamlInput === (to === 'yaml'))
|
|
384
|
+
throw new Error(`Config file is already ${to}: ${input}`);
|
|
385
|
+
const config = await Config.load(input);
|
|
386
|
+
const [output] = args;
|
|
387
|
+
const outPath = output || Path.join(context.projectRoot, `${Path.fileName(input, false)}.${to}`);
|
|
388
|
+
await config.save(outPath);
|
|
389
|
+
out.info(`&C2Converted &C4${Path.fileName(input)} &C7→&R &C4${Path.fileName(outPath)} &C2(&C3${to.toUpperCase()}&C2).`);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
if (boxed)
|
|
393
|
+
out.end();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
Actions.convert = convert;
|
|
397
|
+
/**
|
|
398
|
+
* Edits the resolver aliases of the project or of a named dependency.
|
|
399
|
+
* @param context - The project the action operates on.
|
|
400
|
+
* @param args - The verb and its operands: `add <alias> <local>`, `remove <alias>`, `list` or `clear`.
|
|
401
|
+
* @param options - The mode options of the run; `dep`, `type`, `cdnUrl` and `force` are honored.
|
|
402
|
+
* @returns A promise resolving when the configuration is updated.
|
|
403
|
+
*/
|
|
404
|
+
async function resolver(context, args, options = {}) {
|
|
405
|
+
const policy = Actions.policy(options);
|
|
406
|
+
const out = policy.report;
|
|
407
|
+
const boxed = policy.decoration === 'on';
|
|
408
|
+
try {
|
|
409
|
+
if (boxed)
|
|
410
|
+
out.group(Group.create('#FFB4DC'));
|
|
411
|
+
const [verb, ...rest] = args;
|
|
412
|
+
const config = await Config.load(context.configPath);
|
|
413
|
+
const targets = options.dep ? dependencyOf(config, options.dep).resolver : config.data.resolver;
|
|
414
|
+
const where = options.dep ? `dependency "${options.dep}"` : 'the project';
|
|
415
|
+
switch (verb) {
|
|
416
|
+
case 'add': {
|
|
417
|
+
const [alias, local] = rest;
|
|
418
|
+
if (!alias || local === undefined)
|
|
419
|
+
throw new Error('Usage: dep resolver add <alias> <local> [--type <type>] [--cdn-url <url>] [--dep <name>] [--force]');
|
|
420
|
+
if (alias in targets && !options.force)
|
|
421
|
+
throw new Error(`A resolver alias "${alias}" already exists in ${where}, use --force to overwrite it.`);
|
|
422
|
+
targets[alias] = options.type !== undefined || options.cdnUrl !== undefined
|
|
423
|
+
? schema.Target.process({
|
|
424
|
+
local,
|
|
425
|
+
...(options.type !== undefined ? { type: options.type } : {}),
|
|
426
|
+
...(options.cdnUrl !== undefined ? { cdn: options.cdnUrl } : {})
|
|
427
|
+
})
|
|
428
|
+
: schema.Target.process(local);
|
|
429
|
+
await config.save();
|
|
430
|
+
out.info(`&C2Added resolver alias &C6"${alias}" &C2to &C6${where}&C2.`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
case 'remove': {
|
|
434
|
+
const [alias] = rest;
|
|
435
|
+
if (!alias)
|
|
436
|
+
throw new Error('Usage: dep resolver remove <alias> [--dep <name>]');
|
|
437
|
+
if (!(alias in targets))
|
|
438
|
+
throw new Error(`Resolver alias "${alias}" not found in ${where}.`);
|
|
439
|
+
delete targets[alias];
|
|
440
|
+
await config.save();
|
|
441
|
+
out.info(`&C2Removed resolver alias &C6"${alias}" &C2from &C6${where}&C2.`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
case 'list': {
|
|
445
|
+
const entries = Object.entries(targets);
|
|
446
|
+
if (entries.length === 0) {
|
|
447
|
+
out.info(`No resolver aliases found in ${where}.`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const [alias, target] of entries)
|
|
451
|
+
out.info(`&C6${alias} &C7→ &C2${describeTarget(target)}`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
case 'clear': {
|
|
455
|
+
for (const alias of Object.keys(targets))
|
|
456
|
+
delete targets[alias];
|
|
457
|
+
await config.save();
|
|
458
|
+
out.info(`&C2Cleared the resolver aliases of &C6${where}&C2.`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
default: throw new Error('Usage: dep resolver add|remove|list|clear [--dep <name>]');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
if (boxed)
|
|
466
|
+
out.end();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
Actions.resolver = resolver;
|
|
470
|
+
/**
|
|
471
|
+
* Edits the build pipeline of a dependency or of a named action.
|
|
472
|
+
* @param context - The project the action operates on.
|
|
473
|
+
* @param args - The verb and its operands: `add <kind> ...`, `remove <index>`, `move <from> <to>`, `list` or `clear`.
|
|
474
|
+
* @param options - The mode options of the run; `dep`, `action` and the step options are honored.
|
|
475
|
+
* @returns A promise resolving when the configuration is updated.
|
|
476
|
+
*/
|
|
477
|
+
async function builder(context, args, options = {}) {
|
|
478
|
+
const policy = Actions.policy(options);
|
|
479
|
+
const out = policy.report;
|
|
480
|
+
const boxed = policy.decoration === 'on';
|
|
481
|
+
try {
|
|
482
|
+
if (boxed)
|
|
483
|
+
out.group(Group.create('#FFB4DC'));
|
|
484
|
+
const [verb, ...rest] = args;
|
|
485
|
+
const config = await Config.load(context.configPath);
|
|
486
|
+
if (options.dep && options.action)
|
|
487
|
+
throw new Error('--dep and --action cannot be used together.');
|
|
488
|
+
let steps;
|
|
489
|
+
let where;
|
|
490
|
+
if (options.dep) {
|
|
491
|
+
steps = dependencyOf(config, options.dep).builder;
|
|
492
|
+
where = `dependency "${options.dep}"`;
|
|
493
|
+
}
|
|
494
|
+
else if (options.action) {
|
|
495
|
+
const action = config.data.actions[options.action];
|
|
496
|
+
if (!action)
|
|
497
|
+
throw new Error(`action "${options.action}" not found, create it first with: dep action create ${options.action}`);
|
|
498
|
+
steps = action;
|
|
499
|
+
where = `action "${options.action}"`;
|
|
500
|
+
}
|
|
501
|
+
else
|
|
502
|
+
throw new Error('A builder command needs either --dep <name> or --action <name>.');
|
|
503
|
+
switch (verb) {
|
|
504
|
+
case 'add': {
|
|
505
|
+
const [kind, ...payload] = rest;
|
|
506
|
+
const entry = buildStep(kind, payload, options);
|
|
507
|
+
const index = options.index === undefined ? steps.length : stepIndex(options.index, steps.length, true);
|
|
508
|
+
steps.splice(index, 0, entry);
|
|
509
|
+
await config.save();
|
|
510
|
+
out.info(`&C2Added &C6${kind} &C2step to &C6${where}&C2.`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
case 'remove': {
|
|
514
|
+
const [raw] = rest;
|
|
515
|
+
if (raw === undefined)
|
|
516
|
+
throw new Error('Usage: dep builder remove <index> [--dep <name> | --action <name>]');
|
|
517
|
+
steps.splice(stepIndex(raw, steps.length, false), 1);
|
|
518
|
+
await config.save();
|
|
519
|
+
out.info(`&C2Removed step &C6${raw} &C2from &C6${where}&C2.`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
case 'move': {
|
|
523
|
+
const [rawFrom, rawTo] = rest;
|
|
524
|
+
if (rawFrom === undefined || rawTo === undefined)
|
|
525
|
+
throw new Error('Usage: dep builder move <from> <to> [--dep <name> | --action <name>]');
|
|
526
|
+
const from = stepIndex(rawFrom, steps.length, false);
|
|
527
|
+
const to = stepIndex(rawTo, steps.length, false);
|
|
528
|
+
const [entry] = steps.splice(from, 1);
|
|
529
|
+
steps.splice(to, 0, entry);
|
|
530
|
+
await config.save();
|
|
531
|
+
out.info(`&C2Moved step &C6${rawFrom} &C2to &C6${rawTo} &C2in &C6${where}&C2.`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
case 'list': {
|
|
535
|
+
if (steps.length === 0) {
|
|
536
|
+
out.info(`No build steps found in ${where}.`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
steps.forEach((entry, index) => out.info(`&C6${index} &C7${describeStep(entry)}`));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
case 'clear': {
|
|
543
|
+
steps.splice(0, steps.length);
|
|
544
|
+
await config.save();
|
|
545
|
+
out.info(`&C2Cleared the build steps of &C6${where}&C2.`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
default: throw new Error('Usage: dep builder add|remove|move|list|clear ...');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
if (boxed)
|
|
553
|
+
out.end();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
Actions.builder = builder;
|
|
557
|
+
/**
|
|
558
|
+
* Manages the named build actions of the configuration.
|
|
559
|
+
* @param context - The project the action operates on.
|
|
560
|
+
* @param args - The verb and its operands: `create <name>`, `remove <name>` or `list`.
|
|
561
|
+
* @param options - The mode options of the run.
|
|
562
|
+
* @returns A promise resolving when the configuration is updated.
|
|
563
|
+
*/
|
|
564
|
+
async function action(context, args, options = {}) {
|
|
565
|
+
const policy = Actions.policy(options);
|
|
566
|
+
const out = policy.report;
|
|
567
|
+
const boxed = policy.decoration === 'on';
|
|
568
|
+
try {
|
|
569
|
+
if (boxed)
|
|
570
|
+
out.group(Group.create('#FFB4DC'));
|
|
571
|
+
const [verb, ...rest] = args;
|
|
572
|
+
const config = await Config.load(context.configPath);
|
|
573
|
+
const actions = config.data.actions;
|
|
574
|
+
switch (verb) {
|
|
575
|
+
case 'create': {
|
|
576
|
+
const [name] = rest;
|
|
577
|
+
if (!name)
|
|
578
|
+
throw new Error('Usage: dep action create <name>');
|
|
579
|
+
if (name in actions)
|
|
580
|
+
throw new Error(`action "${name}" already exists.`);
|
|
581
|
+
actions[name] = schema.Builder.process([]);
|
|
582
|
+
await config.save();
|
|
583
|
+
out.info(`&C2Created action &C6${name}&C2.`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
case 'remove': {
|
|
587
|
+
const [name] = rest;
|
|
588
|
+
if (!name)
|
|
589
|
+
throw new Error('Usage: dep action remove <name>');
|
|
590
|
+
if (!(name in actions))
|
|
591
|
+
throw new Error(`action "${name}" not found.`);
|
|
592
|
+
delete actions[name];
|
|
593
|
+
await config.save();
|
|
594
|
+
out.info(`&C2Removed action &C6${name}&C2.`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
case 'list': {
|
|
598
|
+
const names = Object.keys(actions);
|
|
599
|
+
if (names.length === 0) {
|
|
600
|
+
out.info('No actions found.');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
for (const name of names)
|
|
604
|
+
out.info(`&C6${name} &C7(${actions[name].length} step(s))`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
default: throw new Error('Usage: dep action create|remove|list');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
if (boxed)
|
|
612
|
+
out.end();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
Actions.action = action;
|
|
616
|
+
/**
|
|
617
|
+
* Reads and edits the top-level settings of the configuration.
|
|
618
|
+
* @param context - The project the action operates on.
|
|
619
|
+
* @param args - The verb and its operands: `get [key]`, `set <key> <value>` or `unset <key>`.
|
|
620
|
+
* @param options - The mode options of the run.
|
|
621
|
+
* @returns A promise resolving when the configuration is updated.
|
|
622
|
+
*/
|
|
623
|
+
async function config(context, args, options = {}) {
|
|
624
|
+
const policy = Actions.policy(options);
|
|
625
|
+
const out = policy.report;
|
|
626
|
+
const boxed = policy.decoration === 'on';
|
|
627
|
+
try {
|
|
628
|
+
if (boxed)
|
|
629
|
+
out.group(Group.create('#FFB4DC'));
|
|
630
|
+
const [verb, key, value] = args;
|
|
631
|
+
const loaded = await Config.load(context.configPath);
|
|
632
|
+
switch (verb) {
|
|
633
|
+
case 'get': {
|
|
634
|
+
const keys = key ? [key] : [...SETTINGS];
|
|
635
|
+
for (const name of keys) {
|
|
636
|
+
if (!isSetting(name))
|
|
637
|
+
throw new Error(`Unknown setting "${name}", expected one of: ${SETTINGS.join(', ')}.`);
|
|
638
|
+
out.info(`&C6${name} &C7= &C2${loaded.data[name] ?? 'null'}`);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
case 'set': {
|
|
643
|
+
if (!key || value === undefined)
|
|
644
|
+
throw new Error('Usage: dep config set <outDir|tsconfig|importmap> <value>');
|
|
645
|
+
if (!isSetting(key))
|
|
646
|
+
throw new Error(`Unknown setting "${key}", expected one of: ${SETTINGS.join(', ')}.`);
|
|
647
|
+
if (key === 'outDir')
|
|
648
|
+
loaded.data.outDir = value;
|
|
649
|
+
else if (key === 'tsconfig')
|
|
650
|
+
loaded.data.tsconfig = value === 'null' ? null : value;
|
|
651
|
+
else
|
|
652
|
+
loaded.data.importmap = value === 'null' ? null : value;
|
|
653
|
+
await loaded.save();
|
|
654
|
+
out.info(`&C2Set &C6${key} &C2to &C6${value}&C2.`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
case 'unset': {
|
|
658
|
+
if (!key)
|
|
659
|
+
throw new Error('Usage: dep config unset <tsconfig|importmap>');
|
|
660
|
+
if (key === 'outDir')
|
|
661
|
+
throw new Error('"outDir" cannot be unset, no build output directory can be left undefined.');
|
|
662
|
+
if (key !== 'tsconfig' && key !== 'importmap')
|
|
663
|
+
throw new Error(`Unknown setting "${key}", expected one of: ${SETTINGS.join(', ')}.`);
|
|
664
|
+
loaded.data[key] = null;
|
|
665
|
+
await loaded.save();
|
|
666
|
+
out.info(`&C2Unset &C6${key}&C2.`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
default: throw new Error('Usage: dep config get|set|unset ...');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
if (boxed)
|
|
674
|
+
out.end();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
Actions.config = config;
|
|
678
|
+
/** The top-level settings the config command reads and edits. **/
|
|
679
|
+
const SETTINGS = ['outDir', 'tsconfig', 'importmap'];
|
|
680
|
+
/**
|
|
681
|
+
* Whether a name is one of the top-level settings the config command edits.
|
|
682
|
+
* @param name - The name to check.
|
|
683
|
+
* @returns `true` when the setting is one this command understands.
|
|
684
|
+
*/
|
|
685
|
+
function isSetting(name) {
|
|
686
|
+
return SETTINGS.includes(name);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Locates a dependency by its name, whether it is a git or an npm one.
|
|
690
|
+
* @param config - The loaded configuration.
|
|
691
|
+
* @param name - The name of the dependency.
|
|
692
|
+
* @returns The git or npm dependency of that name.
|
|
693
|
+
* @throws { Error } When no dependency of the configuration carries that name.
|
|
694
|
+
*/
|
|
695
|
+
function dependencyOf(config, name) {
|
|
696
|
+
const git = config.data.dependencies.find(dependency => dependency.name === name);
|
|
697
|
+
if (git)
|
|
698
|
+
return git;
|
|
699
|
+
const npm = config.data.npmDependencies.find(dependency => dependency.name === name);
|
|
700
|
+
if (npm)
|
|
701
|
+
return npm;
|
|
702
|
+
throw new Error(`Dependency "${name}" not found.`);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Describes a resolver target as a single line.
|
|
706
|
+
* @param target - The target to describe.
|
|
707
|
+
* @returns The local path of the target, with its type and CDN URL when it carries them.
|
|
708
|
+
*/
|
|
709
|
+
function describeTarget(target) {
|
|
710
|
+
if (typeof target === 'string')
|
|
711
|
+
return target;
|
|
712
|
+
const parts = [target.local];
|
|
713
|
+
if (target.type)
|
|
714
|
+
parts.push(`type: ${target.type}`);
|
|
715
|
+
if (target.cdn)
|
|
716
|
+
parts.push(`cdn: ${target.cdn}`);
|
|
717
|
+
return parts.join(' ');
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Builds a single build step from the kind the command names and the options it carries.
|
|
721
|
+
* @param kind - The kind of the step: `run`, `extract` or `transform`.
|
|
722
|
+
* @param payload - The positional operands that follow the kind.
|
|
723
|
+
* @param options - The step options of the run.
|
|
724
|
+
* @returns The processed build step.
|
|
725
|
+
* @throws { Error } When the kind is unknown or the operands are incomplete.
|
|
726
|
+
*/
|
|
727
|
+
function buildStep(kind, payload, options) {
|
|
728
|
+
switch (kind) {
|
|
729
|
+
case 'run': {
|
|
730
|
+
if (payload.length === 0)
|
|
731
|
+
throw new Error('Usage: dep builder add run <command> [<command>...] [--max-time <ms>] [--allow-fails] [--dep <name> | --action <name>]');
|
|
732
|
+
const run = payload.length === 1 ? payload[0] : payload;
|
|
733
|
+
let maxTimeMs;
|
|
734
|
+
if (options.maxTime !== undefined) {
|
|
735
|
+
maxTimeMs = Number(options.maxTime);
|
|
736
|
+
if (!Number.isFinite(maxTimeMs))
|
|
737
|
+
throw new Error(`Invalid --max-time value: ${options.maxTime}`);
|
|
738
|
+
}
|
|
739
|
+
return schema.RunEntry.process({
|
|
740
|
+
run,
|
|
741
|
+
...(maxTimeMs !== undefined ? { maxTimeMs } : {}),
|
|
742
|
+
...(options.allowFails !== undefined ? { allowFails: options.allowFails } : {})
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
case 'extract': {
|
|
746
|
+
const [glob] = payload;
|
|
747
|
+
if (!glob || options.to === undefined)
|
|
748
|
+
throw new Error('Usage: dep builder add extract <glob> --to <dir> [--map <pattern>] [--dep <name> | --action <name>]');
|
|
749
|
+
return schema.ExtractEntry.process({
|
|
750
|
+
extract: { [glob]: { to: options.to, ...(options.map !== undefined ? { map: options.map } : {}) } }
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
case 'transform': {
|
|
754
|
+
const [glob] = payload;
|
|
755
|
+
if (!glob || options.search === undefined || options.replace === undefined)
|
|
756
|
+
throw new Error('Usage: dep builder add transform <glob> --search <regex> --replace <text> [--flags <flags>] [--dep <name> | --action <name>]');
|
|
757
|
+
return schema.TransformEntry.process({
|
|
758
|
+
transform: {
|
|
759
|
+
glob,
|
|
760
|
+
search: options.search,
|
|
761
|
+
replace: options.replace,
|
|
762
|
+
...(options.flags !== undefined ? { flags: options.flags } : {})
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
default: throw new Error(`Unknown build step "${kind}", expected one of: run, extract, transform.`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Validates a step position against a pipeline.
|
|
771
|
+
* @param raw - The position as written.
|
|
772
|
+
* @param length - The number of steps the pipeline holds.
|
|
773
|
+
* @param allowEnd - Whether the position just past the last step is accepted.
|
|
774
|
+
* @returns The position as a number.
|
|
775
|
+
* @throws { Error } When the position is not a whole number within the accepted range.
|
|
776
|
+
*/
|
|
777
|
+
function stepIndex(raw, length, allowEnd) {
|
|
778
|
+
const index = Number(raw);
|
|
779
|
+
const limit = allowEnd ? length : length - 1;
|
|
780
|
+
if (!Number.isInteger(index) || index < 0 || index > limit)
|
|
781
|
+
throw new Error(`Invalid step index "${raw}", expected a whole number between 0 and ${limit}.`);
|
|
782
|
+
return index;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Describes a build step as a single line.
|
|
786
|
+
* @param entry - The build step to describe.
|
|
787
|
+
* @returns A short description of the step.
|
|
788
|
+
*/
|
|
789
|
+
function describeStep(entry) {
|
|
790
|
+
if ('run' in entry) {
|
|
791
|
+
const run = Array.isArray(entry.run) ? entry.run.join(' && ') : entry.run;
|
|
792
|
+
const limit = entry.maxTimeMs === -1 ? 'no limit' : `${entry.maxTimeMs}ms`;
|
|
793
|
+
return `run: ${run} (${limit}${entry.allowFails ? ', allow fails' : ''})`;
|
|
794
|
+
}
|
|
795
|
+
if ('extract' in entry) {
|
|
796
|
+
const mapping = typeof entry.extract === 'string' ? { [entry.extract]: entry.extract } : entry.extract;
|
|
797
|
+
return `extract: ${Object.keys(mapping).join(', ')}`;
|
|
798
|
+
}
|
|
799
|
+
return `transform: ${entry.transform.glob}`;
|
|
800
|
+
}
|
|
801
|
+
})(Actions || (Actions = {}));
|
|
802
|
+
export default Actions;
|
|
803
|
+
//# sourceMappingURL=Actions.js.map
|