@ogygie/generate 0.22.0 → 0.23.0
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/{chunk-ASXSTGY5.js → chunk-ZPRW6QDW.js} +460 -122
- package/dist/cli/extension-registries.d.ts +9 -0
- package/dist/cli/run.js +14 -9
- package/dist/emit/screen/content.d.ts +10 -0
- package/dist/emit/screen/extensions.d.ts +18 -0
- package/dist/emit/screen/views.d.ts +3 -1
- package/dist/emit/screen-plan.d.ts +29 -6
- package/dist/generation-block.schema.d.ts +1 -1
- package/dist/index.js +12 -4
- package/dist/manifest-generation.schema.d.ts +13 -0
- package/dist/screen-block.schema.d.ts +69 -7
- package/dist/screen-checks.d.ts +6 -1
- package/dist/screen-vocabulary.d.ts +6 -2
- package/dist/verify-generation.d.ts +50 -0
- package/package.json +3 -3
|
@@ -982,7 +982,7 @@ function readYamlBlock(yamlText, location) {
|
|
|
982
982
|
function blockKindOf(raw) {
|
|
983
983
|
if (typeof raw !== "object" || raw === null) return "neither";
|
|
984
984
|
const hasContracts = "contracts" in raw;
|
|
985
|
-
const hasConsumes = "consumes" in raw;
|
|
985
|
+
const hasConsumes = "consumes" in raw || "views" in raw;
|
|
986
986
|
if (hasContracts && hasConsumes) return "both";
|
|
987
987
|
if (hasConsumes) return "screen";
|
|
988
988
|
if (hasContracts) return "feature";
|
|
@@ -1031,7 +1031,18 @@ var FrontTargetSchema = z4.strictObject({
|
|
|
1031
1031
|
contracts: z4.string().regex(/\{feature\}/u, "the contracts specifier names the feature with {feature}, e.g. @acme/api/features/{feature}/schemas"),
|
|
1032
1032
|
testBench: ModuleExportSchema,
|
|
1033
1033
|
/** The product's stylesheet, relative to the front app (e.g. `src/styles.css`): inlined in the preview pages the screen emits (spec ogygie-hosted-mode B10). Absent, a preview carries its markup alone. */
|
|
1034
|
-
stylesheet: z4.string().min(1).optional()
|
|
1034
|
+
stylesheet: z4.string().min(1).optional(),
|
|
1035
|
+
/** D56: the points where a WRITTEN screen of the product accepts contributions — a tab bar in v1; the generator maintains `<folder>/extensions.gen.ts`, the host imports it once. */
|
|
1036
|
+
extensionPoints: z4.array(
|
|
1037
|
+
z4.strictObject({
|
|
1038
|
+
id: z4.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*$/u, "kebab segments joined by dots, e.g. fiche-societe.tabs"),
|
|
1039
|
+
kind: z4.literal("tab"),
|
|
1040
|
+
/** The host screen's folder, relative to the front app. */
|
|
1041
|
+
folder: z4.string().min(1),
|
|
1042
|
+
/** The front app carrying the host (an apps[] name) — required only when the map has several front apps. */
|
|
1043
|
+
app: z4.string().min(1).optional()
|
|
1044
|
+
})
|
|
1045
|
+
).optional()
|
|
1035
1046
|
});
|
|
1036
1047
|
var GuardDenialSchema = z4.strictObject({
|
|
1037
1048
|
status: z4.number().int().min(400).max(499),
|
|
@@ -1411,7 +1422,7 @@ function parseScreenBehavior(input) {
|
|
|
1411
1422
|
|
|
1412
1423
|
// src/screen-block.schema.ts
|
|
1413
1424
|
import { z as z5 } from "zod";
|
|
1414
|
-
var SCREEN_BLOCK_KEYS = ["route", "consumes", "views", "holes"];
|
|
1425
|
+
var SCREEN_BLOCK_KEYS = ["route", "mounts", "consumes", "content", "views", "holes"];
|
|
1415
1426
|
var FeatureIdSchema = z5.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u, "kebab-case feature id expected");
|
|
1416
1427
|
var ScreenRouteSchema = z5.string().regex(/^\/(?:[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*)?$/u, "front path expected, lowercase segments under /");
|
|
1417
1428
|
var ConsumedFeatureSchema = z5.strictObject({
|
|
@@ -1442,7 +1453,30 @@ var CustomViewSchema = z5.strictObject({
|
|
|
1442
1453
|
kind: z5.literal("custom"),
|
|
1443
1454
|
hole: IdentifierSchema
|
|
1444
1455
|
});
|
|
1445
|
-
var
|
|
1456
|
+
var ContentViewSchema = z5.strictObject({
|
|
1457
|
+
id: IdentifierSchema,
|
|
1458
|
+
kind: z5.literal("content"),
|
|
1459
|
+
/** The id of a `content[]` list of the block. */
|
|
1460
|
+
content: IdentifierSchema
|
|
1461
|
+
});
|
|
1462
|
+
var ViewSchema = z5.discriminatedUnion("kind", [ListViewSchema, FormViewSchema, CustomViewSchema, ContentViewSchema]);
|
|
1463
|
+
var isOperationView = (view) => view.kind !== "content";
|
|
1464
|
+
var ContentEntrySchema = z5.strictObject({
|
|
1465
|
+
id: FeatureIdSchema,
|
|
1466
|
+
title: z5.string().min(1),
|
|
1467
|
+
body: z5.string().min(1),
|
|
1468
|
+
/** An in-app path, a query or an anchor — never a script. */
|
|
1469
|
+
href: z5.string().min(1).regex(/^(?!javascript:)/iu, "an href is a path, a query or an anchor").optional()
|
|
1470
|
+
});
|
|
1471
|
+
var ContentListSchema = z5.strictObject({
|
|
1472
|
+
id: IdentifierSchema,
|
|
1473
|
+
entries: z5.array(ContentEntrySchema).min(1)
|
|
1474
|
+
});
|
|
1475
|
+
var ExtensionPointIdSchema = z5.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*$/u, "an extension point id is kebab segments joined by dots, e.g. fiche-societe.tabs");
|
|
1476
|
+
var MountSchema = z5.strictObject({
|
|
1477
|
+
point: ExtensionPointIdSchema,
|
|
1478
|
+
id: FeatureIdSchema
|
|
1479
|
+
});
|
|
1446
1480
|
var ScreenHoleSchema = z5.strictObject({
|
|
1447
1481
|
id: IdentifierSchema,
|
|
1448
1482
|
/** Fields of the operation's output the view receives. */
|
|
@@ -1458,7 +1492,10 @@ var ScreenHoleSchema = z5.strictObject({
|
|
|
1458
1492
|
});
|
|
1459
1493
|
var ScreenBlockShapeSchema = z5.strictObject({
|
|
1460
1494
|
route: ScreenRouteSchema.optional(),
|
|
1461
|
-
|
|
1495
|
+
mounts: MountSchema.optional(),
|
|
1496
|
+
/** Empty for a screen whose every view is content (D56). */
|
|
1497
|
+
consumes: z5.array(ConsumedFeatureSchema).default([]),
|
|
1498
|
+
content: z5.array(ContentListSchema).default([]),
|
|
1462
1499
|
views: z5.array(ViewSchema).min(1),
|
|
1463
1500
|
holes: z5.array(ScreenHoleSchema).default([])
|
|
1464
1501
|
});
|
|
@@ -1475,12 +1512,30 @@ function checkScreenCrossReferences(block, issue) {
|
|
|
1475
1512
|
consumedOperations.set(operation, index);
|
|
1476
1513
|
});
|
|
1477
1514
|
});
|
|
1515
|
+
if (block.route !== void 0 && block.mounts !== void 0) issue(["mounts"], "a screen mounts on a route or on an extension point, never both \u2014 drop route or mounts");
|
|
1516
|
+
const contentIds = /* @__PURE__ */ new Set();
|
|
1517
|
+
block.content.forEach((list, index) => {
|
|
1518
|
+
if (contentIds.has(list.id)) issue(["content", index, "id"], `content list "${list.id}" is declared twice`);
|
|
1519
|
+
contentIds.add(list.id);
|
|
1520
|
+
const entryIds = /* @__PURE__ */ new Set();
|
|
1521
|
+
list.entries.forEach((entry, entryIndex) => {
|
|
1522
|
+
if (entryIds.has(entry.id)) issue(["content", index, "entries", entryIndex, "id"], `entry "${entry.id}" is declared twice in "${list.id}" \u2014 ids anchor the entries`);
|
|
1523
|
+
entryIds.add(entry.id);
|
|
1524
|
+
});
|
|
1525
|
+
});
|
|
1526
|
+
if (block.consumes.length === 0 && block.views.some((view) => view.kind !== "content")) {
|
|
1527
|
+
issue(["consumes"], "a screen that consumes nothing has content views only \u2014 a list, form or custom view needs a consumed operation");
|
|
1528
|
+
}
|
|
1478
1529
|
const seenViews = /* @__PURE__ */ new Set();
|
|
1479
1530
|
const holeIds = new Set(block.holes.map((hole) => hole.id));
|
|
1480
1531
|
const carriedHoles = /* @__PURE__ */ new Map();
|
|
1481
1532
|
block.views.forEach((view, index) => {
|
|
1482
1533
|
if (seenViews.has(view.id)) issue(["views", index, "id"], `view id "${view.id}" is declared twice`);
|
|
1483
1534
|
seenViews.add(view.id);
|
|
1535
|
+
if (view.kind === "content") {
|
|
1536
|
+
if (!contentIds.has(view.content)) issue(["views", index, "content"], `content list "${view.content}" is not declared in content`);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1484
1539
|
if (!consumedOperations.has(view.operation)) {
|
|
1485
1540
|
issue(["views", index, "operation"], `operation "${view.operation}" is not consumed \u2014 list it under consumes[].operations`);
|
|
1486
1541
|
}
|
|
@@ -1518,7 +1573,7 @@ function validateScreenBlock(raw, location) {
|
|
|
1518
1573
|
(key) => blockProblemOf(
|
|
1519
1574
|
location,
|
|
1520
1575
|
`${[...path, key].join(".") || key}: unknown key "${key}"`,
|
|
1521
|
-
path.length === 0 ? `A screen block has
|
|
1576
|
+
path.length === 0 ? `A screen block has consumes, views, holes, content, and an optional route or mounts (D41, D56). What fits none of them is a hole.` : `Remove "${key}" or fix its spelling \u2014 see packages/ogygie-generate/src/screen-block.schema.ts.`,
|
|
1522
1577
|
{ path: [...path, key].join(".") }
|
|
1523
1578
|
)
|
|
1524
1579
|
);
|
|
@@ -1540,7 +1595,7 @@ function consumerOf(block, operationId) {
|
|
|
1540
1595
|
return block.consumes.find((consumed) => consumed.operations.includes(operationId)) ?? null;
|
|
1541
1596
|
}
|
|
1542
1597
|
function primaryViewOf(block) {
|
|
1543
|
-
return block.views.find((view) => view.kind
|
|
1598
|
+
return block.views.find((view) => view.kind === "list" || view.kind === "custom") ?? null;
|
|
1544
1599
|
}
|
|
1545
1600
|
|
|
1546
1601
|
// src/consumed-feature.ts
|
|
@@ -1581,6 +1636,20 @@ function elementFieldsOf(outputModel) {
|
|
|
1581
1636
|
function checkScreenBlock(input) {
|
|
1582
1637
|
const problems = [];
|
|
1583
1638
|
const { block, specPath } = input;
|
|
1639
|
+
if (block.mounts !== void 0 && input.front !== void 0 && input.front !== null) {
|
|
1640
|
+
const points = input.front.extensionPoints ?? [];
|
|
1641
|
+
if (!points.some((point) => point.id === block.mounts?.point)) {
|
|
1642
|
+
problems.push(
|
|
1643
|
+
problem(
|
|
1644
|
+
specPath,
|
|
1645
|
+
"mounts.point",
|
|
1646
|
+
`"${block.mounts.point}" is not an extension point of the front target${points.length === 0 ? " (none is declared)" : ` (declared: ${points.map((point) => point.id).join(", ")})`}`,
|
|
1647
|
+
"Declare the point on the host screen in ogygie.manifest.json \u2014 generation.front.extensionPoints: [{ id, kind: tab, folder }] \u2014 or mount on one that exists."
|
|
1648
|
+
)
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (input.mountOnly === true) return problems;
|
|
1584
1653
|
const operationsByView = /* @__PURE__ */ new Map();
|
|
1585
1654
|
function findOperation(featureId, operationId) {
|
|
1586
1655
|
const consumed = input.consumed.get(featureId);
|
|
@@ -1613,6 +1682,7 @@ function checkScreenBlock(input) {
|
|
|
1613
1682
|
});
|
|
1614
1683
|
});
|
|
1615
1684
|
block.views.forEach((view, index) => {
|
|
1685
|
+
if (!isOperationView(view)) return;
|
|
1616
1686
|
const consumer = block.consumes.find((consumed) => consumed.operations.includes(view.operation));
|
|
1617
1687
|
if (consumer === void 0) return;
|
|
1618
1688
|
const found = findOperation(consumer.feature, view.operation);
|
|
@@ -1671,13 +1741,20 @@ var READ_STATES = ["with:chargement", "with:erreur", "with:acc\xE8s refus\xE9",
|
|
|
1671
1741
|
var READ_RENDERS = ["squelette", "bandeau d'erreur", "explication d'acc\xE8s", "\xE9tat vide", "lignes"];
|
|
1672
1742
|
var FORM_STATES = ["with:formulaire valide", "with:formulaire invalide", "with:\xE9criture refus\xE9e"];
|
|
1673
1743
|
var FORM_RENDERS = ["phrases d'erreur", "rien n'est \xE9crit", "bandeau d'erreur"];
|
|
1744
|
+
var CONTENT_STATES = ["with:contenu"];
|
|
1745
|
+
var CONTENT_RENDERS = ["entr\xE9es", "repli\xE9es"];
|
|
1674
1746
|
function screenCaseKey(polarity, phrase) {
|
|
1675
1747
|
return `${polarity}:${phrase}`;
|
|
1676
1748
|
}
|
|
1677
1749
|
function screenVocabularyOf(view, block, sortableFields = []) {
|
|
1678
1750
|
const states = /* @__PURE__ */ new Set();
|
|
1679
1751
|
const renders = /* @__PURE__ */ new Set();
|
|
1680
|
-
if (view ===
|
|
1752
|
+
if (view?.kind === "content") {
|
|
1753
|
+
for (const state of CONTENT_STATES) states.add(state);
|
|
1754
|
+
for (const render of CONTENT_RENDERS) renders.add(render);
|
|
1755
|
+
const list = block.content.find((candidate) => candidate.id === view.content);
|
|
1756
|
+
for (const entry of list?.entries ?? []) renders.add(`ancre ${entry.id}`);
|
|
1757
|
+
} else if (view === null || view.kind !== "form") {
|
|
1681
1758
|
for (const state of READ_STATES) states.add(state);
|
|
1682
1759
|
for (const render of READ_RENDERS) renders.add(render);
|
|
1683
1760
|
const fields = view?.kind === "list" && view.fields !== void 0 ? view.fields : sortableFields;
|
|
@@ -1729,7 +1806,7 @@ function bindScreenBehavior(parse, context) {
|
|
|
1729
1806
|
);
|
|
1730
1807
|
}
|
|
1731
1808
|
if (view === null && primaryViewOf(block) === null) {
|
|
1732
|
-
return unbound2(context, behavior.raw, "\xAB \xE9cran \xBB speaks of the primary read, and the block has no list or custom view", "
|
|
1809
|
+
return unbound2(context, behavior.raw, "\xAB \xE9cran \xBB speaks of the primary read, and the block has no list or custom view", "Name a view of the block by its id \u2014 a content screen has no primary read, its lines speak of its content views.", { view: SCREEN_SUBJECT });
|
|
1733
1810
|
}
|
|
1734
1811
|
if (behavior.hole !== null) {
|
|
1735
1812
|
const hole = block.holes.find((candidate) => candidate.id === behavior.hole);
|
|
@@ -2677,8 +2754,8 @@ function checkSpec(input) {
|
|
|
2677
2754
|
code: GENERATION_BLOCK_INVALID,
|
|
2678
2755
|
message: `The # G\xE9n\xE9ration block of ${input.specPath} is neither a feature block nor a screen block.`,
|
|
2679
2756
|
file: input.specPath,
|
|
2680
|
-
cause: kind === "both" ? "the block carries both `contracts` (a feature, D18) and `consumes` (a screen, D41)" : "the block carries neither `contracts` (a feature, D18) nor `consumes` (a screen, D41)",
|
|
2681
|
-
suggestedFix: "A feature block has contracts, operations, motifs, holes; a screen block has consumes, views, holes and an optional route \u2014 one spec, one kind.",
|
|
2757
|
+
cause: kind === "both" ? "the block carries both `contracts` (a feature, D18) and `consumes` or `views` (a screen, D41)" : "the block carries neither `contracts` (a feature, D18) nor `consumes` or `views` (a screen, D41)",
|
|
2758
|
+
suggestedFix: "A feature block has contracts, operations, motifs, holes; a screen block has consumes, views, holes, content, and an optional route or mounts \u2014 one spec, one kind.",
|
|
2682
2759
|
details: { path: "" }
|
|
2683
2760
|
})
|
|
2684
2761
|
],
|
|
@@ -2788,10 +2865,12 @@ function checkScreenSpec(input, raw, location) {
|
|
|
2788
2865
|
const screen = parsed.block;
|
|
2789
2866
|
const problems = [];
|
|
2790
2867
|
const consumed = input.consumed ?? null;
|
|
2791
|
-
if (consumed !== null) problems.push(...checkScreenBlock({ block: screen, specPath: input.specPath, consumed }));
|
|
2868
|
+
if (consumed !== null) problems.push(...checkScreenBlock({ block: screen, specPath: input.specPath, consumed, front: input.generation?.front ?? null }));
|
|
2869
|
+
else if (screen.mounts !== void 0 && input.generation?.front !== void 0) problems.push(...checkScreenBlock({ block: screen, specPath: input.specPath, consumed: /* @__PURE__ */ new Map(), front: input.generation.front, mountOnly: true }));
|
|
2792
2870
|
const sortableFieldsOf = (view) => {
|
|
2793
|
-
|
|
2794
|
-
|
|
2871
|
+
const operationId = view.operation;
|
|
2872
|
+
if (consumed === null || view.kind !== "list" || operationId === void 0) return [];
|
|
2873
|
+
const consumer = screen.consumes.find((candidate) => candidate.operations.includes(operationId));
|
|
2795
2874
|
const feature = consumer === void 0 ? void 0 : consumed.get(consumer.feature);
|
|
2796
2875
|
const operation = feature === void 0 ? void 0 : consumableOperationsOf(feature).find((candidate) => candidate.id === view.operation);
|
|
2797
2876
|
if (operation === void 0) return [];
|
|
@@ -8422,10 +8501,18 @@ var SUBMIT_LABEL_BY_KIND = {
|
|
|
8422
8501
|
setOrClear: "Set",
|
|
8423
8502
|
delete: "Delete"
|
|
8424
8503
|
};
|
|
8425
|
-
function
|
|
8504
|
+
function contentConstNameOf(listId) {
|
|
8505
|
+
return listId.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
|
|
8506
|
+
}
|
|
8507
|
+
function viewPlanOf(view, operations, block, contentLists) {
|
|
8508
|
+
const componentName = pascalCaseOf(view.id);
|
|
8509
|
+
if (view.kind === "content") {
|
|
8510
|
+
const list = contentLists.find((candidate) => candidate.id === view.content);
|
|
8511
|
+
invariant3(list !== void 0, `content view ${view.id} names an unknown content list`);
|
|
8512
|
+
return { kind: "content", id: view.id, componentName, list };
|
|
8513
|
+
}
|
|
8426
8514
|
const operation = operations.find((candidate) => candidate.id === view.operation);
|
|
8427
8515
|
invariant3(operation !== void 0, `view ${view.id} serves an operation the plan does not know`);
|
|
8428
|
-
const componentName = pascalCaseOf(view.id);
|
|
8429
8516
|
if (view.kind === "list") {
|
|
8430
8517
|
invariant3(operation.collection !== null, `list view ${view.id} serves an operation without a collection`);
|
|
8431
8518
|
const fields = fieldPlansOf(operation.collection.elementModel, view.fields, `the element of ${operation.outputSchema}`);
|
|
@@ -8467,11 +8554,15 @@ function viewPlanOf(view, operations, block) {
|
|
|
8467
8554
|
}
|
|
8468
8555
|
function buildScreenPlan(input) {
|
|
8469
8556
|
const operations = operationPlansOf(input);
|
|
8470
|
-
const
|
|
8557
|
+
const contentLists = input.block.content.map((list) => ({ id: list.id, constName: contentConstNameOf(list.id), entries: list.entries }));
|
|
8558
|
+
const views = input.block.views.map((view) => viewPlanOf(view, operations, input.block, contentLists));
|
|
8471
8559
|
const primaryView = primaryViewOf(input.block);
|
|
8472
|
-
|
|
8473
|
-
const primary =
|
|
8474
|
-
invariant3(
|
|
8560
|
+
const primaryCandidate = primaryView === null ? null : views.find((view) => view.id === primaryView.id);
|
|
8561
|
+
const primary = primaryCandidate !== null && primaryCandidate !== void 0 && (primaryCandidate.kind === "list" || primaryCandidate.kind === "custom") ? primaryCandidate : null;
|
|
8562
|
+
invariant3(primaryView === null || primary !== null, "the primary view is a list or custom view");
|
|
8563
|
+
const mountPoint = input.block.mounts === void 0 ? void 0 : (input.front.extensionPoints ?? []).find((point) => point.id === input.block.mounts?.point);
|
|
8564
|
+
invariant3(input.block.mounts === void 0 || mountPoint !== void 0, "a mount names an extension point of the front target");
|
|
8565
|
+
const mount = input.block.mounts === void 0 || mountPoint === void 0 ? null : { point: mountPoint, id: input.block.mounts.id };
|
|
8475
8566
|
const pascalName = pascalCaseOf(input.featureId);
|
|
8476
8567
|
const screenName = `${pascalName}Screen`;
|
|
8477
8568
|
const deniedDenial = operations.flatMap((operation) => [...operation.guardDenials]).find((denial) => denial.status === 401) ?? { code: "AUTH_REQUIRED", status: 401, case: "sans session", fixture: "withoutSession" };
|
|
@@ -8500,6 +8591,8 @@ function buildScreenPlan(input) {
|
|
|
8500
8591
|
propsName: `${screenName}BodyProps`,
|
|
8501
8592
|
routeFactoryName: `${camelCaseOf(input.featureId)}Route`,
|
|
8502
8593
|
primary,
|
|
8594
|
+
contentLists,
|
|
8595
|
+
mount,
|
|
8503
8596
|
operations,
|
|
8504
8597
|
reads: operations.filter((operation) => operation.kind === "read"),
|
|
8505
8598
|
writes: operations.filter((operation) => operation.kind !== "read"),
|
|
@@ -8523,22 +8616,26 @@ function emitScreenSpecContracts(plan) {
|
|
|
8523
8616
|
(operation) => `| ${tick4(operation.id)} | ${tick4(operation.featureId)} | ${operation.method} ${tick4(operation.path)} | ${tick4(operation.hookName)} | ${tick4(operation.keyLiteral)} | ${operation.kind === "read" ? "\u2014" : operation.invalidationRoots.map((root) => tick4(`["${root}"]`)).join(", ")} | ${tick4(operation.inputSchema)} \u2192 ${tick4(operation.outputSchema)} |`
|
|
8524
8617
|
);
|
|
8525
8618
|
const viewRows = plan.views.map((view) => {
|
|
8619
|
+
if (view.kind === "content") {
|
|
8620
|
+
return `| ${tick4(view.id)} | content | \u2014 | ${view.list.entries.map((entry) => tick4(entry.id)).join(", ")} | liste ${tick4(view.list.id)} (${view.list.entries.length} entr\xE9es, ${tick4(`content/${view.list.id}.gen.ts`)}) | ${tick4(`views/${view.componentName}.gen.tsx`)} |`;
|
|
8621
|
+
}
|
|
8526
8622
|
const fields = view.kind === "custom" ? view.receives.map((field) => tick4(field.name)).join(", ") : view.fields.map((field) => tick4(field.name)).join(", ");
|
|
8527
8623
|
const file = view.kind === "custom" ? `views/${view.componentName}.tsx (corps \xE9crit)` : `views/${view.componentName}.gen.tsx`;
|
|
8528
8624
|
const detail = view.kind === "list" ? view.sort === null ? "\u2014" : `tri ${view.sort.field} ${view.sort.direction}` : view.kind === "form" ? `bouton \xAB ${view.submitLabel} \xBB${view.multiline.length === 0 ? "" : `, textarea ${view.multiline.map(tick4).join(", ")}`}` : `trou ${tick4(view.hole.id)}${view.actions.length === 0 ? "" : `, actions ${view.actions.map((action) => tick4(action.operation.id)).join(", ")}`}`;
|
|
8529
8625
|
return `| ${tick4(view.id)} | ${view.kind} | ${tick4(view.operation.id)} | ${fields} | ${detail} | ${tick4(file)} |`;
|
|
8530
8626
|
});
|
|
8627
|
+
const mounting = plan.mount === null ? plan.route === null ? " ; aucune route \xE9mise" : ` ; route ${tick4(plan.route)} (${tick4("routes.gen.tsx")})` : ` ; mont\xE9 sur le point ${tick4(plan.mount.point.id)} sous l'id ${tick4(plan.mount.id)} (registre ${tick4(`${plan.mount.point.folder}/extensions.gen.ts`)} de l'\xE9cran h\xF4te), aucune route`;
|
|
8531
8628
|
const customViews = plan.views.filter((view) => view.kind === "custom");
|
|
8532
8629
|
return fileOf(
|
|
8533
8630
|
`G\xE9n\xE9r\xE9 par ogygie-generate ${plan.generatorVersion} depuis le bloc \`# G\xE9n\xE9ration\` de l'\xE9cran et les contrats des features consomm\xE9es ; \`verify\` compare ce qui tient entre les marqueurs. Ce qui est \xE0 changer se change dans le bloc, dans la spec consomm\xE9e ou dans ses \`schemas.ts\`, jamais ici.`,
|
|
8534
8631
|
"",
|
|
8535
8632
|
"## Op\xE9rations consomm\xE9es",
|
|
8536
8633
|
"",
|
|
8537
|
-
`Lecture principale : ${tick4(plan.primary.operation.id)} (le conteneur l'attend par ScreenState)
|
|
8634
|
+
`${plan.primary === null ? "Aucune lecture principale : un \xE9cran de contenu, le conteneur rend le corps sans ScreenState" : `Lecture principale : ${tick4(plan.primary.operation.id)} (le conteneur l'attend par ScreenState)`}${mounting}. Les cl\xE9s de cache sont celles que le client d\xE9riv\xE9 du manifeste calcule, \xE9crites en litt\xE9raux ; une \xE9criture invalide les racines que la carte lui donne.`,
|
|
8538
8635
|
"",
|
|
8539
8636
|
"| Op\xE9ration | Feature | Route | Hook | Cl\xE9 de cache | Invalide | Contrats |",
|
|
8540
8637
|
"|---|---|---|---|---|---|---|",
|
|
8541
|
-
...operationRows,
|
|
8638
|
+
...operationRows.length === 0 ? ["| \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | aucune op\xE9ration consomm\xE9e |"] : operationRows,
|
|
8542
8639
|
"",
|
|
8543
8640
|
"## Vues",
|
|
8544
8641
|
"",
|
|
@@ -8552,7 +8649,7 @@ function emitScreenSpecContracts(plan) {
|
|
|
8552
8649
|
"",
|
|
8553
8650
|
`## Corps`,
|
|
8554
8651
|
"",
|
|
8555
|
-
`${tick4(`${plan.screenName}.tsx`)} est \xC9CRIT : \xE9mis une fois comme composition des vues dans l'ordre du bloc, jamais compar\xE9. Le conteneur ${tick4(`${plan.screenName}.container.gen.tsx`)} lui passe ${tick4("data")} (la lecture principale servie)${plan.writes.length === 0 ? "" : ` et ${plan.writes.map((operation) => tick4(`${operation.id}Mutation`)).join(", ")}`}.`
|
|
8652
|
+
`${tick4(`${plan.screenName}.tsx`)} est \xC9CRIT : \xE9mis une fois comme composition des vues dans l'ordre du bloc, jamais compar\xE9. Le conteneur ${tick4(`${plan.screenName}.container.gen.tsx`)} ${plan.primary === null ? plan.writes.length === 0 ? "l'appelle sans donn\xE9e" : `lui passe ${plan.writes.map((operation) => tick4(`${operation.id}Mutation`)).join(", ")}` : `lui passe ${tick4("data")} (la lecture principale servie)${plan.writes.length === 0 ? "" : ` et ${plan.writes.map((operation) => tick4(`${operation.id}Mutation`)).join(", ")}`}`}${plan.mount === null ? "" : ` ; sa copie porte ${tick4("mountLabel")}, le libell\xE9 de l'onglet que le registre du point lit`}.`
|
|
8556
8653
|
);
|
|
8557
8654
|
}
|
|
8558
8655
|
|
|
@@ -8624,58 +8721,69 @@ function jsonLiteral(value, indent = 0) {
|
|
|
8624
8721
|
}
|
|
8625
8722
|
|
|
8626
8723
|
// src/emit/screen/container.ts
|
|
8724
|
+
function secondaryReadsOf(plan) {
|
|
8725
|
+
return plan.reads.filter((operation) => operation.id !== plan.primary?.operation.id);
|
|
8726
|
+
}
|
|
8727
|
+
function screenCopyLines(plan) {
|
|
8728
|
+
return [
|
|
8729
|
+
"/** The words of the states, written in the body file and translated there. */",
|
|
8730
|
+
"export interface ScreenCopy {",
|
|
8731
|
+
" errorTitle: ReactNode;",
|
|
8732
|
+
" accessDenied: StateCopy;",
|
|
8733
|
+
...plan.mount === null ? [] : [` /** The label of this screen's tab on the \`${plan.mount.point.id}\` point of its host (the registry reads it). */`, " mountLabel: ReactNode;"],
|
|
8734
|
+
"}"
|
|
8735
|
+
];
|
|
8736
|
+
}
|
|
8627
8737
|
function bodyPropsLines(plan) {
|
|
8628
|
-
const secondary =
|
|
8738
|
+
const secondary = secondaryReadsOf(plan);
|
|
8629
8739
|
return [
|
|
8630
|
-
`/** What the body receives: the primary read SERVED, the other reads as QueryLike, the writes as MutationLike \u2014 never a hook. */`,
|
|
8740
|
+
`/** What the body receives: ${plan.primary === null ? "no served read \u2014 a screen of content" : "the primary read SERVED"}, the other reads as QueryLike, the writes as MutationLike \u2014 never a hook. */`,
|
|
8631
8741
|
`export interface ${plan.propsName} {`,
|
|
8632
|
-
` data: ${plan.primary.operation.outputType}
|
|
8742
|
+
...plan.primary === null ? [] : [` data: ${plan.primary.operation.outputType};`],
|
|
8633
8743
|
...secondary.map((operation) => ` ${operation.id}Query: QueryLike<${operation.outputType}>;`),
|
|
8634
8744
|
...plan.writes.map((operation) => ` ${operation.id}Mutation: MutationLike<${mutationVariablesTypeOf(operation)}, ${operation.outputType}>;`),
|
|
8635
8745
|
"}"
|
|
8636
8746
|
];
|
|
8637
8747
|
}
|
|
8638
8748
|
function emitScreenContainer(plan) {
|
|
8639
|
-
const secondary =
|
|
8640
|
-
const
|
|
8641
|
-
const bodyArguments = ["data={data}", ...secondary.map((operation) => `${operation.id}Query={${operation.id}Query}`), ...plan.writes.map((operation) => `${operation.id}Mutation={${operation.id}Mutation}`)];
|
|
8749
|
+
const secondary = secondaryReadsOf(plan);
|
|
8750
|
+
const bodyArguments = [...plan.primary === null ? [] : ["data={data}"], ...secondary.map((operation) => `${operation.id}Query={${operation.id}Query}`), ...plan.writes.map((operation) => `${operation.id}Mutation={${operation.id}Mutation}`)];
|
|
8642
8751
|
const needsQueryLike = secondary.length > 0;
|
|
8643
8752
|
const needsMutationLike = plan.writes.length > 0;
|
|
8644
8753
|
const uiTypes = ["type StateCopy", ...needsQueryLike ? ["type QueryLike"] : [], ...needsMutationLike ? ["type MutationLike"] : []];
|
|
8645
|
-
const tableSkeleton = plan.primary
|
|
8754
|
+
const tableSkeleton = plan.primary?.kind === "list";
|
|
8646
8755
|
const readsImport = plan.reads.map((operation) => operation.hookName).sort();
|
|
8647
8756
|
const writesImport = [...plan.writes.map((operation) => operation.hookName).sort(), ...variablesTypesOf(plan.writes).map((name) => `type ${name}`)];
|
|
8757
|
+
const uiImports = plan.primary === null ? uiTypes : ["ScreenState", ...tableSkeleton ? [] : ["Skeleton"], ...uiTypes];
|
|
8648
8758
|
return fileOf(
|
|
8649
8759
|
tsGeneratedHeader(plan.generatorVersion),
|
|
8650
8760
|
intentBlock(
|
|
8651
8761
|
wrap(
|
|
8652
|
-
`The container of the ${plan.featureId} screen: reads the hooks \u2014 ${plan.operations.map((operation) => operation.hookName).join(", ")} \u2014, waits for the primary read \`${
|
|
8762
|
+
plan.primary === null ? `The container of the ${plan.featureId} screen: a screen of CONTENT (D56) \u2014 no read to wait for, no ScreenState; it ${plan.operations.length === 0 ? "reads no hook and " : `reads the hooks \u2014 ${plan.operations.map((operation) => operation.hookName).join(", ")} \u2014 and `}calls the written body ${plan.bodyName}. The copy comes from the body (screenCopy): the words are the product's, the wiring is generated.` : `The container of the ${plan.featureId} screen: reads the hooks \u2014 ${plan.operations.map((operation) => operation.hookName).join(", ")} \u2014, waits for the primary read \`${plan.primary.operation.id}\` through ScreenState (access denied, error, loading, then the body) and calls the written body ${plan.bodyName} with served data and the writes as MutationLike. The copy of the states comes from the body (screenCopy): the words are the product's, the wiring is generated.`
|
|
8653
8763
|
)
|
|
8654
8764
|
),
|
|
8655
8765
|
'import type { ReactNode } from "react";',
|
|
8656
|
-
`import {
|
|
8766
|
+
`import { ${uiImports.join(", ")} } from "@ogygie/ui";`,
|
|
8657
8767
|
...tableSkeleton ? ['import { DataTable } from "@ogygie/ui/data-table";'] : [],
|
|
8658
8768
|
...contractsImportLines(plan, (operation) => ({ values: [], types: [operation.outputType, ...operation.kind === "read" ? [] : [operation.inputType]] })),
|
|
8659
8769
|
...readsImport.length === 0 ? [] : [`import { ${readsImport.join(", ")} } from "./queries.gen";`],
|
|
8660
8770
|
...writesImport.length === 0 ? [] : [`import { ${writesImport.join(", ")} } from "./writes.gen";`],
|
|
8661
|
-
`import { ${plan.bodyName}, screenCopy } from "./${plan.screenName}";`,
|
|
8771
|
+
plan.primary === null ? `import { ${plan.bodyName} } from "./${plan.screenName}";` : `import { ${plan.bodyName}, screenCopy } from "./${plan.screenName}";`,
|
|
8662
8772
|
"",
|
|
8663
|
-
|
|
8664
|
-
"export interface ScreenCopy {",
|
|
8665
|
-
" errorTitle: ReactNode;",
|
|
8666
|
-
" accessDenied: StateCopy;",
|
|
8667
|
-
"}",
|
|
8773
|
+
...screenCopyLines(plan),
|
|
8668
8774
|
"",
|
|
8669
8775
|
...bodyPropsLines(plan),
|
|
8670
8776
|
"",
|
|
8671
8777
|
`export function ${plan.screenName}() {`,
|
|
8672
8778
|
...plan.reads.map((operation) => ` const ${operation.id}Query = ${operation.hookName}();`),
|
|
8673
8779
|
...plan.writes.map((operation) => ` const ${operation.id}Mutation = ${operation.hookName}();`),
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
"
|
|
8780
|
+
...plan.primary === null ? [` return <${plan.bodyName}${bodyArguments.length === 0 ? "" : ` ${bodyArguments.join(" ")}`} />;`] : [
|
|
8781
|
+
" return (",
|
|
8782
|
+
` <ScreenState query={${plan.primary.operation.id}Query} errorTitle={screenCopy.errorTitle} accessDenied={screenCopy.accessDenied} loading={${tableSkeleton ? "<DataTable.Skeleton />" : "<Skeleton />"}}>`,
|
|
8783
|
+
` {(data) => <${plan.bodyName} ${bodyArguments.join(" ")} />}`,
|
|
8784
|
+
" </ScreenState>",
|
|
8785
|
+
" );"
|
|
8786
|
+
],
|
|
8679
8787
|
"}"
|
|
8680
8788
|
);
|
|
8681
8789
|
}
|
|
@@ -8684,10 +8792,12 @@ function emitScreenBodyDefault(plan) {
|
|
|
8684
8792
|
const viewElements = plan.views.map((view) => {
|
|
8685
8793
|
if (view.kind === "list") return ` <${view.componentName} rows={data.${view.operation.collection?.field ?? "rows"}} />`;
|
|
8686
8794
|
if (view.kind === "form") return ` <${view.componentName} ${view.mutationProp}={${view.mutationProp}} />`;
|
|
8795
|
+
if (view.kind === "content") return ` <${view.componentName} />`;
|
|
8687
8796
|
const actions = view.actions.map((action) => ` ${action.mutationProp}={${action.mutationProp}}`).join("");
|
|
8688
8797
|
return ` <${view.componentName} data={data}${actions} />`;
|
|
8689
8798
|
});
|
|
8690
|
-
const destructured = ["data", ...plan.writes.map((operation) => `${operation.id}Mutation`)].filter((name) => name === "data" || plan.views.some((view) => view.kind === "form" && view.mutationProp === name || view.kind === "custom" && view.actions.some((action) => action.mutationProp === name)));
|
|
8799
|
+
const destructured = [...plan.primary === null ? [] : ["data"], ...plan.writes.map((operation) => `${operation.id}Mutation`)].filter((name) => name === "data" || plan.views.some((view) => view.kind === "form" && view.mutationProp === name || view.kind === "custom" && view.actions.some((action) => action.mutationProp === name)));
|
|
8800
|
+
const mountLabel = plan.mount === null ? null : plan.mount.id.split("-").map((word, index) => index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join(" ");
|
|
8691
8801
|
return fileOf(
|
|
8692
8802
|
intentBlock(
|
|
8693
8803
|
wrap(
|
|
@@ -8695,15 +8805,16 @@ function emitScreenBodyDefault(plan) {
|
|
|
8695
8805
|
)
|
|
8696
8806
|
),
|
|
8697
8807
|
'import { Trans } from "@lingui/react/macro";',
|
|
8698
|
-
`import type { ScreenCopy, ${plan.propsName} } from "./${plan.screenName}.container.gen";`,
|
|
8808
|
+
destructured.length === 0 ? `import type { ScreenCopy } from "./${plan.screenName}.container.gen";` : `import type { ScreenCopy, ${plan.propsName} } from "./${plan.screenName}.container.gen";`,
|
|
8699
8809
|
...viewImports,
|
|
8700
8810
|
"",
|
|
8701
8811
|
"export const screenCopy: ScreenCopy = {",
|
|
8702
8812
|
` errorTitle: <Trans>This view could not be loaded</Trans>,`,
|
|
8703
8813
|
` accessDenied: { title: <Trans>Access denied</Trans>, explanation: <Trans>Sign in with an account allowed to read this screen.</Trans> },`,
|
|
8814
|
+
...mountLabel === null ? [] : [` mountLabel: <Trans>${mountLabel}</Trans>,`],
|
|
8704
8815
|
"};",
|
|
8705
8816
|
"",
|
|
8706
|
-
`export function ${plan.bodyName}({ ${destructured.join(", ")} }: ${plan.propsName}) {`,
|
|
8817
|
+
destructured.length === 0 ? `export function ${plan.bodyName}() {` : `export function ${plan.bodyName}({ ${destructured.join(", ")} }: ${plan.propsName}) {`,
|
|
8707
8818
|
" return (",
|
|
8708
8819
|
' <section className="space-y-6">',
|
|
8709
8820
|
...viewElements,
|
|
@@ -8734,9 +8845,10 @@ function emitScreenIndex(plan) {
|
|
|
8734
8845
|
return fileOf(
|
|
8735
8846
|
tsGeneratedHeader(plan.generatorVersion),
|
|
8736
8847
|
intentBlock(
|
|
8737
|
-
wrap(`SOLE public export point of the ${plan.featureId} screen feature: its container, its body props${plan.route === null ? "" : ", its route factory"} and its hooks. Other features import ONLY from here.`)
|
|
8848
|
+
wrap(`SOLE public export point of the ${plan.featureId} screen feature: its container, its body props${plan.route === null ? "" : ", its route factory"}${plan.mount === null ? "" : ", the copy its tab label lives in"} and its hooks. Other features import ONLY from here.`)
|
|
8738
8849
|
),
|
|
8739
8850
|
`export { ${plan.screenName}, type ${plan.propsName}, type ScreenCopy } from "./${plan.screenName}.container.gen";`,
|
|
8851
|
+
...plan.mount === null ? [] : [`export { screenCopy } from "./${plan.screenName}";`],
|
|
8740
8852
|
...plan.route === null ? [] : [`export { ${plan.routeFactoryName} } from "./routes.gen";`],
|
|
8741
8853
|
...plan.reads.length === 0 ? [] : [`export { ${plan.reads.map((operation) => operation.hookName).sort().join(", ")} } from "./queries.gen";`],
|
|
8742
8854
|
...plan.writes.length === 0 ? [] : [`export { ${plan.writes.map((operation) => operation.hookName).sort().join(", ")} } from "./writes.gen";`],
|
|
@@ -8744,6 +8856,24 @@ function emitScreenIndex(plan) {
|
|
|
8744
8856
|
);
|
|
8745
8857
|
}
|
|
8746
8858
|
|
|
8859
|
+
// src/emit/screen/content.ts
|
|
8860
|
+
function contentFilePath(list) {
|
|
8861
|
+
return `content/${list.id}.gen.ts`;
|
|
8862
|
+
}
|
|
8863
|
+
function emitContentList(plan, list) {
|
|
8864
|
+
return fileOf(
|
|
8865
|
+
tsGeneratedHeader(plan.generatorVersion),
|
|
8866
|
+
intentBlock(
|
|
8867
|
+
wrap(
|
|
8868
|
+
`The content list \`${list.id}\` of the ${plan.featureId} screen: ${list.entries.length} entr${list.entries.length === 1 ? "y" : "ies"} written in the spec's block, the material a content view folds under their titles. Plain text in the product's source language; a wording change is a spec change, regenerated.`
|
|
8869
|
+
)
|
|
8870
|
+
),
|
|
8871
|
+
'import type { DisclosureEntry } from "@ogygie/ui";',
|
|
8872
|
+
"",
|
|
8873
|
+
`export const ${list.constName}: readonly DisclosureEntry[] = ${jsonLiteral(list.entries)};`
|
|
8874
|
+
);
|
|
8875
|
+
}
|
|
8876
|
+
|
|
8747
8877
|
// src/emit/screen/fixtures.ts
|
|
8748
8878
|
var FIXTURE_TIMESTAMP = "2026-01-31T12:00:00.000Z";
|
|
8749
8879
|
function failureBodyOf(code, message, cause, suggestedFix) {
|
|
@@ -8907,6 +9037,28 @@ function emitFormView(plan, view) {
|
|
|
8907
9037
|
"}"
|
|
8908
9038
|
);
|
|
8909
9039
|
}
|
|
9040
|
+
function emitContentView(plan, view) {
|
|
9041
|
+
const linked = view.list.entries.some((entry) => entry.href !== void 0);
|
|
9042
|
+
return fileOf(
|
|
9043
|
+
tsGeneratedHeader(plan.generatorVersion),
|
|
9044
|
+
intentBlock(
|
|
9045
|
+
wrap(
|
|
9046
|
+
`The ${view.id} view of the ${plan.featureId} screen (content): the ${view.list.entries.length} entries of \`${view.list.id}\` (content/${view.list.id}.gen.ts), each folded under its title and anchored \`${view.id}-<id>\`${linked ? ", its link rendered when the entry carries one" : ""}. The material is the spec's; this view only lays it out and never fetches.`
|
|
9047
|
+
)
|
|
9048
|
+
),
|
|
9049
|
+
...linked ? ['import { Trans } from "@lingui/react/macro";'] : [],
|
|
9050
|
+
'import { DisclosureList } from "@ogygie/ui";',
|
|
9051
|
+
`import { ${view.list.constName} } from "../content/${view.list.id}.gen";`,
|
|
9052
|
+
"",
|
|
9053
|
+
`export function ${view.componentName}() {`,
|
|
9054
|
+
" return (",
|
|
9055
|
+
` <section data-view=${quote(view.id)}>`,
|
|
9056
|
+
` <DisclosureList entries={${view.list.constName}} anchorPrefix=${quote(view.id)}${linked ? ` linkLabel={${trans("Open")}}` : ""} />`,
|
|
9057
|
+
" </section>",
|
|
9058
|
+
" );",
|
|
9059
|
+
"}"
|
|
9060
|
+
);
|
|
9061
|
+
}
|
|
8910
9062
|
function customViewPropsLines(view) {
|
|
8911
9063
|
return [
|
|
8912
9064
|
`export interface ${view.componentName}Props {`,
|
|
@@ -9008,19 +9160,19 @@ function emitScreenHoleDoc(plan, view) {
|
|
|
9008
9160
|
// src/emit/screen/previews.ts
|
|
9009
9161
|
var PREVIEWS_FILE_SUFFIX = ".previews.gen.test.tsx";
|
|
9010
9162
|
function emitScreenPreviews(plan) {
|
|
9011
|
-
const primary = plan.primary
|
|
9012
|
-
const primaryPrefix = fixturePrefixOf(primary);
|
|
9013
|
-
const others = plan.reads.filter((read) => read.id !== primary
|
|
9014
|
-
const used =
|
|
9163
|
+
const primary = plan.primary?.operation ?? null;
|
|
9164
|
+
const primaryPrefix = primary === null ? "" : fixturePrefixOf(primary);
|
|
9165
|
+
const others = plan.reads.filter((read) => read.id !== primary?.id);
|
|
9166
|
+
const used = new Set(primary === null ? [] : ["ACCESS_DENIED_FAILURE", "INTERNAL_FAILURE", `${primaryPrefix}_OUTPUT`]);
|
|
9015
9167
|
for (const read of others) used.add(`${fixturePrefixOf(read)}_OUTPUT`);
|
|
9016
|
-
if (primary.emptyOutputFixture !== null) used.add(`${primaryPrefix}_EMPTY`);
|
|
9168
|
+
if (primary !== null && primary.emptyOutputFixture !== null) used.add(`${primaryPrefix}_EMPTY`);
|
|
9017
9169
|
const serveOthers = others.map((read) => ` client.setQueryData(${read.keyLiteral}, ${fixturePrefixOf(read)}_OUTPUT);`);
|
|
9018
9170
|
const arranged = (lines) => [
|
|
9019
9171
|
` render: () => renderStatic(<${plan.screenName} />, (client) => {`,
|
|
9020
9172
|
...lines,
|
|
9021
9173
|
" }),"
|
|
9022
9174
|
];
|
|
9023
|
-
const previews = [
|
|
9175
|
+
const previews = primary === null ? [" {", ` state: "nominal",`, ...serveOthers.length === 0 ? [` render: () => renderStatic(<${plan.screenName} />),`] : arranged(serveOthers), " },"] : [
|
|
9024
9176
|
" {",
|
|
9025
9177
|
` state: "loading",`,
|
|
9026
9178
|
` render: () => renderStatic(<${plan.screenName} />),`,
|
|
@@ -9044,25 +9196,26 @@ function emitScreenPreviews(plan) {
|
|
|
9044
9196
|
"// @vitest-environment node",
|
|
9045
9197
|
intentBlock(
|
|
9046
9198
|
wrap(
|
|
9047
|
-
`Previews of the ${plan.featureId} screen: one self-contained HTML page per UI state \u2014 loading, error, denied${primary.emptyOutputFixture === null ? "" : ", empty"}, nominal \u2014 rendered by the front's bench on the fixtures of fixtures.gen.ts, the state arranged in the query cache before the render as the static tests do. A test first: every state renders in the loop. With OGYGIE_PREVIEW_OUT set to a directory, the pages are also written there (<state>.html) \u2014 how the speculate job publishes them on the spec branch under previews/<id>/, where the hosted studio frames them.`
|
|
9199
|
+
`Previews of the ${plan.featureId} screen: one self-contained HTML page per UI state \u2014 ${primary === null ? "nominal alone, a screen of content waits for no read" : `loading, error, denied${primary.emptyOutputFixture === null ? "" : ", empty"}, nominal`} \u2014 rendered by the front's bench${used.size === 0 ? "" : " on the fixtures of fixtures.gen.ts, the state arranged in the query cache before the render as the static tests do"}. A test first: every state renders in the loop. With OGYGIE_PREVIEW_OUT set to a directory, the pages are also written there (<state>.html) \u2014 how the speculate job publishes them on the spec branch under previews/<id>/, where the hosted studio frames them.`
|
|
9048
9200
|
)
|
|
9049
9201
|
),
|
|
9050
9202
|
'import { mkdirSync, writeFileSync } from "node:fs";',
|
|
9051
9203
|
'import { join } from "node:path";',
|
|
9052
9204
|
'import { describe, expect, it } from "vitest";',
|
|
9053
|
-
'import type { QueryClient } from "@tanstack/react-query";',
|
|
9054
|
-
'import { OgygieFailure, type OgygieError } from "@ogygie/ui";',
|
|
9205
|
+
...primary === null ? [] : ['import type { QueryClient } from "@tanstack/react-query";', 'import { OgygieFailure, type OgygieError } from "@ogygie/ui";'],
|
|
9055
9206
|
`import { ${plan.testBench.exportName === "renderStatic" ? "renderStatic" : `${plan.testBench.exportName} as renderStatic`} } from ${quote(plan.testBench.importPath)};`,
|
|
9056
9207
|
`import { ${plan.screenName} } from "./${plan.screenName}.container.gen";`,
|
|
9057
|
-
`import { ${[...used].sort().join(", ")} } from "./fixtures.gen"
|
|
9208
|
+
...used.size === 0 ? [] : [`import { ${[...used].sort().join(", ")} } from "./fixtures.gen";`],
|
|
9058
9209
|
...plan.stylesheetImport === null ? [] : [`import stylesheet from ${quote(plan.stylesheetImport)};`],
|
|
9059
9210
|
"",
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9211
|
+
...primary === null ? [] : [
|
|
9212
|
+
"/** Arranges a read as FAILED: the cache holds the OgygieFailure the api would have answered, and the read is not retried on mount. */",
|
|
9213
|
+
"function failRead(client: QueryClient, key: readonly unknown[], failure: { readonly status: number; readonly body: OgygieError }): void {",
|
|
9214
|
+
" client.setQueryDefaults(key, { retryOnMount: false });",
|
|
9215
|
+
' client.getQueryCache().build(client, { queryKey: key }).setState({ status: "error", error: new OgygieFailure(failure.body, failure.status), fetchStatus: "idle" });',
|
|
9216
|
+
"}",
|
|
9217
|
+
""
|
|
9218
|
+
],
|
|
9066
9219
|
plan.stylesheetImport === null ? '/** No stylesheet is declared on the front target (generation.front.stylesheet): the page carries the markup alone. */\nconst STYLESHEET = "";' : "/** The product's stylesheet, compiled by Vite and inlined: the page carries the product's tokens. */\nconst STYLESHEET: string = stylesheet;",
|
|
9067
9220
|
"",
|
|
9068
9221
|
"/** A self-contained page around the rendered screen: the hosted studio frames it in a sandbox, nothing else is loaded. */",
|
|
@@ -9190,6 +9343,26 @@ function readLineBody(plan, behavior, view, used) {
|
|
|
9190
9343
|
}
|
|
9191
9344
|
return lines;
|
|
9192
9345
|
}
|
|
9346
|
+
function contentLineBody(plan, behavior, view, used) {
|
|
9347
|
+
const others = plan.reads.map((read) => {
|
|
9348
|
+
used.add(`${fixturePrefixOf(read)}_OUTPUT`);
|
|
9349
|
+
return `client.setQueryData(${read.keyLiteral}, ${fixturePrefixOf(read)}_OUTPUT);`;
|
|
9350
|
+
});
|
|
9351
|
+
const lines = others.length === 0 ? [` const html = renderStatic(<${plan.screenName} />);`] : [` const html = renderStatic(<${plan.screenName} />, (client) => {`, ...others.map((line) => ` ${line}`), " });"];
|
|
9352
|
+
lines.push(` const view = viewMarkup(html, ${quote(view.id)});`);
|
|
9353
|
+
for (const render of behavior.behavior.renders) {
|
|
9354
|
+
if (render === "entr\xE9es") {
|
|
9355
|
+
lines.push(...view.list.entries.map((entry) => ` expect(textOf(view)).toContain(${quote(entry.title)});`));
|
|
9356
|
+
} else if (render === "repli\xE9es") {
|
|
9357
|
+
lines.push(` expect(/<details\\b[^>]*\\sopen\\b/u.test(view)).toBe(false);`);
|
|
9358
|
+
} else {
|
|
9359
|
+
const anchor = /^ancre (\S+)$/u.exec(render);
|
|
9360
|
+
invariant4(anchor !== null && anchor[1] !== void 0, `render ${render} is not of a content view's vocabulary`);
|
|
9361
|
+
lines.push(` expect(view).toContain(${quote(`id="${view.id}-${anchor[1]}"`)});`);
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
return lines;
|
|
9365
|
+
}
|
|
9193
9366
|
function customViewTest(view) {
|
|
9194
9367
|
const actions = view.actions.map((action) => `${action.mutationProp}={idleMutation()}`).join(" ");
|
|
9195
9368
|
return [
|
|
@@ -9205,7 +9378,9 @@ function emitStaticScreenTests(plan) {
|
|
|
9205
9378
|
const tests = plan.behaviors.flatMap((behavior) => {
|
|
9206
9379
|
if (behavior.hole !== null) return ["", ` it.todo(${quote(behavior.raw)});`];
|
|
9207
9380
|
const view = behavior.view ?? plan.primary;
|
|
9381
|
+
invariant4(view !== null, "a line speaks of a view, or of the primary read of a screen that has one");
|
|
9208
9382
|
if (view.kind === "form") return ["", ` it.todo(${quote(`${behavior.raw} \u2014 ${STATIC_RENDER_GESTURE_REASON}`)});`];
|
|
9383
|
+
if (view.kind === "content") return ["", ` it(${quote(behavior.raw)}, () => {`, ...contentLineBody(plan, behavior, view, used), " });"];
|
|
9209
9384
|
return ["", ` it(${quote(behavior.raw)}, () => {`, ...readLineBody(plan, behavior, view, used), " });"];
|
|
9210
9385
|
});
|
|
9211
9386
|
return fileOf(
|
|
@@ -9217,9 +9392,8 @@ function emitStaticScreenTests(plan) {
|
|
|
9217
9392
|
)
|
|
9218
9393
|
),
|
|
9219
9394
|
'import { describe, expect, it } from "vitest";',
|
|
9220
|
-
'import type { QueryClient } from "@tanstack/react-query";',
|
|
9221
|
-
|
|
9222
|
-
`import { ${[...customViews.some((view) => view.actions.length > 0) ? ["idleMutation"] : [], "OgygieFailure", "type OgygieError"].join(", ")} } from "@ogygie/ui";`,
|
|
9395
|
+
...plan.reads.length === 0 ? [] : ['import type { QueryClient } from "@tanstack/react-query";'],
|
|
9396
|
+
...plan.reads.length === 0 && !customViews.some((view) => view.actions.length > 0) ? [] : [`import { ${[...customViews.some((view) => view.actions.length > 0) ? ["idleMutation"] : [], ...plan.reads.length === 0 ? [] : ["OgygieFailure", "type OgygieError"]].join(", ")} } from "@ogygie/ui";`],
|
|
9223
9397
|
`import { ${plan.testBench.exportName === "renderStatic" ? "renderStatic" : `${plan.testBench.exportName} as renderStatic`} } from ${quote(plan.testBench.importPath)};`,
|
|
9224
9398
|
`import { ${plan.screenName} } from "./${plan.screenName}.container.gen";`,
|
|
9225
9399
|
...customViews.map((view) => `import { ${view.componentName} } from "./views/${view.componentName}";`),
|
|
@@ -9228,12 +9402,14 @@ function emitStaticScreenTests(plan) {
|
|
|
9228
9402
|
"// failRead, screenStateOf, viewMarkup, bodyRows, columnTexts, sortedTexts and textOf are emitted into every generated static screen test:",
|
|
9229
9403
|
"// candidates for a test kit of @ogygie/ui at the third repetition (AGENTS.md workflow 5) \u2014 not promoted yet.",
|
|
9230
9404
|
"",
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9405
|
+
...plan.reads.length === 0 ? [] : [
|
|
9406
|
+
"/** Arranges a read as FAILED: the cache holds the OgygieFailure the api would have answered, and the read is not retried on mount \u2014 the markup shows the failure, not the skeleton again. */",
|
|
9407
|
+
"function failRead(client: QueryClient, key: readonly unknown[], failure: { readonly status: number; readonly body: OgygieError }): void {",
|
|
9408
|
+
" client.setQueryDefaults(key, { retryOnMount: false });",
|
|
9409
|
+
' client.getQueryCache().build(client, { queryKey: key }).setState({ status: "error", error: new OgygieFailure(failure.body, failure.status), fetchStatus: "idle" });',
|
|
9410
|
+
"}",
|
|
9411
|
+
""
|
|
9412
|
+
],
|
|
9237
9413
|
"/** The state ScreenState shows, null once the body renders. */",
|
|
9238
9414
|
"function screenStateOf(html: string): string | null {",
|
|
9239
9415
|
' const wrapper = /<[^<>]*\\sdata-pattern="ScreenState"[^<>]*>/u.exec(html)?.[0];',
|
|
@@ -9373,6 +9549,21 @@ function formLineBody(plan, behavior, view) {
|
|
|
9373
9549
|
}
|
|
9374
9550
|
return lines;
|
|
9375
9551
|
}
|
|
9552
|
+
function contentLineBody2(plan, behavior, view) {
|
|
9553
|
+
const lines = [` stubApi(${routesLiteral(defaultReadRoutes(plan, null))});`, ` renderWithProviders(<${plan.screenName} />);`, ` const view = await waitFor(() => viewOf(${quote(view.id)}));`];
|
|
9554
|
+
for (const render of behavior.behavior.renders) {
|
|
9555
|
+
if (render === "entr\xE9es") {
|
|
9556
|
+
lines.push(...view.list.entries.map((entry) => ` expect(view.textContent ?? "").toContain(${quote(entry.title)});`));
|
|
9557
|
+
} else if (render === "repli\xE9es") {
|
|
9558
|
+
lines.push(` expect(view.querySelectorAll("details[open]")).toHaveLength(0);`);
|
|
9559
|
+
} else {
|
|
9560
|
+
const anchor = /^ancre (\S+)$/u.exec(render);
|
|
9561
|
+
invariant5(anchor !== null && anchor[1] !== void 0, `render ${render} is not of a content view's vocabulary`);
|
|
9562
|
+
lines.push(` expect(view.querySelector(${quote(`#${view.id}-${anchor[1]}`)})).not.toBeNull();`);
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
return lines;
|
|
9566
|
+
}
|
|
9376
9567
|
function successStatusOf(operation) {
|
|
9377
9568
|
return operation.kind === "create" || operation.kind === "import" ? 201 : 200;
|
|
9378
9569
|
}
|
|
@@ -9390,7 +9581,7 @@ function emitScreenTests(plan) {
|
|
|
9390
9581
|
}
|
|
9391
9582
|
function emitJsdomScreenTests(plan) {
|
|
9392
9583
|
const customViews = plan.views.filter((view) => view.kind === "custom");
|
|
9393
|
-
const fixtureNames =
|
|
9584
|
+
const fixtureNames = new Set(plan.reads.length === 0 ? [] : ["ACCESS_DENIED_FAILURE", "INTERNAL_FAILURE"]);
|
|
9394
9585
|
for (const operation of plan.reads) {
|
|
9395
9586
|
fixtureNames.add(`${fixturePrefixOf(operation)}_OUTPUT`);
|
|
9396
9587
|
if (operation.emptyOutputFixture !== null) fixtureNames.add(`${fixturePrefixOf(operation)}_EMPTY`);
|
|
@@ -9403,7 +9594,8 @@ function emitJsdomScreenTests(plan) {
|
|
|
9403
9594
|
const tests = plan.behaviors.flatMap((behavior) => {
|
|
9404
9595
|
if (behavior.hole !== null) return ["", ` it.todo(${quote(behavior.raw)});`];
|
|
9405
9596
|
const view = behavior.view ?? plan.primary;
|
|
9406
|
-
|
|
9597
|
+
invariant5(view !== null, "a line speaks of a view, or of the primary read of a screen that has one");
|
|
9598
|
+
const body = view.kind === "form" ? formLineBody(plan, behavior, view) : view.kind === "content" ? contentLineBody2(plan, behavior, view) : readLineBody2(plan, behavior, view);
|
|
9407
9599
|
return ["", ` it(${quote(behavior.raw)}, async () => {`, ...body, " });"];
|
|
9408
9600
|
});
|
|
9409
9601
|
return fileOf(
|
|
@@ -9420,7 +9612,7 @@ function emitJsdomScreenTests(plan) {
|
|
|
9420
9612
|
`import { ${plan.testBench.exportName === "renderWithProviders" ? "renderWithProviders" : `${plan.testBench.exportName} as renderWithProviders`} } from ${quote(plan.testBench.importPath)};`,
|
|
9421
9613
|
`import { ${plan.screenName} } from "./${plan.screenName}.container.gen";`,
|
|
9422
9614
|
...customViews.map((view) => `import { ${view.componentName} } from "./views/${view.componentName}";`),
|
|
9423
|
-
`import { ${[...fixtureNames].sort().join(", ")} } from "./fixtures.gen"
|
|
9615
|
+
...fixtureNames.size === 0 ? [] : [`import { ${[...fixtureNames].sort().join(", ")} } from "./fixtures.gen";`],
|
|
9424
9616
|
"",
|
|
9425
9617
|
"interface StubRoute {",
|
|
9426
9618
|
" method?: string;",
|
|
@@ -9576,9 +9768,12 @@ function generateScreen(plan, options = { existingCustomViews: /* @__PURE__ */ n
|
|
|
9576
9768
|
return [
|
|
9577
9769
|
...plan.reads.length === 0 ? [] : [inFolder("queries.gen.ts", emitScreenQueries(plan))],
|
|
9578
9770
|
...plan.writes.length === 0 ? [] : [inFolder("writes.gen.ts", emitScreenWrites(plan))],
|
|
9771
|
+
// D56: the material of the content views, one constant per list, before the views that render it.
|
|
9772
|
+
...plan.contentLists.map((list) => inFolder(contentFilePath(list), emitContentList(plan, list))),
|
|
9579
9773
|
...plan.views.map((view) => {
|
|
9580
9774
|
if (view.kind === "list") return inFolder(`views/${view.componentName}.gen.tsx`, emitListView(plan, view));
|
|
9581
9775
|
if (view.kind === "form") return inFolder(`views/${view.componentName}.gen.tsx`, emitFormView(plan, view));
|
|
9776
|
+
if (view.kind === "content") return inFolder(`views/${view.componentName}.gen.tsx`, emitContentView(plan, view));
|
|
9582
9777
|
return inFolder(`views/${view.componentName}.tsx`, emitCustomView(plan, view, options.existingCustomViews.get(view.id) ?? null));
|
|
9583
9778
|
}),
|
|
9584
9779
|
inFolder(`${plan.screenName}.container.gen.tsx`, emitScreenContainer(plan)),
|
|
@@ -9593,7 +9788,7 @@ function generateScreen(plan, options = { existingCustomViews: /* @__PURE__ */ n
|
|
|
9593
9788
|
}
|
|
9594
9789
|
|
|
9595
9790
|
// src/verify-generation.ts
|
|
9596
|
-
import { posix as
|
|
9791
|
+
import { posix as posix4 } from "node:path";
|
|
9597
9792
|
|
|
9598
9793
|
// ../ogygie-core/src/scan.ts
|
|
9599
9794
|
var NEVER_SCANNED_DIRECTORIES = [
|
|
@@ -9608,8 +9803,42 @@ function isScannableDirectory(name) {
|
|
|
9608
9803
|
return !NEVER_SCANNED_DIRECTORIES.includes(name);
|
|
9609
9804
|
}
|
|
9610
9805
|
|
|
9806
|
+
// src/emit/screen/extensions.ts
|
|
9807
|
+
import { posix as posix3 } from "node:path";
|
|
9808
|
+
var EXTENSION_REGISTRY_FILE = "extensions.gen.ts";
|
|
9809
|
+
function extensionRegistryPath(appPath, point) {
|
|
9810
|
+
return `${appPath}/${point.folder.replace(/^\.?\//u, "").replace(/\/+$/u, "")}/${EXTENSION_REGISTRY_FILE}`;
|
|
9811
|
+
}
|
|
9812
|
+
function emitExtensionRegistry(input) {
|
|
9813
|
+
const contributions = [...input.contributions].sort((left, right) => left.mountId.localeCompare(right.mountId, "en"));
|
|
9814
|
+
const imports = contributions.map((contribution) => {
|
|
9815
|
+
const relative = posix3.relative(input.hostFolder, contribution.folder);
|
|
9816
|
+
const specifier = relative.startsWith(".") ? relative : `./${relative}`;
|
|
9817
|
+
return `import { ${pascalCaseOf(contribution.featureId)}Screen, screenCopy as ${camelCaseOf(contribution.featureId)}Copy } from ${quote(specifier)};`;
|
|
9818
|
+
});
|
|
9819
|
+
const entries = contributions.map(
|
|
9820
|
+
(contribution) => ` { id: ${quote(contribution.mountId)}, label: ${camelCaseOf(contribution.featureId)}Copy.mountLabel, render: () => createElement(${pascalCaseOf(contribution.featureId)}Screen) },`
|
|
9821
|
+
);
|
|
9822
|
+
return fileOf(
|
|
9823
|
+
tsGeneratedHeader(input.generatorVersion),
|
|
9824
|
+
intentBlock(
|
|
9825
|
+
wrap(
|
|
9826
|
+
`The registry of the \`${input.point.id}\` extension point (${input.point.kind}): ${contributions.length === 0 ? "no screen mounts here yet \u2014 the host's import holds on an empty list" : `the ${contributions.length} screen${contributions.length === 1 ? "" : "s"} mounted here (${contributions.map((contribution) => contribution.featureId).join(", ")}), each a TabContribution \u2014 its label from the copy of its written body, its container to render`}. Regenerated with every mounted screen; the host screen imports it once and keeps its own tab bar.`
|
|
9827
|
+
)
|
|
9828
|
+
),
|
|
9829
|
+
...contributions.length === 0 ? [] : ['import { createElement } from "react";'],
|
|
9830
|
+
'import type { TabContribution } from "@ogygie/ui";',
|
|
9831
|
+
...imports,
|
|
9832
|
+
"",
|
|
9833
|
+
`export const point = ${quote(input.point.id)};`,
|
|
9834
|
+
"",
|
|
9835
|
+
...contributions.length === 0 ? ["export const contributions: readonly TabContribution[] = [];"] : ["export const contributions: readonly TabContribution[] = [", ...entries, "];"]
|
|
9836
|
+
);
|
|
9837
|
+
}
|
|
9838
|
+
|
|
9611
9839
|
// src/verify-generation.ts
|
|
9612
9840
|
var GENERATED_DRIFT = "OGYGIE_GENERATED_DRIFT";
|
|
9841
|
+
var EXTENSION_POINT_UNWIRED = "OGYGIE_EXTENSION_POINT_UNWIRED";
|
|
9613
9842
|
var GENERATOR_VERSION = PACKAGE_VERSION;
|
|
9614
9843
|
function normalizeForComparison(text) {
|
|
9615
9844
|
return `${text.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.replace(/[ \t]+$/u, "")).join("\n").replace(/\n+$/u, "")}
|
|
@@ -9619,6 +9848,20 @@ var GENERATED_HEADER = /@generated by ogygie-generate (\S+)/u;
|
|
|
9619
9848
|
var GENERATED_FILE_NAME = /\.gen\.(?:test\.)?(?:ts|tsx|sql)$/u;
|
|
9620
9849
|
var HEADER_CANDIDATE = /\.(?:ts|tsx|sql)$/u;
|
|
9621
9850
|
var SPEC_FILE_NAME = /^[^/_][^/]*\.spec\.md$/u;
|
|
9851
|
+
function extensionHostAppPathOf(point, frontApps) {
|
|
9852
|
+
if (point.app !== void 0) return frontApps.find((app) => app.name === point.app)?.path ?? null;
|
|
9853
|
+
return frontApps.length === 1 ? frontApps[0]?.path ?? null : null;
|
|
9854
|
+
}
|
|
9855
|
+
function extensionRegistriesOf(input) {
|
|
9856
|
+
return input.points.map((point) => {
|
|
9857
|
+
const appPath = extensionHostAppPathOf(point, input.frontApps);
|
|
9858
|
+
if (appPath === null) return { point, hostFolder: null, file: null };
|
|
9859
|
+
const path = extensionRegistryPath(appPath, point);
|
|
9860
|
+
const hostFolder = path.slice(0, -`/${EXTENSION_REGISTRY_FILE}`.length);
|
|
9861
|
+
const contributions = input.mounted.filter((contribution) => contribution.point === point.id);
|
|
9862
|
+
return { point, hostFolder, file: { path, content: emitExtensionRegistry({ point, hostFolder, contributions, generatorVersion: input.generatorVersion }) } };
|
|
9863
|
+
});
|
|
9864
|
+
}
|
|
9622
9865
|
var GOLDEN_DIRECTORY = "__golden__";
|
|
9623
9866
|
function isScannablePath(relativePath) {
|
|
9624
9867
|
const segments = relativePath.split("/");
|
|
@@ -9748,7 +9991,7 @@ async function regenerateScreenAndCompare(input, target) {
|
|
|
9748
9991
|
}
|
|
9749
9992
|
async function regenerateAndCompare(input, target) {
|
|
9750
9993
|
const problems = [];
|
|
9751
|
-
const migrationsFolder =
|
|
9994
|
+
const migrationsFolder = posix4.normalize(`${target.apiAppPath}/${target.generation.target.migrations}`);
|
|
9752
9995
|
const migrationNames = (await input.repo.listDirectory(migrationsFolder)).map((entry) => entry.path).filter((name) => !name.includes("/"));
|
|
9753
9996
|
const featureId = target.specPath.split("/").pop()?.replace(/\.spec\.md$/u, "") ?? target.specPath;
|
|
9754
9997
|
const plan = buildFeaturePlan({
|
|
@@ -9846,6 +10089,7 @@ async function verifyGeneration(input) {
|
|
|
9846
10089
|
if (input.generation !== null && input.apiAppPath !== void 0) {
|
|
9847
10090
|
await checkDeclaredExports(input.repo, input.generation, input.apiAppPath, problems);
|
|
9848
10091
|
}
|
|
10092
|
+
const mounted = [];
|
|
9849
10093
|
const specEntries = (await input.repo.listDirectory(specsFolder)).filter(
|
|
9850
10094
|
(entry) => SPEC_FILE_NAME.test(entry.path)
|
|
9851
10095
|
);
|
|
@@ -9859,6 +10103,9 @@ async function verifyGeneration(input) {
|
|
|
9859
10103
|
if (first.kind === "screen" && first.screen !== null && input.loadConsumed !== void 0) {
|
|
9860
10104
|
const consumed = await input.loadConsumed(first.screen);
|
|
9861
10105
|
report2 = checkSpec({ markdown, specPath, guards, generation: input.generation, consumed });
|
|
10106
|
+
if (report2.screen?.mounts !== void 0 && owner !== void 0 && folder !== void 0 && owner.appType === "front") {
|
|
10107
|
+
mounted.push({ featureId: owner.featureId ?? folder.split("/").pop() ?? folder, folder, mountId: report2.screen.mounts.id, point: report2.screen.mounts.point });
|
|
10108
|
+
}
|
|
9862
10109
|
if (report2.ok && report2.screen !== null && owner !== void 0 && folder !== void 0 && owner.appType === "front" && owner.appPath !== void 0 && input.generation !== null) {
|
|
9863
10110
|
const regenerated = await regenerateScreenAndCompare(input, {
|
|
9864
10111
|
specPath,
|
|
@@ -9910,6 +10157,48 @@ async function verifyGeneration(input) {
|
|
|
9910
10157
|
specs.push({ specPath, generated: report2.generated, tally: report2.tally });
|
|
9911
10158
|
problems.push(...report2.problems);
|
|
9912
10159
|
}
|
|
10160
|
+
const points = input.generation?.front?.extensionPoints ?? [];
|
|
10161
|
+
if (points.length > 0 && input.frontApps !== void 0) {
|
|
10162
|
+
for (const registry of extensionRegistriesOf({ points, frontApps: input.frontApps, mounted, generatorVersion: GENERATOR_VERSION })) {
|
|
10163
|
+
if (registry.file === null) {
|
|
10164
|
+
problems.push(
|
|
10165
|
+
ogygieError({
|
|
10166
|
+
code: "OGYGIE_GENERATION_BLOCK_INVALID",
|
|
10167
|
+
message: `The extension point ${registry.point.id} names no front app the map knows.`,
|
|
10168
|
+
file: "ogygie.manifest.json",
|
|
10169
|
+
cause: `generation.front.extensionPoints[${registry.point.id}]: the map has ${input.frontApps.length} front apps and the point names none of them`,
|
|
10170
|
+
suggestedFix: "Set `app` on the point to the name of the front app carrying the host screen.",
|
|
10171
|
+
details: { path: "generation.front.extensionPoints", point: registry.point.id }
|
|
10172
|
+
})
|
|
10173
|
+
);
|
|
10174
|
+
continue;
|
|
10175
|
+
}
|
|
10176
|
+
expectedPaths.add(registry.file.path);
|
|
10177
|
+
problems.push(...await compareGeneratedFiles(input, [registry.file], "ogygie.manifest.json", `--all`));
|
|
10178
|
+
if (!await input.repo.exists(registry.hostFolder)) continue;
|
|
10179
|
+
const hostFiles = (await input.repo.listDirectory(registry.hostFolder)).filter((entry) => /\.tsx?$/u.test(entry.path) && !entry.path.endsWith(EXTENSION_REGISTRY_FILE) && !entry.path.includes("/"));
|
|
10180
|
+
let wired = false;
|
|
10181
|
+
for (const entry of hostFiles) {
|
|
10182
|
+
const source = await input.repo.readFile(`${registry.hostFolder}/${entry.path}`);
|
|
10183
|
+
if (/from\s+["']\.\/extensions\.gen(?:\.js)?["']/u.test(source)) {
|
|
10184
|
+
wired = true;
|
|
10185
|
+
break;
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
if (!wired) {
|
|
10189
|
+
problems.push(
|
|
10190
|
+
ogygieError({
|
|
10191
|
+
code: EXTENSION_POINT_UNWIRED,
|
|
10192
|
+
message: `The extension point ${registry.point.id} has a registry nobody reads.`,
|
|
10193
|
+
file: registry.hostFolder,
|
|
10194
|
+
cause: `no file of ${registry.hostFolder} imports "./extensions.gen"`,
|
|
10195
|
+
suggestedFix: `In the host screen, import { contributions } from "./extensions.gen" and render them in its tab bar \u2014 once, by hand; the registry is regenerated with every mounted screen.`,
|
|
10196
|
+
details: { point: registry.point.id, folder: registry.hostFolder }
|
|
10197
|
+
})
|
|
10198
|
+
);
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
}
|
|
9913
10202
|
const generatedFiles = [];
|
|
9914
10203
|
for (const directory of input.scanDirs) {
|
|
9915
10204
|
for (const entry of await input.repo.listDirectory(directory)) {
|
|
@@ -10585,7 +10874,7 @@ function loadGenerateProject(root) {
|
|
|
10585
10874
|
if (feature.spec === null) return [];
|
|
10586
10875
|
const owningApp = feature.app === void 0 ? apiApp : manifest.apps.find((app) => app.name === feature.app);
|
|
10587
10876
|
if (owningApp === void 0) return [];
|
|
10588
|
-
return [{ specPath: feature.spec, folder: `${owningApp.path}/${feature.folder}`, declared: { routes: feature.routes, dbTables: feature.dbTables }, appPath: owningApp.path, appType: owningApp.type }];
|
|
10877
|
+
return [{ featureId: feature.name, specPath: feature.spec, folder: `${owningApp.path}/${feature.folder}`, declared: { routes: feature.routes, dbTables: feature.dbTables }, appPath: owningApp.path, appType: owningApp.type }];
|
|
10589
10878
|
});
|
|
10590
10879
|
return {
|
|
10591
10880
|
ok: true,
|
|
@@ -10712,9 +11001,54 @@ async function loadConsumedFeatures(project, block) {
|
|
|
10712
11001
|
}
|
|
10713
11002
|
|
|
10714
11003
|
// src/cli/generate-screen-command.ts
|
|
11004
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
11005
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
11006
|
+
import { performance } from "node:perf_hooks";
|
|
11007
|
+
|
|
11008
|
+
// src/cli/extension-registries.ts
|
|
10715
11009
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
10716
11010
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
10717
|
-
|
|
11011
|
+
function mountedScreensOf(project) {
|
|
11012
|
+
const mounted = [];
|
|
11013
|
+
for (const feature of project.manifest.features) {
|
|
11014
|
+
if (feature.spec === null || targetKindOf(project, feature.name) !== "screen") continue;
|
|
11015
|
+
const specPath = join4(project.root, feature.spec);
|
|
11016
|
+
if (!existsSync3(specPath)) continue;
|
|
11017
|
+
const report2 = checkSpec({ markdown: readFileSync4(specPath, "utf8"), specPath: feature.spec, generation: project.generation });
|
|
11018
|
+
const mounts = report2.screen?.mounts;
|
|
11019
|
+
if (mounts === void 0) continue;
|
|
11020
|
+
const owningApp = feature.app === void 0 ? void 0 : project.manifest.apps.find((app) => app.name === feature.app);
|
|
11021
|
+
if (owningApp === void 0) continue;
|
|
11022
|
+
mounted.push({ featureId: feature.name, folder: `${owningApp.path}/${feature.folder}`, mountId: mounts.id, point: mounts.point });
|
|
11023
|
+
}
|
|
11024
|
+
return mounted;
|
|
11025
|
+
}
|
|
11026
|
+
function writeExtensionRegistries(project, out, pointIds) {
|
|
11027
|
+
const declared = project.generation?.front?.extensionPoints ?? [];
|
|
11028
|
+
const points = pointIds === void 0 ? declared : declared.filter((point) => pointIds.includes(point.id));
|
|
11029
|
+
if (points.length === 0) return 0;
|
|
11030
|
+
const frontApps = project.manifest.apps.filter((app) => app.type === "front").map((app) => ({ name: app.name, path: app.path }));
|
|
11031
|
+
let code = 0;
|
|
11032
|
+
for (const registry of extensionRegistriesOf({ points, frontApps, mounted: mountedScreensOf(project), generatorVersion: GENERATOR_VERSION })) {
|
|
11033
|
+
if (registry.file === null) {
|
|
11034
|
+
out.error(`ogygie generate \u2014 le point d'extension ${registry.point.id} ne nomme aucune app front de la carte (${frontApps.length} app(s) front) : posez \`app\` sur le point.`);
|
|
11035
|
+
code = 1;
|
|
11036
|
+
continue;
|
|
11037
|
+
}
|
|
11038
|
+
const absolute = join4(project.root, registry.file.path);
|
|
11039
|
+
const exists = existsSync3(absolute);
|
|
11040
|
+
if (exists && readFileSync4(absolute, "utf8") === registry.file.content) {
|
|
11041
|
+
out.log(` identique ${registry.file.path}`);
|
|
11042
|
+
continue;
|
|
11043
|
+
}
|
|
11044
|
+
mkdirSync(dirname2(absolute), { recursive: true });
|
|
11045
|
+
writeFileSync(absolute, registry.file.content, "utf8");
|
|
11046
|
+
out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${registry.file.path} (registre du point ${registry.point.id})`);
|
|
11047
|
+
}
|
|
11048
|
+
return code;
|
|
11049
|
+
}
|
|
11050
|
+
|
|
11051
|
+
// src/cli/generate-screen-command.ts
|
|
10718
11052
|
function consumedInputsOf(loaded) {
|
|
10719
11053
|
const inputs = [];
|
|
10720
11054
|
for (const feature of loaded.values()) {
|
|
@@ -10730,7 +11064,7 @@ async function screenPlanOf(project, featureId, out, command = "ogygie generate"
|
|
|
10730
11064
|
return null;
|
|
10731
11065
|
}
|
|
10732
11066
|
const { target } = lookup;
|
|
10733
|
-
const markdown =
|
|
11067
|
+
const markdown = readFileSync5(join5(project.root, target.specPath), "utf8");
|
|
10734
11068
|
const first = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
|
|
10735
11069
|
if (first.kind !== "screen" || first.screen === null) {
|
|
10736
11070
|
for (const problem2 of first.problems) out.error(formatProblem(problem2));
|
|
@@ -10767,8 +11101,8 @@ async function runGenerateScreenCommand(project, featureId, out) {
|
|
|
10767
11101
|
const existingCustomViews = /* @__PURE__ */ new Map();
|
|
10768
11102
|
for (const view of plan.views) {
|
|
10769
11103
|
if (view.kind !== "custom") continue;
|
|
10770
|
-
const path =
|
|
10771
|
-
existingCustomViews.set(view.id,
|
|
11104
|
+
const path = join5(project.root, plan.folder, "views", `${view.componentName}.tsx`);
|
|
11105
|
+
existingCustomViews.set(view.id, existsSync4(path) ? readFileSync5(path, "utf8") : null);
|
|
10772
11106
|
}
|
|
10773
11107
|
const files = generateScreen(plan, { existingCustomViews });
|
|
10774
11108
|
const specContracts = applyGeneratedContracts(markdown, emitScreenSpecContracts(plan), GENERATOR_VERSION);
|
|
@@ -10776,25 +11110,25 @@ async function runGenerateScreenCommand(project, featureId, out) {
|
|
|
10776
11110
|
let written = 0;
|
|
10777
11111
|
let emitted = 0;
|
|
10778
11112
|
for (const file of files) {
|
|
10779
|
-
const absolute =
|
|
10780
|
-
const exists =
|
|
11113
|
+
const absolute = join5(project.root, file.path);
|
|
11114
|
+
const exists = existsSync4(absolute);
|
|
10781
11115
|
emitted += file.content.split("\n").length - 1;
|
|
10782
|
-
if (exists &&
|
|
11116
|
+
if (exists && readFileSync5(absolute, "utf8") === file.content) {
|
|
10783
11117
|
out.log(` identique ${file.path}`);
|
|
10784
11118
|
continue;
|
|
10785
11119
|
}
|
|
10786
|
-
|
|
10787
|
-
|
|
11120
|
+
mkdirSync2(dirname3(absolute), { recursive: true });
|
|
11121
|
+
writeFileSync2(absolute, file.content, "utf8");
|
|
10788
11122
|
written += 1;
|
|
10789
11123
|
out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${file.path}`);
|
|
10790
11124
|
}
|
|
10791
11125
|
const bodyPath = screenBodyPath(plan);
|
|
10792
|
-
const bodyAbsolute =
|
|
10793
|
-
if (
|
|
11126
|
+
const bodyAbsolute = join5(project.root, bodyPath);
|
|
11127
|
+
if (existsSync4(bodyAbsolute)) {
|
|
10794
11128
|
out.log(` conserv\xE9 ${bodyPath} (corps \xE9crit, jamais compar\xE9 \u2014 D40)`);
|
|
10795
11129
|
} else {
|
|
10796
|
-
|
|
10797
|
-
|
|
11130
|
+
mkdirSync2(dirname3(bodyAbsolute), { recursive: true });
|
|
11131
|
+
writeFileSync2(bodyAbsolute, screenBodyDefault(plan), "utf8");
|
|
10798
11132
|
written += 1;
|
|
10799
11133
|
out.log(` \xE9crit ${bodyPath} (corps par d\xE9faut, une fois : le v\xF4tre d\xE9sormais \u2014 D40)`);
|
|
10800
11134
|
}
|
|
@@ -10805,19 +11139,20 @@ async function runGenerateScreenCommand(project, featureId, out) {
|
|
|
10805
11139
|
if (specContracts.outcome === "unchanged") {
|
|
10806
11140
|
out.log(` identique ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
|
|
10807
11141
|
} else {
|
|
10808
|
-
|
|
11142
|
+
writeFileSync2(join5(project.root, target.specPath), specContracts.markdown, "utf8");
|
|
10809
11143
|
out.log(` ${specContracts.outcome === "inserted" ? "ins\xE9r\xE9 " : "r\xE9\xE9crit "} ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
|
|
10810
11144
|
}
|
|
11145
|
+
const registryCode = plan.mount === null ? 0 : writeExtensionRegistries(project, out, [plan.mount.point.id]);
|
|
10811
11146
|
out.log(
|
|
10812
|
-
`ogygie generate \u2014 ${featureId} (\xE9cran) : ${files.length} fichier(s) \xE9mis en ${elapsedMs} ms (${emitted} lignes \xE9mises, bloc de la spec compris), ${written} \xE9crit(s), ${report2.tally.derivable} ligne(s) li\xE9e(s), ${report2.tally.hole} port\xE9e(s) par un trou ; ${plan.views.filter((view) => view.kind === "custom").length} vue(s) custom sur ${plan.views.length}.`
|
|
11147
|
+
`ogygie generate \u2014 ${featureId} (\xE9cran) : ${files.length} fichier(s) \xE9mis en ${elapsedMs} ms (${emitted} lignes \xE9mises, bloc de la spec compris), ${written} \xE9crit(s), ${report2.tally.derivable} ligne(s) li\xE9e(s), ${report2.tally.hole} port\xE9e(s) par un trou ; ${plan.views.filter((view) => view.kind === "custom").length} vue(s) custom sur ${plan.views.length}${plan.views.some((view) => view.kind === "content") ? `, ${plan.views.filter((view) => view.kind === "content").length} de contenu` : ""}${plan.mount === null ? "" : ` ; mont\xE9 sur ${plan.mount.point.id}`}.`
|
|
10813
11148
|
);
|
|
10814
|
-
return
|
|
11149
|
+
return registryCode;
|
|
10815
11150
|
}
|
|
10816
11151
|
|
|
10817
11152
|
// src/cli/fill-command.ts
|
|
10818
11153
|
import { spawnSync } from "node:child_process";
|
|
10819
|
-
import { appendFileSync, existsSync as
|
|
10820
|
-
import { join as
|
|
11154
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11155
|
+
import { join as join6 } from "node:path";
|
|
10821
11156
|
|
|
10822
11157
|
// ../ogygie-core/src/model.ts
|
|
10823
11158
|
function createMessagesClient(deps) {
|
|
@@ -10925,7 +11260,7 @@ async function planOf(project, featureId, out) {
|
|
|
10925
11260
|
return null;
|
|
10926
11261
|
}
|
|
10927
11262
|
const { target } = lookup;
|
|
10928
|
-
const markdown =
|
|
11263
|
+
const markdown = readFileSync6(join6(project.root, target.specPath), "utf8");
|
|
10929
11264
|
const firstPass = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
|
|
10930
11265
|
if (firstPass.block === null) {
|
|
10931
11266
|
for (const problem2 of firstPass.problems) out.error(formatProblem(problem2));
|
|
@@ -10943,7 +11278,7 @@ async function planOf(project, featureId, out) {
|
|
|
10943
11278
|
for (const problem2 of report2.problems) out.error(formatProblem(problem2));
|
|
10944
11279
|
return null;
|
|
10945
11280
|
}
|
|
10946
|
-
const migrationsDir =
|
|
11281
|
+
const migrationsDir = join6(project.root, target.appPath, target.generation.target.migrations);
|
|
10947
11282
|
const plan = buildFeaturePlan({
|
|
10948
11283
|
featureId,
|
|
10949
11284
|
folder: target.folder,
|
|
@@ -10953,24 +11288,24 @@ async function planOf(project, featureId, out) {
|
|
|
10953
11288
|
schemas,
|
|
10954
11289
|
generation: target.generation,
|
|
10955
11290
|
...target.tenantColumn === void 0 ? {} : { tenantColumn: target.tenantColumn },
|
|
10956
|
-
migrationFileName: migrationFileNameFor(
|
|
11291
|
+
migrationFileName: migrationFileNameFor(existsSync5(migrationsDir) ? readdirSync2(migrationsDir) : [], report2.block.contracts.table === void 0 ? featureId : tableNameOf(report2.block.contracts.table)),
|
|
10957
11292
|
generatorVersion: GENERATOR_VERSION,
|
|
10958
11293
|
behaviors: boundBehaviorsOf(report2.behaviors)
|
|
10959
11294
|
});
|
|
10960
11295
|
return { plan, folder: target.folder };
|
|
10961
11296
|
}
|
|
10962
11297
|
function recordLibraryEntry(libraryDir, entry) {
|
|
10963
|
-
|
|
10964
|
-
const path =
|
|
10965
|
-
if (!
|
|
10966
|
-
|
|
11298
|
+
mkdirSync3(libraryDir, { recursive: true });
|
|
11299
|
+
const path = join6(libraryDir, libraryFileNameOf(entry));
|
|
11300
|
+
if (!existsSync5(path)) {
|
|
11301
|
+
writeFileSync3(path, `${JSON.stringify(entry, null, 2)}
|
|
10967
11302
|
`, "utf8");
|
|
10968
11303
|
return "created";
|
|
10969
11304
|
}
|
|
10970
|
-
const existing = HoleLibraryEntrySchema.parse(JSON.parse(
|
|
11305
|
+
const existing = HoleLibraryEntrySchema.parse(JSON.parse(readFileSync6(path, "utf8")));
|
|
10971
11306
|
const origin = entry.provenance[0];
|
|
10972
11307
|
if (origin === void 0 || existing.provenance.some((known) => known.repo === origin.repo && known.feature === origin.feature && known.hole === origin.hole)) return "unchanged";
|
|
10973
|
-
|
|
11308
|
+
writeFileSync3(path, `${JSON.stringify({ ...existing, provenance: [...existing.provenance, origin] }, null, 2)}
|
|
10974
11309
|
`, "utf8");
|
|
10975
11310
|
return "extended";
|
|
10976
11311
|
}
|
|
@@ -11034,21 +11369,21 @@ async function fillLogicHoles(project, featureId, argv, out, deps) {
|
|
|
11034
11369
|
const built = await planOf(project, featureId, out);
|
|
11035
11370
|
if (built === null) return 1;
|
|
11036
11371
|
const { plan, folder } = built;
|
|
11037
|
-
const logicPath =
|
|
11038
|
-
if (!
|
|
11372
|
+
const logicPath = join6(project.root, folder, "logic.ts");
|
|
11373
|
+
if (!existsSync5(logicPath)) {
|
|
11039
11374
|
out.error(`ogygie generate fill \u2014 ${folder}/logic.ts n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord.`);
|
|
11040
11375
|
return 1;
|
|
11041
11376
|
}
|
|
11042
11377
|
const testPath = `${folder}/holes.gen.test.ts`;
|
|
11043
|
-
const sections = parseLogicSections(
|
|
11378
|
+
const sections = parseLogicSections(readFileSync6(logicPath, "utf8"));
|
|
11044
11379
|
const wiring = wiringOf(project, featureId, argv, out, deps, "logic");
|
|
11045
11380
|
const library = loadHolesLibrary(wiring.libraryDir);
|
|
11046
11381
|
const inputs = plan.holes.filter((hole) => hole.kind !== "view").map((hole) => {
|
|
11047
|
-
const contextPath =
|
|
11382
|
+
const contextPath = join6(project.root, folder, "holes", `${hole.id}.gen.md`);
|
|
11048
11383
|
return {
|
|
11049
11384
|
holeId: hole.id,
|
|
11050
11385
|
featureId: plan.featureId,
|
|
11051
|
-
context:
|
|
11386
|
+
context: existsSync5(contextPath) ? readFileSync6(contextPath, "utf8") : `# ${hole.id}`,
|
|
11052
11387
|
currentBody: sections.get(hole.id)?.body ?? "",
|
|
11053
11388
|
rules: logicHoleRules(hole, plan, library)
|
|
11054
11389
|
};
|
|
@@ -11060,8 +11395,8 @@ async function fillLogicHoles(project, featureId, argv, out, deps) {
|
|
|
11060
11395
|
const runner = deps.runner ?? vitestHoleRunner;
|
|
11061
11396
|
const fillDeps = fillDepsOf(wiring, {
|
|
11062
11397
|
async writeBody(holeId, body) {
|
|
11063
|
-
const logic =
|
|
11064
|
-
|
|
11398
|
+
const logic = readFileSync6(logicPath, "utf8");
|
|
11399
|
+
writeFileSync3(logicPath, replaceHoleBody(logic, holeId, body ?? ` return todo(${JSON.stringify(holeId)});`), "utf8");
|
|
11065
11400
|
},
|
|
11066
11401
|
async runHoleTest(holeId) {
|
|
11067
11402
|
return runner({ projectRoot: project.root, testPath, testName: holeId });
|
|
@@ -11072,15 +11407,15 @@ async function fillLogicHoles(project, featureId, argv, out, deps) {
|
|
|
11072
11407
|
return summary.unfilled === 0 ? 0 : 1;
|
|
11073
11408
|
}
|
|
11074
11409
|
function viewContextOf(projectRoot, plan, view) {
|
|
11075
|
-
const docPath =
|
|
11076
|
-
const doc =
|
|
11077
|
-
const cataloguePath =
|
|
11078
|
-
if (!
|
|
11410
|
+
const docPath = join6(projectRoot, plan.folder, "holes", `${view.hole.id}.gen.md`);
|
|
11411
|
+
const doc = existsSync5(docPath) ? readFileSync6(docPath, "utf8") : `# ${view.hole.id}`;
|
|
11412
|
+
const cataloguePath = join6(projectRoot, PATTERN_CATALOGUE_PATH);
|
|
11413
|
+
if (!existsSync5(cataloguePath)) return doc;
|
|
11079
11414
|
return `${doc.trimEnd()}
|
|
11080
11415
|
|
|
11081
11416
|
## Catalogue des patterns du produit (${PATTERN_CATALOGUE_PATH})
|
|
11082
11417
|
|
|
11083
|
-
${
|
|
11418
|
+
${readFileSync6(cataloguePath, "utf8").trimEnd()}
|
|
11084
11419
|
`;
|
|
11085
11420
|
}
|
|
11086
11421
|
async function fillCustomViews(project, featureId, argv, out, deps) {
|
|
@@ -11092,16 +11427,16 @@ async function fillCustomViews(project, featureId, argv, out, deps) {
|
|
|
11092
11427
|
out.log(`ogygie generate fill \u2014 ${featureId} (\xE9cran) n'a aucune vue custom : rien \xE0 remplir.`);
|
|
11093
11428
|
return 0;
|
|
11094
11429
|
}
|
|
11095
|
-
const fileOf2 = (view) =>
|
|
11430
|
+
const fileOf2 = (view) => join6(project.root, plan.folder, "views", `${view.componentName}.tsx`);
|
|
11096
11431
|
for (const view of views) {
|
|
11097
|
-
if (!
|
|
11432
|
+
if (!existsSync5(fileOf2(view))) {
|
|
11098
11433
|
out.error(`ogygie generate fill \u2014 ${plan.folder}/views/${view.componentName}.tsx n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord.`);
|
|
11099
11434
|
return 1;
|
|
11100
11435
|
}
|
|
11101
11436
|
}
|
|
11102
11437
|
const testPath = `${plan.folder}/${plan.screenName}.gen.test.tsx`;
|
|
11103
11438
|
const previewsPath = `${plan.folder}/${plan.screenName}${PREVIEWS_FILE_SUFFIX}`;
|
|
11104
|
-
const judgesPreview = plan.front.tests === "static-render" &&
|
|
11439
|
+
const judgesPreview = plan.front.tests === "static-render" && existsSync5(join6(project.root, previewsPath));
|
|
11105
11440
|
const wiring = wiringOf(project, featureId, argv, out, deps, "view");
|
|
11106
11441
|
const library = loadHolesLibrary(wiring.libraryDir);
|
|
11107
11442
|
const byHole = new Map(views.map((view) => [view.hole.id, view]));
|
|
@@ -11109,7 +11444,7 @@ async function fillCustomViews(project, featureId, argv, out, deps) {
|
|
|
11109
11444
|
holeId: view.hole.id,
|
|
11110
11445
|
featureId: plan.featureId,
|
|
11111
11446
|
context: viewContextOf(project.root, plan, view),
|
|
11112
|
-
currentBody: currentViewBody(
|
|
11447
|
+
currentBody: currentViewBody(readFileSync6(fileOf2(view), "utf8"), view.hole.id),
|
|
11113
11448
|
rules: customViewRules(view, plan, library)
|
|
11114
11449
|
}));
|
|
11115
11450
|
const runner = deps.runner ?? vitestHoleRunner;
|
|
@@ -11117,7 +11452,7 @@ async function fillCustomViews(project, featureId, argv, out, deps) {
|
|
|
11117
11452
|
async writeBody(holeId, body) {
|
|
11118
11453
|
const view = byHole.get(holeId);
|
|
11119
11454
|
if (view === void 0) throw new Error(`fill: no custom view carries the hole ${holeId}`);
|
|
11120
|
-
|
|
11455
|
+
writeFileSync3(fileOf2(view), replaceViewSections(readFileSync6(fileOf2(view), "utf8"), holeId, body), "utf8");
|
|
11121
11456
|
},
|
|
11122
11457
|
async runHoleTest(holeId) {
|
|
11123
11458
|
const view = byHole.get(holeId);
|
|
@@ -11226,6 +11561,8 @@ export {
|
|
|
11226
11561
|
READ_RENDERS,
|
|
11227
11562
|
FORM_STATES,
|
|
11228
11563
|
FORM_RENDERS,
|
|
11564
|
+
CONTENT_STATES,
|
|
11565
|
+
CONTENT_RENDERS,
|
|
11229
11566
|
screenCaseKey,
|
|
11230
11567
|
screenVocabularyOf,
|
|
11231
11568
|
bindScreenBehavior,
|
|
@@ -11339,6 +11676,7 @@ export {
|
|
|
11339
11676
|
targetKindOf,
|
|
11340
11677
|
screenTargetOf,
|
|
11341
11678
|
loadConsumedFeatures,
|
|
11679
|
+
writeExtensionRegistries,
|
|
11342
11680
|
screenPlanOf,
|
|
11343
11681
|
runGenerateScreenCommand,
|
|
11344
11682
|
parseFillFlags,
|