@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.
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolveCompileOutputPaths } = require('../../compiler.js');
6
+
7
+ const INIT_SOURCE_FILENAME = 'index.web';
8
+ const INIT_AGENT_GUIDE_FILENAME = 'web-lang-agents.md';
9
+ const INIT_DOC_SEQUENCE = [
10
+ 'getting-started.md',
11
+ 'cli.md',
12
+ 'compiler.md',
13
+ ];
14
+ const BUNDLED_DOCS_DIRECTORY = path.join(__dirname, '..', '..', 'docs');
15
+
16
+ function resolveInitProjectArtifacts(targetDirectory, workingDirectory) {
17
+ const projectRoot = resolveInitProjectRoot(targetDirectory, workingDirectory);
18
+ const sourcePath = path.join(projectRoot, INIT_SOURCE_FILENAME);
19
+ const agentGuidePath = path.join(projectRoot, INIT_AGENT_GUIDE_FILENAME);
20
+ const compileOutputs = resolveCompileOutputPaths(sourcePath);
21
+
22
+ return {
23
+ projectRoot,
24
+ projectRootExists: fs.existsSync(projectRoot),
25
+ sourcePath,
26
+ agentGuidePath,
27
+ htmlPath: compileOutputs.htmlPath,
28
+ cssPath: compileOutputs.cssPath,
29
+ };
30
+ }
31
+
32
+ function resolveInitProjectRoot(targetDirectory, workingDirectory) {
33
+ const baseDirectory = path.resolve(workingDirectory || process.cwd());
34
+ const requestedTarget = targetDirectory && targetDirectory !== '' ? targetDirectory : '.';
35
+ const projectRoot = path.resolve(baseDirectory, requestedTarget);
36
+
37
+ if (fs.existsSync(projectRoot) && !fs.statSync(projectRoot).isDirectory()) {
38
+ throw new Error(`The init target "${targetDirectory}" resolves to a file. Point \`web init\` at a directory path instead.`);
39
+ }
40
+
41
+ return projectRoot;
42
+ }
43
+
44
+ function listExistingInitArtifacts(artifacts) {
45
+ return [
46
+ artifacts.sourcePath,
47
+ artifacts.agentGuidePath,
48
+ artifacts.htmlPath,
49
+ artifacts.cssPath,
50
+ ].filter((artifactPath) => fs.existsSync(artifactPath));
51
+ }
52
+
53
+ function writeInitProjectFiles(artifacts) {
54
+ const sourceContent = createStarterIndexSource();
55
+ const agentGuideContent = buildAgentsContextDocument();
56
+
57
+ fs.mkdirSync(artifacts.projectRoot, { recursive: true });
58
+ fs.writeFileSync(artifacts.sourcePath, sourceContent, 'utf8');
59
+ fs.writeFileSync(artifacts.agentGuidePath, agentGuideContent, 'utf8');
60
+
61
+ return {
62
+ sourceContent,
63
+ agentGuideContent,
64
+ sourceBytes: Buffer.byteLength(sourceContent),
65
+ agentGuideBytes: Buffer.byteLength(agentGuideContent),
66
+ bundledDocs: [...INIT_DOC_SEQUENCE],
67
+ };
68
+ }
69
+
70
+ function createStarterIndexSource() {
71
+ return [
72
+ 'define {',
73
+ ' @pageBackground = "linear-gradient(180deg, #050816 0%, #0f172a 100%)";',
74
+ ' @panelSurface = "rgba(15, 23, 42, 0.78)";',
75
+ ' @panelBorder = "1px solid rgba(148, 163, 184, 0.24)";',
76
+ ' @textStrong = "#f8fafc";',
77
+ ' @textMuted = "#cbd5e1";',
78
+ ' @accent = "#c084fc";',
79
+ ' @shadow = "0 32px 80px rgba(2, 6, 23, 0.42)";',
80
+ '',
81
+ ' Main pageMain;',
82
+ ' Section introBanner;',
83
+ ' Span introEyebrow;',
84
+ ' Heading1 introTitle;',
85
+ ' Paragraph introCopy;',
86
+ '}',
87
+ '',
88
+ '::head {',
89
+ ' meta {',
90
+ ' name = "description";',
91
+ ' content = "A starter WEB project scaffolded with web init."; ',
92
+ ' }',
93
+ '',
94
+ ' meta {',
95
+ ' name = "theme-color";',
96
+ ' content = "#050816";',
97
+ ' }',
98
+ '}',
99
+ '',
100
+ '* {',
101
+ ' boxSizing = "border-box";',
102
+ '}',
103
+ '',
104
+ 'html {',
105
+ ' background = @pageBackground;',
106
+ ' color = @textStrong;',
107
+ ' fontFamily = "-apple-system, BlinkMacSystemFont, Segoe UI, Inter, Roboto, Helvetica Neue, Arial, sans-serif";',
108
+ ' lineHeight = 1.5;',
109
+ '}',
110
+ '',
111
+ 'pageMain {',
112
+ ' minHeight = "100vh";',
113
+ ' display = "grid";',
114
+ ' placeItems = "center";',
115
+ ' padding = "2rem";',
116
+ '',
117
+ ' introBanner {',
118
+ ' width = "min(100%, 720px)";',
119
+ ' padding = "3rem";',
120
+ ' background = @panelSurface;',
121
+ ' border = @panelBorder;',
122
+ ' borderRadius = "18px";',
123
+ ' boxShadow = @shadow;',
124
+ ' display = "grid";',
125
+ ' gap = "1rem";',
126
+ ' backdropFilter = "blur(16px)";',
127
+ '',
128
+ ' introEyebrow {',
129
+ ' textContent = "WELCOME TO WEB";',
130
+ ' display = "inline-flex";',
131
+ ' width = "fit-content";',
132
+ ' padding = "0.45rem 0.75rem";',
133
+ ' border = @panelBorder;',
134
+ ' borderRadius = "999px";',
135
+ ' background = "rgba(192, 132, 252, 0.12)";',
136
+ ' color = @accent;',
137
+ ' fontSize = "0.78rem";',
138
+ ' fontWeight = 700;',
139
+ ' letterSpacing = "0.08em";',
140
+ ' textTransform = "uppercase";',
141
+ ' }',
142
+ '',
143
+ ' introTitle {',
144
+ ' textContent = "Start building with one .web file."; ',
145
+ ' margin = "0";',
146
+ ' fontSize = "clamp(2.75rem, 8vw, 4.5rem)";',
147
+ ' lineHeight = 1.04;',
148
+ ' letterSpacing = "-0.04em";',
149
+ ' }',
150
+ '',
151
+ ' introCopy {',
152
+ ' textContent = "Compile to standard HTML and CSS, keep web-lang-agents.md nearby for local agent context, and expand this starter into your first WEB page."; ',
153
+ ' margin = "0";',
154
+ ' maxWidth = "58ch";',
155
+ ' color = @textMuted;',
156
+ ' fontSize = "1.05rem";',
157
+ ' lineHeight = 1.65;',
158
+ ' }',
159
+ ' }',
160
+ '}',
161
+ ].join('\n');
162
+ }
163
+
164
+ function buildAgentsContextDocument() {
165
+ const docSections = INIT_DOC_SEQUENCE.map((fileName) => {
166
+ const content = readBundledDoc(fileName).trim();
167
+ return `<!-- Source: docs/${fileName} -->\n\n${content}`;
168
+ });
169
+
170
+ return [
171
+ '# WEB Lang Agent Context',
172
+ '',
173
+ 'Generated by `web init` so local agents have the current packaged WEB getting-started guide, CLI guide, and compiler reference in one local document.',
174
+ '',
175
+ 'Bundled in this order: `getting-started.md`, `cli.md`, `compiler.md`.',
176
+ '',
177
+ '---',
178
+ '',
179
+ docSections.join('\n\n---\n\n'),
180
+ '',
181
+ ].join('\n');
182
+ }
183
+
184
+ function readBundledDoc(fileName) {
185
+ const docPath = path.join(BUNDLED_DOCS_DIRECTORY, fileName);
186
+
187
+ try {
188
+ return fs.readFileSync(docPath, 'utf8');
189
+ } catch (error) {
190
+ throw new Error(`The init command could not read the bundled doc "${fileName}" from ${docPath}.`);
191
+ }
192
+ }
193
+
194
+ module.exports = {
195
+ INIT_AGENT_GUIDE_FILENAME,
196
+ INIT_DOC_SEQUENCE,
197
+ INIT_SOURCE_FILENAME,
198
+ buildAgentsContextDocument,
199
+ createStarterIndexSource,
200
+ listExistingInitArtifacts,
201
+ resolveInitProjectArtifacts,
202
+ resolveInitProjectRoot,
203
+ writeInitProjectFiles,
204
+ };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SCREENSHOT_DIRECTORY_NAME = 'web-lang-screenshots';
7
+
8
+ function resolveScreenshotArtifactPaths(inputPath, format, workingDirectory = process.cwd(), capturedAt = new Date()) {
9
+ const resolvedInputPath = path.resolve(inputPath);
10
+ const resolvedWorkingDirectory = path.resolve(workingDirectory);
11
+ const screenshotsRoot = path.join(resolvedWorkingDirectory, SCREENSHOT_DIRECTORY_NAME);
12
+ const relativeInputDirectory = path.relative(resolvedWorkingDirectory, path.dirname(resolvedInputPath));
13
+ const outputDirectory = isPathInsideWorkingDirectory(relativeInputDirectory)
14
+ ? path.join(screenshotsRoot, relativeInputDirectory)
15
+ : screenshotsRoot;
16
+ const fileBaseName = path.basename(resolvedInputPath, path.extname(resolvedInputPath));
17
+ const timestampLabel = formatScreenshotTimestamp(capturedAt);
18
+
19
+ return {
20
+ screenshotsRoot,
21
+ outputDirectory,
22
+ timestampLabel,
23
+ outputPath: resolveUniqueTimestampedScreenshotPath(outputDirectory, fileBaseName, timestampLabel, format),
24
+ };
25
+ }
26
+
27
+ function ensureScreenshotOutputDirectory(directoryPath) {
28
+ fs.mkdirSync(directoryPath, { recursive: true });
29
+ }
30
+
31
+ function isPathInsideWorkingDirectory(relativePath) {
32
+ if (!relativePath || relativePath === '.') {
33
+ return false;
34
+ }
35
+
36
+ if (path.isAbsolute(relativePath)) {
37
+ return false;
38
+ }
39
+
40
+ return !relativePath.startsWith('..');
41
+ }
42
+
43
+ function resolveUniqueTimestampedScreenshotPath(outputDirectory, fileBaseName, timestampLabel, format) {
44
+ const baseFileName = `${fileBaseName}-${timestampLabel}`;
45
+ let suffix = 0;
46
+
47
+ while (true) {
48
+ const candidateName = suffix === 0
49
+ ? `${baseFileName}.${format}`
50
+ : `${baseFileName}-${String(suffix + 1).padStart(2, '0')}.${format}`;
51
+ const candidatePath = path.join(outputDirectory, candidateName);
52
+
53
+ if (!fs.existsSync(candidatePath)) {
54
+ return candidatePath;
55
+ }
56
+
57
+ suffix += 1;
58
+ }
59
+ }
60
+
61
+ function formatScreenshotTimestamp(value) {
62
+ const date = value instanceof Date ? value : new Date(value);
63
+
64
+ return [
65
+ date.getFullYear(),
66
+ String(date.getMonth() + 1).padStart(2, '0'),
67
+ String(date.getDate()).padStart(2, '0'),
68
+ ].join('-') + '_' + [
69
+ String(date.getHours()).padStart(2, '0'),
70
+ String(date.getMinutes()).padStart(2, '0'),
71
+ String(date.getSeconds()).padStart(2, '0'),
72
+ String(date.getMilliseconds()).padStart(3, '0'),
73
+ ].join('-');
74
+ }
75
+
76
+ module.exports = {
77
+ SCREENSHOT_DIRECTORY_NAME,
78
+ ensureScreenshotOutputDirectory,
79
+ formatScreenshotTimestamp,
80
+ resolveScreenshotArtifactPaths,
81
+ };
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { performance } = require('perf_hooks');
5
+ const { pathToFileURL } = require('url');
6
+ const {
7
+ ensureScreenshotOutputDirectory,
8
+ resolveScreenshotArtifactPaths,
9
+ } = require('./screenshot-artifacts');
10
+
11
+ const DEFAULT_SCREENSHOT_FORMAT = 'jpg';
12
+ const DEFAULT_VIEWPORT_WIDTH = 1600;
13
+ const DEFAULT_VIEWPORT_HEIGHT = 900;
14
+ const DEFAULT_DEVICE_SCALE_FACTOR = 1;
15
+ const SCREENSHOT_LOAD_TIMEOUT_MS = 15000;
16
+ const JPEG_QUALITY = 90;
17
+
18
+ async function renderScreenshotArtifact(options) {
19
+ const artifacts = resolveScreenshotArtifactPaths(
20
+ options.inputPath,
21
+ options.format || DEFAULT_SCREENSHOT_FORMAT,
22
+ options.workingDirectory || process.cwd(),
23
+ options.capturedAt
24
+ );
25
+
26
+ ensureScreenshotOutputDirectory(artifacts.outputDirectory);
27
+
28
+ const screenshotResult = await captureScreenshot({
29
+ htmlPath: options.htmlPath,
30
+ outputPath: artifacts.outputPath,
31
+ format: options.format || DEFAULT_SCREENSHOT_FORMAT,
32
+ viewportWidth: options.viewportWidth || DEFAULT_VIEWPORT_WIDTH,
33
+ viewportHeight: options.viewportHeight || DEFAULT_VIEWPORT_HEIGHT,
34
+ deviceScaleFactor: options.deviceScaleFactor || DEFAULT_DEVICE_SCALE_FACTOR,
35
+ fullPage: options.fullPage !== false,
36
+ });
37
+
38
+ return {
39
+ ...artifacts,
40
+ ...screenshotResult,
41
+ };
42
+ }
43
+
44
+ async function captureScreenshot(options) {
45
+ const puppeteer = loadPuppeteer();
46
+ const startedAt = performance.now();
47
+ let browser = null;
48
+
49
+ try {
50
+ browser = await puppeteer.launch({
51
+ headless: true,
52
+ });
53
+
54
+ const page = await browser.newPage();
55
+
56
+ await page.setViewport({
57
+ width: options.viewportWidth,
58
+ height: options.viewportHeight,
59
+ deviceScaleFactor: options.deviceScaleFactor,
60
+ });
61
+
62
+ await page.emulateMediaType('screen');
63
+ await page.goto(pathToFileURL(options.htmlPath).href, {
64
+ waitUntil: 'load',
65
+ timeout: SCREENSHOT_LOAD_TIMEOUT_MS,
66
+ });
67
+ await waitForPageToSettle(page);
68
+
69
+ const pageMetrics = await page.evaluate(() => {
70
+ const doc = document.documentElement;
71
+ const body = document.body || doc;
72
+
73
+ return {
74
+ pageWidth: Math.max(doc.scrollWidth, body.scrollWidth, doc.clientWidth, window.innerWidth),
75
+ pageHeight: Math.max(doc.scrollHeight, body.scrollHeight, doc.clientHeight, window.innerHeight),
76
+ };
77
+ });
78
+
79
+ const screenshotOptions = {
80
+ path: options.outputPath,
81
+ type: options.format === 'png' ? 'png' : 'jpeg',
82
+ fullPage: options.fullPage,
83
+ };
84
+
85
+ if (screenshotOptions.type === 'jpeg') {
86
+ screenshotOptions.quality = JPEG_QUALITY;
87
+ }
88
+
89
+ await page.screenshot(screenshotOptions);
90
+ const imageBytes = fs.statSync(options.outputPath).size;
91
+
92
+ return {
93
+ outputPath: options.outputPath,
94
+ format: options.format,
95
+ fullPage: options.fullPage,
96
+ viewportWidth: options.viewportWidth,
97
+ viewportHeight: options.viewportHeight,
98
+ deviceScaleFactor: options.deviceScaleFactor,
99
+ pageWidth: pageMetrics.pageWidth,
100
+ pageHeight: pageMetrics.pageHeight,
101
+ imageBytes,
102
+ durationMs: performance.now() - startedAt,
103
+ };
104
+ } finally {
105
+ if (browser) {
106
+ await browser.close();
107
+ }
108
+ }
109
+ }
110
+
111
+ function loadPuppeteer() {
112
+ try {
113
+ return require('puppeteer');
114
+ } catch (error) {
115
+ if (error && error.code === 'MODULE_NOT_FOUND') {
116
+ throw new Error('The screenshot command requires the `puppeteer` dependency. Run `npm install` in the `web-lang` package before using `web screenshot` or `web watch -s`.');
117
+ }
118
+
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ async function waitForPageToSettle(page) {
124
+ if (typeof page.waitForNetworkIdle === 'function') {
125
+ try {
126
+ await page.waitForNetworkIdle({
127
+ idleTime: 250,
128
+ timeout: 1500,
129
+ });
130
+ } catch (error) {
131
+ // Some pages may keep network activity alive; continue with the last rendered state.
132
+ }
133
+ }
134
+
135
+ await page.evaluate(() => {
136
+ const afterFonts = document.fonts && document.fonts.ready
137
+ ? document.fonts.ready.catch(() => undefined)
138
+ : Promise.resolve();
139
+
140
+ return afterFonts.then(() => new Promise((resolve) => {
141
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
142
+ }));
143
+ });
144
+ }
145
+
146
+ module.exports = {
147
+ DEFAULT_DEVICE_SCALE_FACTOR,
148
+ DEFAULT_SCREENSHOT_FORMAT,
149
+ DEFAULT_VIEWPORT_HEIGHT,
150
+ DEFAULT_VIEWPORT_WIDTH,
151
+ captureScreenshot,
152
+ renderScreenshotArtifact,
153
+ };
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const PACKAGE_JSON_PATH = path.join(__dirname, '..', '..', 'package.json');
7
+ const SPINNER_FRAMES = ['-', '\\', '|', '/'];
8
+ const ANSI = {
9
+ reset: '\u001b[0m',
10
+ bold: '\u001b[1m',
11
+ dim: '\u001b[2m',
12
+ red: '\u001b[31m',
13
+ green: '\u001b[32m',
14
+ yellow: '\u001b[33m',
15
+ blue: '\u001b[34m',
16
+ cyan: '\u001b[36m',
17
+ gray: '\u001b[90m',
18
+ };
19
+
20
+ function readPackageVersion() {
21
+ try {
22
+ const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
23
+ return packageJson.version || '0.1.0';
24
+ } catch (error) {
25
+ return '0.1.0';
26
+ }
27
+ }
28
+
29
+ function displayPath(targetPath) {
30
+ const relativePath = path.relative(process.cwd(), targetPath);
31
+
32
+ if (!relativePath || relativePath === '') {
33
+ return '.';
34
+ }
35
+
36
+ if (!relativePath.startsWith('..')) {
37
+ return relativePath;
38
+ }
39
+
40
+ return targetPath;
41
+ }
42
+
43
+ function formatBytes(byteCount) {
44
+ if (byteCount < 1024) {
45
+ return `${byteCount} B`;
46
+ }
47
+
48
+ if (byteCount < 1024 * 1024) {
49
+ return `${(byteCount / 1024).toFixed(1)} KB`;
50
+ }
51
+
52
+ return `${(byteCount / (1024 * 1024)).toFixed(1)} MB`;
53
+ }
54
+
55
+ function formatDuration(durationMs) {
56
+ if (durationMs < 1000) {
57
+ return `${Math.max(1, Math.round(durationMs))} ms`;
58
+ }
59
+
60
+ return `${(durationMs / 1000).toFixed(2)} s`;
61
+ }
62
+
63
+ function formatLongDuration(durationMs) {
64
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
65
+ const hours = Math.floor(totalSeconds / 3600);
66
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
67
+ const seconds = totalSeconds % 60;
68
+ const parts = [];
69
+
70
+ if (hours > 0) {
71
+ parts.push(`${hours}h`);
72
+ }
73
+
74
+ if (hours > 0 || minutes > 0) {
75
+ parts.push(`${minutes}m`);
76
+ }
77
+
78
+ parts.push(`${seconds}s`);
79
+
80
+ return parts.join(' ');
81
+ }
82
+
83
+ function formatLocalTimestamp(value) {
84
+ const date = value instanceof Date ? value : new Date(value);
85
+
86
+ return [
87
+ date.getFullYear(),
88
+ String(date.getMonth() + 1).padStart(2, '0'),
89
+ String(date.getDate()).padStart(2, '0'),
90
+ ].join('-') + ' ' + [
91
+ String(date.getHours()).padStart(2, '0'),
92
+ String(date.getMinutes()).padStart(2, '0'),
93
+ String(date.getSeconds()).padStart(2, '0'),
94
+ ].join(':');
95
+ }
96
+
97
+ function formatErrorMessage(error) {
98
+ return error instanceof Error ? error.message : String(error);
99
+ }
100
+
101
+ function indentMultiline(text, prefix) {
102
+ return String(text)
103
+ .split('\n')
104
+ .map((line) => `${prefix}${line}`)
105
+ .join('\n');
106
+ }
107
+
108
+ function createSpinner(label) {
109
+ const interactive = process.stdout.isTTY;
110
+ let frameIndex = 0;
111
+ let intervalId = null;
112
+
113
+ return {
114
+ start() {
115
+ if (!interactive) {
116
+ printLine(dim(label));
117
+ return;
118
+ }
119
+
120
+ renderFrame();
121
+ intervalId = setInterval(() => {
122
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
123
+ renderFrame();
124
+ }, 80);
125
+ },
126
+ succeed(message) {
127
+ stop();
128
+
129
+ if (interactive) {
130
+ clearCurrentLine();
131
+ }
132
+
133
+ printLine(color(message, 'green'));
134
+ },
135
+ fail(message) {
136
+ stop();
137
+
138
+ if (interactive) {
139
+ clearCurrentLine();
140
+ }
141
+
142
+ printLine(color(message, 'red'));
143
+ },
144
+ };
145
+
146
+ function stop() {
147
+ if (intervalId !== null) {
148
+ clearInterval(intervalId);
149
+ intervalId = null;
150
+ }
151
+ }
152
+
153
+ function renderFrame() {
154
+ process.stdout.write(`\r${color(SPINNER_FRAMES[frameIndex], 'cyan')} ${label}`);
155
+ }
156
+ }
157
+
158
+ function clearCurrentLine() {
159
+ process.stdout.write('\r');
160
+ process.stdout.write('\u001b[2K');
161
+ }
162
+
163
+ function printCommandHeader(subtitle) {
164
+ const version = readPackageVersion();
165
+ printLine(color(`${bold('WEB CLI')} ${dim(`v${version}`)}`, 'blue'));
166
+ printLine(dim(subtitle));
167
+ printLine('');
168
+ }
169
+
170
+ function printTargetIssues(errors) {
171
+ if (errors.length === 0) {
172
+ return;
173
+ }
174
+
175
+ printLine(color(bold('Target issues'), 'yellow'));
176
+
177
+ for (const error of errors) {
178
+ printLine(color(`[!] ${error}`, 'yellow'));
179
+ }
180
+
181
+ printLine('');
182
+ }
183
+
184
+ function formatCompileStatsLine(result) {
185
+ return `${result.stats.domNodeCount} DOM nodes | ${result.stats.cssRuleCount} CSS rules | ${result.stats.headEntryCount} head entries | ${result.stats.scriptBlockCount} script blocks | ${formatBytes(result.sourceBytes)} in | ${formatBytes(result.htmlBytes + result.cssBytes)} out`;
186
+ }
187
+
188
+ function printUnexpectedCliError(error) {
189
+ const message = formatErrorMessage(error);
190
+ const [firstLine, ...rest] = message.split('\n');
191
+
192
+ printLine(color(`[x] ${firstLine}`, 'red'));
193
+
194
+ if (rest.length > 0) {
195
+ printLine(dim(indentMultiline(rest.join('\n'), ' ')));
196
+ }
197
+ }
198
+
199
+ function pluralize(count, noun) {
200
+ return `${count} ${noun}${count === 1 ? '' : 's'}`;
201
+ }
202
+
203
+ function padLabel(label) {
204
+ return `${label}:`.padEnd(17, ' ');
205
+ }
206
+
207
+ function sum(values, iteratee) {
208
+ return values.reduce((total, value) => total + iteratee(value), 0);
209
+ }
210
+
211
+ function printLine(value) {
212
+ process.stdout.write(`${value}\n`);
213
+ }
214
+
215
+ function bold(value) {
216
+ return color(value, 'bold');
217
+ }
218
+
219
+ function dim(value) {
220
+ return color(value, 'dim');
221
+ }
222
+
223
+ function color(value, ...styles) {
224
+ if (!supportsColor()) {
225
+ return String(value);
226
+ }
227
+
228
+ const prefix = styles.map((style) => ANSI[style] || '').join('');
229
+ return `${prefix}${value}${ANSI.reset}`;
230
+ }
231
+
232
+ function supportsColor() {
233
+ if (process.env.FORCE_COLOR === '0' || process.env.NO_COLOR === '1') {
234
+ return false;
235
+ }
236
+
237
+ return process.stdout.isTTY;
238
+ }
239
+
240
+ module.exports = {
241
+ bold,
242
+ color,
243
+ createSpinner,
244
+ dim,
245
+ displayPath,
246
+ formatBytes,
247
+ formatCompileStatsLine,
248
+ formatDuration,
249
+ formatErrorMessage,
250
+ formatLocalTimestamp,
251
+ formatLongDuration,
252
+ indentMultiline,
253
+ padLabel,
254
+ pluralize,
255
+ printCommandHeader,
256
+ printLine,
257
+ printTargetIssues,
258
+ printUnexpectedCliError,
259
+ readPackageVersion,
260
+ sum,
261
+ };