@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,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { performance } = require('perf_hooks');
|
|
4
|
+
const { compileInputSource } = require('../compile-service');
|
|
5
|
+
const { inferDefaultCompileTargets, resolveTargets } = require('../targets');
|
|
6
|
+
const {
|
|
7
|
+
bold,
|
|
8
|
+
color,
|
|
9
|
+
createSpinner,
|
|
10
|
+
dim,
|
|
11
|
+
displayPath,
|
|
12
|
+
formatCompileStatsLine,
|
|
13
|
+
formatBytes,
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatErrorMessage,
|
|
16
|
+
indentMultiline,
|
|
17
|
+
padLabel,
|
|
18
|
+
pluralize,
|
|
19
|
+
printCommandHeader,
|
|
20
|
+
printLine,
|
|
21
|
+
printTargetIssues,
|
|
22
|
+
sum,
|
|
23
|
+
} = require('../shared');
|
|
24
|
+
|
|
25
|
+
async function executeCompileCommand({ args }) {
|
|
26
|
+
const targets = args.length > 0 ? args : inferDefaultCompileTargets(process.cwd());
|
|
27
|
+
|
|
28
|
+
if (targets.length === 0) {
|
|
29
|
+
printCompileHelp();
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolution = resolveTargets(targets, process.cwd());
|
|
35
|
+
|
|
36
|
+
printTargetIssues(resolution.errors);
|
|
37
|
+
|
|
38
|
+
if (resolution.files.length === 0) {
|
|
39
|
+
printLine(color('[x] No WEB sources found to compile.', 'red'));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startedAt = performance.now();
|
|
45
|
+
const results = [];
|
|
46
|
+
const failures = [];
|
|
47
|
+
|
|
48
|
+
printCommandHeader(`Compiling ${pluralize(resolution.files.length, 'WEB source')} from ${displayPath(process.cwd())}`);
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < resolution.files.length; index += 1) {
|
|
51
|
+
const inputPath = resolution.files[index];
|
|
52
|
+
const spinner = createSpinner(`[${index + 1}/${resolution.files.length}] Compiling ${displayPath(inputPath)}`);
|
|
53
|
+
|
|
54
|
+
spinner.start();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await compileInputSource(inputPath);
|
|
58
|
+
|
|
59
|
+
spinner.succeed(
|
|
60
|
+
`[ok] ${displayPath(result.inputPath)} -> ${displayPath(result.htmlPath)} + ${displayPath(result.cssPath)} (${formatDuration(result.durationMs)})`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
printLine(dim(` ${formatCompileStatsLine(result)}`));
|
|
64
|
+
results.push(result);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
spinner.fail(`[x] ${displayPath(inputPath)} failed`);
|
|
67
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
68
|
+
failures.push({
|
|
69
|
+
inputPath,
|
|
70
|
+
message: formatErrorMessage(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
printCompileSummary(results, failures, performance.now() - startedAt, resolution.errors.length);
|
|
76
|
+
|
|
77
|
+
if (failures.length > 0 || resolution.errors.length > 0) {
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printCompileSummary(results, failures, durationMs, resolutionErrorCount) {
|
|
83
|
+
const totalSourceBytes = sum(results, (result) => result.sourceBytes);
|
|
84
|
+
const totalOutputBytes = sum(results, (result) => result.htmlBytes + result.cssBytes);
|
|
85
|
+
const totalDomNodes = sum(results, (result) => result.stats.domNodeCount);
|
|
86
|
+
const totalCssRules = sum(results, (result) => result.stats.cssRuleCount);
|
|
87
|
+
const totalHeadEntries = sum(results, (result) => result.stats.headEntryCount);
|
|
88
|
+
const totalScriptBlocks = sum(results, (result) => result.stats.scriptBlockCount);
|
|
89
|
+
|
|
90
|
+
printLine('');
|
|
91
|
+
printLine(bold('Summary'));
|
|
92
|
+
printLine(`${padLabel('Succeeded')} ${color(String(results.length), 'green')}`);
|
|
93
|
+
printLine(`${padLabel('Failed')} ${failures.length > 0 ? color(String(failures.length), 'red') : String(failures.length)}`);
|
|
94
|
+
printLine(`${padLabel('Skipped')} ${resolutionErrorCount > 0 ? color(String(resolutionErrorCount), 'yellow') : String(resolutionErrorCount)}`);
|
|
95
|
+
printLine(`${padLabel('Source bytes')} ${formatBytes(totalSourceBytes)}`);
|
|
96
|
+
printLine(`${padLabel('Output bytes')} ${formatBytes(totalOutputBytes)}`);
|
|
97
|
+
printLine(`${padLabel('DOM nodes')} ${totalDomNodes}`);
|
|
98
|
+
printLine(`${padLabel('CSS rules')} ${totalCssRules}`);
|
|
99
|
+
printLine(`${padLabel('Head entries')} ${totalHeadEntries}`);
|
|
100
|
+
printLine(`${padLabel('Script blocks')} ${totalScriptBlocks}`);
|
|
101
|
+
printLine(`${padLabel('Duration')} ${formatDuration(durationMs)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printCompileHelp() {
|
|
105
|
+
printLine(color(bold('Compile Command'), 'blue'));
|
|
106
|
+
printLine('Compile one or more `.web` sources into matching HTML and CSS files.');
|
|
107
|
+
printLine('');
|
|
108
|
+
printLine(bold('Usage'));
|
|
109
|
+
printLine(' web <file.web>');
|
|
110
|
+
printLine(' web <file>');
|
|
111
|
+
printLine(' web <directory>');
|
|
112
|
+
printLine(' web <glob>');
|
|
113
|
+
printLine(' web compile <target...>');
|
|
114
|
+
printLine('');
|
|
115
|
+
printLine(bold('Examples'));
|
|
116
|
+
printLine(' web home.web');
|
|
117
|
+
printLine(' web home');
|
|
118
|
+
printLine(' web .');
|
|
119
|
+
printLine(' web ./code/*');
|
|
120
|
+
printLine('');
|
|
121
|
+
printLine('If no argument is provided and `layout.web` exists, the CLI compiles it by default.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
executeCompileCommand,
|
|
126
|
+
printCompileHelp,
|
|
127
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { performance } = require('perf_hooks');
|
|
4
|
+
const { compileInputSource } = require('../compile-service');
|
|
5
|
+
const {
|
|
6
|
+
INIT_AGENT_GUIDE_FILENAME,
|
|
7
|
+
INIT_DOC_SEQUENCE,
|
|
8
|
+
INIT_SOURCE_FILENAME,
|
|
9
|
+
listExistingInitArtifacts,
|
|
10
|
+
resolveInitProjectArtifacts,
|
|
11
|
+
writeInitProjectFiles,
|
|
12
|
+
} = require('../init-service');
|
|
13
|
+
const {
|
|
14
|
+
bold,
|
|
15
|
+
color,
|
|
16
|
+
createSpinner,
|
|
17
|
+
dim,
|
|
18
|
+
displayPath,
|
|
19
|
+
formatBytes,
|
|
20
|
+
formatCompileStatsLine,
|
|
21
|
+
formatDuration,
|
|
22
|
+
formatErrorMessage,
|
|
23
|
+
indentMultiline,
|
|
24
|
+
padLabel,
|
|
25
|
+
printCommandHeader,
|
|
26
|
+
printLine,
|
|
27
|
+
} = require('../shared');
|
|
28
|
+
|
|
29
|
+
async function executeInitCommand({ args }) {
|
|
30
|
+
let parsedArgs;
|
|
31
|
+
let artifacts;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
parsedArgs = parseInitArgs(args);
|
|
35
|
+
artifacts = resolveInitProjectArtifacts(parsedArgs.targetDirectory, process.cwd());
|
|
36
|
+
} catch (error) {
|
|
37
|
+
printLine(color(`[x] ${formatErrorMessage(error)}`, 'red'));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const startedAt = performance.now();
|
|
43
|
+
|
|
44
|
+
printCommandHeader(`Initializing a new WEB project in ${displayPath(artifacts.projectRoot)}`);
|
|
45
|
+
printLine(dim('This creates a starter `index.web`, a combined `web-lang-agents.md`, and the first compiled HTML/CSS output.'));
|
|
46
|
+
|
|
47
|
+
if (!artifacts.projectRootExists) {
|
|
48
|
+
printLine(dim('The target directory does not exist yet. WEB will create it before writing the starter files.'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
printLine('');
|
|
52
|
+
|
|
53
|
+
const conflicts = listExistingInitArtifacts(artifacts);
|
|
54
|
+
|
|
55
|
+
if (conflicts.length > 0) {
|
|
56
|
+
printLine(color('[x] Cannot initialize this directory because `web init` would overwrite existing files.', 'red'));
|
|
57
|
+
|
|
58
|
+
for (const conflictPath of conflicts) {
|
|
59
|
+
printLine(dim(` - ${displayPath(conflictPath)}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
printLine(dim(' Move those files, choose a clean project directory, or remove the old scaffold before running `web init` again.'));
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const scaffoldSpinner = createSpinner('[1/2] Creating starter project files');
|
|
68
|
+
scaffoldSpinner.start();
|
|
69
|
+
|
|
70
|
+
let scaffoldResult;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
scaffoldResult = writeInitProjectFiles(artifacts);
|
|
74
|
+
scaffoldSpinner.succeed(`[ok] Created ${INIT_SOURCE_FILENAME} + ${INIT_AGENT_GUIDE_FILENAME}`);
|
|
75
|
+
printLine(dim(` ${formatBytes(scaffoldResult.sourceBytes + scaffoldResult.agentGuideBytes)} across 2 files | bundled docs: ${INIT_DOC_SEQUENCE.join(', ')}`));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
scaffoldSpinner.fail('[x] Project scaffolding failed');
|
|
78
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const compileSpinner = createSpinner(`[2/2] Compiling ${displayPath(artifacts.sourcePath)}`);
|
|
84
|
+
compileSpinner.start();
|
|
85
|
+
|
|
86
|
+
let compileResult;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
compileResult = await compileInputSource(artifacts.sourcePath);
|
|
90
|
+
compileSpinner.succeed(
|
|
91
|
+
`[ok] ${displayPath(compileResult.inputPath)} -> ${displayPath(compileResult.htmlPath)} + ${displayPath(compileResult.cssPath)} (${formatDuration(compileResult.durationMs)})`
|
|
92
|
+
);
|
|
93
|
+
printLine(dim(` ${formatCompileStatsLine(compileResult)}`));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
compileSpinner.fail(`[x] ${displayPath(artifacts.sourcePath)} failed`);
|
|
96
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
printInitSummary({
|
|
102
|
+
artifacts,
|
|
103
|
+
scaffoldResult,
|
|
104
|
+
compileResult,
|
|
105
|
+
durationMs: performance.now() - startedAt,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printInitHelp() {
|
|
110
|
+
printLine(color(bold('Init Command'), 'blue'));
|
|
111
|
+
printLine('Initialize a new WEB project in the current directory or in one target directory with a starter page, compiled output, and a local agent context doc.');
|
|
112
|
+
printLine('');
|
|
113
|
+
printLine(bold('Usage'));
|
|
114
|
+
printLine(' web init');
|
|
115
|
+
printLine(' web init .');
|
|
116
|
+
printLine(' web init <directory>');
|
|
117
|
+
printLine('');
|
|
118
|
+
printLine(bold('Behavior'));
|
|
119
|
+
printLine(' Uses the current working directory when no target is provided');
|
|
120
|
+
printLine(' Accepts one directory target such as `.` or `website`');
|
|
121
|
+
printLine(' Creates the target directory automatically when it does not exist yet');
|
|
122
|
+
printLine(' Creates `index.web` with a simple intro banner');
|
|
123
|
+
printLine(' Creates `web-lang-agents.md` by combining `getting-started.md`, `cli.md`, and `compiler.md`');
|
|
124
|
+
printLine(' Compiles the starter page into `index.html` and `index.css`');
|
|
125
|
+
printLine(' Refuses to overwrite existing scaffold files in the target directory');
|
|
126
|
+
printLine('');
|
|
127
|
+
printLine(bold('Example'));
|
|
128
|
+
printLine(' web init .');
|
|
129
|
+
printLine(' web init website');
|
|
130
|
+
printLine('');
|
|
131
|
+
printLine('The generated `web-lang-agents.md` keeps the packaged WEB docs in one file so local agents can load the project context quickly.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseInitArgs(args) {
|
|
135
|
+
if (args.length === 0) {
|
|
136
|
+
return {
|
|
137
|
+
targetDirectory: '.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (args.length === 1) {
|
|
142
|
+
return {
|
|
143
|
+
targetDirectory: args[0],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error('The init command accepts at most one directory target. Examples: `web init .` or `web init website`.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function printInitSummary({ artifacts, scaffoldResult, compileResult, durationMs }) {
|
|
151
|
+
printLine('');
|
|
152
|
+
printLine(bold('Summary'));
|
|
153
|
+
printLine(`${padLabel('Project root')} ${displayPath(artifacts.projectRoot)}`);
|
|
154
|
+
printLine(`${padLabel('Source')} ${displayPath(artifacts.sourcePath)}`);
|
|
155
|
+
printLine(`${padLabel('Agent guide')} ${displayPath(artifacts.agentGuidePath)}`);
|
|
156
|
+
printLine(`${padLabel('HTML')} ${displayPath(compileResult.htmlPath)}`);
|
|
157
|
+
printLine(`${padLabel('CSS')} ${displayPath(compileResult.cssPath)}`);
|
|
158
|
+
printLine(`${padLabel('Bundled docs')} ${scaffoldResult.bundledDocs.join(', ')}`);
|
|
159
|
+
printLine(`${padLabel('Source bytes')} ${formatBytes(scaffoldResult.sourceBytes)}`);
|
|
160
|
+
printLine(`${padLabel('Guide bytes')} ${formatBytes(scaffoldResult.agentGuideBytes)}`);
|
|
161
|
+
printLine(`${padLabel('Output bytes')} ${formatBytes(compileResult.htmlBytes + compileResult.cssBytes)}`);
|
|
162
|
+
printLine(`${padLabel('DOM nodes')} ${compileResult.stats.domNodeCount}`);
|
|
163
|
+
printLine(`${padLabel('CSS rules')} ${compileResult.stats.cssRuleCount}`);
|
|
164
|
+
printLine(`${padLabel('Head entries')} ${compileResult.stats.headEntryCount}`);
|
|
165
|
+
printLine(`${padLabel('Script blocks')} ${compileResult.stats.scriptBlockCount}`);
|
|
166
|
+
printLine(`${padLabel('Duration')} ${formatDuration(durationMs)}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
executeInitCommand,
|
|
171
|
+
printInitHelp,
|
|
172
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { performance } = require('perf_hooks');
|
|
5
|
+
const { compileInputSource } = require('../compile-service');
|
|
6
|
+
const {
|
|
7
|
+
DEFAULT_DEVICE_SCALE_FACTOR,
|
|
8
|
+
DEFAULT_SCREENSHOT_FORMAT,
|
|
9
|
+
DEFAULT_VIEWPORT_HEIGHT,
|
|
10
|
+
DEFAULT_VIEWPORT_WIDTH,
|
|
11
|
+
renderScreenshotArtifact,
|
|
12
|
+
} = require('../screenshot-service');
|
|
13
|
+
const { SCREENSHOT_DIRECTORY_NAME } = require('../screenshot-artifacts');
|
|
14
|
+
const { resolveTargets } = require('../targets');
|
|
15
|
+
const {
|
|
16
|
+
bold,
|
|
17
|
+
color,
|
|
18
|
+
createSpinner,
|
|
19
|
+
dim,
|
|
20
|
+
displayPath,
|
|
21
|
+
formatBytes,
|
|
22
|
+
formatCompileStatsLine,
|
|
23
|
+
formatDuration,
|
|
24
|
+
formatErrorMessage,
|
|
25
|
+
indentMultiline,
|
|
26
|
+
padLabel,
|
|
27
|
+
printCommandHeader,
|
|
28
|
+
printLine,
|
|
29
|
+
printTargetIssues,
|
|
30
|
+
} = require('../shared');
|
|
31
|
+
|
|
32
|
+
const SCREENSHOT_FORMAT_FLAGS = new Map([
|
|
33
|
+
['--jpg', 'jpg'],
|
|
34
|
+
['--jpeg', 'jpg'],
|
|
35
|
+
['--png', 'png'],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
async function executeScreenshotCommand({ args }) {
|
|
39
|
+
let screenshotArgs;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
screenshotArgs = parseScreenshotArgs(args);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
printLine(color(`[x] ${formatErrorMessage(error)}`, 'red'));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolution = resolveTargets([screenshotArgs.target], process.cwd());
|
|
50
|
+
|
|
51
|
+
printTargetIssues(resolution.errors);
|
|
52
|
+
|
|
53
|
+
if (resolution.files.length === 0) {
|
|
54
|
+
printLine(color('[x] No WEB source found to render.', 'red'));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (resolution.files.length !== 1) {
|
|
60
|
+
printLine(color(`[x] The screenshot command expects exactly one WEB source, but "${screenshotArgs.target}" resolved to ${resolution.files.length}.`, 'red'));
|
|
61
|
+
printLine(dim(' Use a single file path, a bare file name, or a target that resolves to one `.web` source.'));
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const inputPath = resolution.files[0];
|
|
67
|
+
const startedAt = performance.now();
|
|
68
|
+
|
|
69
|
+
printCommandHeader(`Capturing a rendered screenshot for ${displayPath(inputPath)} from ${displayPath(process.cwd())}`);
|
|
70
|
+
printLine(dim(`Images will be written to ${displayPath(path.join(process.cwd(), SCREENSHOT_DIRECTORY_NAME))}`));
|
|
71
|
+
printLine('');
|
|
72
|
+
|
|
73
|
+
const compileSpinner = createSpinner(`[1/2] Compiling ${displayPath(inputPath)}`);
|
|
74
|
+
compileSpinner.start();
|
|
75
|
+
|
|
76
|
+
let compileResult;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
compileResult = await compileInputSource(inputPath);
|
|
80
|
+
|
|
81
|
+
compileSpinner.succeed(
|
|
82
|
+
`[ok] ${displayPath(compileResult.inputPath)} -> ${displayPath(compileResult.htmlPath)} + ${displayPath(compileResult.cssPath)} (${formatDuration(compileResult.durationMs)})`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
printLine(dim(` ${formatCompileStatsLine(compileResult)}`));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
compileSpinner.fail(`[x] ${displayPath(inputPath)} failed`);
|
|
88
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const renderSpinner = createSpinner('[2/2] Rendering screenshot artifact');
|
|
94
|
+
renderSpinner.start();
|
|
95
|
+
|
|
96
|
+
let screenshotResult;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
screenshotResult = await renderScreenshotArtifact({
|
|
100
|
+
inputPath,
|
|
101
|
+
htmlPath: compileResult.htmlPath,
|
|
102
|
+
workingDirectory: process.cwd(),
|
|
103
|
+
format: screenshotArgs.format,
|
|
104
|
+
viewportWidth: screenshotArgs.viewportWidth,
|
|
105
|
+
viewportHeight: screenshotArgs.viewportHeight,
|
|
106
|
+
deviceScaleFactor: screenshotArgs.deviceScaleFactor,
|
|
107
|
+
fullPage: screenshotArgs.fullPage,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
renderSpinner.succeed(
|
|
111
|
+
`[ok] ${displayPath(screenshotResult.outputPath)} (${formatDuration(screenshotResult.durationMs)})`
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
printLine(dim(` ${formatScreenshotStatsLine(screenshotResult)}`));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
renderSpinner.fail('[x] Screenshot render failed');
|
|
117
|
+
printLine(dim(indentMultiline(formatErrorMessage(error), ' ')));
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
printScreenshotSummary(compileResult, screenshotResult, performance.now() - startedAt);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function printScreenshotHelp() {
|
|
126
|
+
printLine(color(bold('Screenshot Command'), 'blue'));
|
|
127
|
+
printLine('Compile one `.web` source and render a JPG or PNG screenshot of the compiled page.');
|
|
128
|
+
printLine('');
|
|
129
|
+
printLine(bold('Usage'));
|
|
130
|
+
printLine(' web screenshot <file.web> [--jpg|--png] [width height [deviceScaleFactor]]');
|
|
131
|
+
printLine('');
|
|
132
|
+
printLine(bold('Examples'));
|
|
133
|
+
printLine(' web screenshot home.web');
|
|
134
|
+
printLine(' web screenshot home');
|
|
135
|
+
printLine(' web screenshot home.web --jpg 1080 1080 2');
|
|
136
|
+
printLine(' web screenshot home.web --png 1440 900');
|
|
137
|
+
printLine(' web watch home.web -s');
|
|
138
|
+
printLine('');
|
|
139
|
+
printLine('Defaults');
|
|
140
|
+
printLine(' Format: jpg');
|
|
141
|
+
printLine(' Width: 1600');
|
|
142
|
+
printLine(' Full-page mode: on when no width and height are provided');
|
|
143
|
+
printLine(' Device scale factor: 1');
|
|
144
|
+
printLine(` Screenshot directory: ./${SCREENSHOT_DIRECTORY_NAME}`);
|
|
145
|
+
printLine('');
|
|
146
|
+
printLine('If width and height are provided, the command captures the current viewport instead of forcing a full-page image.');
|
|
147
|
+
printLine(`The CLI creates ./${SCREENSHOT_DIRECTORY_NAME} in the current working directory automatically when needed.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseScreenshotArgs(args) {
|
|
151
|
+
if (args.length === 0) {
|
|
152
|
+
throw new Error('The screenshot command expects a WEB source. Example: web screenshot home.web --jpg 1080 1080 2');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const [target, ...rest] = args;
|
|
156
|
+
const numericArgs = [];
|
|
157
|
+
let format = DEFAULT_SCREENSHOT_FORMAT;
|
|
158
|
+
|
|
159
|
+
for (const token of rest) {
|
|
160
|
+
if (SCREENSHOT_FORMAT_FLAGS.has(token)) {
|
|
161
|
+
format = SCREENSHOT_FORMAT_FLAGS.get(token);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (token.startsWith('-')) {
|
|
166
|
+
throw new Error(`Unknown screenshot option "${token}". Supported flags are --jpg, --jpeg, and --png.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
numericArgs.push(token);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (numericArgs.length !== 0 && numericArgs.length !== 2 && numericArgs.length !== 3) {
|
|
173
|
+
throw new Error('Screenshot size expects `width height [deviceScaleFactor]`. Example: web screenshot home.web --jpg 1080 1080 2');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (numericArgs.length === 0) {
|
|
177
|
+
return {
|
|
178
|
+
target,
|
|
179
|
+
format,
|
|
180
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
181
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT,
|
|
182
|
+
deviceScaleFactor: DEFAULT_DEVICE_SCALE_FACTOR,
|
|
183
|
+
fullPage: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
target,
|
|
189
|
+
format,
|
|
190
|
+
viewportWidth: parsePositiveInteger(numericArgs[0], 'width'),
|
|
191
|
+
viewportHeight: parsePositiveInteger(numericArgs[1], 'height'),
|
|
192
|
+
deviceScaleFactor: numericArgs[2] ? parsePositiveNumber(numericArgs[2], 'deviceScaleFactor') : DEFAULT_DEVICE_SCALE_FACTOR,
|
|
193
|
+
fullPage: false,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parsePositiveInteger(rawValue, label) {
|
|
198
|
+
if (!/^\d+$/.test(rawValue)) {
|
|
199
|
+
throw new Error(`The screenshot ${label} must be a positive whole number. Received "${rawValue}".`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const value = Number(rawValue);
|
|
203
|
+
|
|
204
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
205
|
+
throw new Error(`The screenshot ${label} must be greater than zero. Received "${rawValue}".`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parsePositiveNumber(rawValue, label) {
|
|
212
|
+
if (!/^\d+(\.\d+)?$/.test(rawValue)) {
|
|
213
|
+
throw new Error(`The screenshot ${label} must be a positive number. Received "${rawValue}".`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const value = Number(rawValue);
|
|
217
|
+
|
|
218
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
219
|
+
throw new Error(`The screenshot ${label} must be greater than zero. Received "${rawValue}".`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function formatScreenshotStatsLine(result) {
|
|
226
|
+
const modeLabel = result.fullPage
|
|
227
|
+
? `full page ${result.pageWidth} x ${result.pageHeight}`
|
|
228
|
+
: `viewport ${result.viewportWidth} x ${result.viewportHeight}`;
|
|
229
|
+
|
|
230
|
+
return `${result.format.toUpperCase()} | ${modeLabel} | @ ${result.deviceScaleFactor}x | ${formatBytes(result.imageBytes)} out`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printScreenshotSummary(compileResult, screenshotResult, durationMs) {
|
|
234
|
+
printLine('');
|
|
235
|
+
printLine(bold('Summary'));
|
|
236
|
+
printLine(`${padLabel('Source')} ${displayPath(compileResult.inputPath)}`);
|
|
237
|
+
printLine(`${padLabel('HTML')} ${displayPath(compileResult.htmlPath)}`);
|
|
238
|
+
printLine(`${padLabel('CSS')} ${displayPath(compileResult.cssPath)}`);
|
|
239
|
+
printLine(`${padLabel('Image')} ${displayPath(screenshotResult.outputPath)}`);
|
|
240
|
+
printLine(`${padLabel('Format')} ${screenshotResult.format.toUpperCase()}`);
|
|
241
|
+
printLine(`${padLabel('Mode')} ${screenshotResult.fullPage ? 'full page' : 'viewport'}`);
|
|
242
|
+
printLine(`${padLabel('Viewport')} ${screenshotResult.viewportWidth} x ${screenshotResult.viewportHeight} @ ${screenshotResult.deviceScaleFactor}x`);
|
|
243
|
+
printLine(`${padLabel('Page size')} ${screenshotResult.pageWidth} x ${screenshotResult.pageHeight}`);
|
|
244
|
+
printLine(`${padLabel('Image bytes')} ${formatBytes(screenshotResult.imageBytes)}`);
|
|
245
|
+
printLine(`${padLabel('Source bytes')} ${formatBytes(compileResult.sourceBytes)}`);
|
|
246
|
+
printLine(`${padLabel('Output bytes')} ${formatBytes(compileResult.htmlBytes + compileResult.cssBytes)}`);
|
|
247
|
+
printLine(`${padLabel('DOM nodes')} ${compileResult.stats.domNodeCount}`);
|
|
248
|
+
printLine(`${padLabel('CSS rules')} ${compileResult.stats.cssRuleCount}`);
|
|
249
|
+
printLine(`${padLabel('Head entries')} ${compileResult.stats.headEntryCount}`);
|
|
250
|
+
printLine(`${padLabel('Script blocks')} ${compileResult.stats.scriptBlockCount}`);
|
|
251
|
+
printLine(`${padLabel('Duration')} ${formatDuration(durationMs)}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
executeScreenshotCommand,
|
|
256
|
+
printScreenshotHelp,
|
|
257
|
+
};
|