Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit be2d492

Browse files
authored
Feat/task 64 (#5)
* part of Task64 hygiene tasks * updated tasks, fixed tests --------- Co-authored-by: Jason Nathan <[email protected]>
1 parent 8be5a40 commit be2d492

File tree

11 files changed

+460
-298
lines changed

11 files changed

+460
-298
lines changed

docs/.vitepress/critical-create-generate-failure.md

Lines changed: 199 additions & 292 deletions
Large diffs are not rendered by default.

packages/cli/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
"import": "./dist/version.js",
5454
"default": "./dist/version.js"
5555
},
56+
"./utils/reporter.js": {
57+
"types": "./dist/utils/reporter.d.ts",
58+
"import": "./dist/utils/reporter.js",
59+
"default": "./dist/utils/reporter.js"
60+
},
5661
"./package.json": "./package.json"
5762
},
5863
"files": [
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { WPKernelError } from '@wpkernel/core/error';
2+
import type { Reporter } from '@wpkernel/core/reporter';
3+
import { logPhaseFailure } from '../registry.logging';
4+
5+
type LoggedEntry = {
6+
readonly level: 'info' | 'warn' | 'error' | 'debug';
7+
readonly message: string;
8+
readonly context?: unknown;
9+
};
10+
11+
function createStubReporter(): { reporter: Reporter; entries: LoggedEntry[] } {
12+
const entries: LoggedEntry[] = [];
13+
14+
const makeLogger =
15+
(level: LoggedEntry['level']) =>
16+
(message: string, context?: unknown) => {
17+
entries.push({ level, message, context });
18+
};
19+
20+
const reporter: Reporter = {
21+
info: makeLogger('info'),
22+
warn: makeLogger('warn'),
23+
error: makeLogger('error'),
24+
debug: makeLogger('debug'),
25+
child() {
26+
return reporter;
27+
},
28+
};
29+
30+
return { reporter, entries };
31+
}
32+
33+
describe('logPhaseFailure', () => {
34+
it('renders readable error summaries and detail bullets', () => {
35+
const { reporter, entries } = createStubReporter();
36+
const error = new WPKernelError('DeveloperError', {
37+
reason: 'php.printerPath.mismatch',
38+
message: 'Bundled PHP printer path could not be canonicalised.',
39+
data: {
40+
filePath: '/tmp/plugin.php',
41+
exitCode: 1,
42+
stderrSummary: ['Could not open input file: pretty-print.php'],
43+
},
44+
});
45+
46+
logPhaseFailure(reporter, 'execute', error);
47+
48+
const errorMessages = entries
49+
.filter((entry) => entry.level === 'error')
50+
.map((entry) => entry.message);
51+
52+
expect(errorMessages[0]).toBe(
53+
'Execute phase failed: DeveloperError: Bundled PHP printer path could not be canonicalised.'
54+
);
55+
expect(errorMessages).toContain(
56+
' • Could not open input file: pretty-print.php'
57+
);
58+
expect(errorMessages).toContain(' • file: /tmp/plugin.php');
59+
expect(errorMessages).toContain(' • exit code: 1');
60+
61+
const debugEntry = entries.find((entry) => entry.level === 'debug');
62+
expect(debugEntry?.context).toMatchObject({
63+
error: expect.objectContaining({
64+
code: 'DeveloperError',
65+
message: 'Bundled PHP printer path could not be canonicalised.',
66+
}),
67+
});
68+
});
69+
});

packages/cli/src/dx/readiness/registry.logging.ts

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,18 @@ export function logPhaseFailure(
6363
phase: ReadinessPhase,
6464
error: unknown
6565
): void {
66-
phaseReporter(reporter, phase).error(formatPhaseMessage(phase, 'failed'), {
67-
error: serialiseUnknown(error),
68-
});
66+
const { summary, detailLines, serialized } = describeReadableError(error);
67+
const phaseLog = phaseReporter(reporter, phase);
68+
const base = formatPhaseMessage(phase, 'failed');
69+
const headline = summary ? appendSummary(base, summary) : base;
70+
71+
phaseLog.error(headline);
72+
73+
for (const detail of detailLines) {
74+
phaseLog.error(` • ${detail}`);
75+
}
76+
77+
phaseLog.debug('Failure details.', { error: serialized });
6978
}
7079

7180
export function logDetectionResult(
@@ -148,3 +157,115 @@ export function serialiseUnknown(error: unknown) {
148157
})
149158
);
150159
}
160+
161+
type SerializedKernelError = ReturnType<typeof serializeWPKernelError>;
162+
163+
function describeReadableError(error: unknown): {
164+
summary: string;
165+
detailLines: string[];
166+
serialized: SerializedKernelError;
167+
} {
168+
const serialized = serialiseUnknown(error);
169+
return {
170+
summary: buildErrorSummary(serialized),
171+
detailLines: buildErrorDetailLines(serialized),
172+
serialized,
173+
};
174+
}
175+
176+
function buildErrorSummary(error: SerializedKernelError): string {
177+
const reason = extractReason(error);
178+
const headline = reason ?? error.code ?? error.name;
179+
const fallback = extractString(error.data, 'message');
180+
const message = error.message ?? fallback ?? '';
181+
182+
if (headline && message) {
183+
return `${headline}: ${message}`;
184+
}
185+
186+
return headline ?? message ?? 'Unexpected readiness failure';
187+
}
188+
189+
function buildErrorDetailLines(error: SerializedKernelError): string[] {
190+
const lines: string[] = [];
191+
const stderrSummary = extractStringArray(error.data, 'stderrSummary');
192+
193+
if (stderrSummary.length > 0) {
194+
lines.push(...stderrSummary);
195+
} else {
196+
const stderr = extractString(error.data, 'stderr');
197+
if (stderr) {
198+
lines.push(...stderr.split(/\r?\n/u).filter(Boolean).slice(0, 3));
199+
}
200+
}
201+
202+
const filePath = extractString(error.data, 'filePath');
203+
if (filePath) {
204+
lines.push(`file: ${filePath}`);
205+
}
206+
207+
const manifestPath = extractString(error.data, 'manifestPath');
208+
if (manifestPath) {
209+
lines.push(`manifest: ${manifestPath}`);
210+
}
211+
212+
const exitCode = extractNumber(error.data, 'exitCode');
213+
if (typeof exitCode === 'number') {
214+
lines.push(`exit code: ${exitCode}`);
215+
}
216+
217+
return lines;
218+
}
219+
220+
function extractString(
221+
data: SerializedKernelError['data'],
222+
key: string
223+
): string | undefined {
224+
if (!data || typeof data !== 'object') {
225+
return undefined;
226+
}
227+
228+
const value = (data as Record<string, unknown>)[key];
229+
return typeof value === 'string' ? value : undefined;
230+
}
231+
232+
function extractNumber(
233+
data: SerializedKernelError['data'],
234+
key: string
235+
): number | undefined {
236+
if (!data || typeof data !== 'object') {
237+
return undefined;
238+
}
239+
240+
const value = (data as Record<string, unknown>)[key];
241+
return typeof value === 'number' ? value : undefined;
242+
}
243+
244+
function extractStringArray(
245+
data: SerializedKernelError['data'],
246+
key: string
247+
): string[] {
248+
if (!data || typeof data !== 'object') {
249+
return [];
250+
}
251+
252+
const value = (data as Record<string, unknown>)[key];
253+
if (!Array.isArray(value)) {
254+
return [];
255+
}
256+
257+
return value
258+
.filter((entry) => typeof entry === 'string')
259+
.map((entry) => (entry as string).trim())
260+
.filter((entry) => entry.length > 0);
261+
}
262+
263+
function extractReason(error: SerializedKernelError): string | undefined {
264+
const candidate = (error as { reason?: unknown }).reason;
265+
return typeof candidate === 'string' ? candidate : undefined;
266+
}
267+
268+
function appendSummary(base: string, summary: string): string {
269+
const trimmed = base.endsWith('.') ? base.slice(0, -1) : base;
270+
return `${trimmed}: ${summary}`;
271+
}

