@amxts/cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@amxts/cli",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "The amxts command: create a project or a module, add modules, build, deploy, type-check and test",
6
6
  "author": {
7
7
  "name": "kukson777",
package/src/core.mjs CHANGED
@@ -9,14 +9,14 @@
9
9
  //
10
10
  // Creating a project or a module needs no core: they run on their own.
11
11
  // `--local` takes the core from this machine instead of npm (localCore()).
12
- import { spawnSync } from 'node:child_process';
12
+ import { spawn, spawnSync } from 'node:child_process';
13
13
  import { existsSync, readFileSync } from 'node:fs';
14
14
  import { createRequire } from 'node:module';
15
15
  import { dirname, join, resolve } from 'node:path';
16
16
  import process from 'node:process';
17
17
  import { fileURLToPath, pathToFileURL } from 'node:url';
18
18
  import { addArgs, commandLine, detectPackageManager, execAmxts, versionOf } from './pm.mjs';
19
- import { CliError, debug } from './ui.mjs';
19
+ import { CliError, debug, report } from './ui.mjs';
20
20
 
21
21
  /** The command's own package folder: packages/cli in its repository. */
22
22
  export const CLI_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -24,7 +24,7 @@ export const CLI_PACKAGE = readJson(join(CLI_DIR, 'package.json'));
24
24
  export const VERSION = String(CLI_PACKAGE.version);
25
25
 
26
26
  /** The cores this command works with: the range a new project asks for. */
27
- export const CORE_RANGE = '^0.2.0';
27
+ export const CORE_RANGE = '^0.3.0';
28
28
  /** The version of `@amxts/core/cli-api` this command speaks. */
29
29
  export const CLI_API = 5;
30
30
 
@@ -275,19 +275,42 @@ export function needBun(core, what = 'builds plugins') {
275
275
  * @param {string[]} [args]
276
276
  */
277
277
  export function runTask(core, name, args = []) {
278
+ const result = spawnSync(...taskSpawn(core, name, args, { stdio: 'inherit' }));
279
+ if (result.error) throw new CliError(`Bun did not start: ${result.error.message}`, BUN_HINT);
280
+ return result.status ?? 1;
281
+ }
282
+
283
+ /** A task as spawn takes it: the file, its arguments (none when it goes through the shell) and the options. */
284
+ function taskSpawn(core, name, args, options) {
278
285
  const task = core.api.task(name, args);
279
- const env = { ...process.env, ...(debug() ? { AMXTS_DEBUG: '1' } : {}) };
280
- if (task.runtime === 'node') {
281
- return spawnSync(process.execPath, task.args, { stdio: 'inherit', env }).status ?? 1;
282
- }
286
+ const env = { ...process.env, ...(debug() ? { AMXTS_DEBUG: '1' } : {}), ...options.env };
287
+ if (task.runtime === 'node') return [process.execPath, task.args, { ...options, env }];
283
288
  const binary = needBun(core);
284
289
  // A Bun on PATH goes through the shell on Windows, where it may be
285
290
  // bun.cmd; a path with a space is quoted for it.
286
- const result = binary === 'bun' && process.platform === 'win32'
287
- ? spawnSync(commandLine(['bun', ...task.args]), { stdio: 'inherit', shell: true, env })
288
- : spawnSync(binary, task.args, { stdio: 'inherit', env });
289
- if (result.error) throw new CliError(`Bun did not start: ${result.error.message}`, BUN_HINT);
290
- return result.status ?? 1;
291
+ return binary === 'bun' && process.platform === 'win32'
292
+ ? [commandLine(['bun', ...task.args]), { ...options, shell: true, env }]
293
+ : [binary, task.args, { ...options, env }];
294
+ }
295
+
296
+ /**
297
+ * Starts one of the core's tasks and leaves it running: `dev`'s build, which
298
+ * watches. `pipe` takes its output instead of the terminal, with its colours.
299
+ * @param {Core} core
300
+ * @param {string} name
301
+ * @param {string[]} args
302
+ * @param {{ pipe?: boolean }} [options]
303
+ * @returns {import('node:child_process').ChildProcess} the task
304
+ */
305
+ export function startTask(core, name, args, { pipe = false } = {}) {
306
+ const child = pipe
307
+ ? spawn(...taskSpawn(core, name, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { FORCE_COLOR: process.env.NO_COLOR === undefined ? '1' : '0' } }))
308
+ : spawn(...taskSpawn(core, name, args, { stdio: 'inherit' }));
309
+ child.on('error', (error) => {
310
+ report(new CliError(`Bun did not start: ${error.message}`, BUN_HINT));
311
+ process.exit(1);
312
+ });
313
+ return child;
291
314
  }
292
315
 
293
316
  /** Runs a task and stops with its exit code when it fails. */
@@ -0,0 +1,147 @@
1
+ // What the dev panel knows of the build: read off the lines the core's build
2
+ // task prints, the one place that reads them. The words are the core's; a
3
+ // structured feed from the core would replace this file, and the panel would
4
+ // stay as it is.
5
+ //
6
+ // Plugins 3 in plugins/ the header
7
+ // ◇ compiling hello 1/2 a compile starts (no TTY)
8
+ // ✔ hello · 1.2s one is compiled
9
+ // ✖ hello does not compile: ... an error, its lines under it
10
+ // ✔ 12:01:33 plugins/hello.ts changed: deployed, the server reloaded hello (2.1s)
11
+ // ◇ watching plugins - save a plugin and ... the first build is done
12
+
13
+ /** A line without its colours. */
14
+ export function plain(line) {
15
+ // eslint-disable-next-line no-control-regex
16
+ return line.replace(/\x1B\[[\d;?]*[a-z]|\x1B\]8;[^\x1B]*\x1B\\/gi, '').replace(/\r/g, '');
17
+ }
18
+
19
+ /** The marks a line of the build starts with: a line without one belongs to the error above it. */
20
+ const MARK = /^[✔◇▲✖i] /;
21
+
22
+ /**
23
+ * @typedef {object} Rebuild
24
+ * @property {string[]} files the saved files that started it; none for a start
25
+ * @property {string[]} plugins what it built
26
+ * @property {number | undefined} compile seconds of compiling, the longest one (they compile side by side); undefined when nothing compiled
27
+ * @property {number} total seconds, all of it
28
+ * @property {string} outcome what the deploy did, as the core says it
29
+ * @property {boolean} reloaded whether the server reloaded them all
30
+ * @property {string} at the time it ended, as printed
31
+ * @property {number | undefined} boot seconds before the core counts, for a build the panel started
32
+ */
33
+
34
+ /** Each plugin a line names as not compiling is failed. */
35
+ function markFailed(feed, text) {
36
+ for (const [, name] of text.matchAll(/(\S+) does not compile:/g)) feed.plugins.set(name, 'failed');
37
+ }
38
+
39
+ /** The lines the feed reads: what each is, and what it changes. */
40
+ const RULES = [
41
+ [/^ {2}(Core|Modules|Server|Plugins) +(\S.*)$/, 'header', (feed, [, row, value]) => {
42
+ feed.header[row] = value;
43
+ if (row === 'Plugins') feed.count = Number.parseInt(value, 10) || 0;
44
+ }],
45
+ [/^◇ compiling (.+?) (\d+)\/(\d+)$/, 'compiling', (feed, [, what, , total]) => {
46
+ feed.building ??= { what, done: 0, total: 0, since: Date.now() };
47
+ feed.building.what = what;
48
+ feed.building.total = Number(total);
49
+ }],
50
+ [/^✔ (\S+) · (?:(\d+(?:\.\d+)?)s|prebuilt|unchanged)$/, 'compiled', (feed, [, name, time]) => {
51
+ if (!time) return;
52
+ feed.times.set(name, Number(time));
53
+ feed.expected = Number(time);
54
+ if (feed.building) feed.building.done++;
55
+ }],
56
+ [/^✖ (.*)$/, 'error', (feed, [, text]) => {
57
+ feed.error = [text];
58
+ feed.inError = true;
59
+ feed.broken = true;
60
+ feed.building = null;
61
+ markFailed(feed, text);
62
+ }],
63
+ [/^✔ (?:(\d{1,2}:\d{2}:\d{2}(?: ?[AP]M)?) )?(?:(.+?) changed: )?(.+) \((\d+(?:\.\d+)?)s\)$/, 'rebuilt', (feed, [, at = '', files = '', outcome, total]) => {
64
+ const named = /reloaded (.+)$/.exec(outcome)?.[1]
65
+ ?? /^(?:deployed|built|rebuilt) (.+?)(?: into .*| - .*)?$/.exec(outcome)?.[1]
66
+ ?? /^(.+?) not deployed/.exec(outcome)?.[1] ?? '';
67
+ const plugins = named.split(', ').filter(Boolean);
68
+ for (const name of plugins) feed.plugins.set(name, 'built');
69
+ const times = [...feed.times.values()];
70
+ feed.last = {
71
+ files: files ? files.split(', ') : [],
72
+ plugins,
73
+ compile: times.length ? Math.max(...times) : undefined,
74
+ total: Number(total),
75
+ outcome,
76
+ reloaded: outcome.startsWith('deployed, the server reloaded '),
77
+ at,
78
+ // A build the panel started: the time before the core's own count - Bun, the project, the server asked.
79
+ boot: feed.started === null ? undefined : Math.max(0, (Date.now() - feed.started) / 1000 - Number(total)),
80
+ };
81
+ feed.started = null;
82
+ feed.times.clear();
83
+ feed.broken = false;
84
+ feed.building = null;
85
+ }],
86
+ [/^◇ watching /, 'watching', (feed) => {
87
+ feed.watching = true;
88
+ }],
89
+ ];
90
+
91
+ /** The parser's state, which the panel shows. */
92
+ export function createFeed() {
93
+ const feed = {
94
+ /** @type {Record<string, string>} the header's rows: Core, Modules, Server, Plugins */
95
+ header: {},
96
+ /** How many plugins the project has, from the header. */
97
+ count: 0,
98
+ /** @type {Map<string, 'built' | 'failed'>} each plugin a build has named */
99
+ plugins: new Map(),
100
+ /** @type {{ what: string, done: number, total: number, since: number } | null} the build that compiles now: what started last, how many of how many are done, since when (ms) */
101
+ building: null,
102
+ /** Seconds the last plugin took to compile: what the next one is expected to take. */
103
+ expected: 10,
104
+ /** @type {number | null} when the panel started the build that runs (ms), till it ends; null for one a save started */
105
+ started: null,
106
+ /** @type {Rebuild | null} */
107
+ last: null,
108
+ /** @type {string[]} the last error's lines */
109
+ error: [],
110
+ watching: false,
111
+ /** @type {Map<string, number>} seconds of each plugin compiled since the last summary */
112
+ times: new Map(),
113
+ /** Whether the lines that follow belong to the last error. */
114
+ inError: false,
115
+ /** Whether the last build failed: the server keeps the one before. */
116
+ broken: false,
117
+
118
+ /**
119
+ * Takes one line of the build's output.
120
+ * @returns {string | null} what it was - `header`, `compiling`, `compiled`, `error`, `rebuilt`, `watching` - or null for anything else
121
+ */
122
+ push(raw) {
123
+ const line = plain(raw);
124
+ if (feed.inError && line && !MARK.test(line)) {
125
+ feed.error.push(line);
126
+ markFailed(feed, line);
127
+ return 'error';
128
+ }
129
+ feed.inError = false;
130
+ for (const [pattern, kind, take] of RULES) {
131
+ const match = pattern.exec(line);
132
+ if (match) {
133
+ take(feed, match);
134
+ return kind;
135
+ }
136
+ }
137
+ return null;
138
+ },
139
+
140
+ /** How many plugins are built and how many failed. */
141
+ counts() {
142
+ const states = [...feed.plugins.values()];
143
+ return { built: states.filter(each => each === 'built').length, failed: states.filter(each => each === 'failed').length };
144
+ },
145
+ };
146
+ return feed;
147
+ }