@webmate-studio/cli 0.3.62 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -0
- package/bin/wm.mjs +208 -0
- package/package.json +5 -1
- package/src/commands/build.js +335 -0
- package/src/commands/clone.js +414 -0
- package/src/commands/components.js +101 -0
- package/src/commands/core.js +1039 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/install.js +312 -0
- package/src/commands/login.js +158 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/projects.js +91 -0
- package/src/commands/pull.js +192 -0
- package/src/commands/push.js +231 -0
- package/src/commands/reset.js +118 -0
- package/src/commands/status.js +118 -0
- package/src/commands/versions.js +130 -0
- package/src/commands/whoami.js +64 -0
- package/src/utils/api-client.js +131 -0
- package/src/utils/auth-resolver.js +145 -0
- package/src/utils/auth-storage.js +104 -0
- package/src/utils/component-files.js +195 -0
- package/src/utils/device-flow.js +111 -0
- package/src/utils/git-snapshot.js +63 -0
- package/src/utils/tenant-api.js +103 -0
- package/src/utils/webmate-meta.js +75 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wm build` — local dry-run of the component build pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Calls the same `buildComponent()` function the cloud build-service uses,
|
|
5
|
+
* so a local pass should produce the same artefacts as a cloud build for
|
|
6
|
+
* 95% of cases. Reports build errors with full esbuild output + a heuristic
|
|
7
|
+
* hint about the likely cause, optionally writes the produced HTML / CSS /
|
|
8
|
+
* island bundles to disk for inspection.
|
|
9
|
+
*
|
|
10
|
+
* Does NOT push, upload, or talk to the CMS. Pure local validation.
|
|
11
|
+
*
|
|
12
|
+
* Typical workflow when a `wm push` fails with a cryptic build error:
|
|
13
|
+
* 1. `wm build` in the same directory → see the full error locally
|
|
14
|
+
* 2. Fix the cause
|
|
15
|
+
* 3. `wm build` again until green
|
|
16
|
+
* 4. `wm push`
|
|
17
|
+
*
|
|
18
|
+
* The build runs in a tmp dir inside the OS temp; cleanup is automatic
|
|
19
|
+
* unless --keep is passed (debug).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'fs';
|
|
23
|
+
import { basename, resolve, join, dirname } from 'path';
|
|
24
|
+
import { performance } from 'node:perf_hooks';
|
|
25
|
+
import pc from 'picocolors';
|
|
26
|
+
import { buildComponent } from '@webmate-studio/builder/build-service.js';
|
|
27
|
+
import { readComponentFiles } from '../utils/component-files.js';
|
|
28
|
+
|
|
29
|
+
const KB = 1024;
|
|
30
|
+
|
|
31
|
+
function resolveComponentDir(arg) {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
if (!arg) return cwd;
|
|
34
|
+
return resolve(cwd, arg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Take the {relPath: content-or-base64-string} map produced by
|
|
39
|
+
* readComponentFiles and shape it into the `buildComponent()` payload.
|
|
40
|
+
* Mirrors the transformation in webmate-studio's component-sync-processor.js
|
|
41
|
+
* — keep those two in sync.
|
|
42
|
+
*/
|
|
43
|
+
function buildPayloadFromFiles(files, componentName) {
|
|
44
|
+
let componentMetadata = null;
|
|
45
|
+
let html = null;
|
|
46
|
+
let packageJson = null;
|
|
47
|
+
const islands = [];
|
|
48
|
+
const assets = [];
|
|
49
|
+
|
|
50
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
51
|
+
if (relPath === 'component.json') {
|
|
52
|
+
try {
|
|
53
|
+
componentMetadata = JSON.parse(content);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`component.json is not valid JSON: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (relPath === 'component.html') {
|
|
60
|
+
html = content;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (relPath === 'package.json') {
|
|
64
|
+
try {
|
|
65
|
+
packageJson = JSON.parse(content);
|
|
66
|
+
} catch {
|
|
67
|
+
// Non-fatal: tolerate broken package.json by passing null —
|
|
68
|
+
// build-service skips npm install when absent.
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (relPath.startsWith('islands/')) {
|
|
74
|
+
const fileName = relPath.slice('islands/'.length);
|
|
75
|
+
// Islands are always text — base64-encoded svelte/js files
|
|
76
|
+
// wouldn't make sense. Strip the prefix defensively.
|
|
77
|
+
const text = content.startsWith('base64:')
|
|
78
|
+
? Buffer.from(content.slice(7), 'base64').toString('utf-8')
|
|
79
|
+
: content;
|
|
80
|
+
islands.push({ file: fileName, content: text });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (relPath.startsWith('assets/')) {
|
|
85
|
+
const fileName = relPath.slice('assets/'.length);
|
|
86
|
+
if (content.startsWith('base64:')) {
|
|
87
|
+
assets.push({ file: fileName, content: content.slice(7), encoding: 'base64' });
|
|
88
|
+
} else {
|
|
89
|
+
assets.push({ file: fileName, content, encoding: 'utf-8' });
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Everything else (README, .webmate.json, etc.) is intentionally
|
|
95
|
+
// excluded from the build payload — neither the build-service nor
|
|
96
|
+
// the runtime cares about it.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!componentMetadata) {
|
|
100
|
+
throw new Error('component.json is missing — required for building');
|
|
101
|
+
}
|
|
102
|
+
if (!componentMetadata.id) {
|
|
103
|
+
throw new Error('component.json is missing the "id" field — required (UUID)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
componentName,
|
|
108
|
+
packageJson,
|
|
109
|
+
islands,
|
|
110
|
+
assets,
|
|
111
|
+
html,
|
|
112
|
+
componentMetadata
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Try to recognise common build failures and append a short hint about the
|
|
118
|
+
* most likely cause. Returns null when nothing applies — we never emit a
|
|
119
|
+
* misleading guess.
|
|
120
|
+
*/
|
|
121
|
+
function diagnoseError(message) {
|
|
122
|
+
if (!message) return null;
|
|
123
|
+
const m = String(message);
|
|
124
|
+
|
|
125
|
+
if (/Tag name must be lowercase and hyphenated/i.test(m)) {
|
|
126
|
+
return [
|
|
127
|
+
'Likely cause: a Svelte island in a subfolder (islands/Foo/Bar.svelte)',
|
|
128
|
+
'produced an invalid custom-element tag. The build-service should',
|
|
129
|
+
'strip the directory prefix; this was fixed in @webmate-studio/builder',
|
|
130
|
+
'>= 0.2.185. Run `npm ls -g @webmate-studio/cli` to check the version.'
|
|
131
|
+
].join('\n ');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (/ENOENT.*islands\/.+\//i.test(m)) {
|
|
135
|
+
return [
|
|
136
|
+
'Likely cause: an island file in a subfolder failed to write because',
|
|
137
|
+
'the parent directory was not created first. Fixed in',
|
|
138
|
+
'@webmate-studio/builder >= 0.2.185 (mkdir parent dirs).'
|
|
139
|
+
].join('\n ');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (/Could not resolve "([^"]+)"/i.test(m)) {
|
|
143
|
+
const match = m.match(/Could not resolve "([^"]+)"/i);
|
|
144
|
+
const dep = match?.[1];
|
|
145
|
+
return [
|
|
146
|
+
`Likely cause: missing dependency "${dep}". Add it to the component's`,
|
|
147
|
+
'package.json so the build-service installs it in the build sandbox.'
|
|
148
|
+
].join('\n ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/Unexpected token|Expected/i.test(m) && /esbuild|svelte/i.test(m)) {
|
|
152
|
+
return 'Likely cause: syntax error in island source. The location/line is shown above.';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (/customElement.*invalid|svelte_options_invalid/i.test(m)) {
|
|
156
|
+
return 'Likely cause: <svelte:options customElement={...}> tag name violates web-components rules (must be lowercase + contain at least one hyphen).';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatBytes(n) {
|
|
163
|
+
if (n < KB) return `${n} B`;
|
|
164
|
+
return `${(n / KB).toFixed(1)} KB`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function printSuccess(result, durationMs, options = {}) {
|
|
168
|
+
const islands = result.bundledIslands ?? [];
|
|
169
|
+
const cssBytes = (result.css ?? '').length;
|
|
170
|
+
const htmlBytes = (result.html ?? '').length;
|
|
171
|
+
const totalIslandBytes = islands.reduce((s, i) => s + (i.size ?? i.content?.length ?? 0), 0);
|
|
172
|
+
const total = htmlBytes + cssBytes + totalIslandBytes;
|
|
173
|
+
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(pc.green('✓ Build OK') + pc.dim(` ${durationMs.toFixed(0)} ms`));
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(pc.dim(' HTML ') + formatBytes(htmlBytes));
|
|
178
|
+
console.log(pc.dim(' CSS ') + formatBytes(cssBytes));
|
|
179
|
+
console.log(pc.dim(' Islands ') + `${islands.length}`);
|
|
180
|
+
|
|
181
|
+
if (islands.length > 0) {
|
|
182
|
+
const showAll = options.verbose || islands.length <= 6;
|
|
183
|
+
const head = showAll ? islands : islands.slice(0, 5);
|
|
184
|
+
for (const i of head) {
|
|
185
|
+
const size = i.size ?? i.content?.length ?? 0;
|
|
186
|
+
console.log(pc.dim(' ' + i.file.padEnd(38)) + formatBytes(size));
|
|
187
|
+
}
|
|
188
|
+
if (!showAll) {
|
|
189
|
+
console.log(pc.dim(` … ${islands.length - 5} more (use --verbose for full list)`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(pc.dim(' Total bundle: ') + pc.bold(formatBytes(total)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function printFailure(err, durationMs) {
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(pc.red('✖ Build failed') + pc.dim(` ${durationMs.toFixed(0)} ms`));
|
|
199
|
+
console.log();
|
|
200
|
+
const msg = err?.message ?? String(err);
|
|
201
|
+
// The error message often includes a multi-line esbuild block — indent
|
|
202
|
+
// the whole thing two spaces so it visually nests under the heading.
|
|
203
|
+
const indented = msg.split('\n').map((l) => ' ' + l).join('\n');
|
|
204
|
+
console.log(indented);
|
|
205
|
+
|
|
206
|
+
const hint = diagnoseError(msg);
|
|
207
|
+
if (hint) {
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(pc.yellow(' Hint:'));
|
|
210
|
+
console.log(pc.yellow(' ' + hint));
|
|
211
|
+
}
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Persist build artefacts so the developer can inspect them with their
|
|
217
|
+
* editor. Layout matches what the cloud build-service writes to R2.
|
|
218
|
+
*/
|
|
219
|
+
function writeArtefacts(outDir, result) {
|
|
220
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
221
|
+
mkdirSync(outDir, { recursive: true });
|
|
222
|
+
if (result.html != null) {
|
|
223
|
+
writeFileSync(join(outDir, 'component.html'), result.html);
|
|
224
|
+
}
|
|
225
|
+
if (result.css != null) {
|
|
226
|
+
writeFileSync(join(outDir, 'component.css'), result.css);
|
|
227
|
+
}
|
|
228
|
+
const islands = result.bundledIslands ?? [];
|
|
229
|
+
if (islands.length > 0) {
|
|
230
|
+
const islandsDir = join(outDir, 'islands');
|
|
231
|
+
mkdirSync(islandsDir, { recursive: true });
|
|
232
|
+
for (const i of islands) {
|
|
233
|
+
const file = i.file;
|
|
234
|
+
const target = join(islandsDir, file);
|
|
235
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
236
|
+
writeFileSync(target, i.content);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function buildCommand(componentArg, options = {}) {
|
|
242
|
+
const rootDir = resolveComponentDir(componentArg);
|
|
243
|
+
|
|
244
|
+
if (!existsSync(rootDir)) {
|
|
245
|
+
console.error(pc.red(`✖ Directory not found: ${rootDir}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const stat = statSync(rootDir);
|
|
249
|
+
if (!stat.isDirectory()) {
|
|
250
|
+
console.error(pc.red(`✖ Not a directory: ${rootDir}`));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const componentName = basename(rootDir);
|
|
255
|
+
console.log(pc.dim(`Building ${pc.cyan(componentName)} from ${rootDir}`));
|
|
256
|
+
|
|
257
|
+
let files;
|
|
258
|
+
try {
|
|
259
|
+
files = readComponentFiles(rootDir).files;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(pc.red(`✖ Failed to read component files: ${err.message}`));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let payload;
|
|
266
|
+
try {
|
|
267
|
+
payload = buildPayloadFromFiles(files, componentName);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(pc.red(`✖ ${err.message}`));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// The buildComponent function logs progress to stdout/stderr via
|
|
274
|
+
// console.log/error. That noise drowns out our clean summary on a
|
|
275
|
+
// successful build. Capture it and replay only when --verbose or on
|
|
276
|
+
// failure (so users still see what crashed).
|
|
277
|
+
const captured = [];
|
|
278
|
+
const origLog = console.log;
|
|
279
|
+
const origInfo = console.info;
|
|
280
|
+
const origErr = console.error;
|
|
281
|
+
const sink = (...args) => {
|
|
282
|
+
captured.push(args.join(' '));
|
|
283
|
+
};
|
|
284
|
+
if (!options.verbose) {
|
|
285
|
+
console.log = sink;
|
|
286
|
+
console.info = sink;
|
|
287
|
+
console.error = sink;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const t0 = performance.now();
|
|
291
|
+
let result;
|
|
292
|
+
let buildError = null;
|
|
293
|
+
try {
|
|
294
|
+
result = await buildComponent(payload);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
buildError = err;
|
|
297
|
+
}
|
|
298
|
+
const duration = performance.now() - t0;
|
|
299
|
+
|
|
300
|
+
if (!options.verbose) {
|
|
301
|
+
console.log = origLog;
|
|
302
|
+
console.info = origInfo;
|
|
303
|
+
console.error = origErr;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (buildError) {
|
|
307
|
+
// On failure, the captured internal logs are exactly the breadcrumb
|
|
308
|
+
// trail the user needs — replay them right above our pretty error.
|
|
309
|
+
if (!options.verbose && captured.length > 0) {
|
|
310
|
+
console.log(pc.dim('--- build log ---'));
|
|
311
|
+
for (const line of captured) console.log(pc.dim(line));
|
|
312
|
+
console.log(pc.dim('--- end build log ---'));
|
|
313
|
+
}
|
|
314
|
+
printFailure(buildError, duration);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!result || result.success === false) {
|
|
319
|
+
printFailure(new Error(result?.error ?? 'Unknown build failure'), duration);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
printSuccess(result, duration, options);
|
|
324
|
+
|
|
325
|
+
if (options.out) {
|
|
326
|
+
const outDir = resolve(process.cwd(), options.out);
|
|
327
|
+
try {
|
|
328
|
+
writeArtefacts(outDir, result);
|
|
329
|
+
console.log();
|
|
330
|
+
console.log(pc.dim(' Artefacts written to ') + pc.cyan(outDir));
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error(pc.yellow(` ⚠ Failed to write artefacts: ${err.message}`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|