@wire-dsl/engine 0.2.2 → 0.2.4
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/index.cjs +406 -10
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +406 -10
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
49
49
|
|
|
50
50
|
// src/parser/index.ts
|
|
51
51
|
var import_chevrotain = require("chevrotain");
|
|
52
|
+
var import_components = require("@wire-dsl/language-support/components");
|
|
52
53
|
|
|
53
54
|
// src/sourcemap/builder.ts
|
|
54
55
|
var SourceMapBuilder = class {
|
|
@@ -1343,6 +1344,374 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
1343
1344
|
}
|
|
1344
1345
|
};
|
|
1345
1346
|
var visitor = new WireDSLVisitor();
|
|
1347
|
+
function getEnumOptions(property) {
|
|
1348
|
+
if (property.type === "enum" && Array.isArray(property.options)) {
|
|
1349
|
+
return property.options;
|
|
1350
|
+
}
|
|
1351
|
+
return void 0;
|
|
1352
|
+
}
|
|
1353
|
+
function buildComponentRulesFromMetadata() {
|
|
1354
|
+
return Object.fromEntries(
|
|
1355
|
+
Object.entries(import_components.COMPONENTS).map(([componentName, metadata]) => {
|
|
1356
|
+
const allowedProps = Object.keys(metadata.properties);
|
|
1357
|
+
const requiredProps = Object.entries(metadata.properties).filter(([, property]) => property.required === true && property.defaultValue === void 0).map(([propertyName]) => propertyName);
|
|
1358
|
+
const enumProps = {};
|
|
1359
|
+
const booleanProps = /* @__PURE__ */ new Set();
|
|
1360
|
+
Object.entries(metadata.properties).forEach(([propertyName, property]) => {
|
|
1361
|
+
const enumOptions = getEnumOptions(property);
|
|
1362
|
+
if (enumOptions) {
|
|
1363
|
+
enumProps[propertyName] = enumOptions;
|
|
1364
|
+
}
|
|
1365
|
+
if (property.type === "boolean") {
|
|
1366
|
+
booleanProps.add(propertyName);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
return [
|
|
1370
|
+
componentName,
|
|
1371
|
+
{
|
|
1372
|
+
allowedProps,
|
|
1373
|
+
requiredProps,
|
|
1374
|
+
enumProps,
|
|
1375
|
+
booleanProps
|
|
1376
|
+
}
|
|
1377
|
+
];
|
|
1378
|
+
})
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
function buildLayoutRulesFromMetadata() {
|
|
1382
|
+
return Object.fromEntries(
|
|
1383
|
+
Object.entries(import_components.LAYOUTS).map(([layoutName, metadata]) => {
|
|
1384
|
+
const allowedParams = Object.keys(metadata.properties);
|
|
1385
|
+
const requiredParamsFromProperties = Object.entries(metadata.properties).filter(([, property]) => property.required === true && property.defaultValue === void 0).map(([propertyName]) => propertyName);
|
|
1386
|
+
const requiredParams = Array.from(
|
|
1387
|
+
/* @__PURE__ */ new Set([...metadata.requiredProperties || [], ...requiredParamsFromProperties])
|
|
1388
|
+
);
|
|
1389
|
+
const enumParams = {};
|
|
1390
|
+
Object.entries(metadata.properties).forEach(([propertyName, property]) => {
|
|
1391
|
+
const enumOptions = getEnumOptions(property);
|
|
1392
|
+
if (enumOptions) {
|
|
1393
|
+
enumParams[propertyName] = enumOptions;
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
return [
|
|
1397
|
+
layoutName,
|
|
1398
|
+
{
|
|
1399
|
+
allowedParams,
|
|
1400
|
+
requiredParams,
|
|
1401
|
+
enumParams
|
|
1402
|
+
}
|
|
1403
|
+
];
|
|
1404
|
+
})
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
var BUILT_IN_COMPONENTS = new Set(Object.keys(import_components.COMPONENTS));
|
|
1408
|
+
var COMPONENT_RULES = buildComponentRulesFromMetadata();
|
|
1409
|
+
var LAYOUT_RULES = buildLayoutRulesFromMetadata();
|
|
1410
|
+
function toFallbackRange() {
|
|
1411
|
+
return {
|
|
1412
|
+
start: { line: 1, column: 0 },
|
|
1413
|
+
end: { line: 1, column: 1 }
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function splitDiagnostics(diagnostics) {
|
|
1417
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
1418
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
1419
|
+
return { errors, warnings };
|
|
1420
|
+
}
|
|
1421
|
+
function buildParseDiagnosticsResult(diagnostics, ast, sourceMap) {
|
|
1422
|
+
const { errors, warnings } = splitDiagnostics(diagnostics);
|
|
1423
|
+
return {
|
|
1424
|
+
ast,
|
|
1425
|
+
sourceMap,
|
|
1426
|
+
diagnostics,
|
|
1427
|
+
errors,
|
|
1428
|
+
warnings,
|
|
1429
|
+
hasErrors: errors.length > 0
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function buildParseResult(ast, sourceMap, diagnostics) {
|
|
1433
|
+
const { errors, warnings } = splitDiagnostics(diagnostics);
|
|
1434
|
+
return {
|
|
1435
|
+
ast,
|
|
1436
|
+
sourceMap,
|
|
1437
|
+
diagnostics,
|
|
1438
|
+
errors,
|
|
1439
|
+
warnings,
|
|
1440
|
+
hasErrors: errors.length > 0
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function tokenToRange(token) {
|
|
1444
|
+
if (!token) return toFallbackRange();
|
|
1445
|
+
const startLine = token.startLine || 1;
|
|
1446
|
+
const startColumn = Math.max(0, (token.startColumn || 1) - 1);
|
|
1447
|
+
const endLine = token.endLine || startLine;
|
|
1448
|
+
const endColumn = token.endColumn ?? token.startColumn ?? startColumn + 1;
|
|
1449
|
+
return {
|
|
1450
|
+
start: {
|
|
1451
|
+
line: startLine,
|
|
1452
|
+
column: startColumn,
|
|
1453
|
+
offset: token.startOffset
|
|
1454
|
+
},
|
|
1455
|
+
end: {
|
|
1456
|
+
line: endLine,
|
|
1457
|
+
column: endColumn,
|
|
1458
|
+
offset: token.endOffset
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function createLexerDiagnostic(error) {
|
|
1463
|
+
const startLine = error.line || 1;
|
|
1464
|
+
const startColumn = Math.max(0, (error.column || 1) - 1);
|
|
1465
|
+
const length = Math.max(1, error.length || 1);
|
|
1466
|
+
return {
|
|
1467
|
+
message: error.message || "Lexer error",
|
|
1468
|
+
severity: "error",
|
|
1469
|
+
phase: "lexer",
|
|
1470
|
+
code: "LEXER_ERROR",
|
|
1471
|
+
range: {
|
|
1472
|
+
start: {
|
|
1473
|
+
line: startLine,
|
|
1474
|
+
column: startColumn,
|
|
1475
|
+
offset: error.offset
|
|
1476
|
+
},
|
|
1477
|
+
end: {
|
|
1478
|
+
line: startLine,
|
|
1479
|
+
column: startColumn + length,
|
|
1480
|
+
offset: error.offset !== void 0 ? error.offset + length : void 0
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
function createParserDiagnostic(error) {
|
|
1486
|
+
const token = error?.token || error?.previousToken || error?.resyncedTokens?.[0];
|
|
1487
|
+
const range = token ? tokenToRange(token) : toFallbackRange();
|
|
1488
|
+
return {
|
|
1489
|
+
message: error?.message || "Parser error",
|
|
1490
|
+
severity: "error",
|
|
1491
|
+
phase: "parser",
|
|
1492
|
+
code: "PARSER_ERROR",
|
|
1493
|
+
range
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function isBooleanLike(value) {
|
|
1497
|
+
if (typeof value === "number") return value === 0 || value === 1;
|
|
1498
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1499
|
+
return normalized === "true" || normalized === "false";
|
|
1500
|
+
}
|
|
1501
|
+
function getPropertyRange(entry, propertyName, mode = "full") {
|
|
1502
|
+
const prop = entry?.properties?.[propertyName];
|
|
1503
|
+
if (!prop) return entry?.range || toFallbackRange();
|
|
1504
|
+
if (mode === "name") return prop.nameRange;
|
|
1505
|
+
if (mode === "value") return prop.valueRange;
|
|
1506
|
+
return prop.range;
|
|
1507
|
+
}
|
|
1508
|
+
function formatAllowedNames(names, emptyMessage) {
|
|
1509
|
+
return names.length > 0 ? names.join(", ") : emptyMessage;
|
|
1510
|
+
}
|
|
1511
|
+
function getMissingRequiredNames(requiredNames, providedValues) {
|
|
1512
|
+
return requiredNames.filter((name) => providedValues[name] === void 0);
|
|
1513
|
+
}
|
|
1514
|
+
function validateSemanticDiagnostics(ast, sourceMap) {
|
|
1515
|
+
const diagnostics = [];
|
|
1516
|
+
const sourceMapByNodeId = new Map(sourceMap.map((entry) => [entry.nodeId, entry]));
|
|
1517
|
+
const definedComponents = new Set(ast.definedComponents.map((dc) => dc.name));
|
|
1518
|
+
const emitWarning = (message, code, range, nodeId, suggestion) => {
|
|
1519
|
+
diagnostics.push({
|
|
1520
|
+
message,
|
|
1521
|
+
code,
|
|
1522
|
+
severity: "warning",
|
|
1523
|
+
phase: "semantic",
|
|
1524
|
+
range,
|
|
1525
|
+
nodeId,
|
|
1526
|
+
suggestion
|
|
1527
|
+
});
|
|
1528
|
+
};
|
|
1529
|
+
const checkComponent = (component) => {
|
|
1530
|
+
const nodeId = component._meta?.nodeId;
|
|
1531
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1532
|
+
const componentType = component.componentType;
|
|
1533
|
+
if (!BUILT_IN_COMPONENTS.has(componentType) && !definedComponents.has(componentType)) {
|
|
1534
|
+
emitWarning(
|
|
1535
|
+
`Component "${componentType}" is not a built-in component and has no local definition.`,
|
|
1536
|
+
"COMPONENT_UNRESOLVED",
|
|
1537
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1538
|
+
nodeId,
|
|
1539
|
+
`Define it with: define Component "${componentType}" { ... }`
|
|
1540
|
+
);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const rules = COMPONENT_RULES[componentType];
|
|
1544
|
+
if (!rules) return;
|
|
1545
|
+
const missingRequiredProps = getMissingRequiredNames(rules.requiredProps, component.props);
|
|
1546
|
+
if (missingRequiredProps.length > 0) {
|
|
1547
|
+
emitWarning(
|
|
1548
|
+
`Component "${componentType}" is missing required propert${missingRequiredProps.length === 1 ? "y" : "ies"}: ${missingRequiredProps.join(", ")}.`,
|
|
1549
|
+
"COMPONENT_MISSING_REQUIRED_PROPERTY",
|
|
1550
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1551
|
+
nodeId,
|
|
1552
|
+
`Add: ${missingRequiredProps.map((name) => `${name}: ...`).join(" ")}`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
const allowed = new Set(rules.allowedProps);
|
|
1556
|
+
for (const [propName, propValue] of Object.entries(component.props)) {
|
|
1557
|
+
if (!allowed.has(propName)) {
|
|
1558
|
+
emitWarning(
|
|
1559
|
+
`Property "${propName}" is not recognized for component "${componentType}".`,
|
|
1560
|
+
"COMPONENT_UNKNOWN_PROPERTY",
|
|
1561
|
+
getPropertyRange(entry, propName, "name"),
|
|
1562
|
+
nodeId,
|
|
1563
|
+
`Allowed properties: ${formatAllowedNames(
|
|
1564
|
+
rules.allowedProps,
|
|
1565
|
+
"(this component does not accept properties)"
|
|
1566
|
+
)}`
|
|
1567
|
+
);
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const enumValues = rules.enumProps?.[propName];
|
|
1571
|
+
if (enumValues) {
|
|
1572
|
+
const normalizedValue = String(propValue);
|
|
1573
|
+
if (!enumValues.includes(normalizedValue)) {
|
|
1574
|
+
emitWarning(
|
|
1575
|
+
`Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
|
|
1576
|
+
"COMPONENT_INVALID_PROPERTY_VALUE",
|
|
1577
|
+
getPropertyRange(entry, propName, "value"),
|
|
1578
|
+
nodeId,
|
|
1579
|
+
`Expected one of: ${enumValues.join(", ")}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (rules.booleanProps.has(propName) && !isBooleanLike(propValue)) {
|
|
1584
|
+
emitWarning(
|
|
1585
|
+
`Property "${propName}" in component "${componentType}" expects a boolean value.`,
|
|
1586
|
+
"COMPONENT_BOOLEAN_PROPERTY_EXPECTED",
|
|
1587
|
+
getPropertyRange(entry, propName, "value"),
|
|
1588
|
+
nodeId,
|
|
1589
|
+
"Use true or false."
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
const checkLayout = (layout) => {
|
|
1595
|
+
const nodeId = layout._meta?.nodeId;
|
|
1596
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1597
|
+
const rules = LAYOUT_RULES[layout.layoutType];
|
|
1598
|
+
if (layout.children.length === 0) {
|
|
1599
|
+
emitWarning(
|
|
1600
|
+
`Layout "${layout.layoutType}" is empty.`,
|
|
1601
|
+
"LAYOUT_EMPTY",
|
|
1602
|
+
entry?.bodyRange || entry?.range || toFallbackRange(),
|
|
1603
|
+
nodeId,
|
|
1604
|
+
"Add at least one child: component, layout, or cell."
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
if (!rules) {
|
|
1608
|
+
emitWarning(
|
|
1609
|
+
`Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
|
|
1610
|
+
"LAYOUT_UNKNOWN_TYPE",
|
|
1611
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1612
|
+
nodeId,
|
|
1613
|
+
`Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
|
|
1614
|
+
);
|
|
1615
|
+
} else {
|
|
1616
|
+
const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
|
|
1617
|
+
if (missingRequiredParams.length > 0) {
|
|
1618
|
+
emitWarning(
|
|
1619
|
+
`Layout "${layout.layoutType}" is missing required parameter${missingRequiredParams.length === 1 ? "" : "s"}: ${missingRequiredParams.join(", ")}.`,
|
|
1620
|
+
"LAYOUT_MISSING_REQUIRED_PARAMETER",
|
|
1621
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1622
|
+
nodeId,
|
|
1623
|
+
`Add: ${missingRequiredParams.map((name) => `${name}: ...`).join(", ")}`
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
const allowed = new Set(rules.allowedParams);
|
|
1627
|
+
for (const [paramName, paramValue] of Object.entries(layout.params)) {
|
|
1628
|
+
if (!allowed.has(paramName)) {
|
|
1629
|
+
emitWarning(
|
|
1630
|
+
`Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
|
|
1631
|
+
"LAYOUT_UNKNOWN_PARAMETER",
|
|
1632
|
+
getPropertyRange(entry, paramName, "name"),
|
|
1633
|
+
nodeId,
|
|
1634
|
+
`Allowed parameters: ${formatAllowedNames(
|
|
1635
|
+
rules.allowedParams,
|
|
1636
|
+
"(this layout does not accept parameters)"
|
|
1637
|
+
)}`
|
|
1638
|
+
);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const enumValues = rules.enumParams?.[paramName];
|
|
1642
|
+
if (enumValues) {
|
|
1643
|
+
const normalizedValue = String(paramValue);
|
|
1644
|
+
if (!enumValues.includes(normalizedValue)) {
|
|
1645
|
+
emitWarning(
|
|
1646
|
+
`Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
|
|
1647
|
+
"LAYOUT_INVALID_PARAMETER_VALUE",
|
|
1648
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1649
|
+
nodeId,
|
|
1650
|
+
`Expected one of: ${enumValues.join(", ")}`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (layout.layoutType === "grid" && paramName === "columns") {
|
|
1655
|
+
const columns = Number(paramValue);
|
|
1656
|
+
if (!Number.isFinite(columns) || columns < 1 || columns > 12) {
|
|
1657
|
+
emitWarning(
|
|
1658
|
+
`Grid "columns" must be a number between 1 and 12.`,
|
|
1659
|
+
"LAYOUT_GRID_COLUMNS_RANGE",
|
|
1660
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1661
|
+
nodeId,
|
|
1662
|
+
"Use values from 1 to 12."
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (layout.layoutType === "split" && paramName === "sidebar") {
|
|
1667
|
+
const sidebar = Number(paramValue);
|
|
1668
|
+
if (!Number.isFinite(sidebar) || sidebar <= 0) {
|
|
1669
|
+
emitWarning(
|
|
1670
|
+
'Split "sidebar" must be a positive number.',
|
|
1671
|
+
"LAYOUT_SPLIT_SIDEBAR_INVALID",
|
|
1672
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1673
|
+
nodeId,
|
|
1674
|
+
"Use a value like sidebar: 240."
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
for (const child of layout.children) {
|
|
1681
|
+
if (child.type === "component") {
|
|
1682
|
+
checkComponent(child);
|
|
1683
|
+
} else if (child.type === "layout") {
|
|
1684
|
+
checkLayout(child);
|
|
1685
|
+
} else if (child.type === "cell") {
|
|
1686
|
+
checkCell(child);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
const checkCell = (cell) => {
|
|
1691
|
+
const nodeId = cell._meta?.nodeId;
|
|
1692
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1693
|
+
if (cell.props.span !== void 0) {
|
|
1694
|
+
const span = Number(cell.props.span);
|
|
1695
|
+
if (!Number.isFinite(span) || span < 1 || span > 12) {
|
|
1696
|
+
emitWarning(
|
|
1697
|
+
'Cell "span" should be a number between 1 and 12.',
|
|
1698
|
+
"CELL_SPAN_RANGE",
|
|
1699
|
+
getPropertyRange(entry, "span", "value"),
|
|
1700
|
+
nodeId,
|
|
1701
|
+
"Use values from 1 to 12."
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
for (const child of cell.children) {
|
|
1706
|
+
if (child.type === "component") checkComponent(child);
|
|
1707
|
+
if (child.type === "layout") checkLayout(child);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
ast.screens.forEach((screen) => {
|
|
1711
|
+
checkLayout(screen.layout);
|
|
1712
|
+
});
|
|
1713
|
+
return diagnostics;
|
|
1714
|
+
}
|
|
1346
1715
|
function parseWireDSL(input) {
|
|
1347
1716
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
1348
1717
|
if (lexResult.errors.length > 0) {
|
|
@@ -1359,29 +1728,56 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
1359
1728
|
validateComponentDefinitionCycles(ast);
|
|
1360
1729
|
return ast;
|
|
1361
1730
|
}
|
|
1362
|
-
function parseWireDSLWithSourceMap(input, filePath = "<input>") {
|
|
1731
|
+
function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
|
|
1732
|
+
const throwOnError = options?.throwOnError ?? true;
|
|
1733
|
+
const includeSemanticWarnings = options?.includeSemanticWarnings ?? true;
|
|
1734
|
+
const diagnostics = [];
|
|
1363
1735
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
1364
1736
|
if (lexResult.errors.length > 0) {
|
|
1365
|
-
|
|
1737
|
+
diagnostics.push(...lexResult.errors.map(createLexerDiagnostic));
|
|
1738
|
+
if (throwOnError) {
|
|
1739
|
+
throw new Error(`Lexer errors:
|
|
1366
1740
|
${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
1741
|
+
}
|
|
1742
|
+
return buildParseDiagnosticsResult(diagnostics);
|
|
1367
1743
|
}
|
|
1368
1744
|
parserInstance.input = lexResult.tokens;
|
|
1369
1745
|
const cst = parserInstance.project();
|
|
1370
1746
|
if (parserInstance.errors.length > 0) {
|
|
1371
|
-
|
|
1747
|
+
diagnostics.push(...parserInstance.errors.map(createParserDiagnostic));
|
|
1748
|
+
if (throwOnError) {
|
|
1749
|
+
throw new Error(`Parser errors:
|
|
1372
1750
|
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
1751
|
+
}
|
|
1752
|
+
return buildParseDiagnosticsResult(diagnostics);
|
|
1373
1753
|
}
|
|
1374
1754
|
const sourceMapBuilder = new SourceMapBuilder(filePath, input);
|
|
1375
1755
|
const visitorWithSourceMap = new WireDSLVisitorWithSourceMap(sourceMapBuilder);
|
|
1376
1756
|
const ast = visitorWithSourceMap.visit(cst);
|
|
1377
|
-
validateComponentDefinitionCycles(ast);
|
|
1378
1757
|
const sourceMap = sourceMapBuilder.build();
|
|
1379
|
-
|
|
1380
|
-
ast
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1758
|
+
try {
|
|
1759
|
+
validateComponentDefinitionCycles(ast);
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
const projectEntry = sourceMap.find((entry) => entry.type === "project");
|
|
1762
|
+
diagnostics.push({
|
|
1763
|
+
message: error instanceof Error ? error.message : "Semantic validation error",
|
|
1764
|
+
severity: "error",
|
|
1765
|
+
phase: "semantic",
|
|
1766
|
+
code: "COMPONENT_CIRCULAR_DEFINITION",
|
|
1767
|
+
range: projectEntry?.range || toFallbackRange(),
|
|
1768
|
+
nodeId: projectEntry?.nodeId
|
|
1769
|
+
});
|
|
1770
|
+
if (throwOnError) {
|
|
1771
|
+
throw error;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (includeSemanticWarnings) {
|
|
1775
|
+
diagnostics.push(...validateSemanticDiagnostics(ast, sourceMap));
|
|
1776
|
+
}
|
|
1777
|
+
if (!throwOnError) {
|
|
1778
|
+
return buildParseDiagnosticsResult(diagnostics, ast, sourceMap);
|
|
1779
|
+
}
|
|
1780
|
+
return buildParseResult(ast, sourceMap, diagnostics);
|
|
1385
1781
|
}
|
|
1386
1782
|
function validateComponentDefinitionCycles(ast) {
|
|
1387
1783
|
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
package/dist/index.d.cts
CHANGED
|
@@ -70,7 +70,22 @@ interface InsertionPoint {
|
|
|
70
70
|
interface ParseResult {
|
|
71
71
|
ast: AST;
|
|
72
72
|
sourceMap: SourceMapEntry[];
|
|
73
|
+
diagnostics: ParseError[];
|
|
73
74
|
errors: ParseError[];
|
|
75
|
+
warnings: ParseError[];
|
|
76
|
+
hasErrors: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse result with diagnostics and optional AST/SourceMap
|
|
80
|
+
* Used by tolerant editor flows where diagnostics are needed even when parsing fails
|
|
81
|
+
*/
|
|
82
|
+
interface ParseDiagnosticsResult {
|
|
83
|
+
ast?: AST;
|
|
84
|
+
sourceMap?: SourceMapEntry[];
|
|
85
|
+
diagnostics: ParseError[];
|
|
86
|
+
errors: ParseError[];
|
|
87
|
+
warnings: ParseError[];
|
|
88
|
+
hasErrors: boolean;
|
|
74
89
|
}
|
|
75
90
|
/**
|
|
76
91
|
* Parse error with optional nodeId reference
|
|
@@ -79,6 +94,11 @@ interface ParseError {
|
|
|
79
94
|
message: string;
|
|
80
95
|
range: CodeRange;
|
|
81
96
|
severity: 'error' | 'warning';
|
|
97
|
+
code?: string;
|
|
98
|
+
phase?: 'lexer' | 'parser' | 'semantic';
|
|
99
|
+
expected?: string;
|
|
100
|
+
actual?: string;
|
|
101
|
+
suggestion?: string;
|
|
82
102
|
nodeId?: string;
|
|
83
103
|
}
|
|
84
104
|
/**
|
|
@@ -148,6 +168,10 @@ interface ASTComponent {
|
|
|
148
168
|
nodeId: string;
|
|
149
169
|
};
|
|
150
170
|
}
|
|
171
|
+
interface ParseWireDSLWithSourceMapOptions {
|
|
172
|
+
throwOnError?: boolean;
|
|
173
|
+
includeSemanticWarnings?: boolean;
|
|
174
|
+
}
|
|
151
175
|
declare function parseWireDSL(input: string): AST;
|
|
152
176
|
/**
|
|
153
177
|
* Parse Wire DSL with SourceMap generation
|
|
@@ -177,6 +201,9 @@ declare function parseWireDSL(input: string): AST;
|
|
|
177
201
|
* ```
|
|
178
202
|
*/
|
|
179
203
|
declare function parseWireDSLWithSourceMap(input: string, filePath?: string): ParseResult;
|
|
204
|
+
declare function parseWireDSLWithSourceMap(input: string, filePath: string | undefined, options: ParseWireDSLWithSourceMapOptions & {
|
|
205
|
+
throwOnError: false;
|
|
206
|
+
}): ParseDiagnosticsResult;
|
|
180
207
|
interface ParsedWireframe {
|
|
181
208
|
name: string;
|
|
182
209
|
components: ParsedComponent[];
|
|
@@ -1287,4 +1314,4 @@ declare class SourceMapResolver {
|
|
|
1287
1314
|
|
|
1288
1315
|
declare const version = "0.0.1";
|
|
1289
1316
|
|
|
1290
|
-
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseError, type ParseResult, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
|
1317
|
+
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
package/dist/index.d.ts
CHANGED
|
@@ -70,7 +70,22 @@ interface InsertionPoint {
|
|
|
70
70
|
interface ParseResult {
|
|
71
71
|
ast: AST;
|
|
72
72
|
sourceMap: SourceMapEntry[];
|
|
73
|
+
diagnostics: ParseError[];
|
|
73
74
|
errors: ParseError[];
|
|
75
|
+
warnings: ParseError[];
|
|
76
|
+
hasErrors: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse result with diagnostics and optional AST/SourceMap
|
|
80
|
+
* Used by tolerant editor flows where diagnostics are needed even when parsing fails
|
|
81
|
+
*/
|
|
82
|
+
interface ParseDiagnosticsResult {
|
|
83
|
+
ast?: AST;
|
|
84
|
+
sourceMap?: SourceMapEntry[];
|
|
85
|
+
diagnostics: ParseError[];
|
|
86
|
+
errors: ParseError[];
|
|
87
|
+
warnings: ParseError[];
|
|
88
|
+
hasErrors: boolean;
|
|
74
89
|
}
|
|
75
90
|
/**
|
|
76
91
|
* Parse error with optional nodeId reference
|
|
@@ -79,6 +94,11 @@ interface ParseError {
|
|
|
79
94
|
message: string;
|
|
80
95
|
range: CodeRange;
|
|
81
96
|
severity: 'error' | 'warning';
|
|
97
|
+
code?: string;
|
|
98
|
+
phase?: 'lexer' | 'parser' | 'semantic';
|
|
99
|
+
expected?: string;
|
|
100
|
+
actual?: string;
|
|
101
|
+
suggestion?: string;
|
|
82
102
|
nodeId?: string;
|
|
83
103
|
}
|
|
84
104
|
/**
|
|
@@ -148,6 +168,10 @@ interface ASTComponent {
|
|
|
148
168
|
nodeId: string;
|
|
149
169
|
};
|
|
150
170
|
}
|
|
171
|
+
interface ParseWireDSLWithSourceMapOptions {
|
|
172
|
+
throwOnError?: boolean;
|
|
173
|
+
includeSemanticWarnings?: boolean;
|
|
174
|
+
}
|
|
151
175
|
declare function parseWireDSL(input: string): AST;
|
|
152
176
|
/**
|
|
153
177
|
* Parse Wire DSL with SourceMap generation
|
|
@@ -177,6 +201,9 @@ declare function parseWireDSL(input: string): AST;
|
|
|
177
201
|
* ```
|
|
178
202
|
*/
|
|
179
203
|
declare function parseWireDSLWithSourceMap(input: string, filePath?: string): ParseResult;
|
|
204
|
+
declare function parseWireDSLWithSourceMap(input: string, filePath: string | undefined, options: ParseWireDSLWithSourceMapOptions & {
|
|
205
|
+
throwOnError: false;
|
|
206
|
+
}): ParseDiagnosticsResult;
|
|
180
207
|
interface ParsedWireframe {
|
|
181
208
|
name: string;
|
|
182
209
|
components: ParsedComponent[];
|
|
@@ -1287,4 +1314,4 @@ declare class SourceMapResolver {
|
|
|
1287
1314
|
|
|
1288
1315
|
declare const version = "0.0.1";
|
|
1289
1316
|
|
|
1290
|
-
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseError, type ParseResult, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
|
1317
|
+
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/parser/index.ts
|
|
2
2
|
import { Lexer, createToken, CstParser } from "chevrotain";
|
|
3
|
+
import { COMPONENTS, LAYOUTS } from "@wire-dsl/language-support/components";
|
|
3
4
|
|
|
4
5
|
// src/sourcemap/builder.ts
|
|
5
6
|
var SourceMapBuilder = class {
|
|
@@ -1294,6 +1295,374 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
|
|
|
1294
1295
|
}
|
|
1295
1296
|
};
|
|
1296
1297
|
var visitor = new WireDSLVisitor();
|
|
1298
|
+
function getEnumOptions(property) {
|
|
1299
|
+
if (property.type === "enum" && Array.isArray(property.options)) {
|
|
1300
|
+
return property.options;
|
|
1301
|
+
}
|
|
1302
|
+
return void 0;
|
|
1303
|
+
}
|
|
1304
|
+
function buildComponentRulesFromMetadata() {
|
|
1305
|
+
return Object.fromEntries(
|
|
1306
|
+
Object.entries(COMPONENTS).map(([componentName, metadata]) => {
|
|
1307
|
+
const allowedProps = Object.keys(metadata.properties);
|
|
1308
|
+
const requiredProps = Object.entries(metadata.properties).filter(([, property]) => property.required === true && property.defaultValue === void 0).map(([propertyName]) => propertyName);
|
|
1309
|
+
const enumProps = {};
|
|
1310
|
+
const booleanProps = /* @__PURE__ */ new Set();
|
|
1311
|
+
Object.entries(metadata.properties).forEach(([propertyName, property]) => {
|
|
1312
|
+
const enumOptions = getEnumOptions(property);
|
|
1313
|
+
if (enumOptions) {
|
|
1314
|
+
enumProps[propertyName] = enumOptions;
|
|
1315
|
+
}
|
|
1316
|
+
if (property.type === "boolean") {
|
|
1317
|
+
booleanProps.add(propertyName);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
return [
|
|
1321
|
+
componentName,
|
|
1322
|
+
{
|
|
1323
|
+
allowedProps,
|
|
1324
|
+
requiredProps,
|
|
1325
|
+
enumProps,
|
|
1326
|
+
booleanProps
|
|
1327
|
+
}
|
|
1328
|
+
];
|
|
1329
|
+
})
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
function buildLayoutRulesFromMetadata() {
|
|
1333
|
+
return Object.fromEntries(
|
|
1334
|
+
Object.entries(LAYOUTS).map(([layoutName, metadata]) => {
|
|
1335
|
+
const allowedParams = Object.keys(metadata.properties);
|
|
1336
|
+
const requiredParamsFromProperties = Object.entries(metadata.properties).filter(([, property]) => property.required === true && property.defaultValue === void 0).map(([propertyName]) => propertyName);
|
|
1337
|
+
const requiredParams = Array.from(
|
|
1338
|
+
/* @__PURE__ */ new Set([...metadata.requiredProperties || [], ...requiredParamsFromProperties])
|
|
1339
|
+
);
|
|
1340
|
+
const enumParams = {};
|
|
1341
|
+
Object.entries(metadata.properties).forEach(([propertyName, property]) => {
|
|
1342
|
+
const enumOptions = getEnumOptions(property);
|
|
1343
|
+
if (enumOptions) {
|
|
1344
|
+
enumParams[propertyName] = enumOptions;
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
return [
|
|
1348
|
+
layoutName,
|
|
1349
|
+
{
|
|
1350
|
+
allowedParams,
|
|
1351
|
+
requiredParams,
|
|
1352
|
+
enumParams
|
|
1353
|
+
}
|
|
1354
|
+
];
|
|
1355
|
+
})
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
var BUILT_IN_COMPONENTS = new Set(Object.keys(COMPONENTS));
|
|
1359
|
+
var COMPONENT_RULES = buildComponentRulesFromMetadata();
|
|
1360
|
+
var LAYOUT_RULES = buildLayoutRulesFromMetadata();
|
|
1361
|
+
function toFallbackRange() {
|
|
1362
|
+
return {
|
|
1363
|
+
start: { line: 1, column: 0 },
|
|
1364
|
+
end: { line: 1, column: 1 }
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function splitDiagnostics(diagnostics) {
|
|
1368
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
1369
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
1370
|
+
return { errors, warnings };
|
|
1371
|
+
}
|
|
1372
|
+
function buildParseDiagnosticsResult(diagnostics, ast, sourceMap) {
|
|
1373
|
+
const { errors, warnings } = splitDiagnostics(diagnostics);
|
|
1374
|
+
return {
|
|
1375
|
+
ast,
|
|
1376
|
+
sourceMap,
|
|
1377
|
+
diagnostics,
|
|
1378
|
+
errors,
|
|
1379
|
+
warnings,
|
|
1380
|
+
hasErrors: errors.length > 0
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function buildParseResult(ast, sourceMap, diagnostics) {
|
|
1384
|
+
const { errors, warnings } = splitDiagnostics(diagnostics);
|
|
1385
|
+
return {
|
|
1386
|
+
ast,
|
|
1387
|
+
sourceMap,
|
|
1388
|
+
diagnostics,
|
|
1389
|
+
errors,
|
|
1390
|
+
warnings,
|
|
1391
|
+
hasErrors: errors.length > 0
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function tokenToRange(token) {
|
|
1395
|
+
if (!token) return toFallbackRange();
|
|
1396
|
+
const startLine = token.startLine || 1;
|
|
1397
|
+
const startColumn = Math.max(0, (token.startColumn || 1) - 1);
|
|
1398
|
+
const endLine = token.endLine || startLine;
|
|
1399
|
+
const endColumn = token.endColumn ?? token.startColumn ?? startColumn + 1;
|
|
1400
|
+
return {
|
|
1401
|
+
start: {
|
|
1402
|
+
line: startLine,
|
|
1403
|
+
column: startColumn,
|
|
1404
|
+
offset: token.startOffset
|
|
1405
|
+
},
|
|
1406
|
+
end: {
|
|
1407
|
+
line: endLine,
|
|
1408
|
+
column: endColumn,
|
|
1409
|
+
offset: token.endOffset
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function createLexerDiagnostic(error) {
|
|
1414
|
+
const startLine = error.line || 1;
|
|
1415
|
+
const startColumn = Math.max(0, (error.column || 1) - 1);
|
|
1416
|
+
const length = Math.max(1, error.length || 1);
|
|
1417
|
+
return {
|
|
1418
|
+
message: error.message || "Lexer error",
|
|
1419
|
+
severity: "error",
|
|
1420
|
+
phase: "lexer",
|
|
1421
|
+
code: "LEXER_ERROR",
|
|
1422
|
+
range: {
|
|
1423
|
+
start: {
|
|
1424
|
+
line: startLine,
|
|
1425
|
+
column: startColumn,
|
|
1426
|
+
offset: error.offset
|
|
1427
|
+
},
|
|
1428
|
+
end: {
|
|
1429
|
+
line: startLine,
|
|
1430
|
+
column: startColumn + length,
|
|
1431
|
+
offset: error.offset !== void 0 ? error.offset + length : void 0
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function createParserDiagnostic(error) {
|
|
1437
|
+
const token = error?.token || error?.previousToken || error?.resyncedTokens?.[0];
|
|
1438
|
+
const range = token ? tokenToRange(token) : toFallbackRange();
|
|
1439
|
+
return {
|
|
1440
|
+
message: error?.message || "Parser error",
|
|
1441
|
+
severity: "error",
|
|
1442
|
+
phase: "parser",
|
|
1443
|
+
code: "PARSER_ERROR",
|
|
1444
|
+
range
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function isBooleanLike(value) {
|
|
1448
|
+
if (typeof value === "number") return value === 0 || value === 1;
|
|
1449
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1450
|
+
return normalized === "true" || normalized === "false";
|
|
1451
|
+
}
|
|
1452
|
+
function getPropertyRange(entry, propertyName, mode = "full") {
|
|
1453
|
+
const prop = entry?.properties?.[propertyName];
|
|
1454
|
+
if (!prop) return entry?.range || toFallbackRange();
|
|
1455
|
+
if (mode === "name") return prop.nameRange;
|
|
1456
|
+
if (mode === "value") return prop.valueRange;
|
|
1457
|
+
return prop.range;
|
|
1458
|
+
}
|
|
1459
|
+
function formatAllowedNames(names, emptyMessage) {
|
|
1460
|
+
return names.length > 0 ? names.join(", ") : emptyMessage;
|
|
1461
|
+
}
|
|
1462
|
+
function getMissingRequiredNames(requiredNames, providedValues) {
|
|
1463
|
+
return requiredNames.filter((name) => providedValues[name] === void 0);
|
|
1464
|
+
}
|
|
1465
|
+
function validateSemanticDiagnostics(ast, sourceMap) {
|
|
1466
|
+
const diagnostics = [];
|
|
1467
|
+
const sourceMapByNodeId = new Map(sourceMap.map((entry) => [entry.nodeId, entry]));
|
|
1468
|
+
const definedComponents = new Set(ast.definedComponents.map((dc) => dc.name));
|
|
1469
|
+
const emitWarning = (message, code, range, nodeId, suggestion) => {
|
|
1470
|
+
diagnostics.push({
|
|
1471
|
+
message,
|
|
1472
|
+
code,
|
|
1473
|
+
severity: "warning",
|
|
1474
|
+
phase: "semantic",
|
|
1475
|
+
range,
|
|
1476
|
+
nodeId,
|
|
1477
|
+
suggestion
|
|
1478
|
+
});
|
|
1479
|
+
};
|
|
1480
|
+
const checkComponent = (component) => {
|
|
1481
|
+
const nodeId = component._meta?.nodeId;
|
|
1482
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1483
|
+
const componentType = component.componentType;
|
|
1484
|
+
if (!BUILT_IN_COMPONENTS.has(componentType) && !definedComponents.has(componentType)) {
|
|
1485
|
+
emitWarning(
|
|
1486
|
+
`Component "${componentType}" is not a built-in component and has no local definition.`,
|
|
1487
|
+
"COMPONENT_UNRESOLVED",
|
|
1488
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1489
|
+
nodeId,
|
|
1490
|
+
`Define it with: define Component "${componentType}" { ... }`
|
|
1491
|
+
);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const rules = COMPONENT_RULES[componentType];
|
|
1495
|
+
if (!rules) return;
|
|
1496
|
+
const missingRequiredProps = getMissingRequiredNames(rules.requiredProps, component.props);
|
|
1497
|
+
if (missingRequiredProps.length > 0) {
|
|
1498
|
+
emitWarning(
|
|
1499
|
+
`Component "${componentType}" is missing required propert${missingRequiredProps.length === 1 ? "y" : "ies"}: ${missingRequiredProps.join(", ")}.`,
|
|
1500
|
+
"COMPONENT_MISSING_REQUIRED_PROPERTY",
|
|
1501
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1502
|
+
nodeId,
|
|
1503
|
+
`Add: ${missingRequiredProps.map((name) => `${name}: ...`).join(" ")}`
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const allowed = new Set(rules.allowedProps);
|
|
1507
|
+
for (const [propName, propValue] of Object.entries(component.props)) {
|
|
1508
|
+
if (!allowed.has(propName)) {
|
|
1509
|
+
emitWarning(
|
|
1510
|
+
`Property "${propName}" is not recognized for component "${componentType}".`,
|
|
1511
|
+
"COMPONENT_UNKNOWN_PROPERTY",
|
|
1512
|
+
getPropertyRange(entry, propName, "name"),
|
|
1513
|
+
nodeId,
|
|
1514
|
+
`Allowed properties: ${formatAllowedNames(
|
|
1515
|
+
rules.allowedProps,
|
|
1516
|
+
"(this component does not accept properties)"
|
|
1517
|
+
)}`
|
|
1518
|
+
);
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
const enumValues = rules.enumProps?.[propName];
|
|
1522
|
+
if (enumValues) {
|
|
1523
|
+
const normalizedValue = String(propValue);
|
|
1524
|
+
if (!enumValues.includes(normalizedValue)) {
|
|
1525
|
+
emitWarning(
|
|
1526
|
+
`Invalid value "${normalizedValue}" for property "${propName}" in component "${componentType}".`,
|
|
1527
|
+
"COMPONENT_INVALID_PROPERTY_VALUE",
|
|
1528
|
+
getPropertyRange(entry, propName, "value"),
|
|
1529
|
+
nodeId,
|
|
1530
|
+
`Expected one of: ${enumValues.join(", ")}`
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (rules.booleanProps.has(propName) && !isBooleanLike(propValue)) {
|
|
1535
|
+
emitWarning(
|
|
1536
|
+
`Property "${propName}" in component "${componentType}" expects a boolean value.`,
|
|
1537
|
+
"COMPONENT_BOOLEAN_PROPERTY_EXPECTED",
|
|
1538
|
+
getPropertyRange(entry, propName, "value"),
|
|
1539
|
+
nodeId,
|
|
1540
|
+
"Use true or false."
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
const checkLayout = (layout) => {
|
|
1546
|
+
const nodeId = layout._meta?.nodeId;
|
|
1547
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1548
|
+
const rules = LAYOUT_RULES[layout.layoutType];
|
|
1549
|
+
if (layout.children.length === 0) {
|
|
1550
|
+
emitWarning(
|
|
1551
|
+
`Layout "${layout.layoutType}" is empty.`,
|
|
1552
|
+
"LAYOUT_EMPTY",
|
|
1553
|
+
entry?.bodyRange || entry?.range || toFallbackRange(),
|
|
1554
|
+
nodeId,
|
|
1555
|
+
"Add at least one child: component, layout, or cell."
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (!rules) {
|
|
1559
|
+
emitWarning(
|
|
1560
|
+
`Layout type "${layout.layoutType}" is not recognized by semantic validation rules.`,
|
|
1561
|
+
"LAYOUT_UNKNOWN_TYPE",
|
|
1562
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1563
|
+
nodeId,
|
|
1564
|
+
`Use one of: ${Object.keys(LAYOUT_RULES).join(", ")}.`
|
|
1565
|
+
);
|
|
1566
|
+
} else {
|
|
1567
|
+
const missingRequiredParams = getMissingRequiredNames(rules.requiredParams, layout.params);
|
|
1568
|
+
if (missingRequiredParams.length > 0) {
|
|
1569
|
+
emitWarning(
|
|
1570
|
+
`Layout "${layout.layoutType}" is missing required parameter${missingRequiredParams.length === 1 ? "" : "s"}: ${missingRequiredParams.join(", ")}.`,
|
|
1571
|
+
"LAYOUT_MISSING_REQUIRED_PARAMETER",
|
|
1572
|
+
entry?.nameRange || entry?.range || toFallbackRange(),
|
|
1573
|
+
nodeId,
|
|
1574
|
+
`Add: ${missingRequiredParams.map((name) => `${name}: ...`).join(", ")}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
const allowed = new Set(rules.allowedParams);
|
|
1578
|
+
for (const [paramName, paramValue] of Object.entries(layout.params)) {
|
|
1579
|
+
if (!allowed.has(paramName)) {
|
|
1580
|
+
emitWarning(
|
|
1581
|
+
`Parameter "${paramName}" is not recognized for layout "${layout.layoutType}".`,
|
|
1582
|
+
"LAYOUT_UNKNOWN_PARAMETER",
|
|
1583
|
+
getPropertyRange(entry, paramName, "name"),
|
|
1584
|
+
nodeId,
|
|
1585
|
+
`Allowed parameters: ${formatAllowedNames(
|
|
1586
|
+
rules.allowedParams,
|
|
1587
|
+
"(this layout does not accept parameters)"
|
|
1588
|
+
)}`
|
|
1589
|
+
);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const enumValues = rules.enumParams?.[paramName];
|
|
1593
|
+
if (enumValues) {
|
|
1594
|
+
const normalizedValue = String(paramValue);
|
|
1595
|
+
if (!enumValues.includes(normalizedValue)) {
|
|
1596
|
+
emitWarning(
|
|
1597
|
+
`Invalid value "${normalizedValue}" for parameter "${paramName}" in layout "${layout.layoutType}".`,
|
|
1598
|
+
"LAYOUT_INVALID_PARAMETER_VALUE",
|
|
1599
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1600
|
+
nodeId,
|
|
1601
|
+
`Expected one of: ${enumValues.join(", ")}`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (layout.layoutType === "grid" && paramName === "columns") {
|
|
1606
|
+
const columns = Number(paramValue);
|
|
1607
|
+
if (!Number.isFinite(columns) || columns < 1 || columns > 12) {
|
|
1608
|
+
emitWarning(
|
|
1609
|
+
`Grid "columns" must be a number between 1 and 12.`,
|
|
1610
|
+
"LAYOUT_GRID_COLUMNS_RANGE",
|
|
1611
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1612
|
+
nodeId,
|
|
1613
|
+
"Use values from 1 to 12."
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (layout.layoutType === "split" && paramName === "sidebar") {
|
|
1618
|
+
const sidebar = Number(paramValue);
|
|
1619
|
+
if (!Number.isFinite(sidebar) || sidebar <= 0) {
|
|
1620
|
+
emitWarning(
|
|
1621
|
+
'Split "sidebar" must be a positive number.',
|
|
1622
|
+
"LAYOUT_SPLIT_SIDEBAR_INVALID",
|
|
1623
|
+
getPropertyRange(entry, paramName, "value"),
|
|
1624
|
+
nodeId,
|
|
1625
|
+
"Use a value like sidebar: 240."
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
for (const child of layout.children) {
|
|
1632
|
+
if (child.type === "component") {
|
|
1633
|
+
checkComponent(child);
|
|
1634
|
+
} else if (child.type === "layout") {
|
|
1635
|
+
checkLayout(child);
|
|
1636
|
+
} else if (child.type === "cell") {
|
|
1637
|
+
checkCell(child);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
const checkCell = (cell) => {
|
|
1642
|
+
const nodeId = cell._meta?.nodeId;
|
|
1643
|
+
const entry = nodeId ? sourceMapByNodeId.get(nodeId) : void 0;
|
|
1644
|
+
if (cell.props.span !== void 0) {
|
|
1645
|
+
const span = Number(cell.props.span);
|
|
1646
|
+
if (!Number.isFinite(span) || span < 1 || span > 12) {
|
|
1647
|
+
emitWarning(
|
|
1648
|
+
'Cell "span" should be a number between 1 and 12.',
|
|
1649
|
+
"CELL_SPAN_RANGE",
|
|
1650
|
+
getPropertyRange(entry, "span", "value"),
|
|
1651
|
+
nodeId,
|
|
1652
|
+
"Use values from 1 to 12."
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
for (const child of cell.children) {
|
|
1657
|
+
if (child.type === "component") checkComponent(child);
|
|
1658
|
+
if (child.type === "layout") checkLayout(child);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
ast.screens.forEach((screen) => {
|
|
1662
|
+
checkLayout(screen.layout);
|
|
1663
|
+
});
|
|
1664
|
+
return diagnostics;
|
|
1665
|
+
}
|
|
1297
1666
|
function parseWireDSL(input) {
|
|
1298
1667
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
1299
1668
|
if (lexResult.errors.length > 0) {
|
|
@@ -1310,29 +1679,56 @@ ${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
|
1310
1679
|
validateComponentDefinitionCycles(ast);
|
|
1311
1680
|
return ast;
|
|
1312
1681
|
}
|
|
1313
|
-
function parseWireDSLWithSourceMap(input, filePath = "<input>") {
|
|
1682
|
+
function parseWireDSLWithSourceMap(input, filePath = "<input>", options) {
|
|
1683
|
+
const throwOnError = options?.throwOnError ?? true;
|
|
1684
|
+
const includeSemanticWarnings = options?.includeSemanticWarnings ?? true;
|
|
1685
|
+
const diagnostics = [];
|
|
1314
1686
|
const lexResult = WireDSLLexer.tokenize(input);
|
|
1315
1687
|
if (lexResult.errors.length > 0) {
|
|
1316
|
-
|
|
1688
|
+
diagnostics.push(...lexResult.errors.map(createLexerDiagnostic));
|
|
1689
|
+
if (throwOnError) {
|
|
1690
|
+
throw new Error(`Lexer errors:
|
|
1317
1691
|
${lexResult.errors.map((e) => e.message).join("\n")}`);
|
|
1692
|
+
}
|
|
1693
|
+
return buildParseDiagnosticsResult(diagnostics);
|
|
1318
1694
|
}
|
|
1319
1695
|
parserInstance.input = lexResult.tokens;
|
|
1320
1696
|
const cst = parserInstance.project();
|
|
1321
1697
|
if (parserInstance.errors.length > 0) {
|
|
1322
|
-
|
|
1698
|
+
diagnostics.push(...parserInstance.errors.map(createParserDiagnostic));
|
|
1699
|
+
if (throwOnError) {
|
|
1700
|
+
throw new Error(`Parser errors:
|
|
1323
1701
|
${parserInstance.errors.map((e) => e.message).join("\n")}`);
|
|
1702
|
+
}
|
|
1703
|
+
return buildParseDiagnosticsResult(diagnostics);
|
|
1324
1704
|
}
|
|
1325
1705
|
const sourceMapBuilder = new SourceMapBuilder(filePath, input);
|
|
1326
1706
|
const visitorWithSourceMap = new WireDSLVisitorWithSourceMap(sourceMapBuilder);
|
|
1327
1707
|
const ast = visitorWithSourceMap.visit(cst);
|
|
1328
|
-
validateComponentDefinitionCycles(ast);
|
|
1329
1708
|
const sourceMap = sourceMapBuilder.build();
|
|
1330
|
-
|
|
1331
|
-
ast
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1709
|
+
try {
|
|
1710
|
+
validateComponentDefinitionCycles(ast);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
const projectEntry = sourceMap.find((entry) => entry.type === "project");
|
|
1713
|
+
diagnostics.push({
|
|
1714
|
+
message: error instanceof Error ? error.message : "Semantic validation error",
|
|
1715
|
+
severity: "error",
|
|
1716
|
+
phase: "semantic",
|
|
1717
|
+
code: "COMPONENT_CIRCULAR_DEFINITION",
|
|
1718
|
+
range: projectEntry?.range || toFallbackRange(),
|
|
1719
|
+
nodeId: projectEntry?.nodeId
|
|
1720
|
+
});
|
|
1721
|
+
if (throwOnError) {
|
|
1722
|
+
throw error;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (includeSemanticWarnings) {
|
|
1726
|
+
diagnostics.push(...validateSemanticDiagnostics(ast, sourceMap));
|
|
1727
|
+
}
|
|
1728
|
+
if (!throwOnError) {
|
|
1729
|
+
return buildParseDiagnosticsResult(diagnostics, ast, sourceMap);
|
|
1730
|
+
}
|
|
1731
|
+
return buildParseResult(ast, sourceMap, diagnostics);
|
|
1336
1732
|
}
|
|
1337
1733
|
function validateComponentDefinitionCycles(ast) {
|
|
1338
1734
|
if (!ast.definedComponents || ast.definedComponents.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wire-dsl/engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "WireDSL engine - Parser, IR generator, layout engine, and SVG renderer (browser-safe, pure JS/TS)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"chevrotain": "11.1.1",
|
|
34
|
-
"zod": "4.3.6"
|
|
34
|
+
"zod": "4.3.6",
|
|
35
|
+
"@wire-dsl/language-support": "0.2.4"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "25.2.0",
|