@jujulego/jill 3.0.0-alpha.10 → 3.0.0-alpha.11

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/dist/main.js CHANGED
@@ -1,21 +1,21 @@
1
- ;{try{(function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d71a4689-40f0-48b6-8162-43fd470f2d95",e._sentryDebugIdIdentifier="sentry-dbid-d71a4689-40f0-48b6-8162-43fd470f2d95");})();}catch(e){}};!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"3.0.0-alpha.10"};}catch(e){}}();import { token$, inject$, asyncScope$ } from '@kyrielle/injector';
2
- import { startInactiveSpan, startSpan, getActiveSpan, updateSpanName, getRootSpan, captureException } from '@sentry/node';
3
- import { filter$, waitFor$, pipe$, map$, collect$, asyncIterator$, var$, flow$ } from 'kyrielle';
1
+ ;{try{(function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1635b077-95c7-4391-8a7c-36d52dbe9982",e._sentryDebugIdIdentifier="sentry-dbid-1635b077-95c7-4391-8a7c-36d52dbe9982");})();}catch(e){}};!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"3.0.0-alpha.11"};}catch(e){}}();import { token$, inject$, asyncScope$ } from '@kyrielle/injector';
2
+ import { startSpan, startInactiveSpan, getActiveSpan, updateSpanName, getRootSpan, captureException } from '@sentry/node';
3
+ import { pipe$, waitFor$, var$, filter$, collect$, map$, asyncIterator$, flow$ } from 'kyrielle';
4
4
  import process$1 from 'node:process';
5
5
  import { hideBin } from 'yargs/helpers';
6
- import { scheduler$, isWorkloadEnded, WorkloadState, spawn$, parallelFlow$, fallbackFlow$, sequenceFlow$ } from '@jujulego/tasks';
7
- import { logger$, withTimestamp, LogLevel, withLabel, qLogDelay, LogGateway, logDelay$, toStderr } from '@kyrielle/logger';
6
+ import { scheduler$, isWorkloadEnded, WorkloadState, spawn$, sequenceFlow$, parallelFlow$, fallbackFlow$ } from '@kyrielle/workload';
7
+ import path from 'node:path';
8
8
  import { _ } from '@swc/helpers/_/_apply_decs_2203_r';
9
- import { text } from 'node:stream/consumers';
9
+ import { logger$, withTimestamp, LogLevel, withLabel, qLogDelay, LogGateway, logDelay$, toStderr } from '@kyrielle/logger';
10
10
  import fs from 'node:fs';
11
11
  import { PathScurry } from 'path-scurry';
12
- import assert from 'assert';
13
- import { Writable } from 'node:stream';
14
- import moo from 'moo';
15
- import path from 'node:path';
16
12
  import { Glob } from 'glob';
17
13
  import normalize from 'normalize-package-data';
18
14
  import { satisfies, compare, parse } from 'semver';
15
+ import assert from 'assert';
16
+ import { Writable } from 'node:stream';
17
+ import { text } from 'node:stream/consumers';
18
+ import moo from 'moo';
19
19
  import { spawn } from 'node:child_process';
20
20
  import chalk from 'chalk';
21
21
  import slugify from 'slugify';
@@ -26,27 +26,37 @@ import os from 'node:os';
26
26
  import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
27
27
  import { chalkTemplateStderr } from 'chalk-template';
28
28
 
29
- // Filter
30
- function hasSomeScript$(scripts) {
31
- return filter$((wks)=>{
32
- if (!wks.manifest.scripts) {
33
- return false;
34
- }
35
- return scripts.some((script)=>script in wks.manifest.scripts);
36
- });
29
+ function trace(fun, opts) {
30
+ const name = typeof opts === 'string' ? opts : opts.name;
31
+ const op = typeof opts === 'object' ? opts.op : undefined;
32
+ const use = typeof opts === 'object' && opts.use || ((_, r)=>r);
33
+ return function(...args) {
34
+ return pipe$(startSpan({
35
+ name,
36
+ op
37
+ }, ()=>fun.call(this, ...args)), (r)=>use(name, r));
38
+ };
37
39
  }
38
- function hasEveryScript$(scripts) {
39
- return filter$((wks)=>{
40
- if (!wks.manifest.scripts) {
41
- return false;
42
- }
43
- return scripts.every((script)=>script in wks.manifest.scripts);
44
- });
40
+ function instrument(opts) {
41
+ return (target, context)=>{
42
+ const name = typeof opts === 'string' ? opts : opts?.name ?? context.name.toString();
43
+ const use = typeof opts === 'object' ? opts.use : (_, r)=>r;
44
+ return trace(target, {
45
+ name,
46
+ use
47
+ });
48
+ };
49
+ }
50
+ function traceImport(name, loader) {
51
+ return startSpan({
52
+ name: name,
53
+ op: 'resource.script'
54
+ }, loader);
45
55
  }
46
56
 
47
57
  // Tokens
48
58
  const CONFIG = token$('Config', async ()=>{
49
- const { ConfigService } = await Promise.resolve().then(function () { return config_service; });
59
+ const { ConfigService } = await traceImport('ConfigService', ()=>Promise.resolve().then(function () { return config_service; }));
50
60
  return waitFor$(inject$(ConfigService, asyncScope$()).config$);
51
61
  });
52
62
  const CWD = token$('cwd', ()=>process$1.cwd());
@@ -107,46 +117,32 @@ const SCHEDULER = token$('Scheduler', async ()=>{
107
117
  return scheduler;
108
118
  });
109
119
 
110
- function trace(fun, opts) {
111
- const name = typeof opts === 'string' ? opts : opts.name;
112
- const op = typeof opts === 'object' ? opts.op : undefined;
113
- const use = typeof opts === 'object' && opts.use || ((_, r)=>r);
114
- return function(...args) {
115
- return pipe$(startSpan({
116
- name,
117
- op
118
- }, ()=>fun.call(this, ...args)), (r)=>use(name, r));
119
- };
120
- }
121
- function instrument(opts) {
122
- return (target, context)=>{
123
- const name = typeof opts === 'string' ? opts : opts?.name ?? context.name.toString();
124
- const use = typeof opts === 'object' ? opts.use : (_, r)=>r;
125
- return trace(target, {
126
- name,
127
- use
128
- });
120
+ function mutex$() {
121
+ const count$ = var$(0);
122
+ return {
123
+ async acquire () {
124
+ let cnt = count$.defer();
125
+ while(cnt > 0){
126
+ cnt = await waitFor$(count$);
127
+ }
128
+ count$.mutate(cnt + 1);
129
+ },
130
+ release () {
131
+ const cnt = count$.defer();
132
+ if (cnt > 0) {
133
+ count$.mutate(cnt - 1);
134
+ }
135
+ }
129
136
  };
130
137
  }
131
- function traceImport(name, loader) {
132
- return startSpan({
133
- name: name,
134
- op: 'resource.script'
135
- }, loader);
136
- }
137
-
138
- class ClientError extends Error {
139
- name = 'ClientError';
140
- constructor(message){
141
- super(message);
138
+ async function with$(lock, fn) {
139
+ try {
140
+ await lock.acquire();
141
+ return await fn();
142
+ } finally{
143
+ lock.release();
142
144
  }
143
145
  }
144
- class TaskExpressionError extends ClientError {
145
- name = 'TaskExpressionError';
146
- }
147
- class TaskSyntaxError extends ClientError {
148
- name = 'TaskSyntaxError';
149
- }
150
146
 
151
147
  function logStreamedLines(logger, level) {
152
148
  let leftover = '';
@@ -170,6 +166,140 @@ function logStreamedLines(logger, level) {
170
166
  });
171
167
  }
172
168
 
169
+ function command$(workspace, cmd, opts = {}) {
170
+ const { superCommand, logger = inject$(LOGGER), ...rest } = opts;
171
+ // Apply super command
172
+ if (superCommand) {
173
+ cmd = superCommand + ' ' + cmd;
174
+ }
175
+ // Prepare job
176
+ const job = spawn$(cmd, {
177
+ ...rest,
178
+ cwd: workspace.root,
179
+ env: {
180
+ FORCE_COLOR: '1',
181
+ ...rest.env
182
+ },
183
+ shell: true
184
+ });
185
+ job.stdout.pipe(logStreamedLines(logger, LogLevel.info));
186
+ job.stderr.pipe(logStreamedLines(logger, LogLevel.info));
187
+ return job;
188
+ }
189
+
190
+ class ClientError extends Error {
191
+ name = 'ClientError';
192
+ constructor(message){
193
+ super(message);
194
+ }
195
+ }
196
+ class TaskExpressionError extends ClientError {
197
+ name = 'TaskExpressionError';
198
+ }
199
+ class TaskSyntaxError extends ClientError {
200
+ name = 'TaskSyntaxError';
201
+ }
202
+
203
+ // Utils
204
+ function capitalize(txt) {
205
+ return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase();
206
+ }
207
+ function splitCommandLine(line) {
208
+ line = line.trim();
209
+ const parts = [];
210
+ let current_cote = '';
211
+ let last = 0;
212
+ for(let i = 1; i < line.length; ++i){
213
+ const c = line[i];
214
+ if (current_cote) {
215
+ if (c === current_cote) {
216
+ current_cote = '';
217
+ }
218
+ } else {
219
+ if ([
220
+ '"',
221
+ '\''
222
+ ].includes(c)) {
223
+ current_cote = c;
224
+ } else if (c === ' ') {
225
+ parts.push(line.slice(last, i));
226
+ last = i + 1;
227
+ }
228
+ }
229
+ }
230
+ parts.push(line.slice(last));
231
+ return parts;
232
+ }
233
+ function escapeCommandLineArg(arg) {
234
+ if (arg.match(/[ ()]/)) {
235
+ return `"${arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
236
+ }
237
+ return arg;
238
+ }
239
+
240
+ async function runScript$(workspace, script, args, opts = {}) {
241
+ // Run script itself
242
+ const jobs = [
243
+ await planScript$(workspace, script, args, opts)
244
+ ];
245
+ if (!jobs[0]) {
246
+ throw new ScriptNotFound(`No script ${script} in ${workspace.name}`);
247
+ }
248
+ // Run hooks
249
+ if (opts.runHooks) {
250
+ jobs.unshift(await planScript$(workspace, `pre${script}`, [], opts));
251
+ jobs.push(await planScript$(workspace, `post${script}`, [], opts));
252
+ }
253
+ // Prepare workflow
254
+ const flow = pipe$(jobs, filter$((job)=>job !== null), collect$(sequenceFlow$({
255
+ label: script,
256
+ type: 'script'
257
+ })));
258
+ return {
259
+ ...flow,
260
+ script,
261
+ workspace
262
+ };
263
+ }
264
+ // Utils
265
+ async function planScript$(workspace, script, args, opts) {
266
+ // Load script
267
+ let line = workspace?.getScript(script);
268
+ if (!line) {
269
+ return null;
270
+ }
271
+ // Parse script
272
+ const [command, ...commandArgs] = splitCommandLine(line);
273
+ if (!command) {
274
+ return null;
275
+ }
276
+ if (command === 'jill') {
277
+ const argv = [
278
+ ...commandArgs,
279
+ ...args
280
+ ].map((arg)=>arg.replace(/^["'](.+)["']$/, '$1'));
281
+ const { PlannerService } = await traceImport('PlannerService', ()=>import('./planner.service.js'));
282
+ const plannerService = inject$(PlannerService);
283
+ const job = await plannerService.plan(argv, workspace.root);
284
+ if (job) {
285
+ return job;
286
+ }
287
+ }
288
+ // Run command
289
+ const pm = await workspace.project.packageManager();
290
+ line = [
291
+ line,
292
+ args.map(escapeCommandLineArg)
293
+ ].join(' ').trim();
294
+ return command$(workspace, line, {
295
+ logger: opts.logger,
296
+ superCommand: pm === 'yarn' && command !== 'yarn' ? 'yarn exec' : undefined
297
+ });
298
+ }
299
+ class ScriptNotFound extends ClientError {
300
+ name = 'ScriptNotFound';
301
+ }
302
+
173
303
  var _dec$2, _dec1$1, _dec2, _initProto$2;
174
304
  _dec$2 = instrument('GitService.isAffected'), _dec1$1 = instrument('GitService.listBranches'), _dec2 = instrument('GitService.listTags');
175
305
  class GitService {
@@ -198,13 +328,13 @@ class GitService {
198
328
  // Methods
199
329
  /**
200
330
  * Runs a git command inside
201
- */ async command(cmd, args, opts = {}) {
331
+ */ async command(cmd, opts = {}) {
202
332
  const { logger = this._logger, ...props } = opts;
203
333
  // Create job
204
- const job = spawn$('git', [
205
- cmd,
206
- ...args
207
- ], props);
334
+ const job = spawn$(`git ${cmd}`, {
335
+ ...props,
336
+ shell: true
337
+ });
208
338
  job.stdout.pipe(logStreamedLines(logger, LogLevel.debug));
209
339
  job.stderr.pipe(logStreamedLines(logger, LogLevel.warning));
210
340
  (await this._scheduler).register(job);
@@ -213,17 +343,17 @@ class GitService {
213
343
  /**
214
344
  * Runs git branch
215
345
  */ branch(args, opts) {
216
- return this.command('branch', args, opts);
346
+ return this.command(`branch ${args.join(' ')}`, opts);
217
347
  }
218
348
  /**
219
349
  * Runs git diff
220
350
  */ diff(args, opts) {
221
- return this.command('diff', args, opts);
351
+ return this.command(`diff ${args.join(' ')}`, opts);
222
352
  }
223
353
  /**
224
354
  * Runs git tag
225
355
  */ tag(args, opts) {
226
- return this.command('tag', args, opts);
356
+ return this.command(`tag ${args.join(' ')}`, opts);
227
357
  }
228
358
  /**
229
359
  * Uses git diff to detect if given files have been affected since given reference
@@ -265,873 +395,728 @@ class GitService {
265
395
  }
266
396
  }
267
397
 
268
- // Filter
269
- function isAffected$(opts) {
270
- return (origin)=>{
271
- return asyncIterator$(async function*() {
272
- for await (const wks of origin){
273
- const revision = await formatRevision(opts, wks);
274
- if (await wks.isAffected(revision)) {
275
- yield wks;
276
- }
277
- }
278
- }());
279
- };
398
+ // Utils
399
+ async function* combine(...generators) {
400
+ for (const gen of generators){
401
+ yield* gen;
402
+ }
280
403
  }
281
- async function formatRevision(opts, workspace) {
282
- const git = inject$(GitService);
283
- const logger = inject$(LOGGER).child(withLabel(workspace.name));
284
- // Format revision
285
- let result = opts.format;
286
- result = result.replace(/(?<!\\)((?:\\\\)*)%name/g, `$1${workspace.name}`);
287
- result = result.replace(/\\(.)/g, '$1');
288
- // Ask git to complete it
289
- const sortArgs = opts.sort ? [
290
- '--sort',
291
- opts.sort
292
- ] : [];
293
- // - search in branches
294
- if (result.includes('*')) {
295
- const branches = await git.listBranches([
296
- ...sortArgs,
297
- result
404
+
405
+ class Workspace {
406
+ manifest;
407
+ project;
408
+ // Attributes
409
+ _affectedCache = new Map();
410
+ _logger;
411
+ _git = inject$(GitService);
412
+ _root;
413
+ _jobs = new Map();
414
+ // Constructor
415
+ constructor(root, manifest, project){
416
+ this.manifest = manifest;
417
+ this.project = project;
418
+ this._root = root;
419
+ this._logger = inject$(LOGGER).child(withLabel(manifest.name));
420
+ }
421
+ // Methods
422
+ async _buildDependencies(job, opts) {
423
+ const generators = [];
424
+ switch(opts.buildDeps ?? 'all'){
425
+ case 'all':
426
+ generators.unshift(this.devDependencies());
427
+ // eslint-disable-next no-fallthrough
428
+ case 'prod':
429
+ generators.unshift(this.dependencies());
430
+ }
431
+ // Build deps
432
+ for await (const dep of combine(...generators)){
433
+ const build = await dep.build(opts);
434
+ if (build) {
435
+ job.dependsOn(build);
436
+ }
437
+ }
438
+ }
439
+ async *_loadDependencies(dependencies, kind) {
440
+ for (const [dep, range] of Object.entries(dependencies)){
441
+ const ws = await this.project.workspace(dep);
442
+ if (ws) {
443
+ if (ws._satisfies(this, range)) {
444
+ yield ws;
445
+ } else {
446
+ this._logger.warning(`ignoring ${kind} ${ws.reference} as it does not match requirement ${range}`);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ async _isAffected(reference) {
452
+ const isAffected = await this._git.isAffected(reference, [
453
+ this.root
298
454
  ], {
299
- cwd: workspace.root,
300
- logger: logger
455
+ cwd: this.project.root,
456
+ logger: this._logger
301
457
  });
302
- if (branches.length > 0) {
303
- result = branches[branches.length - 1];
458
+ if (isAffected) {
459
+ return true;
460
+ }
461
+ // Test dependencies
462
+ const proms = [];
463
+ for await (const dep of combine(this.dependencies(), this.devDependencies())){
464
+ proms.push(dep.isAffected(reference));
304
465
  }
466
+ const results = await Promise.all(proms);
467
+ return results.some((r)=>r);
305
468
  }
306
- // - search in tags
307
- if (result.includes('*')) {
308
- const tags = await git.listTags([
309
- ...sortArgs,
310
- result
311
- ], {
312
- cwd: workspace.root,
313
- logger: logger
469
+ _satisfies(from, range) {
470
+ if (range.startsWith('file:')) {
471
+ return path.resolve(from.root, range.substring(5)) === this.root;
472
+ }
473
+ if (range.startsWith('workspace:')) {
474
+ range = range.substring(10);
475
+ }
476
+ return !this.version || satisfies(this.version, range);
477
+ }
478
+ async isAffected(reference) {
479
+ let isAffected = this._affectedCache.get(reference);
480
+ if (!isAffected) {
481
+ isAffected = this._isAffected(reference);
482
+ this._affectedCache.set(reference, isAffected);
483
+ }
484
+ return await isAffected;
485
+ }
486
+ async *dependencies() {
487
+ if (!this.manifest.dependencies) return;
488
+ for await (const ws of this._loadDependencies(this.manifest.dependencies, 'dependency')){
489
+ yield ws;
490
+ }
491
+ }
492
+ async *devDependencies() {
493
+ if (!this.manifest.devDependencies) return;
494
+ for await (const ws of this._loadDependencies(this.manifest.devDependencies, 'devDependency')){
495
+ yield ws;
496
+ }
497
+ }
498
+ async build(opts = {}) {
499
+ const script = opts.buildScript ?? 'build';
500
+ const job = await this.run(script, [], opts);
501
+ if (!job) {
502
+ this._logger.warning(`will not be built (no "${script}" script found)`);
503
+ }
504
+ return job;
505
+ }
506
+ async exec(command, opts = {}) {
507
+ const pm = await this.project.packageManager();
508
+ const job = command$(this, command, {
509
+ ...opts,
510
+ logger: this._logger.child(withLabel(`${this.name}$${command}`)),
511
+ superCommand: pm === 'yarn' ? 'yarn exec' : undefined
314
512
  });
315
- if (tags.length > 0) {
316
- result = tags[tags.length - 1];
513
+ await this._buildDependencies(job, opts);
514
+ return job;
515
+ }
516
+ getScript(script) {
517
+ const { scripts = {} } = this.manifest;
518
+ return scripts[script] || null;
519
+ }
520
+ async run(script, args = [], opts = {}) {
521
+ // Script not found
522
+ if (!this.getScript(script)) {
523
+ return null;
524
+ }
525
+ // Create task if it doesn't exist yet
526
+ let job = this._jobs.get(script);
527
+ if (!job) {
528
+ const config = await inject$(CONFIG, asyncScope$());
529
+ job = await runScript$(this, script, args, {
530
+ ...opts,
531
+ logger: this._logger.child(withLabel(`${this.name}#${script}`)),
532
+ runHooks: config.hooks
533
+ });
534
+ await this._buildDependencies(job, opts);
535
+ this._jobs.set(script, job);
317
536
  }
537
+ return job;
318
538
  }
319
- if (result !== opts.format) {
320
- logger.verbose(`resolved ${opts.format} into ${result}`);
539
+ // Properties
540
+ get name() {
541
+ return this.manifest.name;
321
542
  }
322
- if (result.includes('*')) {
323
- logger.warning(`no revision found matching ${result}, using fallback ${opts.fallback}`);
324
- return opts.fallback;
543
+ get reference() {
544
+ return this.version ? `${this.name}@${this.version}` : this.name;
545
+ }
546
+ get root() {
547
+ return path.resolve(this.project.root, this._root);
548
+ }
549
+ get version() {
550
+ return this.manifest.version;
325
551
  }
326
- return result;
327
- }
328
-
329
- // Filter
330
- function isPrivate$(value) {
331
- return filter$((wks)=>(wks.manifest.private ?? false) === value);
332
552
  }
333
553
 
334
- var _dec$1, _dec1, _initProto$1;
335
- _dec$1 = instrument('TaskParserService.parse'), _dec1 = instrument('TaskParserService.buildJob');
336
- // Service
337
- class TaskParserService {
338
- static{
339
- ({ e: [_initProto$1] } = _(this, [
340
- [
341
- _dec$1,
342
- 2,
343
- "parse"
344
- ],
345
- [
346
- _dec1,
347
- 2,
348
- "buildJob"
349
- ]
350
- ], []));
351
- }
554
+ class Project {
352
555
  // Attributes
353
- _logger = (_initProto$1(this), inject$(LOGGER).child(withLabel('task-parser')));
354
- // Statics
355
- static isTaskNode(node) {
356
- return 'script' in node;
556
+ _isFullyLoaded = false;
557
+ _lock = mutex$();
558
+ _mainWorkspace;
559
+ _packageManager;
560
+ _workspaceGlob;
561
+ _names = new Map();
562
+ _logger = inject$(LOGGER).child(withLabel('project'));
563
+ _root;
564
+ _scurry = inject$(PATH_SCURRY);
565
+ _workspaces = new Map();
566
+ // Constructor
567
+ constructor(root, opts = {}){
568
+ this._root = root;
569
+ if (opts.packageManager) {
570
+ this._logger.verbose`Forced use of ${opts.packageManager} in ${root}`;
571
+ this._packageManager = opts.packageManager;
572
+ }
357
573
  }
358
- static _sequenceOperatorWarn = true;
359
574
  // Methods
360
- _lexer() {
361
- return moo.states({
362
- task: {
363
- lparen: '(',
364
- whitespace: /[ \t]+/,
365
- script: {
366
- match: /[-_:a-zA-Z0-9]+/,
367
- push: 'operatorOrArgument'
368
- },
369
- string: [
370
- {
371
- match: /'(?:\\['\\]|[^\r\n'\\])+'/,
372
- push: 'operator',
373
- value: (x)=>x.slice(1, -1).replace(/\\(['\\])/g, '$1')
374
- },
375
- {
376
- match: /"(?:\\["\\]|[^\r\n"\\])+"/,
377
- push: 'operator',
378
- value: (x)=>x.slice(1, -1).replace(/\\(["\\])/g, '$1')
379
- }
380
- ]
381
- },
382
- operator: {
383
- rparen: ')',
384
- whitespace: /[ \t]+/,
385
- operator: {
386
- match: [
387
- '->',
388
- '&&',
389
- '//',
390
- '||'
391
- ],
392
- pop: 1
393
- }
394
- },
395
- operatorOrArgument: {
396
- rparen: ')',
397
- whitespace: /[ \t]+/,
398
- operator: {
399
- match: [
400
- '->',
401
- '&&',
402
- '//',
403
- '||'
404
- ],
405
- pop: 1
406
- },
407
- argument: [
408
- {
409
- match: /[-_:a-zA-Z0-9]+/
410
- },
411
- {
412
- match: /'(?:\\['\\]|[^\r\n'\\])+'/,
413
- value: (x)=>x.slice(1, -1).replace(/\\(['\\])/g, '$1')
414
- },
415
- {
416
- match: /"(?:\\["\\]|[^\r\n"\\])+"/,
417
- value: (x)=>x.slice(1, -1).replace(/\\(["\\])/g, '$1')
418
- }
419
- ]
575
+ async _loadManifest(dir) {
576
+ const file = path.resolve(this.root, dir, 'package.json');
577
+ const relative = path.relative(this.root, path.dirname(file));
578
+ const logger = this._logger.child(withLabel(relative ? `project@${relative}` : 'project'));
579
+ logger.debug('loading package.json ...');
580
+ const data = await fs.promises.readFile(file, 'utf-8');
581
+ const mnf = JSON.parse(data);
582
+ normalize(mnf, (msg)=>logger.verbose(msg));
583
+ return mnf;
584
+ }
585
+ _loadWorkspace(dir) {
586
+ return with$(this._lock, async ()=>{
587
+ let wks = this._workspaces.get(dir);
588
+ if (!wks) {
589
+ const manifest = await this._loadManifest(dir);
590
+ wks = new Workspace(dir, manifest, this);
591
+ this._workspaces.set(dir, wks);
592
+ this._names.set(wks.name, wks);
420
593
  }
594
+ return wks;
421
595
  });
422
596
  }
423
- _nextNode(lexer, i = 0) {
424
- let node = null;
425
- for (const token of lexer){
426
- // Ignore whitespaces
427
- if (token.type === 'whitespace') {
428
- continue;
429
- }
430
- // rparen = end of group
431
- if (token.type === 'rparen') {
432
- break;
433
- }
434
- // Handle argument
435
- if (token.type === 'argument') {
436
- if (!node) {
437
- throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected argument'));
438
- } else if (TaskParserService.isTaskNode(node)) {
439
- node.args.push(token.value);
440
- } else {
441
- const lastTask = node.tasks[node.tasks.length - 1];
442
- if (!lastTask || !TaskParserService.isTaskNode(lastTask)) {
443
- throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected argument'));
444
- } else {
445
- lastTask.args.push(token.value);
446
- }
447
- }
448
- continue;
449
- }
450
- // Handle operator
451
- if (token.type === 'operator') {
452
- const operator = token.value;
453
- if (!node) {
454
- throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected operator'));
455
- } else if (TaskParserService.isTaskNode(node)) {
456
- node = {
457
- operator,
458
- tasks: [
459
- node
460
- ]
461
- };
462
- continue;
463
- } else {
464
- if (node.operator !== operator) {
465
- node = {
466
- operator,
467
- tasks: [
468
- node
469
- ]
470
- };
471
- }
472
- continue;
473
- }
474
- }
475
- // Build "child"
476
- let child;
477
- if (token.type === 'script') {
478
- child = {
479
- script: token.value,
480
- args: []
481
- };
482
- } else if (token.type === 'string') {
483
- const [script, ...args] = token.value.split(/ +/);
484
- child = {
485
- script,
486
- args
487
- };
488
- } else if (token.type === 'lparen') {
489
- const res = this._nextNode(lexer, i + 1);
490
- if (!res) {
491
- throw new TaskSyntaxError(lexer.formatError(token, 'Empty group found'));
492
- }
493
- child = res;
494
- } else {
495
- throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected token'));
496
- }
497
- if (!node) {
498
- node = child;
499
- } else if (TaskParserService.isTaskNode(node)) {
500
- throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected token, expected an operator'));
501
- } else {
502
- node.tasks.push(child);
597
+ async currentWorkspace(cwd = inject$(CWD, asyncScope$())) {
598
+ let workspace = null;
599
+ cwd = path.resolve(cwd);
600
+ for await (const wks of this.workspaces()){
601
+ if (cwd.startsWith(wks.root)) {
602
+ workspace = wks;
603
+ if (wks.root !== this.root) return wks;
503
604
  }
504
605
  }
505
- return node;
606
+ return workspace;
506
607
  }
507
- parse(expr) {
508
- const lexer = this._lexer().reset(expr);
509
- const tree = {
510
- roots: []
511
- };
512
- while(true){
513
- const node = this._nextNode(lexer);
514
- if (node) {
515
- tree.roots.push(node);
608
+ async mainWorkspace() {
609
+ if (!this._mainWorkspace) {
610
+ const manifest = await this._loadManifest('.');
611
+ this._mainWorkspace = new Workspace('.', manifest, this);
612
+ this._names.set(this._mainWorkspace.name, this._mainWorkspace);
613
+ }
614
+ return this._mainWorkspace;
615
+ }
616
+ async packageManager() {
617
+ if (!this._packageManager) {
618
+ this._logger.debug`searching lockfile in ${this.root}`;
619
+ const files = await this._scurry.readdir(this.root, {
620
+ withFileTypes: false
621
+ });
622
+ if (files.includes('yarn.lock')) {
623
+ this._logger.debug`detected yarn in ${this.root}`;
624
+ this._packageManager = 'yarn';
625
+ } else if (files.includes('package-lock.json')) {
626
+ this._logger.debug`detected npm in ${this.root}`;
627
+ this._packageManager = 'npm';
516
628
  } else {
517
- break;
629
+ this._logger.debug`no package manager recognized in ${this.root}, defaults to npm`;
630
+ this._packageManager = 'npm';
518
631
  }
519
632
  }
520
- return tree;
633
+ return this._packageManager;
521
634
  }
522
- *extractScripts(node) {
523
- if ('roots' in node) {
524
- for (const child of node.roots){
525
- yield* this.extractScripts(child);
526
- }
527
- } else if (TaskParserService.isTaskNode(node)) {
528
- yield node.script;
529
- } else {
530
- for (const child of node.tasks){
531
- yield* this.extractScripts(child);
635
+ async workspace(name) {
636
+ // With current directory
637
+ if (!name) {
638
+ const dir = path.relative(this.root, inject$(CWD, asyncScope$()));
639
+ return this._loadWorkspace(dir);
640
+ }
641
+ // Try name index
642
+ const wks = this._names.get(name);
643
+ if (wks) {
644
+ return wks;
645
+ }
646
+ // Load workspaces
647
+ if (!this._isFullyLoaded) {
648
+ for await (const ws of this.workspaces()){
649
+ if (ws.name === name) {
650
+ return ws;
651
+ }
532
652
  }
653
+ this._isFullyLoaded = true;
533
654
  }
655
+ return null;
534
656
  }
535
- async buildJob(node, workspace, opts) {
536
- if (TaskParserService.isTaskNode(node)) {
537
- const job = await workspace.run(node.script, node.args, opts);
538
- if (!job) {
539
- throw new TaskExpressionError(`Workspace ${workspace.name} have no ${node.script} script`);
657
+ async *workspaces() {
658
+ const main = await this.mainWorkspace();
659
+ yield main;
660
+ if (this._isFullyLoaded) {
661
+ for (const wks of this._names.values()){
662
+ if (wks.name !== main.name) yield wks;
540
663
  }
541
- return job;
542
664
  } else {
543
- let flow;
544
- if (node.operator === '//') {
545
- flow = parallelFlow$({
546
- label: 'In parallel'
547
- });
548
- } else if (node.operator === '||') {
549
- flow = fallbackFlow$({
550
- label: 'Fallbacks'
551
- });
552
- } else {
553
- if (node.operator === '->' && TaskParserService._sequenceOperatorWarn) {
554
- this._logger.warn('Sequence operator -> is deprecated in favor of &&. It will be removed in a next major release.');
555
- TaskParserService._sequenceOperatorWarn = true;
665
+ // Load child workspaces
666
+ const patterns = main.manifest.workspaces ?? [];
667
+ this._scurry.chdir(this.root);
668
+ this._workspaceGlob ??= new Glob(patterns, {
669
+ scurry: this._scurry,
670
+ withFileTypes: true
671
+ });
672
+ for await (const dir of this._workspaceGlob){
673
+ try {
674
+ // Check if dir is a directory
675
+ if (dir.isDirectory()) {
676
+ yield await this._loadWorkspace(dir.fullpath());
677
+ }
678
+ } catch (error) {
679
+ if (error.code === 'ENOENT') {
680
+ continue;
681
+ }
682
+ throw error;
556
683
  }
557
- flow = sequenceFlow$({
558
- label: 'In sequence'
559
- });
560
- }
561
- for (const child of node.tasks){
562
- flow.push(await this.buildJob(child, workspace, opts));
563
684
  }
564
- return flow;
685
+ this._isFullyLoaded = true;
565
686
  }
566
687
  }
688
+ // Properties
689
+ get root() {
690
+ return path.resolve(this._root);
691
+ }
567
692
  }
568
693
 
569
- function mutex$() {
570
- const count$ = var$(0);
571
- return {
572
- async acquire () {
573
- let cnt = count$.defer();
574
- while(cnt > 0){
575
- cnt = await waitFor$(count$);
576
- }
577
- count$.mutate(cnt + 1);
578
- },
579
- release () {
580
- const cnt = count$.defer();
581
- if (cnt > 0) {
582
- count$.mutate(cnt - 1);
583
- }
584
- }
585
- };
586
- }
587
- async function with$(lock, fn) {
588
- try {
589
- await lock.acquire();
590
- return await fn();
591
- } finally{
592
- lock.release();
694
+ var _dec$1, _initProto$1;
695
+ _dec$1 = instrument();
696
+ /**
697
+ * Helps detecting projects folders
698
+ */ class ProjectsRepository {
699
+ static{
700
+ ({ e: [_initProto$1] } = _(this, [
701
+ [
702
+ _dec$1,
703
+ 2,
704
+ "searchProjectRoot"
705
+ ]
706
+ ], []));
593
707
  }
594
- }
595
-
596
- function command$(workspace, cmd, args, opts = {}) {
597
- const { superCommand, logger = inject$(LOGGER), ...rest } = opts;
598
- // Apply super command
599
- if (superCommand) {
600
- if (typeof superCommand === 'string') {
601
- args = [
602
- cmd,
603
- ...args
604
- ];
605
- cmd = superCommand;
606
- } else if (superCommand.length) {
607
- args = [
608
- ...superCommand.slice(1),
609
- cmd,
610
- ...args
611
- ];
612
- cmd = superCommand[0];
708
+ // Attributes
709
+ _cache = (_initProto$1(this), new Map());
710
+ _logger = inject$(LOGGER).child(withLabel('projects'));
711
+ _scurry = inject$(PATH_SCURRY);
712
+ // Methods
713
+ async isProjectRoot(dir) {
714
+ this._logger.debug`testing ${dir}`;
715
+ const files = await this._scurry.readdir(dir, {
716
+ withFileTypes: false
717
+ });
718
+ return {
719
+ hasManifest: files.includes(MANIFEST),
720
+ hasLockFile: LOCK_FILES.some((lock)=>files.includes(lock))
721
+ };
722
+ }
723
+ async searchProjectRoot(directory) {
724
+ directory = path.resolve(directory);
725
+ // Test all ancestors
726
+ let foundManifest = false;
727
+ let projectRoot = directory;
728
+ let dir = directory;
729
+ let prev = dir;
730
+ do {
731
+ // Look for files
732
+ const { hasManifest, hasLockFile } = await this.isProjectRoot(dir);
733
+ if (hasManifest) {
734
+ projectRoot = dir;
735
+ foundManifest = true;
736
+ }
737
+ if (hasLockFile) {
738
+ break;
739
+ }
740
+ prev = dir;
741
+ dir = path.dirname(dir);
742
+ }while (prev !== dir);
743
+ // Log it
744
+ if (foundManifest) {
745
+ this._logger.verbose`project root found at ${projectRoot}`;
746
+ } else {
747
+ this._logger.verbose`project root not found, keeping ${projectRoot}`;
613
748
  }
749
+ return projectRoot;
614
750
  }
615
- // Prepare job
616
- const job = spawn$(cmd, args, {
617
- ...rest,
618
- cwd: workspace.root,
619
- env: {
620
- FORCE_COLOR: '1',
621
- ...rest.env
751
+ getProject(root, opts) {
752
+ let project = this._cache.get(root);
753
+ if (!project) {
754
+ project = new Project(root, opts);
755
+ this._cache.set(root, project);
622
756
  }
757
+ return project;
758
+ }
759
+ }
760
+ // Constants
761
+ const MANIFEST = 'package.json';
762
+ const LOCK_FILES = [
763
+ 'package-lock.json',
764
+ 'yarn.lock'
765
+ ];
766
+
767
+ /**
768
+ * Adds arguments to load a project.
769
+ */ function withProject(parser) {
770
+ return parser.option('project', {
771
+ alias: 'p',
772
+ type: 'string',
773
+ default: '',
774
+ defaultDescription: 'current working directory',
775
+ description: 'Project root directory',
776
+ normalize: true
777
+ }).option('package-manager', {
778
+ choices: [
779
+ 'yarn',
780
+ 'npm'
781
+ ],
782
+ type: 'string',
783
+ description: 'Force package manager'
784
+ }).middleware((args)=>startSpan({
785
+ name: 'project',
786
+ op: 'cli.middleware'
787
+ }, async ()=>{
788
+ const repository = inject$(ProjectsRepository);
789
+ const directory = path.resolve(inject$(CWD, asyncScope$()), args.project);
790
+ args.project = await repository.searchProjectRoot(directory);
791
+ }));
792
+ }
793
+ /**
794
+ * Loads a project, based on arguments.
795
+ */ function loadProject(args) {
796
+ const repository = inject$(ProjectsRepository);
797
+ return repository.getProject(args.project, {
798
+ packageManager: args.packageManager
623
799
  });
624
- job.stdout.pipe(logStreamedLines(logger, LogLevel.info));
625
- job.stderr.pipe(logStreamedLines(logger, LogLevel.info));
626
- return job;
627
800
  }
628
801
 
629
- // Utils
630
- function capitalize(txt) {
631
- return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase();
802
+ // Filter
803
+ function hasSomeScript$(scripts) {
804
+ return filter$((wks)=>{
805
+ if (!wks.manifest.scripts) {
806
+ return false;
807
+ }
808
+ return scripts.some((script)=>script in wks.manifest.scripts);
809
+ });
632
810
  }
633
- function splitCommandLine(line) {
634
- line = line.trim();
635
- const parts = [];
636
- let current_cote = '';
637
- let last = 0;
638
- for(let i = 1; i < line.length; ++i){
639
- const c = line[i];
640
- if (current_cote) {
641
- if (c === current_cote) {
642
- current_cote = '';
643
- }
644
- } else {
645
- if ([
646
- '"',
647
- '\''
648
- ].includes(c)) {
649
- current_cote = c;
650
- } else if (c === ' ') {
651
- parts.push(line.slice(last, i));
652
- last = i + 1;
653
- }
811
+ function hasEveryScript$(scripts) {
812
+ return filter$((wks)=>{
813
+ if (!wks.manifest.scripts) {
814
+ return false;
654
815
  }
655
- }
656
- parts.push(line.slice(last));
657
- return parts;
816
+ return scripts.every((script)=>script in wks.manifest.scripts);
817
+ });
658
818
  }
659
819
 
660
- async function runScript$(workspace, script, args, opts = {}) {
661
- // Run script itself
662
- const jobs = [
663
- await planScript$(workspace, script, args, opts)
664
- ];
665
- if (!jobs[0]) {
666
- throw new ScriptNotFound(`No script ${script} in ${workspace.name}`);
667
- }
668
- // Run hooks
669
- if (opts.runHooks) {
670
- jobs.unshift(await planScript$(workspace, `pre${script}`, [], opts));
671
- jobs.push(await planScript$(workspace, `post${script}`, [], opts));
672
- }
673
- // Prepare workflow
674
- const flow = pipe$(jobs, filter$((job)=>job !== null), collect$(sequenceFlow$({
675
- label: script,
676
- type: 'script'
677
- })));
678
- return {
679
- ...flow,
680
- script,
681
- workspace
820
+ // Filter
821
+ function isAffected$(opts) {
822
+ return (origin)=>{
823
+ return asyncIterator$(async function*() {
824
+ for await (const wks of origin){
825
+ const revision = await formatRevision(opts, wks);
826
+ if (await wks.isAffected(revision)) {
827
+ yield wks;
828
+ }
829
+ }
830
+ }());
682
831
  };
683
832
  }
684
- // Utils
685
- async function planScript$(workspace, script, args, opts) {
686
- // Load script
687
- const line = workspace?.getScript(script);
688
- if (!line) {
689
- return null;
690
- }
691
- // Parse script
692
- const [command, ...commandArgs] = splitCommandLine(line);
693
- if (!command) {
694
- return null;
833
+ async function formatRevision(opts, workspace) {
834
+ const git = inject$(GitService);
835
+ const logger = inject$(LOGGER).child(withLabel(workspace.name));
836
+ // Format revision
837
+ let result = opts.format;
838
+ result = result.replace(/(?<!\\)((?:\\\\)*)%name/g, `$1${workspace.name}`);
839
+ result = result.replace(/\\(.)/g, '$1');
840
+ // Ask git to complete it
841
+ const sortArgs = opts.sort ? [
842
+ '--sort',
843
+ opts.sort
844
+ ] : [];
845
+ // - search in branches
846
+ if (result.includes('*')) {
847
+ const branches = await git.listBranches([
848
+ ...sortArgs,
849
+ result
850
+ ], {
851
+ cwd: workspace.root,
852
+ logger: logger
853
+ });
854
+ if (branches.length > 0) {
855
+ result = branches[branches.length - 1];
856
+ }
695
857
  }
696
- if (command === 'jill') {
697
- const argv = commandArgs.map((arg)=>arg.replace(/^["'](.+)["']$/, '$1'));
698
- const { PlannerService } = await import('./planner.service.js');
699
- const plannerService = inject$(PlannerService);
700
- const job = await plannerService.plan(argv, workspace.root);
701
- if (job) {
702
- return job;
858
+ // - search in tags
859
+ if (result.includes('*')) {
860
+ const tags = await git.listTags([
861
+ ...sortArgs,
862
+ result
863
+ ], {
864
+ cwd: workspace.root,
865
+ logger: logger
866
+ });
867
+ if (tags.length > 0) {
868
+ result = tags[tags.length - 1];
703
869
  }
704
870
  }
705
- // Run command
706
- const pm = await workspace.project.packageManager();
707
- return command$(workspace, command, [
708
- ...commandArgs,
709
- ...args
710
- ], {
711
- logger: opts.logger,
712
- superCommand: pm === 'yarn' && command !== 'yarn' ? [
713
- 'yarn',
714
- 'exec'
715
- ] : undefined
716
- });
717
- }
718
- class ScriptNotFound extends ClientError {
719
- name = 'ScriptNotFound';
871
+ if (result !== opts.format) {
872
+ logger.verbose(`resolved ${opts.format} into ${result}`);
873
+ }
874
+ if (result.includes('*')) {
875
+ logger.warning(`no revision found matching ${result}, using fallback ${opts.fallback}`);
876
+ return opts.fallback;
877
+ }
878
+ return result;
720
879
  }
721
880
 
722
- // Utils
723
- async function* combine(...generators) {
724
- for (const gen of generators){
725
- yield* gen;
726
- }
881
+ // Filter
882
+ function isPrivate$(value) {
883
+ return filter$((wks)=>(wks.manifest.private ?? false) === value);
727
884
  }
728
885
 
729
- class Workspace {
730
- manifest;
731
- project;
886
+ var _dec, _dec1, _initProto;
887
+ _dec = instrument('TaskParserService.parse'), _dec1 = instrument('TaskParserService.buildJob');
888
+ // Service
889
+ class TaskParserService {
890
+ static{
891
+ ({ e: [_initProto] } = _(this, [
892
+ [
893
+ _dec,
894
+ 2,
895
+ "parse"
896
+ ],
897
+ [
898
+ _dec1,
899
+ 2,
900
+ "buildJob"
901
+ ]
902
+ ], []));
903
+ }
732
904
  // Attributes
733
- _affectedCache = new Map();
734
- _logger;
735
- _git = inject$(GitService);
736
- _root;
737
- _jobs = new Map();
738
- // Constructor
739
- constructor(root, manifest, project){
740
- this.manifest = manifest;
741
- this.project = project;
742
- this._root = root;
743
- this._logger = inject$(LOGGER).child(withLabel(manifest.name));
905
+ _logger = (_initProto(this), inject$(LOGGER).child(withLabel('task-parser')));
906
+ // Statics
907
+ static isTaskNode(node) {
908
+ return 'script' in node;
744
909
  }
910
+ static _sequenceOperatorWarn = true;
745
911
  // Methods
746
- async _buildDependencies(job, opts) {
747
- const generators = [];
748
- switch(opts.buildDeps ?? 'all'){
749
- case 'all':
750
- generators.unshift(this.devDependencies());
751
- // eslint-disable-next no-fallthrough
752
- case 'prod':
753
- generators.unshift(this.dependencies());
754
- }
755
- // Build deps
756
- for await (const dep of combine(...generators)){
757
- const build = await dep.build(opts);
758
- if (build) {
759
- job.dependsOn(build);
912
+ _lexer() {
913
+ return moo.states({
914
+ task: {
915
+ lparen: '(',
916
+ whitespace: /[ \t]+/,
917
+ script: {
918
+ match: /[-_:a-zA-Z0-9]+/,
919
+ push: 'operatorOrArgument'
920
+ },
921
+ string: [
922
+ {
923
+ match: /'(?:\\['\\]|[^\r\n'\\])+'/,
924
+ push: 'operator',
925
+ value: (x)=>x.slice(1, -1).replace(/\\(['\\])/g, '$1')
926
+ },
927
+ {
928
+ match: /"(?:\\["\\]|[^\r\n"\\])+"/,
929
+ push: 'operator',
930
+ value: (x)=>x.slice(1, -1).replace(/\\(["\\])/g, '$1')
931
+ }
932
+ ]
933
+ },
934
+ operator: {
935
+ rparen: ')',
936
+ whitespace: /[ \t]+/,
937
+ operator: {
938
+ match: [
939
+ '->',
940
+ '&&',
941
+ '//',
942
+ '||'
943
+ ],
944
+ pop: 1
945
+ }
946
+ },
947
+ operatorOrArgument: {
948
+ rparen: ')',
949
+ whitespace: /[ \t]+/,
950
+ operator: {
951
+ match: [
952
+ '->',
953
+ '&&',
954
+ '//',
955
+ '||'
956
+ ],
957
+ pop: 1
958
+ },
959
+ argument: [
960
+ {
961
+ match: /[-_:a-zA-Z0-9]+/
962
+ },
963
+ {
964
+ match: /'(?:\\['\\]|[^\r\n'\\])+'/,
965
+ value: (x)=>x.slice(1, -1).replace(/\\(['\\])/g, '$1')
966
+ },
967
+ {
968
+ match: /"(?:\\["\\]|[^\r\n"\\])+"/,
969
+ value: (x)=>x.slice(1, -1).replace(/\\(["\\])/g, '$1')
970
+ }
971
+ ]
760
972
  }
761
- }
973
+ });
762
974
  }
763
- async *_loadDependencies(dependencies, kind) {
764
- for (const [dep, range] of Object.entries(dependencies)){
765
- const ws = await this.project.workspace(dep);
766
- if (ws) {
767
- if (ws._satisfies(this, range)) {
768
- yield ws;
975
+ _nextNode(lexer, i = 0) {
976
+ let node = null;
977
+ for (const token of lexer){
978
+ // Ignore whitespaces
979
+ if (token.type === 'whitespace') {
980
+ continue;
981
+ }
982
+ // rparen = end of group
983
+ if (token.type === 'rparen') {
984
+ break;
985
+ }
986
+ // Handle argument
987
+ if (token.type === 'argument') {
988
+ if (!node) {
989
+ throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected argument'));
990
+ } else if (TaskParserService.isTaskNode(node)) {
991
+ node.args.push(token.value);
769
992
  } else {
770
- this._logger.warning(`ignoring ${kind} ${ws.reference} as it does not match requirement ${range}`);
993
+ const lastTask = node.tasks[node.tasks.length - 1];
994
+ if (!lastTask || !TaskParserService.isTaskNode(lastTask)) {
995
+ throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected argument'));
996
+ } else {
997
+ lastTask.args.push(token.value);
998
+ }
771
999
  }
1000
+ continue;
772
1001
  }
773
- }
774
- }
775
- async _isAffected(reference) {
776
- const isAffected = await this._git.isAffected(reference, [
777
- this.root
778
- ], {
779
- cwd: this.project.root,
780
- logger: this._logger
781
- });
782
- if (isAffected) {
783
- return true;
784
- }
785
- // Test dependencies
786
- const proms = [];
787
- for await (const dep of combine(this.dependencies(), this.devDependencies())){
788
- proms.push(dep.isAffected(reference));
789
- }
790
- const results = await Promise.all(proms);
791
- return results.some((r)=>r);
792
- }
793
- _satisfies(from, range) {
794
- if (range.startsWith('file:')) {
795
- return path.resolve(from.root, range.substring(5)) === this.root;
796
- }
797
- if (range.startsWith('workspace:')) {
798
- range = range.substring(10);
799
- }
800
- return !this.version || satisfies(this.version, range);
801
- }
802
- async isAffected(reference) {
803
- let isAffected = this._affectedCache.get(reference);
804
- if (!isAffected) {
805
- isAffected = this._isAffected(reference);
806
- this._affectedCache.set(reference, isAffected);
807
- }
808
- return await isAffected;
809
- }
810
- async *dependencies() {
811
- if (!this.manifest.dependencies) return;
812
- for await (const ws of this._loadDependencies(this.manifest.dependencies, 'dependency')){
813
- yield ws;
814
- }
815
- }
816
- async *devDependencies() {
817
- if (!this.manifest.devDependencies) return;
818
- for await (const ws of this._loadDependencies(this.manifest.devDependencies, 'devDependency')){
819
- yield ws;
820
- }
821
- }
822
- async build(opts = {}) {
823
- const script = opts.buildScript ?? 'build';
824
- const job = await this.run(script, [], opts);
825
- if (!job) {
826
- this._logger.warning(`will not be built (no "${script}" script found)`);
827
- }
828
- return job;
829
- }
830
- async exec(command, args = [], opts = {}) {
831
- const pm = await this.project.packageManager();
832
- const job = command$(this, command, args, {
833
- ...opts,
834
- logger: this._logger.child(withLabel(`${this.name}$${command}`)),
835
- superCommand: pm === 'yarn' ? [
836
- 'yarn',
837
- 'exec'
838
- ] : undefined
839
- });
840
- await this._buildDependencies(job, opts);
841
- return job;
842
- }
843
- getScript(script) {
844
- const { scripts = {} } = this.manifest;
845
- return scripts[script] || null;
846
- }
847
- async run(script, args = [], opts = {}) {
848
- // Script not found
849
- if (!this.getScript(script)) {
850
- return null;
851
- }
852
- // Create task if it doesn't exist yet
853
- let job = this._jobs.get(script);
854
- if (!job) {
855
- const config = await inject$(CONFIG, asyncScope$());
856
- job = await runScript$(this, script, args, {
857
- ...opts,
858
- logger: this._logger.child(withLabel(`${this.name}#${script}`)),
859
- runHooks: config.hooks
860
- });
861
- await this._buildDependencies(job, opts);
862
- this._jobs.set(script, job);
863
- }
864
- return job;
865
- }
866
- toJSON() {
867
- return {
868
- name: this.name,
869
- version: this.version,
870
- root: this.root
871
- };
872
- }
873
- // Properties
874
- get name() {
875
- return this.manifest.name;
876
- }
877
- get reference() {
878
- return this.version ? `${this.name}@${this.version}` : this.name;
879
- }
880
- get root() {
881
- return path.resolve(this.project.root, this._root);
882
- }
883
- get version() {
884
- return this.manifest.version;
885
- }
886
- }
887
-
888
- class Project {
889
- // Attributes
890
- _isFullyLoaded = false;
891
- _lock = mutex$();
892
- _mainWorkspace;
893
- _packageManager;
894
- _workspaceGlob;
895
- _names = new Map();
896
- _logger = inject$(LOGGER).child(withLabel('project'));
897
- _root;
898
- _scurry = inject$(PATH_SCURRY);
899
- _workspaces = new Map();
900
- // Constructor
901
- constructor(root, opts = {}){
902
- this._root = root;
903
- if (opts.packageManager) {
904
- this._logger.verbose`Forced use of ${opts.packageManager} in ${root}`;
905
- this._packageManager = opts.packageManager;
906
- }
907
- }
908
- // Methods
909
- async _loadManifest(dir) {
910
- const file = path.resolve(this.root, dir, 'package.json');
911
- const relative = path.relative(this.root, path.dirname(file));
912
- const logger = this._logger.child(withLabel(relative ? `project@${relative}` : 'project'));
913
- logger.debug('loading package.json ...');
914
- const data = await fs.promises.readFile(file, 'utf-8');
915
- const mnf = JSON.parse(data);
916
- normalize(mnf, (msg)=>logger.verbose(msg));
917
- return mnf;
918
- }
919
- _loadWorkspace(dir) {
920
- return with$(this._lock, async ()=>{
921
- let wks = this._workspaces.get(dir);
922
- if (!wks) {
923
- const manifest = await this._loadManifest(dir);
924
- wks = new Workspace(dir, manifest, this);
925
- this._workspaces.set(dir, wks);
926
- this._names.set(wks.name, wks);
1002
+ // Handle operator
1003
+ if (token.type === 'operator') {
1004
+ const operator = token.value;
1005
+ if (!node) {
1006
+ throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected operator'));
1007
+ } else if (TaskParserService.isTaskNode(node)) {
1008
+ node = {
1009
+ operator,
1010
+ tasks: [
1011
+ node
1012
+ ]
1013
+ };
1014
+ continue;
1015
+ } else {
1016
+ if (node.operator !== operator) {
1017
+ node = {
1018
+ operator,
1019
+ tasks: [
1020
+ node
1021
+ ]
1022
+ };
1023
+ }
1024
+ continue;
1025
+ }
1026
+ }
1027
+ // Build "child"
1028
+ let child;
1029
+ if (token.type === 'script') {
1030
+ child = {
1031
+ script: token.value,
1032
+ args: []
1033
+ };
1034
+ } else if (token.type === 'string') {
1035
+ const [script, ...args] = token.value.split(/ +/);
1036
+ child = {
1037
+ script,
1038
+ args
1039
+ };
1040
+ } else if (token.type === 'lparen') {
1041
+ const res = this._nextNode(lexer, i + 1);
1042
+ if (!res) {
1043
+ throw new TaskSyntaxError(lexer.formatError(token, 'Empty group found'));
1044
+ }
1045
+ child = res;
1046
+ } else {
1047
+ throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected token'));
927
1048
  }
928
- return wks;
929
- });
930
- }
931
- async currentWorkspace(cwd = inject$(CWD, asyncScope$())) {
932
- let workspace = null;
933
- cwd = path.resolve(cwd);
934
- for await (const wks of this.workspaces()){
935
- if (cwd.startsWith(wks.root)) {
936
- workspace = wks;
937
- if (wks.root !== this.root) return wks;
1049
+ if (!node) {
1050
+ node = child;
1051
+ } else if (TaskParserService.isTaskNode(node)) {
1052
+ throw new TaskSyntaxError(lexer.formatError(token, 'Unexpected token, expected an operator'));
1053
+ } else {
1054
+ node.tasks.push(child);
938
1055
  }
939
1056
  }
940
- return workspace;
941
- }
942
- async mainWorkspace() {
943
- if (!this._mainWorkspace) {
944
- const manifest = await this._loadManifest('.');
945
- this._mainWorkspace = new Workspace('.', manifest, this);
946
- this._names.set(this._mainWorkspace.name, this._mainWorkspace);
947
- }
948
- return this._mainWorkspace;
1057
+ return node;
949
1058
  }
950
- async packageManager() {
951
- if (!this._packageManager) {
952
- this._logger.debug`searching lockfile in ${this.root}`;
953
- const files = await this._scurry.readdir(this.root, {
954
- withFileTypes: false
955
- });
956
- if (files.includes('yarn.lock')) {
957
- this._logger.debug`detected yarn in ${this.root}`;
958
- this._packageManager = 'yarn';
959
- } else if (files.includes('package-lock.json')) {
960
- this._logger.debug`detected npm in ${this.root}`;
961
- this._packageManager = 'npm';
1059
+ parse(expr) {
1060
+ const lexer = this._lexer().reset(expr);
1061
+ const tree = {
1062
+ roots: []
1063
+ };
1064
+ while(true){
1065
+ const node = this._nextNode(lexer);
1066
+ if (node) {
1067
+ tree.roots.push(node);
962
1068
  } else {
963
- this._logger.debug`no package manager recognized in ${this.root}, defaults to npm`;
964
- this._packageManager = 'npm';
1069
+ break;
965
1070
  }
966
1071
  }
967
- return this._packageManager;
1072
+ return tree;
968
1073
  }
969
- async workspace(name) {
970
- // With current directory
971
- if (!name) {
972
- const dir = path.relative(this.root, inject$(CWD, asyncScope$()));
973
- return this._loadWorkspace(dir);
974
- }
975
- // Try name index
976
- const wks = this._names.get(name);
977
- if (wks) {
978
- return wks;
979
- }
980
- // Load workspaces
981
- if (!this._isFullyLoaded) {
982
- for await (const ws of this.workspaces()){
983
- if (ws.name === name) {
984
- return ws;
985
- }
1074
+ *extractScripts(node) {
1075
+ if ('roots' in node) {
1076
+ for (const child of node.roots){
1077
+ yield* this.extractScripts(child);
1078
+ }
1079
+ } else if (TaskParserService.isTaskNode(node)) {
1080
+ yield node.script;
1081
+ } else {
1082
+ for (const child of node.tasks){
1083
+ yield* this.extractScripts(child);
986
1084
  }
987
- this._isFullyLoaded = true;
988
1085
  }
989
- return null;
990
1086
  }
991
- async *workspaces() {
992
- const main = await this.mainWorkspace();
993
- yield main;
994
- if (this._isFullyLoaded) {
995
- for (const wks of this._names.values()){
996
- if (wks.name !== main.name) yield wks;
1087
+ async buildJob(node, workspace, opts) {
1088
+ if (TaskParserService.isTaskNode(node)) {
1089
+ const job = await workspace.run(node.script, node.args, opts);
1090
+ if (!job) {
1091
+ throw new TaskExpressionError(`Workspace ${workspace.name} have no ${node.script} script`);
997
1092
  }
1093
+ return job;
998
1094
  } else {
999
- // Load child workspaces
1000
- const patterns = main.manifest.workspaces ?? [];
1001
- this._scurry.chdir(this.root);
1002
- this._workspaceGlob ??= new Glob(patterns, {
1003
- scurry: this._scurry,
1004
- withFileTypes: true
1005
- });
1006
- for await (const dir of this._workspaceGlob){
1007
- try {
1008
- // Check if dir is a directory
1009
- if (dir.isDirectory()) {
1010
- yield await this._loadWorkspace(dir.fullpath());
1011
- }
1012
- } catch (error) {
1013
- if (error.code === 'ENOENT') {
1014
- continue;
1015
- }
1016
- throw error;
1095
+ let flow;
1096
+ if (node.operator === '//') {
1097
+ flow = parallelFlow$({
1098
+ label: 'In parallel'
1099
+ });
1100
+ } else if (node.operator === '||') {
1101
+ flow = fallbackFlow$({
1102
+ label: 'Fallbacks'
1103
+ });
1104
+ } else {
1105
+ if (node.operator === '->' && TaskParserService._sequenceOperatorWarn) {
1106
+ this._logger.warn('Sequence operator -> is deprecated in favor of &&. It will be removed in a next major release.');
1107
+ TaskParserService._sequenceOperatorWarn = true;
1017
1108
  }
1109
+ flow = sequenceFlow$({
1110
+ label: 'In sequence'
1111
+ });
1018
1112
  }
1019
- this._isFullyLoaded = true;
1020
- }
1021
- }
1022
- // Properties
1023
- get root() {
1024
- return path.resolve(this._root);
1025
- }
1026
- }
1027
-
1028
- var _dec, _initProto;
1029
- _dec = instrument();
1030
- /**
1031
- * Helps detecting projects folders
1032
- */ class ProjectsRepository {
1033
- static{
1034
- ({ e: [_initProto] } = _(this, [
1035
- [
1036
- _dec,
1037
- 2,
1038
- "searchProjectRoot"
1039
- ]
1040
- ], []));
1041
- }
1042
- // Attributes
1043
- _cache = (_initProto(this), new Map());
1044
- _logger = inject$(LOGGER).child(withLabel('projects'));
1045
- _scurry = inject$(PATH_SCURRY);
1046
- // Methods
1047
- async isProjectRoot(dir) {
1048
- this._logger.debug`testing ${dir}`;
1049
- const files = await this._scurry.readdir(dir, {
1050
- withFileTypes: false
1051
- });
1052
- return {
1053
- hasManifest: files.includes(MANIFEST),
1054
- hasLockFile: LOCK_FILES.some((lock)=>files.includes(lock))
1055
- };
1056
- }
1057
- async searchProjectRoot(directory) {
1058
- directory = path.resolve(directory);
1059
- // Test all ancestors
1060
- let foundManifest = false;
1061
- let projectRoot = directory;
1062
- let dir = directory;
1063
- let prev = dir;
1064
- do {
1065
- // Look for files
1066
- const { hasManifest, hasLockFile } = await this.isProjectRoot(dir);
1067
- if (hasManifest) {
1068
- projectRoot = dir;
1069
- foundManifest = true;
1070
- }
1071
- if (hasLockFile) {
1072
- break;
1113
+ for (const child of node.tasks){
1114
+ flow.push(await this.buildJob(child, workspace, opts));
1073
1115
  }
1074
- prev = dir;
1075
- dir = path.dirname(dir);
1076
- }while (prev !== dir);
1077
- // Log it
1078
- if (foundManifest) {
1079
- this._logger.verbose`project root found at ${projectRoot}`;
1080
- } else {
1081
- this._logger.verbose`project root not found, keeping ${projectRoot}`;
1082
- }
1083
- return projectRoot;
1084
- }
1085
- getProject(root, opts) {
1086
- let project = this._cache.get(root);
1087
- if (!project) {
1088
- project = new Project(root, opts);
1089
- this._cache.set(root, project);
1116
+ return flow;
1090
1117
  }
1091
- return project;
1092
1118
  }
1093
1119
  }
1094
- // Constants
1095
- const MANIFEST = 'package.json';
1096
- const LOCK_FILES = [
1097
- 'package-lock.json',
1098
- 'yarn.lock'
1099
- ];
1100
-
1101
- /**
1102
- * Adds arguments to load a project.
1103
- */ function withProject(parser) {
1104
- return parser.option('project', {
1105
- alias: 'p',
1106
- type: 'string',
1107
- default: '',
1108
- defaultDescription: 'current working directory',
1109
- description: 'Project root directory',
1110
- normalize: true
1111
- }).option('package-manager', {
1112
- choices: [
1113
- 'yarn',
1114
- 'npm'
1115
- ],
1116
- type: 'string',
1117
- description: 'Force package manager'
1118
- }).middleware((args)=>startSpan({
1119
- name: 'project',
1120
- op: 'cli.middleware'
1121
- }, async ()=>{
1122
- const repository = inject$(ProjectsRepository);
1123
- const directory = path.resolve(inject$(CWD, asyncScope$()), args.project);
1124
- args.project = await repository.searchProjectRoot(directory);
1125
- }));
1126
- }
1127
- /**
1128
- * Loads a project, based on arguments.
1129
- */ function loadProject(args) {
1130
- const repository = inject$(ProjectsRepository);
1131
- return repository.getProject(args.project, {
1132
- packageManager: args.packageManager
1133
- });
1134
- }
1135
1120
 
1136
1121
  function pipeline$() {
1137
1122
  const steps = [];
@@ -1304,17 +1289,22 @@ const command$4 = {
1304
1289
  async prepare (args) {
1305
1290
  const workspace = await loadWorkspace(args);
1306
1291
  // Extract arguments
1307
- const rest = args._.map((arg)=>arg.toString());
1292
+ let rest = args._;
1308
1293
  if (rest[0] === 'exec') {
1309
1294
  rest.splice(0, 1);
1310
1295
  }
1311
1296
  // Run script in workspace
1312
- return await workspace.exec(args.command, rest, {
1297
+ rest = rest.map((arg)=>escapeCommandLineArg(arg.toString()));
1298
+ return await workspace.exec([
1299
+ args.command,
1300
+ ...rest
1301
+ ].join(' '), {
1313
1302
  buildScript: args.buildScript,
1314
1303
  buildDeps: args.depsMode
1315
1304
  });
1316
1305
  },
1317
1306
  async execute (args, arg) {
1307
+ const logger = inject$(LOGGER);
1318
1308
  const job = arg;
1319
1309
  if (job.dependencies().length > 0) {
1320
1310
  const dependencies = pipe$(job.dependencies(), collect$(parallelFlow$({
@@ -1333,17 +1323,14 @@ const command$4 = {
1333
1323
  return;
1334
1324
  }
1335
1325
  } else {
1336
- const logger = inject$(LOGGER);
1337
1326
  logger.verbose('No dependency to build');
1338
1327
  }
1339
1328
  await startSpan({
1340
1329
  op: 'subprocess',
1341
- name: [
1342
- job.cmd,
1343
- ...job.args
1344
- ].join(' ')
1330
+ name: job.cmd
1345
1331
  }, async ()=>{
1346
- const child = spawn(job.cmd, job.args, {
1332
+ logger.verbose(`spawn "${job.cmd}"`);
1333
+ const child = spawn(job.cmd, {
1347
1334
  stdio: 'inherit',
1348
1335
  cwd: job.cwd,
1349
1336
  env: {
@@ -1627,7 +1614,7 @@ const command$1 = {
1627
1614
  }
1628
1615
  };
1629
1616
 
1630
- var version = "3.0.0-alpha.10";
1617
+ var version = "3.0.0-alpha.11";
1631
1618
 
1632
1619
  // Utils
1633
1620
  function dynamicImport(filepath) {
@@ -1802,7 +1789,7 @@ const logFormat = qwrap(chalkTemplateStderr).fun`#?:${qprop('label')}{grey [${q$
1802
1789
  const logGateway = inject$(LogGateway);
1803
1790
  flow$(inject$(LOGGER), filter$((log)=>log.level >= logLevel), logDelay$(), logGateway);
1804
1791
  logGateway.connect('console', toStderr(logFormat));
1805
- }));
1792
+ }), true);
1806
1793
  }
1807
1794
  // Utils
1808
1795
  const VERBOSITY_LEVEL = {
@@ -1843,47 +1830,19 @@ function command(module) {
1843
1830
  });
1844
1831
  }
1845
1832
 
1846
- function jobCommandPlan(module, job$) {
1847
- if ('prepare' in module) {
1848
- const prepare = trace(module.prepare, {
1849
- name: commandName(module),
1850
- op: 'cli.prepare'
1851
- });
1852
- return command({
1853
- ...module,
1854
- builder (base) {
1855
- const parser = withPlanMode(base);
1856
- if (module.builder) {
1857
- return module.builder(parser);
1858
- } else {
1859
- return parser;
1860
- }
1861
- },
1862
- async handler (args) {
1863
- job$.mutate(await prepare(args) ?? null);
1864
- }
1865
- });
1866
- } else {
1867
- return command({
1868
- ...module,
1869
- handler: ()=>undefined
1870
- });
1871
- }
1872
- }
1873
- // Utils
1874
- function withPlanMode(parser) {
1833
+ function withPlan(parser) {
1875
1834
  return parser.option('plan', {
1876
1835
  type: 'boolean',
1877
1836
  default: false,
1878
1837
  describe: 'Only prints tasks to be run'
1879
- }).option('plan-mode', {
1838
+ }).option('plan-format', {
1880
1839
  type: 'string',
1881
- desc: 'Plan output mode',
1840
+ desc: 'Plan output format',
1882
1841
  choices: [
1883
1842
  'json',
1884
- 'list'
1843
+ 'tree'
1885
1844
  ],
1886
- default: 'list'
1845
+ default: 'tree'
1887
1846
  });
1888
1847
  }
1889
1848
 
@@ -1899,7 +1858,7 @@ function jobCommandExecute(module) {
1899
1858
  return command({
1900
1859
  ...module,
1901
1860
  builder (base) {
1902
- const parser = withPlanMode(base);
1861
+ const parser = withPlan(base);
1903
1862
  if (module.builder) {
1904
1863
  return module.builder(parser);
1905
1864
  } else {
@@ -1915,8 +1874,21 @@ function jobCommandExecute(module) {
1915
1874
  return;
1916
1875
  }
1917
1876
  if (args.plan) {
1918
- const { jobPlan } = await traceImport('printPlan', ()=>import('./job-plan.js'));
1919
- jobPlan(job);
1877
+ switch(args.planFormat){
1878
+ case 'json':
1879
+ {
1880
+ const { jobPlanJson } = await traceImport('printPlanJson', ()=>import('./job-plan.json.js'));
1881
+ jobPlanJson(job);
1882
+ break;
1883
+ }
1884
+ case 'tree':
1885
+ default:
1886
+ {
1887
+ const { jobPlan } = await traceImport('printPlan', ()=>import('./job-plan.js'));
1888
+ jobPlan(job);
1889
+ break;
1890
+ }
1891
+ }
1920
1892
  } else {
1921
1893
  if (execute) {
1922
1894
  await execute(args, job);
@@ -1973,5 +1945,5 @@ void startSpan({
1973
1945
  }
1974
1946
  });
1975
1947
 
1976
- export { CWD as C, LOGGER as L, SCHEDULER as S, ConfigService as a, cliParser as b, capitalize as c, command$5 as d, command$4 as e, command$3 as f, command$2 as g, command$1 as h, instrument as i, jobCommandPlan as j, logFormat as l };
1948
+ export { CWD as C, LOGGER as L, SCHEDULER as S, commandName as a, command as b, capitalize as c, ConfigService as d, cliParser as e, command$5 as f, command$4 as g, command$3 as h, command$2 as i, command$1 as j, instrument as k, logFormat as l, printJson as p, trace as t, withPlan as w };
1977
1949
  //# sourceMappingURL=main.js.map