handoff-app 1.2.0-0 → 1.2.0-2
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/dist/app-builder/build.js +24 -10
- package/dist/app-builder/config-diff/index.d.ts +1 -1
- package/dist/app-builder/config-diff/registry.d.ts +2 -2
- package/dist/app-builder/config-diff/registry.js +2 -2
- package/dist/app-builder/config-diff/strategies/pattern.js +14 -1
- package/dist/app-builder/config-diff/types.d.ts +15 -1
- package/dist/app-builder/paths.js +18 -1
- package/dist/app-builder/watchers.js +35 -3
- package/dist/app-builder/websocket.js +10 -4
- package/dist/cli.js +1 -1
- package/dist/config/normalizers/pattern.d.ts +0 -1
- package/dist/config/normalizers/pattern.js +0 -3
- package/dist/config/runtime.js +10 -4
- package/dist/pipeline/patterns.d.ts +3 -1
- package/dist/pipeline/patterns.js +2 -2
- package/dist/transformers/plugins/csf-render.js +2 -1
- package/dist/transformers/plugins/ssr-render.js +2 -1
- package/dist/transformers/preview/component/api.d.ts +2 -13
- package/dist/transformers/preview/component/api.js +7 -46
- package/dist/transformers/preview/component/artifacts.d.ts +4 -0
- package/dist/transformers/preview/component/artifacts.js +126 -0
- package/dist/transformers/preview/component/builder.js +62 -6
- package/dist/transformers/preview/component/css.d.ts +2 -0
- package/dist/transformers/preview/component/css.js +25 -9
- package/dist/transformers/preview/component/javascript.d.ts +1 -1
- package/dist/transformers/preview/component/javascript.js +45 -19
- package/dist/transformers/preview/component/summary.d.ts +6 -0
- package/dist/transformers/preview/component/summary.js +55 -0
- package/dist/transformers/preview/component.js +3 -25
- package/dist/transformers/preview/pattern/api.d.ts +4 -0
- package/dist/transformers/preview/pattern/api.js +19 -1
- package/dist/transformers/preview/pattern/builder.d.ts +9 -1
- package/dist/transformers/preview/pattern/builder.js +123 -42
- package/dist/transformers/utils/vite-logger.js +4 -4
- package/dist/transformers/vite-config.js +2 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +6 -0
- package/dist/utils/logger.d.ts +42 -5
- package/dist/utils/logger.js +135 -15
- package/package.json +1 -1
- package/src/app/pages/system/pattern/[pattern]/index.tsx +178 -55
- package/src/app-builder/build.ts +26 -10
- package/src/app-builder/config-diff/index.ts +1 -1
- package/src/app-builder/config-diff/registry.ts +3 -3
- package/src/app-builder/config-diff/strategies/pattern.ts +17 -2
- package/src/app-builder/config-diff/types.ts +16 -1
- package/src/app-builder/paths.ts +22 -1
- package/src/app-builder/watchers.ts +15 -3
- package/src/app-builder/websocket.ts +16 -5
- package/src/cli.ts +1 -1
- package/src/config/normalizers/pattern.ts +0 -7
- package/src/config/runtime.ts +11 -4
- package/src/pipeline/patterns.ts +5 -3
- package/src/transformers/plugins/csf-render.ts +2 -1
- package/src/transformers/plugins/ssr-render.ts +2 -1
- package/src/transformers/preview/component/api.ts +2 -50
- package/src/transformers/preview/component/artifacts.ts +131 -0
- package/src/transformers/preview/component/builder.ts +72 -6
- package/src/transformers/preview/component/css.ts +26 -10
- package/src/transformers/preview/component/javascript.ts +50 -23
- package/src/transformers/preview/component/summary.ts +54 -0
- package/src/transformers/preview/component.ts +2 -1
- package/src/transformers/preview/pattern/api.ts +17 -0
- package/src/transformers/preview/pattern/builder.ts +156 -58
- package/src/transformers/utils/vite-logger.ts +4 -4
- package/src/transformers/vite-config.ts +2 -0
- package/src/utils/duration.ts +3 -0
- package/src/utils/logger.ts +143 -16
|
@@ -46,6 +46,23 @@ export const writePatternSummaryApi = async (
|
|
|
46
46
|
);
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Reads the pattern list summary when present (used for partial pattern rebuilds).
|
|
51
|
+
*/
|
|
52
|
+
export const readPatternSummaryApi = async (handoff: Handoff): Promise<PatternListObject[] | null> => {
|
|
53
|
+
const summaryPath = path.resolve(handoff.workingPath, 'public/api/patterns.json');
|
|
54
|
+
if (!fs.existsSync(summaryPath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const raw = await fs.readFile(summaryPath, 'utf8');
|
|
59
|
+
const parsed = JSON.parse(raw) as PatternListObject[];
|
|
60
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
49
66
|
export const syncPatternArtifacts = async (
|
|
50
67
|
handoff: Handoff,
|
|
51
68
|
validIds: Iterable<string>
|
|
@@ -2,93 +2,191 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '../../../index';
|
|
4
4
|
import { Logger } from '../../../utils/logger';
|
|
5
|
-
import { PatternListObject } from '../types';
|
|
6
|
-
import { syncPatternArtifacts, writePatternApi, writePatternHtml, writePatternSummaryApi } from './api';
|
|
5
|
+
import { PatternListObject, PatternObject } from '../types';
|
|
6
|
+
import { readPatternSummaryApi, syncPatternArtifacts, writePatternApi, writePatternHtml, writePatternSummaryApi } from './api';
|
|
7
7
|
import { composePatternHtml } from './html';
|
|
8
8
|
|
|
9
|
+
export type ProcessPatternsOptions = {
|
|
10
|
+
/** When set, only these pattern ids are recomposed; others keep existing artifacts. */
|
|
11
|
+
onlyPatternIds?: Set<string>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns pattern ids whose declarations reference any of the given component ids.
|
|
16
|
+
*/
|
|
17
|
+
export const getPatternIdsReferencingComponents = (handoff: Handoff, componentIds: string[]): string[] => {
|
|
18
|
+
if (componentIds.length === 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const idSet = new Set(componentIds);
|
|
22
|
+
const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {};
|
|
23
|
+
const result: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (const patternId of Object.keys(runtimePatterns)) {
|
|
26
|
+
const pattern = runtimePatterns[patternId];
|
|
27
|
+
if (pattern.components?.some((ref) => idSet.has(ref.id))) {
|
|
28
|
+
result.push(patternId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function buildPattern(
|
|
36
|
+
handoff: Handoff,
|
|
37
|
+
patternId: string,
|
|
38
|
+
pattern: PatternObject,
|
|
39
|
+
componentOutputDir: string,
|
|
40
|
+
basePath: string
|
|
41
|
+
): Promise<PatternListObject | null> {
|
|
42
|
+
const fragments: { componentId: string; html: string }[] = [];
|
|
43
|
+
let hasErrors = false;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < pattern.components.length; i++) {
|
|
46
|
+
const ref = pattern.components[i];
|
|
47
|
+
|
|
48
|
+
if (ref.resolved === false && !ref.resolvedPreview) {
|
|
49
|
+
hasErrors = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const previewKey = ref.resolvedPreview || ref.preview;
|
|
54
|
+
if (!previewKey) {
|
|
55
|
+
const error =
|
|
56
|
+
`Pattern "${patternId}" component[${i}] ("${ref.id}") has no resolved preview key. Skipping.`;
|
|
57
|
+
Logger.warn(error);
|
|
58
|
+
ref.resolved = false;
|
|
59
|
+
hasErrors = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const htmlFileName = `${ref.id}-${previewKey}.html`;
|
|
63
|
+
const htmlFilePath = path.resolve(componentOutputDir, htmlFileName);
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(htmlFilePath)) {
|
|
66
|
+
const error =
|
|
67
|
+
`Pattern "${patternId}" component[${i}] ("${ref.id}") preview file not found: ${htmlFileName}. ` +
|
|
68
|
+
`Ensure the component has a preview named "${previewKey}".`;
|
|
69
|
+
Logger.warn(error);
|
|
70
|
+
ref.resolved = false;
|
|
71
|
+
hasErrors = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ref.resolved = true;
|
|
76
|
+
const html = await fs.readFile(htmlFilePath, 'utf8');
|
|
77
|
+
fragments.push({ componentId: ref.id, html });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (fragments.length === 0) {
|
|
81
|
+
Logger.warn(`Pattern "${patternId}" produced no fragments. Skipping.`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (hasErrors) {
|
|
86
|
+
Logger.warn(`Pattern "${patternId}" has missing components but will be composed from available fragments.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const composedHtml = composePatternHtml(patternId, pattern.title, fragments, basePath);
|
|
90
|
+
const patternUrl = `${patternId}.html`;
|
|
91
|
+
|
|
92
|
+
const patternData: PatternListObject = {
|
|
93
|
+
...pattern,
|
|
94
|
+
id: patternId,
|
|
95
|
+
url: patternUrl,
|
|
96
|
+
path: `${basePath}/api/pattern/${patternId}.json`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await writePatternHtml(patternId, composedHtml, handoff);
|
|
100
|
+
await writePatternApi(patternId, patternData, handoff);
|
|
101
|
+
|
|
102
|
+
Logger.debug(`Built pattern: ${patternId} (${fragments.length} component(s))`);
|
|
103
|
+
return patternData;
|
|
104
|
+
}
|
|
105
|
+
|
|
9
106
|
/**
|
|
10
107
|
* Process all pattern declarations and compose their HTML from pre-built
|
|
11
108
|
* component preview files. This function must run AFTER buildComponents
|
|
12
109
|
* so that all component preview HTML files already exist on disk.
|
|
13
110
|
*/
|
|
14
|
-
export async function processPatterns(handoff: Handoff): Promise<PatternListObject[]> {
|
|
111
|
+
export async function processPatterns(handoff: Handoff, options?: ProcessPatternsOptions): Promise<PatternListObject[]> {
|
|
15
112
|
const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {};
|
|
16
113
|
const patternIds = Object.keys(runtimePatterns);
|
|
17
|
-
const
|
|
114
|
+
const onlyPatternIds = options?.onlyPatternIds;
|
|
115
|
+
const isPartial = onlyPatternIds !== undefined;
|
|
18
116
|
|
|
19
117
|
if (patternIds.length === 0) {
|
|
20
118
|
await syncPatternArtifacts(handoff, []);
|
|
21
|
-
await writePatternSummaryApi(handoff,
|
|
22
|
-
return
|
|
119
|
+
await writePatternSummaryApi(handoff, []);
|
|
120
|
+
return [];
|
|
23
121
|
}
|
|
24
122
|
|
|
25
|
-
Logger.info(`Building ${patternIds.length} pattern(s)...`);
|
|
26
|
-
|
|
27
123
|
const componentOutputDir = path.resolve(handoff.workingPath, 'public/api/component');
|
|
28
124
|
const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
29
125
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
let hasErrors = false;
|
|
34
|
-
|
|
35
|
-
for (let i = 0; i < pattern.components.length; i++) {
|
|
36
|
-
const ref = pattern.components[i];
|
|
37
|
-
|
|
38
|
-
if (ref.resolved === false && !ref.resolvedPreview) {
|
|
39
|
-
hasErrors = true;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
126
|
+
if (!isPartial) {
|
|
127
|
+
Logger.info(`Building ${patternIds.length} pattern(s)...`);
|
|
128
|
+
const result: PatternListObject[] = [];
|
|
42
129
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const error =
|
|
49
|
-
`Pattern "${patternId}" component[${i}] ("${ref.id}") preview file not found: ${htmlFileName}. ` +
|
|
50
|
-
`Ensure the component has a preview named "${previewKey}".`;
|
|
51
|
-
Logger.warn(error);
|
|
52
|
-
ref.resolved = false;
|
|
53
|
-
hasErrors = true;
|
|
54
|
-
continue;
|
|
130
|
+
for (const patternId of patternIds) {
|
|
131
|
+
const pattern = runtimePatterns[patternId];
|
|
132
|
+
const built = await buildPattern(handoff, patternId, pattern, componentOutputDir, basePath);
|
|
133
|
+
if (built) {
|
|
134
|
+
result.push(built);
|
|
55
135
|
}
|
|
56
|
-
|
|
57
|
-
ref.resolved = true;
|
|
58
|
-
const html = await fs.readFile(htmlFilePath, 'utf8');
|
|
59
|
-
fragments.push({ componentId: ref.id, html });
|
|
60
136
|
}
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
138
|
+
await syncPatternArtifacts(handoff, result.map((p) => p.id));
|
|
139
|
+
await writePatternSummaryApi(handoff, result);
|
|
140
|
+
Logger.info(`Finished building ${result.length} pattern(s).`);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
66
143
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
144
|
+
if (onlyPatternIds.size === 0) {
|
|
145
|
+
const existing = await readPatternSummaryApi(handoff);
|
|
146
|
+
return existing ?? [];
|
|
147
|
+
}
|
|
70
148
|
|
|
71
|
-
|
|
72
|
-
|
|
149
|
+
const existingList = await readPatternSummaryApi(handoff);
|
|
150
|
+
if (!existingList) {
|
|
151
|
+
Logger.warn('Partial pattern rebuild requested but patterns.json is missing; rebuilding all patterns.');
|
|
152
|
+
return processPatterns(handoff);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existingById = new Map(existingList.map((p) => [p.id, p]));
|
|
73
156
|
|
|
74
|
-
|
|
75
|
-
...pattern,
|
|
76
|
-
url: patternUrl,
|
|
77
|
-
path: `${basePath}/api/pattern/${patternId}.json`,
|
|
78
|
-
};
|
|
157
|
+
Logger.info(`Rebuilding ${onlyPatternIds.size} of ${patternIds.length} pattern(s) (incremental)...`);
|
|
79
158
|
|
|
80
|
-
|
|
81
|
-
|
|
159
|
+
for (const patternId of patternIds) {
|
|
160
|
+
if (onlyPatternIds.has(patternId)) {
|
|
161
|
+
const pattern = runtimePatterns[patternId];
|
|
162
|
+
const built = await buildPattern(handoff, patternId, pattern, componentOutputDir, basePath);
|
|
163
|
+
if (built) {
|
|
164
|
+
existingById.set(patternId, built);
|
|
165
|
+
} else {
|
|
166
|
+
existingById.delete(patternId);
|
|
167
|
+
}
|
|
168
|
+
} else if (!existingById.has(patternId)) {
|
|
169
|
+
const pattern = runtimePatterns[patternId];
|
|
170
|
+
const built = await buildPattern(handoff, patternId, pattern, componentOutputDir, basePath);
|
|
171
|
+
if (built) {
|
|
172
|
+
existingById.set(patternId, built);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
82
176
|
|
|
83
|
-
|
|
84
|
-
|
|
177
|
+
const merged: PatternListObject[] = [];
|
|
178
|
+
for (const patternId of patternIds) {
|
|
179
|
+
const entry = existingById.get(patternId);
|
|
180
|
+
if (entry) {
|
|
181
|
+
merged.push(entry);
|
|
182
|
+
}
|
|
85
183
|
}
|
|
86
184
|
|
|
87
|
-
await syncPatternArtifacts(handoff,
|
|
88
|
-
await writePatternSummaryApi(handoff,
|
|
89
|
-
Logger.info(`Finished
|
|
185
|
+
await syncPatternArtifacts(handoff, merged.map((p) => p.id));
|
|
186
|
+
await writePatternSummaryApi(handoff, merged);
|
|
187
|
+
Logger.info(`Finished incremental pattern build (${onlyPatternIds.size} recomposed).`);
|
|
90
188
|
|
|
91
|
-
return
|
|
189
|
+
return merged;
|
|
92
190
|
}
|
|
93
191
|
|
|
94
192
|
export default processPatterns;
|
|
@@ -7,20 +7,20 @@ export const createViteLogger = (): ViteLogger => {
|
|
|
7
7
|
return {
|
|
8
8
|
hasWarned: false,
|
|
9
9
|
info(msg: string, options?: LogOptions) {
|
|
10
|
-
Logger.info(msg);
|
|
10
|
+
Logger.info(msg, 'vite');
|
|
11
11
|
},
|
|
12
12
|
warn(msg: string, options?: LogOptions) {
|
|
13
13
|
this.hasWarned = true;
|
|
14
|
-
Logger.warn(msg);
|
|
14
|
+
Logger.warn(msg, 'vite');
|
|
15
15
|
},
|
|
16
16
|
warnOnce(msg: string, options?: LogOptions) {
|
|
17
17
|
if (warnedMessages.has(msg)) return;
|
|
18
18
|
warnedMessages.add(msg);
|
|
19
19
|
this.hasWarned = true;
|
|
20
|
-
Logger.warn(msg);
|
|
20
|
+
Logger.warn(msg, 'vite');
|
|
21
21
|
},
|
|
22
22
|
error(msg: string, options?: LogOptions) {
|
|
23
|
-
Logger.error(msg);
|
|
23
|
+
Logger.error(msg, undefined, 'vite');
|
|
24
24
|
},
|
|
25
25
|
clearScreen(type: string) {
|
|
26
26
|
// No-op to preserve terminal history
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,12 +1,78 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
1
3
|
import chalk from 'chalk';
|
|
2
4
|
|
|
5
|
+
/** Logical source of a log line — used for a fixed-width–style colored tag after the timestamp. */
|
|
6
|
+
export type LogScope = 'handoff' | 'vite' | 'next';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Env (e.g. project `.env` via `dotenv/config`):
|
|
10
|
+
* - `HANDOFF_LOG_LEVEL` — `debug` | `info` | `warn` | `error` | `silent` (default: `info`).
|
|
11
|
+
* Controls minimum severity: at `warn`, info/success/debug lines are hidden; at `error`, only errors.
|
|
12
|
+
* - `HANDOFF_LOG_SCOPES` — comma-separated allowlist: `handoff`, `vite`, `next`. Defaults to all three if unset or empty.
|
|
13
|
+
*
|
|
14
|
+
* Programmatic `Logger.init({ debug: true })` (Handoff `debug` flag) forces debug-level verbosity unless overridden.
|
|
15
|
+
*/
|
|
3
16
|
export class Logger {
|
|
17
|
+
/** From Handoff constructor; enables debug lines together with env. */
|
|
4
18
|
private static debugMode = false;
|
|
5
19
|
|
|
20
|
+
/** Minimum message severity to print: 0=debug, 1=info, 2=warn, 3=error. `silent` uses 99. */
|
|
21
|
+
private static severityThreshold = 1;
|
|
22
|
+
|
|
23
|
+
/** Enabled scopes; defaults to all of `handoff`, `vite`, `next`. */
|
|
24
|
+
private static scopesFilter: Set<LogScope> = new Set<LogScope>(['handoff', 'vite', 'next']);
|
|
25
|
+
|
|
6
26
|
static init(options?: { debug?: boolean }) {
|
|
27
|
+
this.applyEnvConfig();
|
|
7
28
|
if (options?.debug !== undefined) {
|
|
8
29
|
this.debugMode = options.debug;
|
|
9
30
|
}
|
|
31
|
+
if (options?.debug === true) {
|
|
32
|
+
this.severityThreshold = 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Re-read `process.env` (useful if env is loaded after first `init`). */
|
|
37
|
+
static applyEnvConfig() {
|
|
38
|
+
this.severityThreshold = 1;
|
|
39
|
+
const allScopes: LogScope[] = ['handoff', 'vite', 'next'];
|
|
40
|
+
this.scopesFilter = new Set(allScopes);
|
|
41
|
+
|
|
42
|
+
const levelRaw = process.env.HANDOFF_LOG_LEVEL?.trim().toLowerCase();
|
|
43
|
+
const levelMap: Record<string, number> = {
|
|
44
|
+
debug: 0,
|
|
45
|
+
info: 1,
|
|
46
|
+
warn: 2,
|
|
47
|
+
error: 3,
|
|
48
|
+
silent: 99,
|
|
49
|
+
};
|
|
50
|
+
if (levelRaw && levelRaw in levelMap) {
|
|
51
|
+
this.severityThreshold = levelMap[levelRaw];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const scopesRaw = process.env.HANDOFF_LOG_SCOPES?.trim();
|
|
55
|
+
if (scopesRaw) {
|
|
56
|
+
const set = new Set<LogScope>();
|
|
57
|
+
for (const part of scopesRaw.split(',')) {
|
|
58
|
+
const s = part.trim().toLowerCase();
|
|
59
|
+
if (s === 'handoff' || s === 'vite' || s === 'next') {
|
|
60
|
+
set.add(s);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (set.size > 0) {
|
|
64
|
+
this.scopesFilter = set;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static scopeEnabled(scope: LogScope): boolean {
|
|
70
|
+
return this.scopesFilter.has(scope);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Message severities: debug=0, info-tier=1, warn=2, error=3 */
|
|
74
|
+
private static shouldEmit(severity: 0 | 1 | 2 | 3): boolean {
|
|
75
|
+
return severity >= this.severityThreshold;
|
|
10
76
|
}
|
|
11
77
|
|
|
12
78
|
private static getTimestamp(): string {
|
|
@@ -14,36 +80,97 @@ export class Logger {
|
|
|
14
80
|
return chalk.gray(`[${now.toISOString()}]`);
|
|
15
81
|
}
|
|
16
82
|
|
|
17
|
-
|
|
18
|
-
|
|
83
|
+
/** Short label shown after the timestamp so you can scan for handoff vs vite vs next. */
|
|
84
|
+
private static scopeTag(scope: LogScope): string {
|
|
85
|
+
const tags: Record<LogScope, string> = {
|
|
86
|
+
handoff: chalk.blue.bold('[handoff]'),
|
|
87
|
+
vite: chalk.magenta.bold('[vite]'),
|
|
88
|
+
next: chalk.green.bold('[next]'),
|
|
89
|
+
};
|
|
90
|
+
return tags[scope];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static basePrefix(scope: LogScope): string {
|
|
94
|
+
return `${this.getTimestamp()} ${this.scopeTag(scope)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static log(message: string, scope: LogScope = 'handoff') {
|
|
98
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(1)) return;
|
|
99
|
+
console.log(`${this.basePrefix(scope)} ${message}`);
|
|
19
100
|
}
|
|
20
101
|
|
|
21
|
-
|
|
22
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Vite often passes strings that already contain ANSI; avoid wrapping those in chalk.cyan so colors stay correct.
|
|
104
|
+
*/
|
|
105
|
+
static info(message: string, scope: LogScope = 'handoff') {
|
|
106
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(1)) return;
|
|
107
|
+
const body = scope === 'vite' ? message : chalk.cyan(message);
|
|
108
|
+
console.log(`${this.basePrefix(scope)} ${body}`);
|
|
23
109
|
}
|
|
24
110
|
|
|
25
|
-
static success(message: string) {
|
|
26
|
-
|
|
111
|
+
static success(message: string, scope: LogScope = 'handoff') {
|
|
112
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(1)) return;
|
|
113
|
+
console.log(`${this.basePrefix(scope)} ${chalk.green(message)}`);
|
|
27
114
|
}
|
|
28
115
|
|
|
29
|
-
static warn(message: string) {
|
|
30
|
-
|
|
116
|
+
static warn(message: string, scope: LogScope = 'handoff') {
|
|
117
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(2)) return;
|
|
118
|
+
const body = scope === 'vite' ? message : chalk.yellow(message);
|
|
119
|
+
console.warn(`${this.basePrefix(scope)} ${body}`);
|
|
31
120
|
}
|
|
32
121
|
|
|
33
|
-
static error(message: string, error?: any) {
|
|
34
|
-
|
|
122
|
+
static error(message: string, error?: any, scope: LogScope = 'handoff') {
|
|
123
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(3)) return;
|
|
124
|
+
const body = scope === 'vite' ? message : chalk.red(message);
|
|
125
|
+
console.error(`${this.basePrefix(scope)} ${body}`);
|
|
35
126
|
if (error) {
|
|
36
127
|
console.error(error);
|
|
37
128
|
}
|
|
38
129
|
}
|
|
39
130
|
|
|
40
131
|
static debug(message: string, data?: any) {
|
|
41
|
-
if (this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
132
|
+
if (this.severityThreshold >= 99) return;
|
|
133
|
+
if (!this.scopeEnabled('handoff')) return;
|
|
134
|
+
const allowDebug = this.debugMode || this.severityThreshold === 0;
|
|
135
|
+
if (!allowDebug) return;
|
|
136
|
+
console.log(`${this.basePrefix('handoff')} ${chalk.gray(`[DEBUG] ${message}`)}`);
|
|
137
|
+
if (data) {
|
|
138
|
+
console.log(data);
|
|
46
139
|
}
|
|
47
140
|
}
|
|
48
|
-
}
|
|
49
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Log one line from a child process with timestamp + scope tag. Child ANSI is preserved (no extra chalk on the line).
|
|
144
|
+
*/
|
|
145
|
+
static childProcessLine(line: string, scope: LogScope = 'next') {
|
|
146
|
+
if (!this.scopeEnabled(scope) || !this.shouldEmit(1)) return;
|
|
147
|
+
console.log(`${this.basePrefix(scope)} ${line}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Pipe stdout/stderr from a spawned child through {@link childProcessLine}. Use with
|
|
152
|
+
* `stdio: ['inherit' | 'ignore', 'pipe', 'pipe']`.
|
|
153
|
+
*/
|
|
154
|
+
static pipeChildStreams(
|
|
155
|
+
stdout: NodeJS.ReadableStream | null,
|
|
156
|
+
stderr: NodeJS.ReadableStream | null,
|
|
157
|
+
scope: LogScope = 'next'
|
|
158
|
+
) {
|
|
159
|
+
for (const stream of [stdout, stderr]) {
|
|
160
|
+
if (!stream) continue;
|
|
161
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
162
|
+
rl.on('line', (line) => {
|
|
163
|
+
this.childProcessLine(line, scope);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Forward a completed child-process buffer (e.g. from `spawn.sync` with stdio pipe) line-by-line. */
|
|
169
|
+
static childProcessBuffer(buffer: Buffer | null | undefined, scope: LogScope = 'next') {
|
|
170
|
+
if (!buffer?.length) return;
|
|
171
|
+
const text = buffer.toString('utf8');
|
|
172
|
+
for (const line of text.split(/\r?\n/)) {
|
|
173
|
+
this.childProcessLine(line, scope);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|