packages/cli/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ export * from './builders';
7575
export * from './workspace';
7676
export * from './commands';
7777
export * from './dx';
78+
// export * from './utils';

packages/cli/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export {
88
type FileWriteRecord,
99
type FileWriteStatus,
1010
} from './file-writer';
11+
export { createReporterCLI } from './reporter';

packages/create-wpk/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import path from 'node:path';
55
import { performance } from 'node:perf_hooks';
66
import process from 'node:process';
77
import { WPK_NAMESPACE } from '@wpkernel/core/contracts';
8-
import { createReporter, type Reporter } from '@wpkernel/core/reporter';
8+
import { type Reporter } from '@wpkernel/core/reporter';
9+
import { createReporterCLI } from '@wpkernel/cli/utils/reporter.js';
910

1011
const BOOTSTRAP_NAMESPACE = `${WPK_NAMESPACE}.cli.bootstrap`;
1112

@@ -35,7 +36,7 @@ function extractForwardedFlagNames(
3536
}
3637

3738
function createBootstrapReporter(): Reporter {
38-
return createReporter({
39+
return createReporterCLI({
3940
namespace: BOOTSTRAP_NAMESPACE,
4041
channel: 'console',
4142
});

packages/php-json-ast/src/__tests__/driver/ingestionRunner.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,49 @@ describe('ingestionRunner', () => {
9898
);
9999
});
100100

101+
it('preserves existing autoload env when no overrides are provided', async () => {
102+
const stdout = new MockStream();
103+
const stderr = new MockStream();
104+
const closeHandlers: Array<(code: number | null) => void> = [];
105+
106+
spawnMock.mockReturnValue({
107+
stdout,
108+
stderr,
109+
once: jest.fn(
110+
(event: string, handler: (...args: unknown[]) => void) => {
111+
if (event === 'close') {
112+
closeHandlers.push(
113+
handler as (code: number | null) => void
114+
);
115+
}
116+
}
117+
),
118+
} as unknown as ReturnType<typeof spawn>);
119+
120+
process.env.PHP_DRIVER_AUTOLOAD_PATHS = '/existing/vendor/autoload.php';
121+
process.env.WPK_PHP_AUTOLOAD_PATHS = '/workspace/vendor/autoload.php';
122+
123+
const runPromise = runPhpCodemodIngestion({
124+
workspaceRoot: '/workspace/project',
125+
files: ['/workspace/project/plugin.php'],
126+
});
127+
128+
closeHandlers.forEach((handler) => handler(0));
129+
const result = await runPromise;
130+
expect(result.exitCode).toBe(0);
131+
132+
expect(spawnMock).toHaveBeenCalledTimes(1);
133+
const env = spawnMock.mock.calls[0]?.[2]?.env as
134+
| NodeJS.ProcessEnv
135+
| undefined;
136+
expect(env?.PHP_DRIVER_AUTOLOAD_PATHS).toBe(
137+
'/existing/vendor/autoload.php'
138+
);
139+
expect(env?.WPK_PHP_AUTOLOAD_PATHS).toBe(
140+
'/workspace/vendor/autoload.php'
141+
);
142+
});
143+
101144
it('threads autoload path overrides through the environment', async () => {
102145
const stdout = new MockStream();
103146
const stderr = new MockStream();

scripts/test/smoke-create-generate.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ async function main() {
5353
logStep('Running "pnpm exec wpk generate" inside scaffold');
5454
await runPnpm(['exec', 'wpk', 'generate'], { cwd: projectDir });
5555

56+
logStep('Running "pnpm exec wpk apply --yes" inside scaffold');
57+
await runPnpm(['exec', 'wpk', 'apply', '--yes'], {
58+
cwd: projectDir,
59+
});
60+
5661
success = true;
5762
logSuccess(summary);
5863
return summary;
@@ -98,7 +103,10 @@ function logSuccess(summary) {
98103
' pnpm exec wpk doctor -- --plan quickstart\n\n' +
99104
'Or regenerate:\n' +
100105
` cd ${summary.projectDir}\n` +
101-
' pnpm exec wpk generate\n'
106+
' pnpm exec wpk generate\n\n' +
107+
'Or apply immediately:\n' +
108+
` cd ${summary.projectDir}\n` +
109+
' pnpm exec wpk apply --yes\n'
102110
);
103111
}
104112

tsconfig.base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@wpkernel/ui": ["./packages/ui/src/index.ts"],
2121
"@wpkernel/ui/*": ["./packages/ui/src/*"],
2222
"@wpkernel/cli": ["./packages/cli/src/index.ts"],
23+
"@wpkernel/cli/utils/reporter.js": ["./types/cli-reporter"],
2324
"@wpkernel/cli/commands/*": ["./packages/cli/src/commands/*"],
2425
"@wpkernel/cli/src/*": ["./packages/cli/src/*"],
2526
"@wpkernel/cli/*": ["./packages/cli/src/*"],

0 commit comments

Comments
 (0)