@smallpen/cli 0.1.0-alpha.13 → 0.1.0-alpha.15
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/bin/command-contract.mjs +6 -1
- package/bin/design-output.mjs +3 -2
- package/bin/schema.mjs +2 -2
- package/bin/smallpen.mjs +101 -74
- package/bin/validation-runner.mjs +62 -0
- package/bin/validation-worker.mjs +29 -0
- package/package.json +3 -3
package/bin/command-contract.mjs
CHANGED
|
@@ -28,6 +28,11 @@ const integer = (option, description, extra = {}) =>
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const PARAMETERS = {
|
|
31
|
+
targetTimeout: integer(
|
|
32
|
+
"--target-timeout",
|
|
33
|
+
"Maximum milliseconds per validation target, including native layout; progress uses JSON lines on stderr and interruption returns partial coverage",
|
|
34
|
+
{ minimum: 1, maximum: 600_000, default: 30_000 },
|
|
35
|
+
),
|
|
31
36
|
stdout: flag(
|
|
32
37
|
"--stdout",
|
|
33
38
|
"Print large results to stdout instead of saving a temporary result file",
|
|
@@ -488,7 +493,7 @@ export const COMMAND_CONTRACTS = {
|
|
|
488
493
|
),
|
|
489
494
|
validate: command(
|
|
490
495
|
"Validate canonical data and report text/layout/contrast issues and coverage; a page checks every version and a component every variant unless one is named",
|
|
491
|
-
[...TARGET, ...VIEW, "designSystem", "limit", "offset"],
|
|
496
|
+
[...TARGET, ...VIEW, "designSystem", "limit", "offset", "targetTimeout"],
|
|
492
497
|
{ conflicts: { designSystem: ["page", "component"] } },
|
|
493
498
|
),
|
|
494
499
|
inspect: command("Read package identity, revision and counts"),
|
package/bin/design-output.mjs
CHANGED
|
@@ -261,7 +261,7 @@ export function componentLookup(workspace) {
|
|
|
261
261
|
}
|
|
262
262
|
return (reference) =>
|
|
263
263
|
reference
|
|
264
|
-
? (sets.get(`${reference.packageId}\0${reference.assetId}`)
|
|
264
|
+
? (reference.packageId ? sets.get(`${reference.packageId}\0${reference.assetId}`) : sets.get(reference.assetId))
|
|
265
265
|
: undefined;
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -468,7 +468,8 @@ function variantPairs(selection, set) {
|
|
|
468
468
|
// Node IDs show only with full: agents read and edit by name.
|
|
469
469
|
export function outlineTree(tree, projection, { full = false, components, ids = full } = {}) {
|
|
470
470
|
const lines = [];
|
|
471
|
-
const
|
|
471
|
+
const dimension = value => Number.isFinite(value) ? Math.round(value) : "?";
|
|
472
|
+
const size = (node) => `${dimension(node.bounds?.width)}×${dimension(node.bounds?.height)}`;
|
|
472
473
|
const describe = (node) => {
|
|
473
474
|
const raw = projection.nodes[node.id];
|
|
474
475
|
const reference = instanceReference(node, projection);
|
package/bin/schema.mjs
CHANGED
|
@@ -765,8 +765,8 @@ function setTopic(command) {
|
|
|
765
765
|
command,
|
|
766
766
|
fields: {
|
|
767
767
|
set: "FIELD=VALUE, repeatable; VALUE is JSON (320, [8,16], {\"Style\":\"ghost\"}, true) or plain text; {token.path} binds a Token",
|
|
768
|
-
element: "fields: width, height (number, fill, hug), fill, stroke, radius, opacity, shadow, visible, rotation, margin, min/max sizes; texts: text, color, font, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textTransform, textDecoration, textAlign; containers: layout (row|column|none), gap, padding, align, justify",
|
|
769
|
-
copy: "a copy of a component: props, text {\"Element\": \"text\"}, set {\"Element.field\": value}, width
|
|
768
|
+
element: "fields: width, height (number, px, Token, fill, hug), fill, stroke, radius, opacity, shadow, visible, rotation, margin, min/max sizes; texts: text, color, font, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textTransform, textDecoration, textAlign; containers: layout (row|column|none), gap, padding, align, justify",
|
|
769
|
+
copy: "a copy of a component: props, text {\"Element\": \"text\"}, set {\"Element.field\": value}, width/height (numbers, px, Tokens, hug, or fill in a row/column parent), visible, opacity. Sizes inside set (Element.width/height) take numbers or Tokens",
|
|
770
770
|
itself: "without --element: the page version (or the variant) itself, for example its width and height",
|
|
771
771
|
},
|
|
772
772
|
example: [
|
package/bin/smallpen.mjs
CHANGED
|
@@ -99,7 +99,9 @@ import {
|
|
|
99
99
|
staleInstanceDiagnostics,
|
|
100
100
|
} from "@smallpen/core";
|
|
101
101
|
import {
|
|
102
|
+
BATCH_HISTORY_LIMITS,
|
|
102
103
|
applyOperationBatch,
|
|
104
|
+
batchHistoryPath,
|
|
103
105
|
defaultLibraryCacheRoot,
|
|
104
106
|
fontVariantName,
|
|
105
107
|
importDraft,
|
|
@@ -169,6 +171,8 @@ import {
|
|
|
169
171
|
viewSummary,
|
|
170
172
|
} from "./read-output.mjs";
|
|
171
173
|
|
|
174
|
+
import { runValidationJobs } from "./validation-runner.mjs";
|
|
175
|
+
|
|
172
176
|
let writeState = "not-applied";
|
|
173
177
|
async function trackedWrite(action) {
|
|
174
178
|
writeState = "unknown";
|
|
@@ -438,6 +442,8 @@ function printJson(value) {
|
|
|
438
442
|
"libraryRevisions",
|
|
439
443
|
"batchId",
|
|
440
444
|
"alreadyApplied",
|
|
445
|
+
"history",
|
|
446
|
+
"historyWindow",
|
|
441
447
|
"changed",
|
|
442
448
|
"dryRun",
|
|
443
449
|
"noChange",
|
|
@@ -1181,7 +1187,7 @@ async function writeReply(result, args) {
|
|
|
1181
1187
|
],
|
|
1182
1188
|
}
|
|
1183
1189
|
: {
|
|
1184
|
-
ledgerPath:
|
|
1190
|
+
ledgerPath: await batchHistoryPath(resolve(args[1])),
|
|
1185
1191
|
batchId: result.batchId,
|
|
1186
1192
|
}),
|
|
1187
1193
|
};
|
|
@@ -1973,8 +1979,11 @@ async function designCommand(packagePath, args, override) {
|
|
|
1973
1979
|
};
|
|
1974
1980
|
}
|
|
1975
1981
|
|
|
1976
|
-
async function
|
|
1977
|
-
if (!flag(args, "--all"))
|
|
1982
|
+
async function validationJobs(workspace, args) {
|
|
1983
|
+
if (!flag(args, "--all")) {
|
|
1984
|
+
const design = await designForCommand(workspace, args);
|
|
1985
|
+
return [{ design, selection: design.selection }];
|
|
1986
|
+
}
|
|
1978
1987
|
const componentId = option(args, "--component-id"),
|
|
1979
1988
|
screenId = option(args, "--screen-id");
|
|
1980
1989
|
if (!componentId && !screenId)
|
|
@@ -1992,11 +2001,9 @@ async function validationDesigns(workspace, args) {
|
|
|
1992
2001
|
(snapshot) => snapshot?.manifest.packageId === first.selection.packageId,
|
|
1993
2002
|
);
|
|
1994
2003
|
return owner.domain.componentSets.get(componentId).variants.map((variant) =>
|
|
1995
|
-
|
|
1996
|
-
componentId,
|
|
1997
|
-
packageId: first.selection.packageId,
|
|
1998
|
-
variantId: variant.id,
|
|
1999
|
-
}),
|
|
2004
|
+
({ selector: designSelector(args),
|
|
2005
|
+
target: { componentId, packageId: first.selection.packageId, variantId: variant.id },
|
|
2006
|
+
selection: { componentId, packageId: first.selection.packageId, variantId: variant.id } }),
|
|
2000
2007
|
);
|
|
2001
2008
|
}
|
|
2002
2009
|
const screen = workspace.product.manifest.entries.screens
|
|
@@ -2005,11 +2012,8 @@ async function validationDesigns(workspace, args) {
|
|
|
2005
2012
|
if (!screen)
|
|
2006
2013
|
throw new SmallPenError("missing_screen", `No Screen ${screenId}`);
|
|
2007
2014
|
return screen.presentations.map((presentation) =>
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
screenId,
|
|
2011
|
-
presentationId: presentation.id,
|
|
2012
|
-
}),
|
|
2015
|
+
({ selector: { ...designSelector(args), screenId, presentationId: presentation.id },
|
|
2016
|
+
selection: { screenId, presentationId: presentation.id } }),
|
|
2013
2017
|
);
|
|
2014
2018
|
}
|
|
2015
2019
|
|
|
@@ -3235,6 +3239,13 @@ async function changesCommand(packagePath, args) {
|
|
|
3235
3239
|
const revisions = {
|
|
3236
3240
|
revision: product.revision,
|
|
3237
3241
|
...(foundation ? { foundationRevision: foundation.revision } : {}),
|
|
3242
|
+
historyWindow: packages.map((snapshot, index) => ({
|
|
3243
|
+
packageId: snapshot.manifest.packageId,
|
|
3244
|
+
...BATCH_HISTORY_LIMITS,
|
|
3245
|
+
retainedBatches: histories[index].length,
|
|
3246
|
+
oldestRevision: histories[index][0]?.baseRevision,
|
|
3247
|
+
newestRevision: histories[index].at(-1)?.revision,
|
|
3248
|
+
})),
|
|
3238
3249
|
};
|
|
3239
3250
|
if (since === undefined) {
|
|
3240
3251
|
const limit = integerOption(args, "--limit") || 10;
|
|
@@ -3265,6 +3276,7 @@ async function changesCommand(packagePath, args) {
|
|
|
3265
3276
|
"unknown_revision",
|
|
3266
3277
|
"This revision is not in the recorded history; read the current design with view instead",
|
|
3267
3278
|
{
|
|
3279
|
+
historyWindow: revisions.historyWindow,
|
|
3268
3280
|
nextOperations: [
|
|
3269
3281
|
{ argv: ["changes", packagePath, "--json"], operation: "smallpen.changes" },
|
|
3270
3282
|
{ argv: ["view", packagePath, "--as", "text", "--json"], operation: "smallpen.view" },
|
|
@@ -3570,30 +3582,27 @@ async function validateWholePackage(packagePath, args, workspace, resolution, on
|
|
|
3570
3582
|
};
|
|
3571
3583
|
const issues = [], checked = [], skipped = [], hints = [];
|
|
3572
3584
|
let unreadable = 0;
|
|
3585
|
+
const jobs = [];
|
|
3573
3586
|
for (const target of targets) {
|
|
3574
3587
|
try {
|
|
3575
|
-
|
|
3576
|
-
for (const issue of report.issues ?? []) {
|
|
3577
|
-
const named = selectionNames(workspace, issue.target ?? {});
|
|
3578
|
-
const at = [named.page ?? named.component ?? target.where, named.platform ?? named.variant].filter(Boolean);
|
|
3579
|
-
const { target: _target, ...rest } = issue;
|
|
3580
|
-
issues.push({ target: at.length > 1 ? `${at[0]} (${at[1]})` : at[0], ...rest });
|
|
3581
|
-
}
|
|
3582
|
-
checked.push(...(report.coverage?.targets ?? []));
|
|
3583
|
-
skipped.push(...(report.coverage?.skipped ?? []).map((skip) => ({ ...skip, label: targetLabel(skip.target) })));
|
|
3584
|
-
for (const hint of report.hints ?? [])
|
|
3585
|
-
if (!hints.some((other) => JSON.stringify(other) === JSON.stringify(hint))) hints.push(hint);
|
|
3588
|
+
jobs.push(...await validationJobs(workspace, ["validate", packagePath, ...target.ids, ...pass]));
|
|
3586
3589
|
} catch (error) {
|
|
3587
3590
|
unreadable += 1;
|
|
3588
|
-
issues.push({
|
|
3589
|
-
target: target.where,
|
|
3590
|
-
code: error?.code ?? "unreadable_target",
|
|
3591
|
-
severity: "error",
|
|
3592
|
-
check: "read",
|
|
3593
|
-
message: `${target.where} cannot be drawn: ${error?.message ?? error}`,
|
|
3594
|
-
});
|
|
3591
|
+
issues.push({ target: target.where, code: error.code ?? "unreadable_target", severity: "error", check: "read", message: error.message });
|
|
3595
3592
|
}
|
|
3596
3593
|
}
|
|
3594
|
+
const outcome = await runChecks(workspace, jobs, args);
|
|
3595
|
+
for (const result of outcome.results) {
|
|
3596
|
+
const report = validationResult(result);
|
|
3597
|
+
for (const issue of report.issues ?? []) {
|
|
3598
|
+
const { target: _target, ...rest } = issue;
|
|
3599
|
+
issues.push({ target: validationLabel(workspace, report.selection), ...rest });
|
|
3600
|
+
}
|
|
3601
|
+
if (result.report) checked.push(report.selection);
|
|
3602
|
+
skipped.push(...(report.coverage?.skipped ?? []).map(skip => ({ ...skip, target: report.selection, label: validationLabel(workspace, report.selection) })));
|
|
3603
|
+
for (const hint of report.hints ?? [])
|
|
3604
|
+
if (!hints.some(other => JSON.stringify(other) === JSON.stringify(hint))) hints.push(hint);
|
|
3605
|
+
}
|
|
3597
3606
|
// Layout limits belong to the tool, with full diagnostics kept separately
|
|
3598
3607
|
// from design issues and every skipped layout check counted in coverage.
|
|
3599
3608
|
const limits = issues.filter((issue) => issue.code === "layout_projection_partial");
|
|
@@ -3604,9 +3613,9 @@ async function validateWholePackage(packagePath, args, workspace, resolution, on
|
|
|
3604
3613
|
return {
|
|
3605
3614
|
packageId: product.manifest.packageId,
|
|
3606
3615
|
...workspaceRevisions(product, workspace.foundation, workspace.libraries),
|
|
3607
|
-
status: unreadable ? "invalid" : "valid",
|
|
3616
|
+
status: outcome.interrupted ? "interrupted" : outcome.results.some(result => result.error) ? "partial" : unreadable ? "invalid" : "valid",
|
|
3608
3617
|
dataStatus: unreadable ? "invalid" : "valid",
|
|
3609
|
-
visualStatus: design.length ? "issues" : "no-issues-found",
|
|
3618
|
+
visualStatus: design.length ? "issues" : outcome.interrupted ? "partial" : "no-issues-found",
|
|
3610
3619
|
issueCount: design.length,
|
|
3611
3620
|
issues: full ? design : page.items,
|
|
3612
3621
|
...(full ? {} : { page: page.page }),
|
|
@@ -3618,7 +3627,8 @@ async function validateWholePackage(packagePath, args, workspace, resolution, on
|
|
|
3618
3627
|
selection: { themes: themeSettings(workspace).selection.themes },
|
|
3619
3628
|
coverage: {
|
|
3620
3629
|
// Every target drawn and every check run on it.
|
|
3621
|
-
complete: unreadable === 0 && notChecked.length === 0,
|
|
3630
|
+
complete: unreadable === 0 && notChecked.length === 0 && !outcome.interrupted,
|
|
3631
|
+
...executionCoverage(outcome),
|
|
3622
3632
|
targetCount: checked.length,
|
|
3623
3633
|
// By name, short: "Home (desktop)", "Button (Style=primary)".
|
|
3624
3634
|
// Pages by version; components as one line each unless --full.
|
|
@@ -3763,48 +3773,63 @@ function projectSummary(snapshot) {
|
|
|
3763
3773
|
};
|
|
3764
3774
|
}
|
|
3765
3775
|
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3776
|
+
function validationLabel(workspace, selection) {
|
|
3777
|
+
const named = selectionNames(workspace, selection);
|
|
3778
|
+
const parts = [named.page ?? named.component, named.platform ?? named.variant].filter(Boolean);
|
|
3779
|
+
return parts.length > 1 ? `${parts[0]} (${parts[1]})` : parts[0] ?? "design";
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
async function runChecks(workspace, jobs, args) {
|
|
3783
|
+
const controller = new AbortController();
|
|
3784
|
+
const interrupt = () => controller.abort();
|
|
3785
|
+
process.once("SIGINT", interrupt);
|
|
3786
|
+
process.once("SIGTERM", interrupt);
|
|
3787
|
+
try {
|
|
3788
|
+
const outcome = await runValidationJobs(workspace, jobs.map(job => ({
|
|
3789
|
+
...job, label: validationLabel(workspace, job.selection),
|
|
3790
|
+
})), {
|
|
3791
|
+
timeoutMs: Number(option(args, "--target-timeout") ?? 30_000),
|
|
3792
|
+
signal: controller.signal,
|
|
3793
|
+
progress: event => process.stderr.write(JSON.stringify(event) + "\n"),
|
|
3783
3794
|
});
|
|
3784
|
-
if (
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
...workspace,
|
|
3790
|
-
context: design.projection.context,
|
|
3791
|
-
initialProjection: design.projection,
|
|
3792
|
-
},
|
|
3793
|
-
);
|
|
3794
|
-
report.issues.push(
|
|
3795
|
-
...flow.issues.map((issue) => ({
|
|
3796
|
-
...issue,
|
|
3797
|
-
check: "prototype-connections",
|
|
3798
|
-
})),
|
|
3799
|
-
);
|
|
3800
|
-
report.coverage.checks.push("prototype-connections");
|
|
3801
|
-
report.coverage.connectionCount = flow.connections.length;
|
|
3802
|
-
}
|
|
3803
|
-
reports.push(report);
|
|
3795
|
+
if (outcome.interrupted) process.exitCode = 130;
|
|
3796
|
+
return outcome;
|
|
3797
|
+
} finally {
|
|
3798
|
+
process.removeListener("SIGINT", interrupt);
|
|
3799
|
+
process.removeListener("SIGTERM", interrupt);
|
|
3804
3800
|
}
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
function executionCoverage(outcome) {
|
|
3804
|
+
return {
|
|
3805
|
+
totalTargetCount: outcome.total,
|
|
3806
|
+
completedTargetCount: outcome.results.length,
|
|
3807
|
+
failedTargetCount: outcome.results.filter(result => result.error).length,
|
|
3808
|
+
remainingCount: outcome.remaining.length,
|
|
3809
|
+
remaining: outcome.remaining,
|
|
3810
|
+
interrupted: outcome.interrupted,
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
function validationResult({ job, report, error }) {
|
|
3815
|
+
return report ?? {
|
|
3816
|
+
selection: job.selection,
|
|
3817
|
+
issues: [{ ...error, severity: "error", check: "execution" }],
|
|
3818
|
+
coverage: { complete: false, nodeCount: 0, visibleNodeCount: 0, checks: [],
|
|
3819
|
+
skipped: [{ check: "native-layout", reason: error.message, code: error.code, target: job.selection }] },
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// Every check of the selected targets, untruncated.
|
|
3824
|
+
async function validateTargets(workspace, args) {
|
|
3825
|
+
const outcome = await runChecks(workspace, await validationJobs(workspace, args), args);
|
|
3826
|
+
const reports = outcome.results.map(validationResult);
|
|
3827
|
+
const empty = { issues: [], coverage: { complete: false, checks: [], skipped: [], nodeCount: 0, visibleNodeCount: 0 } };
|
|
3805
3828
|
const all = flag(args, "--all");
|
|
3806
3829
|
const visual = {
|
|
3807
|
-
...reports[0],
|
|
3830
|
+
...(reports[0] ?? empty),
|
|
3831
|
+
...(outcome.interrupted ? { status: "interrupted", visualStatus: "partial" }
|
|
3832
|
+
: outcome.results.some(result => result.error) ? { status: "partial", visualStatus: "partial" } : {}),
|
|
3808
3833
|
...(reports.some((report) => report.hints?.length) ? { hints: reports.flatMap((report) => report.hints ?? []) } : {}),
|
|
3809
3834
|
issues: reports.flatMap((report) =>
|
|
3810
3835
|
report.issues.map((issue) =>
|
|
@@ -3812,7 +3837,9 @@ async function validateTargets(workspace, args) {
|
|
|
3812
3837
|
),
|
|
3813
3838
|
),
|
|
3814
3839
|
coverage: {
|
|
3815
|
-
...reports[0].coverage,
|
|
3840
|
+
...(reports[0]?.coverage ?? empty.coverage),
|
|
3841
|
+
...executionCoverage(outcome),
|
|
3842
|
+
complete: !outcome.interrupted && reports.every(report => report.coverage.complete),
|
|
3816
3843
|
targets: reports.map((report) => ({ ...selectionNames(workspace, report.selection), ...report.selection })),
|
|
3817
3844
|
targetCount: reports.length,
|
|
3818
3845
|
nodeCount: reports.reduce(
|
|
@@ -3859,7 +3886,7 @@ async function validateTargets(workspace, args) {
|
|
|
3859
3886
|
...(option(args, "--component-id")
|
|
3860
3887
|
? {
|
|
3861
3888
|
componentId: option(args, "--component-id"),
|
|
3862
|
-
packageId: reports[0]
|
|
3889
|
+
packageId: reports[0]?.selection.packageId,
|
|
3863
3890
|
}
|
|
3864
3891
|
: { screenId: option(args, "--screen-id") }),
|
|
3865
3892
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
|
|
3
|
+
// A persistent worker reuses the shared App runtime. Terminating the worker
|
|
4
|
+
// also stops synchronous WASM; a Promise timeout in this process cannot.
|
|
5
|
+
export async function runValidationJobs(workspace, jobs, {
|
|
6
|
+
timeoutMs = 30_000, signal, progress = () => {},
|
|
7
|
+
workerUrl = new URL("./validation-worker.mjs", import.meta.url),
|
|
8
|
+
} = {}) {
|
|
9
|
+
const results = [];
|
|
10
|
+
let worker;
|
|
11
|
+
const emit = (event, extra = {}) => progress({
|
|
12
|
+
type: "validation-progress", event, total: jobs.length, completed: results.length, ...extra,
|
|
13
|
+
});
|
|
14
|
+
emit("start");
|
|
15
|
+
try {
|
|
16
|
+
for (const job of jobs) {
|
|
17
|
+
if (signal?.aborted) break;
|
|
18
|
+
emit("target", { target: job.label });
|
|
19
|
+
worker ??= new Worker(workerUrl, { workerData: workspace });
|
|
20
|
+
const result = await new Promise(resolve => {
|
|
21
|
+
let finished = false;
|
|
22
|
+
const finish = value => {
|
|
23
|
+
if (finished) return;
|
|
24
|
+
finished = true;
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
worker.off("message", onMessage);
|
|
27
|
+
worker.off("error", onError);
|
|
28
|
+
worker.off("exit", onExit);
|
|
29
|
+
signal?.removeEventListener("abort", onAbort);
|
|
30
|
+
resolve(value);
|
|
31
|
+
};
|
|
32
|
+
const onMessage = value => finish(value);
|
|
33
|
+
const onError = error => finish({ error: { code: "validation_worker_failed", message: error.message }, restart: true });
|
|
34
|
+
const onExit = code => onError(new Error(`Validation worker exited (${code})`));
|
|
35
|
+
const onAbort = () => finish({ interrupted: true, restart: true });
|
|
36
|
+
const timer = setTimeout(() => finish({ error: {
|
|
37
|
+
code: "validation_target_timeout", message: `Target exceeded ${timeoutMs}ms`,
|
|
38
|
+
details: { timeoutMs, target: job.label, selection: job.selection },
|
|
39
|
+
}, restart: true }), timeoutMs);
|
|
40
|
+
worker.once("message", onMessage);
|
|
41
|
+
worker.once("error", onError);
|
|
42
|
+
worker.once("exit", onExit);
|
|
43
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
if (signal?.aborted) onAbort();
|
|
45
|
+
else worker.postMessage(job);
|
|
46
|
+
});
|
|
47
|
+
if (result.restart) {
|
|
48
|
+
await worker.terminate();
|
|
49
|
+
worker = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (result.interrupted) break;
|
|
52
|
+
results.push({ job, ...result });
|
|
53
|
+
emit("completed", { target: job.label, status: result.error ? result.error.code : "checked" });
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
await worker?.terminate();
|
|
57
|
+
}
|
|
58
|
+
const remaining = jobs.slice(results.length).map(job => ({ target: job.label, selection: job.selection }));
|
|
59
|
+
const interrupted = Boolean(signal?.aborted);
|
|
60
|
+
emit(interrupted ? "interrupted" : "finished", { remaining: remaining.length });
|
|
61
|
+
return { results, interrupted, remaining, total: jobs.length };
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { elementPath, prototypeConnections } from "@smallpen/core";
|
|
3
|
+
import { selectedDesign, validateDesign } from "./design-output.mjs";
|
|
4
|
+
|
|
5
|
+
const workspace = workerData;
|
|
6
|
+
parentPort.on("message", async (job) => {
|
|
7
|
+
try {
|
|
8
|
+
const design = job.design ?? selectedDesign(workspace, job.selector, job.target);
|
|
9
|
+
const report = { ...(await validateDesign(workspace, design)), selection: design.selection };
|
|
10
|
+
const nodes = design.projection.nodes;
|
|
11
|
+
report.coverage.skipped = (report.coverage.skipped ?? []).map(skip => {
|
|
12
|
+
const at = [skip.path, skip.nodeId].find(id => typeof id === "string" && Object.hasOwn(nodes, id));
|
|
13
|
+
if (!at) return skip;
|
|
14
|
+
const { path: _path, ...rest } = skip;
|
|
15
|
+
return { ...rest, element: elementPath(nodes, design.projection.rootId, at) || nodes[at].name };
|
|
16
|
+
});
|
|
17
|
+
if (design.selection.screenId) {
|
|
18
|
+
const flow = prototypeConnections(workspace.product, design.selection, {
|
|
19
|
+
...workspace, context: design.projection.context, initialProjection: design.projection,
|
|
20
|
+
});
|
|
21
|
+
report.issues.push(...flow.issues.map(issue => ({ ...issue, check: "prototype-connections" })));
|
|
22
|
+
report.coverage.checks.push("prototype-connections");
|
|
23
|
+
report.coverage.connectionCount = flow.connections.length;
|
|
24
|
+
}
|
|
25
|
+
parentPort.postMessage({ report });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
parentPort.postMessage({ error: { code: error.code ?? "unreadable_target", message: error.message, details: error.details } });
|
|
28
|
+
}
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smallpen/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.15",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"bin"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@smallpen/core": "0.1.0-alpha.
|
|
19
|
-
"@smallpen/local-package": "0.1.0-alpha.
|
|
18
|
+
"@smallpen/core": "0.1.0-alpha.15",
|
|
19
|
+
"@smallpen/local-package": "0.1.0-alpha.15"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|