@bytefaceinc/web-lang 0.1.1
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/LICENSE +21 -0
- package/README.md +277 -0
- package/bin/web.js +10 -0
- package/compiler.js +4602 -0
- package/docs/cli.md +657 -0
- package/docs/compiler.md +1433 -0
- package/docs/error-handling.md +863 -0
- package/docs/getting-started.md +805 -0
- package/docs/language-guide.md +945 -0
- package/lib/cli/commands/compile.js +127 -0
- package/lib/cli/commands/init.js +172 -0
- package/lib/cli/commands/screenshot.js +257 -0
- package/lib/cli/commands/watch.js +458 -0
- package/lib/cli/compile-service.js +19 -0
- package/lib/cli/compile-worker.js +32 -0
- package/lib/cli/compiler-runner.js +37 -0
- package/lib/cli/index.js +154 -0
- package/lib/cli/init-service.js +204 -0
- package/lib/cli/screenshot-artifacts.js +81 -0
- package/lib/cli/screenshot-service.js +153 -0
- package/lib/cli/shared.js +261 -0
- package/lib/cli/targets.js +199 -0
- package/lib/cli/watch-settings.js +37 -0
- package/package.json +50 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { compileInputSource } = require('../compile-service');
|
|
6
|
+
const { DEFAULT_SCREENSHOT_FORMAT, DEFAULT_VIEWPORT_HEIGHT, DEFAULT_VIEWPORT_WIDTH, renderScreenshotArtifact } = require('../screenshot-service');
|
|
7
|
+
const { getWatchRuntimeSettings } = require('../watch-settings');
|
|
8
|
+
const { resolveTargets } = require('../targets');
|
|
9
|
+
const { SCREENSHOT_DIRECTORY_NAME } = require('../screenshot-artifacts');
|
|
10
|
+
const {
|
|
11
|
+
bold,
|
|
12
|
+
color,
|
|
13
|
+
createSpinner,
|
|
14
|
+
dim,
|
|
15
|
+
displayPath,
|
|
16
|
+
formatBytes,
|
|
17
|
+
formatCompileStatsLine,
|
|
18
|
+
formatDuration,
|
|
19
|
+
formatErrorMessage,
|
|
20
|
+
formatLongDuration,
|
|
21
|
+
formatLocalTimestamp,
|
|
22
|
+
indentMultiline,
|
|
23
|
+
padLabel,
|
|
24
|
+
printCommandHeader,
|
|
25
|
+
printLine,
|
|
26
|
+
printTargetIssues,
|
|
27
|
+
} = require('../shared');
|
|
28
|
+
|
|
29
|
+
const WATCH_SCREENSHOT_FLAGS = new Set(['-s', '-screenshot', '--screenshot']);
|
|
30
|
+
|
|
31
|
+
async function executeWatchCommand({ args }) {
|
|
32
|
+
let parsedArgs;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
parsedArgs = parseWatchArgs(args);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
printLine(color(`[x] ${formatErrorMessage(error)}`, 'red'));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resolution = resolveTargets([parsedArgs.target], process.cwd());
|
|
43
|
+
|
|
44
|
+
printTargetIssues(resolution.errors);
|
|
45
|
+
|
|
46
|
+
if (resolution.files.length === 0) {
|
|
47
|
+
printLine(color('[x] No WEB source found to watch.', 'red'));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (resolution.files.length !== 1) {
|
|
53
|
+
printLine(color(`[x] The watch command expects exactly one WEB source, but "${parsedArgs.target}" resolved to ${resolution.files.length}.`, 'red'));
|
|
54
|
+
printLine(dim(' Use a single file path or bare file name when starting a watch session.'));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const session = new WatchSession({
|
|
60
|
+
inputPath: resolution.files[0],
|
|
61
|
+
screenshotEnabled: parsedArgs.screenshotEnabled,
|
|
62
|
+
workingDirectory: process.cwd(),
|
|
63
|
+
settings: getWatchRuntimeSettings(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await session.start();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printWatchHelp() {
|
|
70
|
+
printLine(color(bold('Watch Command'), 'blue'));
|
|
71
|
+
printLine('Watch one `.web` source, recompile it when the file changes, and optionally capture periodic screenshots.');
|
|
72
|
+
printLine('');
|
|
73
|
+
printLine(bold('Usage'));
|
|
74
|
+
printLine(' web watch <file.web> [-screenshot|-s]');
|
|
75
|
+
printLine(' web watch <file> [-screenshot|-s]');
|
|
76
|
+
printLine('');
|
|
77
|
+
printLine(bold('Examples'));
|
|
78
|
+
printLine(' web watch home.web');
|
|
79
|
+
printLine(' web watch home');
|
|
80
|
+
printLine(' web watch home.web -s');
|
|
81
|
+
printLine(' web watch home.web -screenshot');
|
|
82
|
+
printLine('');
|
|
83
|
+
printLine('Defaults');
|
|
84
|
+
printLine(' Recompile on source changes');
|
|
85
|
+
printLine(' Idle shutdown: after 1 hour with no recent source activity');
|
|
86
|
+
printLine(' Screenshot cadence with `-s`: initial baseline capture, then every 5 minutes');
|
|
87
|
+
printLine(` Screenshot directory: ./${SCREENSHOT_DIRECTORY_NAME}`);
|
|
88
|
+
printLine('');
|
|
89
|
+
printLine('The watch session prints a verbose exit summary when it stops due to inactivity.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseWatchArgs(args) {
|
|
93
|
+
if (args.length === 0) {
|
|
94
|
+
throw new Error('The watch command expects one WEB source. Example: web watch home.web -s');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const targets = [];
|
|
98
|
+
let screenshotEnabled = false;
|
|
99
|
+
|
|
100
|
+
for (const token of args) {
|
|
101
|
+
if (WATCH_SCREENSHOT_FLAGS.has(token)) {
|
|
102
|
+
screenshotEnabled = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (token.startsWith('-')) {
|
|
107
|
+
throw new Error(`Unknown watch option "${token}". Supported flags are -screenshot and -s.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
targets.push(token);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (targets.length === 0) {
|
|
114
|
+
throw new Error('The watch command expects one WEB source. Example: web watch home.web -s');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (targets.length > 1) {
|
|
118
|
+
throw new Error('The watch command currently supports watching exactly one WEB source at a time.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
target: targets[0],
|
|
123
|
+
screenshotEnabled,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class WatchSession {
|
|
128
|
+
constructor(options) {
|
|
129
|
+
this.inputPath = path.resolve(options.inputPath);
|
|
130
|
+
this.workingDirectory = path.resolve(options.workingDirectory || process.cwd());
|
|
131
|
+
this.screenshotEnabled = Boolean(options.screenshotEnabled);
|
|
132
|
+
this.settings = options.settings;
|
|
133
|
+
|
|
134
|
+
this.lastActivityAt = Date.now();
|
|
135
|
+
this.startedAt = Date.now();
|
|
136
|
+
this.lastSuccessfulCompile = null;
|
|
137
|
+
this.lastFailedCompile = null;
|
|
138
|
+
this.lastScreenshotAt = null;
|
|
139
|
+
this.buildCount = 0;
|
|
140
|
+
this.failedBuildCount = 0;
|
|
141
|
+
this.screenshotCount = 0;
|
|
142
|
+
this.pendingBuildReason = null;
|
|
143
|
+
this.pendingPeriodicScreenshot = false;
|
|
144
|
+
this.needsInitialScreenshot = this.screenshotEnabled;
|
|
145
|
+
this.isCompiling = false;
|
|
146
|
+
this.isCapturingScreenshot = false;
|
|
147
|
+
this.isShuttingDown = false;
|
|
148
|
+
this.lastKnownFingerprint = readFileFingerprint(this.inputPath);
|
|
149
|
+
this.debounceTimer = null;
|
|
150
|
+
this.idleTimer = null;
|
|
151
|
+
this.screenshotTimer = null;
|
|
152
|
+
this.stopReason = null;
|
|
153
|
+
this.watchHandler = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async start() {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
this.resolveStop = resolve;
|
|
159
|
+
|
|
160
|
+
printCommandHeader(`Watching ${displayPath(this.inputPath)} for changes from ${displayPath(this.workingDirectory)}`);
|
|
161
|
+
printLine(dim('Outputs will update next to the source file.'));
|
|
162
|
+
printLine(dim(`Idle timeout is ${formatLongDuration(this.settings.idleTimeoutMs)} without recent source activity.`));
|
|
163
|
+
|
|
164
|
+
if (this.screenshotEnabled) {
|
|
165
|
+
printLine(dim(`Screenshots are enabled: one baseline capture, then every ${formatLongDuration(this.settings.screenshotIntervalMs)} into ${SCREENSHOT_DIRECTORY_NAME}.`));
|
|
166
|
+
} else {
|
|
167
|
+
printLine(dim('Screenshots are disabled for this watch session. Use `-s` or `-screenshot` to turn them on.'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
printLine('');
|
|
171
|
+
|
|
172
|
+
this.startWatchingSourceFile();
|
|
173
|
+
this.resetIdleTimer();
|
|
174
|
+
|
|
175
|
+
if (this.screenshotEnabled) {
|
|
176
|
+
this.startScreenshotTimer();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void this.runBuild('initial compile');
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
startWatchingSourceFile() {
|
|
184
|
+
this.watchHandler = (currentStats, previousStats) => {
|
|
185
|
+
const nextFingerprint = normalizeStatsFingerprint(currentStats);
|
|
186
|
+
|
|
187
|
+
if (!didFileFingerprintChange(this.lastKnownFingerprint, nextFingerprint)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.lastKnownFingerprint = nextFingerprint;
|
|
192
|
+
this.noteSourceActivity('source change detected');
|
|
193
|
+
this.scheduleBuild('source change detected');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
fs.watchFile(this.inputPath, { interval: this.settings.pollIntervalMs }, this.watchHandler);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
startScreenshotTimer() {
|
|
200
|
+
this.screenshotTimer = setInterval(() => {
|
|
201
|
+
this.pendingPeriodicScreenshot = true;
|
|
202
|
+
void this.flushPendingWork();
|
|
203
|
+
}, this.settings.screenshotIntervalMs);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
noteSourceActivity(reason) {
|
|
207
|
+
this.lastActivityAt = Date.now();
|
|
208
|
+
this.resetIdleTimer();
|
|
209
|
+
printLine(dim(`[watch] ${reason} at ${formatLocalTimestamp(this.lastActivityAt)}.`));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resetIdleTimer() {
|
|
213
|
+
if (this.idleTimer) {
|
|
214
|
+
clearTimeout(this.idleTimer);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.idleTimer = setTimeout(() => {
|
|
218
|
+
void this.stopDueToInactivity();
|
|
219
|
+
}, this.settings.idleTimeoutMs);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
scheduleBuild(reason) {
|
|
223
|
+
this.pendingBuildReason = reason;
|
|
224
|
+
|
|
225
|
+
if (this.debounceTimer) {
|
|
226
|
+
clearTimeout(this.debounceTimer);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.debounceTimer = setTimeout(() => {
|
|
230
|
+
this.debounceTimer = null;
|
|
231
|
+
void this.flushPendingWork();
|
|
232
|
+
}, this.settings.debounceMs);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async flushPendingWork() {
|
|
236
|
+
if (this.isShuttingDown) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.isCompiling || this.isCapturingScreenshot) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.pendingBuildReason) {
|
|
245
|
+
const buildReason = this.pendingBuildReason;
|
|
246
|
+
this.pendingBuildReason = null;
|
|
247
|
+
await this.runBuild(buildReason);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.pendingPeriodicScreenshot) {
|
|
252
|
+
this.pendingPeriodicScreenshot = false;
|
|
253
|
+
await this.capturePeriodicScreenshot();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async runBuild(reason) {
|
|
258
|
+
if (this.isShuttingDown) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.isCompiling = true;
|
|
263
|
+
const spinner = createSpinner(`[watch] Compiling ${displayPath(this.inputPath)} (${reason})`);
|
|
264
|
+
spinner.start();
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const compileResult = await compileInputSource(this.inputPath);
|
|
268
|
+
this.buildCount += 1;
|
|
269
|
+
this.lastSuccessfulCompile = {
|
|
270
|
+
at: Date.now(),
|
|
271
|
+
result: compileResult,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
spinner.succeed(
|
|
275
|
+
`[ok] ${displayPath(compileResult.inputPath)} -> ${displayPath(compileResult.htmlPath)} + ${displayPath(compileResult.cssPath)} (${formatDuration(compileResult.durationMs)})`
|
|
276
|
+
);
|
|
277
|
+
printLine(dim(` ${formatCompileStatsLine(compileResult)}`));
|
|
278
|
+
|
|
279
|
+
if (this.screenshotEnabled && this.needsInitialScreenshot) {
|
|
280
|
+
this.needsInitialScreenshot = false;
|
|
281
|
+
await this.captureScreenshot('Initial screenshot');
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.failedBuildCount += 1;
|
|
285
|
+
this.lastFailedCompile = {
|
|
286
|
+
at: Date.now(),
|
|
287
|
+
message: formatErrorMessage(error),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
spinner.fail(`[x] ${displayPath(this.inputPath)} failed`);
|
|
291
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
292
|
+
} finally {
|
|
293
|
+
this.isCompiling = false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await this.flushPendingWork();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async capturePeriodicScreenshot() {
|
|
300
|
+
if (this.isShuttingDown || !this.screenshotEnabled) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!this.lastSuccessfulCompile) {
|
|
305
|
+
printLine(color('[!] Skipping scheduled screenshot because no successful compile is available yet.', 'yellow'));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await this.captureScreenshot('Scheduled screenshot');
|
|
310
|
+
await this.flushPendingWork();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async captureScreenshot(label) {
|
|
314
|
+
if (!this.lastSuccessfulCompile || this.isShuttingDown) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.isCapturingScreenshot = true;
|
|
319
|
+
const spinner = createSpinner(`[watch] ${label} for ${displayPath(this.inputPath)}`);
|
|
320
|
+
spinner.start();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const screenshotResult = await renderScreenshotArtifact({
|
|
324
|
+
inputPath: this.inputPath,
|
|
325
|
+
htmlPath: this.lastSuccessfulCompile.result.htmlPath,
|
|
326
|
+
workingDirectory: this.workingDirectory,
|
|
327
|
+
format: DEFAULT_SCREENSHOT_FORMAT,
|
|
328
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
329
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT,
|
|
330
|
+
fullPage: true,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this.screenshotCount += 1;
|
|
334
|
+
this.lastScreenshotAt = Date.now();
|
|
335
|
+
|
|
336
|
+
spinner.succeed(`[ok] ${displayPath(screenshotResult.outputPath)} (${formatDuration(screenshotResult.durationMs)})`);
|
|
337
|
+
printLine(dim(` ${screenshotResult.format.toUpperCase()} | full page ${screenshotResult.pageWidth} x ${screenshotResult.pageHeight} | @ 1x | ${formatBytes(screenshotResult.imageBytes)} out`));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
spinner.fail('[x] Watch screenshot failed');
|
|
340
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
341
|
+
} finally {
|
|
342
|
+
this.isCapturingScreenshot = false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async stopDueToInactivity() {
|
|
347
|
+
if (this.isShuttingDown) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.isShuttingDown = true;
|
|
352
|
+
this.stopReason = 'Stopping the watch process due to no recent source activity.';
|
|
353
|
+
|
|
354
|
+
this.cleanup();
|
|
355
|
+
this.printExitSummary();
|
|
356
|
+
|
|
357
|
+
if (this.resolveStop) {
|
|
358
|
+
this.resolveStop();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
process.exitCode = this.failedBuildCount > 0 && this.buildCount === 0 ? 1 : 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
cleanup() {
|
|
365
|
+
if (this.debounceTimer) {
|
|
366
|
+
clearTimeout(this.debounceTimer);
|
|
367
|
+
this.debounceTimer = null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (this.idleTimer) {
|
|
371
|
+
clearTimeout(this.idleTimer);
|
|
372
|
+
this.idleTimer = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (this.screenshotTimer) {
|
|
376
|
+
clearInterval(this.screenshotTimer);
|
|
377
|
+
this.screenshotTimer = null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (this.watchHandler) {
|
|
381
|
+
fs.unwatchFile(this.inputPath, this.watchHandler);
|
|
382
|
+
this.watchHandler = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printExitSummary() {
|
|
387
|
+
printLine('');
|
|
388
|
+
printLine(color(bold('Watch Session Ended'), 'yellow'));
|
|
389
|
+
printLine(this.stopReason);
|
|
390
|
+
printLine(`No source file changes were detected for ${formatLongDuration(this.settings.idleTimeoutMs)} while watching ${displayPath(this.inputPath)}.`);
|
|
391
|
+
printLine(`Last observed source activity: ${formatLocalTimestamp(this.lastActivityAt)}.`);
|
|
392
|
+
|
|
393
|
+
if (this.lastSuccessfulCompile) {
|
|
394
|
+
printLine(`Last successful compile: ${formatLocalTimestamp(this.lastSuccessfulCompile.at)}.`);
|
|
395
|
+
} else {
|
|
396
|
+
printLine('Last successful compile: none during this watch session.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (this.lastFailedCompile) {
|
|
400
|
+
printLine(`Last failed compile: ${formatLocalTimestamp(this.lastFailedCompile.at)}.`);
|
|
401
|
+
printLine(dim(indentMultiline(this.lastFailedCompile.message, ' ')));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this.screenshotEnabled) {
|
|
405
|
+
printLine(`Screenshots captured: ${this.screenshotCount}.`);
|
|
406
|
+
|
|
407
|
+
if (this.lastScreenshotAt) {
|
|
408
|
+
printLine(`Last screenshot: ${formatLocalTimestamp(this.lastScreenshotAt)}.`);
|
|
409
|
+
} else {
|
|
410
|
+
printLine('Last screenshot: none captured during this watch session.');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
printLine(`${padLabel('Successful builds')} ${this.buildCount}`);
|
|
415
|
+
printLine(`${padLabel('Failed builds')} ${this.failedBuildCount}`);
|
|
416
|
+
printLine(`${padLabel('Elapsed')} ${formatLongDuration(Date.now() - this.startedAt)}`);
|
|
417
|
+
printLine('Run the watch command again when you are ready to continue watching for updates.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function readFileFingerprint(filePath) {
|
|
422
|
+
try {
|
|
423
|
+
return normalizeStatsFingerprint(fs.statSync(filePath));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return {
|
|
426
|
+
exists: false,
|
|
427
|
+
mtimeMs: 0,
|
|
428
|
+
size: 0,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeStatsFingerprint(stats) {
|
|
434
|
+
if (!stats || typeof stats.mtimeMs !== 'number') {
|
|
435
|
+
return {
|
|
436
|
+
exists: false,
|
|
437
|
+
mtimeMs: 0,
|
|
438
|
+
size: 0,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
exists: true,
|
|
444
|
+
mtimeMs: Number(stats.mtimeMs) || 0,
|
|
445
|
+
size: Number(stats.size) || 0,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function didFileFingerprintChange(previousFingerprint, nextFingerprint) {
|
|
450
|
+
return previousFingerprint.exists !== nextFingerprint.exists
|
|
451
|
+
|| previousFingerprint.mtimeMs !== nextFingerprint.mtimeMs
|
|
452
|
+
|| previousFingerprint.size !== nextFingerprint.size;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
executeWatchCommand,
|
|
457
|
+
printWatchHelp,
|
|
458
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { resolveCompileOutputPaths } = require('../../compiler.js');
|
|
5
|
+
const { compileFileInWorker } = require('./compiler-runner');
|
|
6
|
+
|
|
7
|
+
async function compileInputSource(inputPath, options = {}) {
|
|
8
|
+
const outputPaths = resolveCompileOutputPaths(inputPath, options);
|
|
9
|
+
|
|
10
|
+
return compileFileInWorker(inputPath, {
|
|
11
|
+
htmlPath: outputPaths.htmlPath,
|
|
12
|
+
cssPath: outputPaths.cssPath,
|
|
13
|
+
stylesheetHref: path.basename(outputPaths.cssPath),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
compileInputSource,
|
|
19
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { performance } = require('perf_hooks');
|
|
4
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
5
|
+
const { compileFile } = require('../../compiler.js');
|
|
6
|
+
|
|
7
|
+
const startedAt = performance.now();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const result = compileFile(workerData.inputPath, workerData.options);
|
|
11
|
+
|
|
12
|
+
parentPort.postMessage({
|
|
13
|
+
ok: true,
|
|
14
|
+
result: {
|
|
15
|
+
inputPath: result.inputPath,
|
|
16
|
+
htmlPath: result.htmlPath,
|
|
17
|
+
cssPath: result.cssPath,
|
|
18
|
+
sourceBytes: result.sourceBytes,
|
|
19
|
+
htmlBytes: result.htmlBytes,
|
|
20
|
+
cssBytes: result.cssBytes,
|
|
21
|
+
stats: result.stats,
|
|
22
|
+
durationMs: performance.now() - startedAt,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
parentPort.postMessage({
|
|
27
|
+
ok: false,
|
|
28
|
+
error: {
|
|
29
|
+
message: error instanceof Error ? error.message : String(error),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { Worker } = require('worker_threads');
|
|
5
|
+
|
|
6
|
+
const COMPILE_WORKER_PATH = path.join(__dirname, 'compile-worker.js');
|
|
7
|
+
|
|
8
|
+
function compileFileInWorker(inputPath, options) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const worker = new Worker(COMPILE_WORKER_PATH, {
|
|
11
|
+
workerData: {
|
|
12
|
+
inputPath,
|
|
13
|
+
options,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
worker.once('message', (message) => {
|
|
18
|
+
if (message.ok) {
|
|
19
|
+
resolve(message.result);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
reject(new Error(message.error.message));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
worker.once('error', reject);
|
|
27
|
+
worker.once('exit', (code) => {
|
|
28
|
+
if (code !== 0) {
|
|
29
|
+
reject(new Error(`Compiler worker exited with code ${code}`));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
compileFileInWorker,
|
|
37
|
+
};
|
package/lib/cli/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { executeInitCommand, printInitHelp } = require('./commands/init');
|
|
4
|
+
const { executeCompileCommand, printCompileHelp } = require('./commands/compile');
|
|
5
|
+
const { executeScreenshotCommand, printScreenshotHelp } = require('./commands/screenshot');
|
|
6
|
+
const { executeWatchCommand, printWatchHelp } = require('./commands/watch');
|
|
7
|
+
const { bold, color, printLine, readPackageVersion } = require('./shared');
|
|
8
|
+
|
|
9
|
+
async function main(rawArgs = process.argv.slice(2)) {
|
|
10
|
+
const invocation = parseInvocation(rawArgs);
|
|
11
|
+
|
|
12
|
+
if (invocation.kind === 'version') {
|
|
13
|
+
printLine(readPackageVersion());
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (invocation.kind === 'help') {
|
|
18
|
+
printHelp(invocation.topic);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (invocation.command === 'init') {
|
|
23
|
+
await executeInitCommand({ args: invocation.args });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (invocation.command === 'watch') {
|
|
28
|
+
await executeWatchCommand({ args: invocation.args });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (invocation.command === 'screenshot') {
|
|
33
|
+
await executeScreenshotCommand({ args: invocation.args });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await executeCompileCommand({ args: invocation.args });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseInvocation(args) {
|
|
41
|
+
if (args.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'command',
|
|
44
|
+
command: 'compile',
|
|
45
|
+
args: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [first, ...rest] = args;
|
|
50
|
+
|
|
51
|
+
if (first === '--version' || first === '-v') {
|
|
52
|
+
return {
|
|
53
|
+
kind: 'version',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (first === '--help' || first === '-h') {
|
|
58
|
+
return {
|
|
59
|
+
kind: 'help',
|
|
60
|
+
topic: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (first === 'help') {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'help',
|
|
67
|
+
topic: rest[0] || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (first === 'compile' || first === 'init' || first === 'screenshot' || first === 'watch') {
|
|
72
|
+
const topic = rest.includes('--help') || rest.includes('-h') ? first : null;
|
|
73
|
+
|
|
74
|
+
if (topic) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'help',
|
|
77
|
+
topic,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
kind: 'command',
|
|
83
|
+
command: first,
|
|
84
|
+
args: rest,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
89
|
+
return {
|
|
90
|
+
kind: 'help',
|
|
91
|
+
topic: 'compile',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
kind: 'command',
|
|
97
|
+
command: 'compile',
|
|
98
|
+
args,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printHelp(topic) {
|
|
103
|
+
switch (topic) {
|
|
104
|
+
case 'compile':
|
|
105
|
+
printCompileHelp();
|
|
106
|
+
return;
|
|
107
|
+
case 'init':
|
|
108
|
+
printInitHelp();
|
|
109
|
+
return;
|
|
110
|
+
case 'screenshot':
|
|
111
|
+
printScreenshotHelp();
|
|
112
|
+
return;
|
|
113
|
+
case 'watch':
|
|
114
|
+
printWatchHelp();
|
|
115
|
+
return;
|
|
116
|
+
default:
|
|
117
|
+
printGeneralHelp();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printGeneralHelp() {
|
|
123
|
+
printLine(color(bold('WEB CLI'), 'blue'));
|
|
124
|
+
printLine('Initialize WEB projects, compile `.web` power docs, render screenshots, or keep a watch session running for live rebuilds.');
|
|
125
|
+
printLine('');
|
|
126
|
+
printLine(bold('Usage'));
|
|
127
|
+
printLine(' web init [directory]');
|
|
128
|
+
printLine(' web <file.web>');
|
|
129
|
+
printLine(' web <file>');
|
|
130
|
+
printLine(' web <directory>');
|
|
131
|
+
printLine(' web <glob>');
|
|
132
|
+
printLine(' web compile <target...>');
|
|
133
|
+
printLine(' web screenshot <file.web> [--jpg|--png] [width height [deviceScaleFactor]]');
|
|
134
|
+
printLine(' web watch <file.web> [-screenshot|-s]');
|
|
135
|
+
printLine('');
|
|
136
|
+
printLine(bold('Examples'));
|
|
137
|
+
printLine(' web init');
|
|
138
|
+
printLine(' web init .');
|
|
139
|
+
printLine(' web init website');
|
|
140
|
+
printLine(' web home.web');
|
|
141
|
+
printLine(' web home');
|
|
142
|
+
printLine(' web .');
|
|
143
|
+
printLine(' web ./code/*');
|
|
144
|
+
printLine(' web screenshot home.web');
|
|
145
|
+
printLine(' web screenshot home.web --jpg 1080 1080 2');
|
|
146
|
+
printLine(' web watch home.web');
|
|
147
|
+
printLine(' web watch home.web -s');
|
|
148
|
+
printLine('');
|
|
149
|
+
printLine('If no argument is provided and `layout.web` exists, the CLI compiles it by default.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
main,
|
|
154
|
+
};
|