@wire-dsl/engine 0.2.2 → 0.2.3

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 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
- throw new Error(`Lexer errors:
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
- throw new Error(`Parser errors:
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
- return {
1380
- ast,
1381
- sourceMap,
1382
- errors: []
1383
- // No errors if we got here (errors throw exceptions)
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
- throw new Error(`Lexer errors:
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
- throw new Error(`Parser errors:
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
- return {
1331
- ast,
1332
- sourceMap,
1333
- errors: []
1334
- // No errors if we got here (errors throw exceptions)
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.2",
3
+ "version": "0.2.3",
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.3"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "25.2.0",