typebars 1.0.10 → 1.0.12

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.
Files changed (52) hide show
  1. package/README.md +156 -8
  2. package/dist/cjs/analyzer.d.ts +47 -13
  3. package/dist/cjs/analyzer.js +1 -1
  4. package/dist/cjs/analyzer.js.map +1 -1
  5. package/dist/cjs/compiled-template.d.ts +15 -13
  6. package/dist/cjs/compiled-template.js +1 -1
  7. package/dist/cjs/compiled-template.js.map +1 -1
  8. package/dist/cjs/dispatch.d.ts +99 -0
  9. package/dist/cjs/dispatch.js +2 -0
  10. package/dist/cjs/dispatch.js.map +1 -0
  11. package/dist/cjs/errors.d.ts +7 -0
  12. package/dist/cjs/errors.js +2 -2
  13. package/dist/cjs/errors.js.map +1 -1
  14. package/dist/cjs/executor.d.ts +9 -2
  15. package/dist/cjs/executor.js +1 -1
  16. package/dist/cjs/executor.js.map +1 -1
  17. package/dist/cjs/index.d.ts +2 -1
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/parser.d.ts +62 -0
  20. package/dist/cjs/parser.js +1 -1
  21. package/dist/cjs/parser.js.map +1 -1
  22. package/dist/cjs/typebars.d.ts +11 -8
  23. package/dist/cjs/typebars.js +1 -1
  24. package/dist/cjs/typebars.js.map +1 -1
  25. package/dist/cjs/types.d.ts +26 -1
  26. package/dist/cjs/types.js.map +1 -1
  27. package/dist/esm/analyzer.d.ts +47 -13
  28. package/dist/esm/analyzer.js +1 -1
  29. package/dist/esm/analyzer.js.map +1 -1
  30. package/dist/esm/compiled-template.d.ts +15 -13
  31. package/dist/esm/compiled-template.js +1 -1
  32. package/dist/esm/compiled-template.js.map +1 -1
  33. package/dist/esm/dispatch.d.ts +99 -0
  34. package/dist/esm/dispatch.js +2 -0
  35. package/dist/esm/dispatch.js.map +1 -0
  36. package/dist/esm/errors.d.ts +7 -0
  37. package/dist/esm/errors.js +1 -1
  38. package/dist/esm/errors.js.map +1 -1
  39. package/dist/esm/executor.d.ts +9 -2
  40. package/dist/esm/executor.js +1 -1
  41. package/dist/esm/executor.js.map +1 -1
  42. package/dist/esm/index.d.ts +2 -1
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/parser.d.ts +62 -0
  45. package/dist/esm/parser.js +1 -1
  46. package/dist/esm/parser.js.map +1 -1
  47. package/dist/esm/typebars.d.ts +11 -8
  48. package/dist/esm/typebars.js +1 -1
  49. package/dist/esm/typebars.js.map +1 -1
  50. package/dist/esm/types.d.ts +26 -1
  51. package/dist/esm/types.js.map +1 -1
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -79,6 +79,7 @@ The output schema is inferred **statically** from the template structure and the
79
79
  - [`registerHelper`](#registerhelper)
80
80
  - [`defineHelper` (Type-Safe)](#definehelper-type-safe)
81
81
  - [Template Identifiers (`{{key:N}}`)](#template-identifiers-keyn)
82
+ - [Output Type Coercion (`coerceSchema`)](#output-type-coercion-coerceschema)
82
83
  - [Error Handling](#error-handling)
83
84
  - [Configuration & API Reference](#configuration--api-reference)
84
85
 
@@ -1348,7 +1349,7 @@ The `{{key:N}}` syntax references variables from **different data sources**, ide
1348
1349
 
1349
1350
  ### Analysis with Identifier Schemas
1350
1351
 
1351
- Each identifier maps to its own JSON Schema:
1352
+ Each identifier maps to its own JSON Schema. Pass them via the `options` object:
1352
1353
 
1353
1354
  ```ts
1354
1355
  const engine = new Typebars();
@@ -1367,15 +1368,15 @@ const identifierSchemas = {
1367
1368
  };
1368
1369
 
1369
1370
  // ✅ Valid — meetingId exists in identifier 1's schema
1370
- engine.analyze("{{meetingId:1}}", inputSchema, identifierSchemas);
1371
+ engine.analyze("{{meetingId:1}}", inputSchema, { identifierSchemas });
1371
1372
  // → valid: true, outputSchema: { type: "string" }
1372
1373
 
1373
1374
  // ❌ Invalid — identifier 1 doesn't have "badKey"
1374
- engine.analyze("{{badKey:1}}", inputSchema, identifierSchemas);
1375
+ engine.analyze("{{badKey:1}}", inputSchema, { identifierSchemas });
1375
1376
  // → valid: false, code: IDENTIFIER_PROPERTY_NOT_FOUND
1376
1377
 
1377
1378
  // ❌ Invalid — identifier 99 doesn't exist
1378
- engine.analyze("{{meetingId:99}}", inputSchema, identifierSchemas);
1379
+ engine.analyze("{{meetingId:99}}", inputSchema, { identifierSchemas });
1379
1380
  // → valid: false, code: UNKNOWN_IDENTIFIER
1380
1381
 
1381
1382
  // ❌ Invalid — identifiers used but no schemas provided
@@ -1401,7 +1402,9 @@ const idSchemas = {
1401
1402
  };
1402
1403
 
1403
1404
  // ✅ "name" validated against schema, "meetingId:1" against idSchemas[1]
1404
- engine.analyze("{{name}} — {{meetingId:1}}", schema, idSchemas);
1405
+ engine.analyze("{{name}} — {{meetingId:1}}", schema, {
1406
+ identifierSchemas: idSchemas,
1407
+ });
1405
1408
  // → valid: true
1406
1409
  ```
1407
1410
 
@@ -1451,6 +1454,142 @@ value; // 99.95
1451
1454
 
1452
1455
  ---
1453
1456
 
1457
+ ## Output Type Coercion (`coerceSchema`)
1458
+
1459
+ By default, static literal values in templates are auto-detected by `detectLiteralType`:
1460
+ - `"123"` → `number`
1461
+ - `"true"` → `boolean`
1462
+ - `"null"` → `null`
1463
+ - `"hello"` → `string`
1464
+
1465
+ The `inputSchema` **never** influences this detection. However, you can provide an explicit `coerceSchema` in the options to override the output type inference for static literals.
1466
+
1467
+ ### Why `coerceSchema`?
1468
+
1469
+ When building objects from templates, you may want to force the output type of a static value to match a specific schema — for example, keeping `"123"` as a `string` instead of auto-detecting it as `number`. The `coerceSchema` is a **separate source of truth** from `inputSchema`, which avoids false positives in validation.
1470
+
1471
+ ### Basic Usage
1472
+
1473
+ ```ts
1474
+ const engine = new Typebars();
1475
+
1476
+ // Without coerceSchema — "123" is auto-detected as number
1477
+ engine.analyze("123", { type: "string" });
1478
+ // → outputSchema: { type: "number" }
1479
+
1480
+ // With coerceSchema — "123" respects the coercion schema
1481
+ engine.analyze("123", { type: "string" }, {
1482
+ coerceSchema: { type: "string" },
1483
+ });
1484
+ // → outputSchema: { type: "string" }
1485
+ ```
1486
+
1487
+ ### Object Templates with `coerceSchema`
1488
+
1489
+ For object templates, `coerceSchema` is resolved per-property and propagated recursively through nested objects:
1490
+
1491
+ ```ts
1492
+ const inputSchema = {
1493
+ type: "object",
1494
+ properties: {
1495
+ userName: { type: "string" },
1496
+ },
1497
+ };
1498
+
1499
+ const coerceSchema = {
1500
+ type: "object",
1501
+ properties: {
1502
+ meetingId: { type: "string" },
1503
+ config: {
1504
+ type: "object",
1505
+ properties: {
1506
+ retries: { type: "string" },
1507
+ },
1508
+ },
1509
+ },
1510
+ };
1511
+
1512
+ const result = engine.analyze(
1513
+ {
1514
+ meetingId: "12345", // coerceSchema says string → stays string
1515
+ count: "42", // not in coerceSchema → detectLiteralType → number
1516
+ label: "{{userName}}", // Handlebars expression → resolved from inputSchema
1517
+ config: {
1518
+ retries: "3", // coerceSchema says string → stays string
1519
+ },
1520
+ },
1521
+ inputSchema,
1522
+ { coerceSchema },
1523
+ );
1524
+
1525
+ result.outputSchema;
1526
+ // → {
1527
+ // type: "object",
1528
+ // properties: {
1529
+ // meetingId: { type: "string" }, ← coerced
1530
+ // count: { type: "number" }, ← auto-detected
1531
+ // label: { type: "string" }, ← from inputSchema
1532
+ // config: {
1533
+ // type: "object",
1534
+ // properties: {
1535
+ // retries: { type: "string" }, ← coerced (deep propagation)
1536
+ // },
1537
+ // required: ["retries"],
1538
+ // },
1539
+ // },
1540
+ // required: ["meetingId", "count", "label", "config"],
1541
+ // }
1542
+ ```
1543
+
1544
+ ### Rules
1545
+
1546
+ | Scenario | Output type |
1547
+ |----------|-------------|
1548
+ | Static literal, no `coerceSchema` | `detectLiteralType` (e.g. `"123"` → `number`) |
1549
+ | Static literal, `coerceSchema` with primitive type | Respects `coerceSchema` type |
1550
+ | Static literal, `coerceSchema` with non-primitive type (object, array) | Falls back to `detectLiteralType` |
1551
+ | Static literal, `coerceSchema` with no `type` | Falls back to `detectLiteralType` |
1552
+ | Handlebars expression (`{{expr}}`) | Always resolved from `inputSchema` — `coerceSchema` ignored |
1553
+ | Mixed template (`text + {{expr}}`) | Always `string` — `coerceSchema` ignored |
1554
+ | JS primitive literal (`42`, `true`, `null`) | Always `inferPrimitiveSchema` — `coerceSchema` ignored |
1555
+ | Property not in `coerceSchema` | Falls back to `detectLiteralType` |
1556
+
1557
+ ### With `analyzeAndExecute`
1558
+
1559
+ `coerceSchema` also works with `analyzeAndExecute`:
1560
+
1561
+ ```ts
1562
+ const { analysis, value } = engine.analyzeAndExecute(
1563
+ { meetingId: "12345", name: "{{userName}}" },
1564
+ inputSchema,
1565
+ { userName: "Alice" },
1566
+ {
1567
+ coerceSchema: {
1568
+ type: "object",
1569
+ properties: { meetingId: { type: "string" } },
1570
+ },
1571
+ },
1572
+ );
1573
+
1574
+ analysis.outputSchema;
1575
+ // → { type: "object", properties: { meetingId: { type: "string" }, name: { type: "string" } }, ... }
1576
+ value;
1577
+ // → { meetingId: "12345", name: "Alice" }
1578
+ ```
1579
+
1580
+ ### Standalone `analyze()` Function
1581
+
1582
+ The standalone `analyze()` function from `src/analyzer.ts` also accepts `coerceSchema`:
1583
+
1584
+ ```ts
1585
+ import { analyze } from "typebars/analyzer";
1586
+
1587
+ analyze("123", { type: "string" }, { coerceSchema: { type: "string" } });
1588
+ // → outputSchema: { type: "string" }
1589
+ ```
1590
+
1591
+ ---
1592
+
1454
1593
  ## Error Handling
1455
1594
 
1456
1595
  ### `TemplateParseError`
@@ -1542,10 +1681,10 @@ type TemplateInput =
1542
1681
 
1543
1682
  | Method | Description |
1544
1683
  |--------|-------------|
1545
- | `analyze(template, inputSchema, identifierSchemas?)` | Validates template + infers output schema. Returns `AnalysisResult` |
1546
- | `validate(template, inputSchema, identifierSchemas?)` | Like `analyze()` but without `outputSchema`. Returns `ValidationResult` |
1684
+ | `analyze(template, inputSchema, options?)` | Validates template + infers output schema. Options: `{ identifierSchemas?, coerceSchema? }`. Returns `AnalysisResult` |
1685
+ | `validate(template, inputSchema, options?)` | Like `analyze()` but without `outputSchema`. Returns `ValidationResult` |
1547
1686
  | `execute(template, data, options?)` | Renders the template. Options: `{ schema?, identifierData?, identifierSchemas? }` |
1548
- | `analyzeAndExecute(template, inputSchema, data, options?)` | Analyze + execute in one call. Returns `{ analysis, value }` |
1687
+ | `analyzeAndExecute(template, inputSchema, data, options?)` | Analyze + execute in one call. Options: `{ identifierSchemas?, identifierData?, coerceSchema? }`. Returns `{ analysis, value }` |
1549
1688
  | `compile(template)` | Returns a `CompiledTemplate` (parse-once, execute-many) |
1550
1689
  | `isValidSyntax(template)` | Syntax check only (no schema needed). Returns `boolean` |
1551
1690
  | `registerHelper(name, definition)` | Register a custom helper. Returns `this` |
@@ -1553,6 +1692,15 @@ type TemplateInput =
1553
1692
  | `hasHelper(name)` | Check if a helper is registered |
1554
1693
  | `clearCaches()` | Clear all internal caches |
1555
1694
 
1695
+ ### `AnalyzeOptions`
1696
+
1697
+ ```ts
1698
+ interface AnalyzeOptions {
1699
+ identifierSchemas?: Record<number, JSONSchema7>; // schemas by identifier
1700
+ coerceSchema?: JSONSchema7; // output type coercion
1701
+ }
1702
+ ```
1703
+
1556
1704
  ### `AnalysisResult`
1557
1705
 
1558
1706
  ```ts
@@ -15,27 +15,61 @@ interface AnalysisContext {
15
15
  /** Registered custom helpers (for static analysis) */
16
16
  helpers?: Map<string, HelperDefinition>;
17
17
  /**
18
- * Expected output type from the inputSchema.
19
- * When the inputSchema declares a specific type (e.g. `{ type: "string" }`),
20
- * static literal values like `"123"` should respect that type instead of
21
- * being auto-detected as `number`. This allows the schema contract to
22
- * override the default `detectLiteralType` inference.
18
+ * Explicit coercion schema provided by the caller.
19
+ * When set, static literal values like `"123"` will respect the type
20
+ * declared in this schema instead of being auto-detected by
21
+ * `detectLiteralType`. Unlike the previous `expectedOutputType`,
22
+ * this is NEVER derived from the inputSchema it must be explicitly
23
+ * provided via the `coerceSchema` option.
23
24
  */
24
- expectedOutputType?: JSONSchema7;
25
+ coerceSchema?: JSONSchema7;
26
+ }
27
+ /** Options for the standalone `analyze()` function */
28
+ export interface AnalyzeOptions {
29
+ /** Schemas by template identifier (for the `{{key:N}}` syntax) */
30
+ identifierSchemas?: Record<number, JSONSchema7>;
31
+ /**
32
+ * Explicit coercion schema. When provided, static literal values
33
+ * will respect the types declared in this schema instead of being
34
+ * auto-detected by `detectLiteralType`.
35
+ *
36
+ * This schema is independent from the `inputSchema` (which describes
37
+ * available variables) — it only controls the output type inference
38
+ * for static content.
39
+ */
40
+ coerceSchema?: JSONSchema7;
41
+ /**
42
+ * When `true`, properties whose values contain Handlebars expressions
43
+ * (i.e. any `{{…}}` syntax) are excluded from the output schema.
44
+ *
45
+ * Only the properties with static values (literals, plain strings
46
+ * without expressions) are retained. This is useful when you want
47
+ * the output schema to describe only the known, compile-time-constant
48
+ * portion of the template.
49
+ *
50
+ * This option only has an effect on **object** and **array** templates.
51
+ * A root-level string template with expressions is analyzed normally
52
+ * (there is no parent property to exclude it from).
53
+ *
54
+ * @default false
55
+ */
56
+ excludeTemplateExpression?: boolean;
25
57
  }
26
58
  /**
27
59
  * Statically analyzes a template against a JSON Schema v7 describing the
28
60
  * available context.
29
61
  *
30
62
  * Backward-compatible version — parses the template internally.
63
+ * Uses `dispatchAnalyze` for the recursive array/object/literal dispatching,
64
+ * delegating only the string (template) case to `analyzeFromAst`.
31
65
  *
32
66
  * @param template - The template string (e.g. `"Hello {{user.name}}"`)
33
67
  * @param inputSchema - JSON Schema v7 describing the available variables
34
- * @param identifierSchemas - (optional) Schemas by identifier `{ [id]: JSONSchema7 }`
68
+ * @param options - (optional) Analysis options (identifierSchemas, coerceSchema)
35
69
  * @returns An `AnalysisResult` containing validity, diagnostics, and the
36
70
  * inferred output schema.
37
71
  */
38
- export declare function analyze(template: TemplateInput, inputSchema: JSONSchema7, identifierSchemas?: Record<number, JSONSchema7>, expectedOutputType?: JSONSchema7): AnalysisResult;
72
+ export declare function analyze(template: TemplateInput, inputSchema?: JSONSchema7, options?: AnalyzeOptions): AnalysisResult;
39
73
  /**
40
74
  * Statically analyzes a template from an already-parsed AST.
41
75
  *
@@ -48,15 +82,15 @@ export declare function analyze(template: TemplateInput, inputSchema: JSONSchema
48
82
  * @param options - Additional options
49
83
  * @returns An `AnalysisResult`
50
84
  */
51
- export declare function analyzeFromAst(ast: hbs.AST.Program, template: string, inputSchema: JSONSchema7, options?: {
85
+ export declare function analyzeFromAst(ast: hbs.AST.Program, template: string, inputSchema?: JSONSchema7, options?: {
52
86
  identifierSchemas?: Record<number, JSONSchema7>;
53
87
  helpers?: Map<string, HelperDefinition>;
54
88
  /**
55
- * When set, provides the expected output type from the parent context
56
- * (e.g. the inputSchema's property sub-schema for an object template key).
57
- * Static literal values will respect this type instead of auto-detecting.
89
+ * Explicit coercion schema. When set, static literal values will
90
+ * respect the types declared in this schema instead of auto-detecting.
91
+ * Unlike `expectedOutputType`, this is NEVER derived from inputSchema.
58
92
  */
59
- expectedOutputType?: JSONSchema7;
93
+ coerceSchema?: JSONSchema7;
60
94
  }): AnalysisResult;
61
95
  /**
62
96
  * Infers the output type of a BlockStatement and validates its content.
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:Object.getOwnPropertyDescriptor(all,name).get})}_export(exports,{get analyze(){return analyze},get analyzeFromAst(){return analyzeFromAst},get inferBlockType(){return inferBlockType}});const _errors=require("./errors.js");const _parser=require("./parser.js");const _schemaresolver=require("./schema-resolver.js");const _typests=require("./types.js");const _utils=require("./utils.js");function analyze(template,inputSchema,identifierSchemas,expectedOutputType){if((0,_typests.isArrayInput)(template)){return analyzeArrayTemplate(template,inputSchema,identifierSchemas)}if((0,_typests.isObjectInput)(template)){return analyzeObjectTemplate(template,inputSchema,identifierSchemas,expectedOutputType)}if((0,_typests.isLiteralInput)(template)){return{valid:true,diagnostics:[],outputSchema:(0,_typests.inferPrimitiveSchema)(template)}}const ast=(0,_parser.parse)(template);return analyzeFromAst(ast,template,inputSchema,{identifierSchemas,expectedOutputType:expectedOutputType??inputSchema})}function analyzeArrayTemplate(template,inputSchema,identifierSchemas){return(0,_utils.aggregateArrayAnalysis)(template.length,index=>analyze(template[index],inputSchema,identifierSchemas))}function analyzeObjectTemplate(template,inputSchema,identifierSchemas,expectedOutputType){const schemaForProperties=expectedOutputType??inputSchema;return(0,_utils.aggregateObjectAnalysis)(Object.keys(template),key=>{const propertySchema=(0,_schemaresolver.resolveSchemaPath)(schemaForProperties,[key]);return analyze(template[key],inputSchema,identifierSchemas,propertySchema)})}function analyzeFromAst(ast,template,inputSchema,options){(0,_schemaresolver.assertNoConditionalSchema)(inputSchema);if(options?.identifierSchemas){for(const[id,idSchema]of Object.entries(options.identifierSchemas)){(0,_schemaresolver.assertNoConditionalSchema)(idSchema,`/identifierSchemas/${id}`)}}const ctx={root:inputSchema,current:inputSchema,diagnostics:[],template,identifierSchemas:options?.identifierSchemas,helpers:options?.helpers,expectedOutputType:options?.expectedOutputType};const outputSchema=inferProgramType(ast,ctx);const hasErrors=ctx.diagnostics.some(d=>d.severity==="error");return{valid:!hasErrors,diagnostics:ctx.diagnostics,outputSchema:(0,_schemaresolver.simplifySchema)(outputSchema)}}function processStatement(stmt,ctx){switch(stmt.type){case"ContentStatement":case"CommentStatement":return undefined;case"MustacheStatement":return processMustache(stmt,ctx);case"BlockStatement":return inferBlockType(stmt,ctx);default:addDiagnostic(ctx,"UNANALYZABLE","warning",`Unsupported AST node type: "${stmt.type}"`,stmt);return undefined}}function processMustache(stmt,ctx){if(stmt.path.type==="SubExpression"){addDiagnostic(ctx,"UNANALYZABLE","warning","Sub-expressions are not statically analyzable",stmt);return{}}if(stmt.params.length>0||stmt.hash){const helperName=getExpressionName(stmt.path);const helper=ctx.helpers?.get(helperName);if(helper){const helperParams=helper.params;if(helperParams){const requiredCount=helperParams.filter(p=>!p.optional).length;if(stmt.params.length<requiredCount){addDiagnostic(ctx,"MISSING_ARGUMENT","error",`Helper "${helperName}" expects at least ${requiredCount} argument(s), but got ${stmt.params.length}`,stmt,{helperName,expected:`${requiredCount} argument(s)`,actual:`${stmt.params.length} argument(s)`})}}for(let i=0;i<stmt.params.length;i++){const resolvedSchema=resolveExpressionWithDiagnostics(stmt.params[i],ctx,stmt);const helperParam=helperParams?.[i];if(resolvedSchema&&helperParam?.type){const expectedType=helperParam.type;if(!isParamTypeCompatible(resolvedSchema,expectedType)){const paramName=helperParam.name;addDiagnostic(ctx,"TYPE_MISMATCH","error",`Helper "${helperName}" parameter "${paramName}" expects ${schemaTypeLabel(expectedType)}, but got ${schemaTypeLabel(resolvedSchema)}`,stmt,{helperName,expected:schemaTypeLabel(expectedType),actual:schemaTypeLabel(resolvedSchema)})}}}return helper.returnType??{type:"string"}}addDiagnostic(ctx,"UNKNOWN_HELPER","warning",`Unknown inline helper "${helperName}" — cannot analyze statically`,stmt,{helperName});return{type:"string"}}return resolveExpressionWithDiagnostics(stmt.path,ctx,stmt)??{}}function isParamTypeCompatible(resolved,expected){if(!expected.type||!resolved.type)return true;const expectedTypes=Array.isArray(expected.type)?expected.type:[expected.type];const resolvedTypes=Array.isArray(resolved.type)?resolved.type:[resolved.type];return resolvedTypes.some(rt=>expectedTypes.some(et=>rt===et||et==="number"&&rt==="integer"||et==="integer"&&rt==="number"))}function inferProgramType(program,ctx){const effective=(0,_parser.getEffectiveBody)(program);if(effective.length===0){return{type:"string"}}const singleExpr=(0,_parser.getEffectivelySingleExpression)(program);if(singleExpr){return processMustache(singleExpr,ctx)}const singleBlock=(0,_parser.getEffectivelySingleBlock)(program);if(singleBlock){return inferBlockType(singleBlock,ctx)}const allContent=effective.every(s=>s.type==="ContentStatement");if(allContent){const text=effective.map(s=>s.value).join("").trim();if(text==="")return{type:"string"};const expectedType=ctx.expectedOutputType?.type;if(typeof expectedType==="string"&&(expectedType==="string"||expectedType==="number"||expectedType==="integer"||expectedType==="boolean"||expectedType==="null")){return{type:expectedType}}const literalType=(0,_parser.detectLiteralType)(text);if(literalType)return{type:literalType}}const allBlocks=effective.every(s=>s.type==="BlockStatement");if(allBlocks){const types=[];for(const stmt of effective){const t=inferBlockType(stmt,ctx);if(t)types.push(t)}if(types.length===1)return types[0];if(types.length>1)return(0,_schemaresolver.simplifySchema)({oneOf:types});return{type:"string"}}for(const stmt of program.body){processStatement(stmt,ctx)}return{type:"string"}}function inferBlockType(stmt,ctx){const helperName=getBlockHelperName(stmt);switch(helperName){case"if":case"unless":{const arg=getBlockArgument(stmt);if(arg){resolveExpressionWithDiagnostics(arg,ctx,stmt)}else{addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)(helperName),stmt,{helperName})}const thenType=inferProgramType(stmt.program,ctx);if(stmt.inverse){const elseType=inferProgramType(stmt.inverse,ctx);if((0,_utils.deepEqual)(thenType,elseType))return thenType;return(0,_schemaresolver.simplifySchema)({oneOf:[thenType,elseType]})}return thenType}case"each":{const arg=getBlockArgument(stmt);if(!arg){addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)("each"),stmt,{helperName:"each"});const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const collectionSchema=resolveExpressionWithDiagnostics(arg,ctx,stmt);if(!collectionSchema){const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const itemSchema=(0,_schemaresolver.resolveArrayItems)(collectionSchema,ctx.root);if(!itemSchema){addDiagnostic(ctx,"TYPE_MISMATCH","error",(0,_errors.createTypeMismatchMessage)("each","an array",schemaTypeLabel(collectionSchema)),stmt,{helperName:"each",expected:"array",actual:schemaTypeLabel(collectionSchema)});const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const saved=ctx.current;ctx.current=itemSchema;inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}case"with":{const arg=getBlockArgument(stmt);if(!arg){addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)("with"),stmt,{helperName:"with"});const saved=ctx.current;ctx.current={};const result=inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return result}const innerSchema=resolveExpressionWithDiagnostics(arg,ctx,stmt);const saved=ctx.current;ctx.current=innerSchema??{};const result=inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return result}default:{const helper=ctx.helpers?.get(helperName);if(helper){for(const param of stmt.params){resolveExpressionWithDiagnostics(param,ctx,stmt)}inferProgramType(stmt.program,ctx);if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return helper.returnType??{type:"string"}}addDiagnostic(ctx,"UNKNOWN_HELPER","warning",(0,_errors.createUnknownHelperMessage)(helperName),stmt,{helperName});inferProgramType(stmt.program,ctx);if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}}}function resolveExpressionWithDiagnostics(expr,ctx,parentNode){if((0,_parser.isThisExpression)(expr)){return ctx.current}if(expr.type==="SubExpression"){return resolveSubExpression(expr,ctx,parentNode)}const segments=(0,_parser.extractPathSegments)(expr);if(segments.length===0){if(expr.type==="StringLiteral")return{type:"string"};if(expr.type==="NumberLiteral")return{type:"number"};if(expr.type==="BooleanLiteral")return{type:"boolean"};if(expr.type==="NullLiteral")return{type:"null"};if(expr.type==="UndefinedLiteral")return{};addDiagnostic(ctx,"UNANALYZABLE","warning",(0,_errors.createUnanalyzableMessage)(expr.type),parentNode??expr);return undefined}const{cleanSegments,identifier}=(0,_parser.extractExpressionIdentifier)(segments);if(identifier!==null){return resolveWithIdentifier(cleanSegments,identifier,ctx,parentNode??expr)}const resolved=(0,_schemaresolver.resolveSchemaPath)(ctx.current,cleanSegments);if(resolved===undefined){const fullPath=cleanSegments.join(".");const availableProperties=(0,_utils.getSchemaPropertyNames)(ctx.current);addDiagnostic(ctx,"UNKNOWN_PROPERTY","error",(0,_errors.createPropertyNotFoundMessage)(fullPath,availableProperties),parentNode??expr,{path:fullPath,availableProperties});return undefined}return resolved}function resolveWithIdentifier(cleanSegments,identifier,ctx,node){const fullPath=cleanSegments.join(".");if(!ctx.identifierSchemas){addDiagnostic(ctx,"MISSING_IDENTIFIER_SCHEMAS","error",`Property "${fullPath}:${identifier}" uses an identifier but no identifier schemas were provided`,node,{path:`${fullPath}:${identifier}`,identifier});return undefined}const idSchema=ctx.identifierSchemas[identifier];if(!idSchema){addDiagnostic(ctx,"UNKNOWN_IDENTIFIER","error",`Property "${fullPath}:${identifier}" references identifier ${identifier} but no schema exists for this identifier`,node,{path:`${fullPath}:${identifier}`,identifier});return undefined}const resolved=(0,_schemaresolver.resolveSchemaPath)(idSchema,cleanSegments);if(resolved===undefined){const availableProperties=(0,_utils.getSchemaPropertyNames)(idSchema);addDiagnostic(ctx,"IDENTIFIER_PROPERTY_NOT_FOUND","error",`Property "${fullPath}" does not exist in the schema for identifier ${identifier}`,node,{path:fullPath,identifier,availableProperties});return undefined}return resolved}function resolveSubExpression(expr,ctx,parentNode){const helperName=getExpressionName(expr.path);const helper=ctx.helpers?.get(helperName);if(!helper){addDiagnostic(ctx,"UNKNOWN_HELPER","warning",`Unknown sub-expression helper "${helperName}" — cannot analyze statically`,parentNode??expr,{helperName});return{type:"string"}}const helperParams=helper.params;if(helperParams){const requiredCount=helperParams.filter(p=>!p.optional).length;if(expr.params.length<requiredCount){addDiagnostic(ctx,"MISSING_ARGUMENT","error",`Helper "${helperName}" expects at least ${requiredCount} argument(s), but got ${expr.params.length}`,parentNode??expr,{helperName,expected:`${requiredCount} argument(s)`,actual:`${expr.params.length} argument(s)`})}}for(let i=0;i<expr.params.length;i++){const resolvedSchema=resolveExpressionWithDiagnostics(expr.params[i],ctx,parentNode??expr);const helperParam=helperParams?.[i];if(resolvedSchema&&helperParam?.type){const expectedType=helperParam.type;if(!isParamTypeCompatible(resolvedSchema,expectedType)){const paramName=helperParam.name;addDiagnostic(ctx,"TYPE_MISMATCH","error",`Helper "${helperName}" parameter "${paramName}" expects ${schemaTypeLabel(expectedType)}, but got ${schemaTypeLabel(resolvedSchema)}`,parentNode??expr,{helperName,expected:schemaTypeLabel(expectedType),actual:schemaTypeLabel(resolvedSchema)})}}}return helper.returnType??{type:"string"}}function getBlockArgument(stmt){return stmt.params[0]}function getBlockHelperName(stmt){if(stmt.path.type==="PathExpression"){return stmt.path.original}return""}function getExpressionName(expr){if(expr.type==="PathExpression"){return expr.original}return""}function addDiagnostic(ctx,code,severity,message,node,details){const diagnostic={severity,code,message};if(node&&"loc"in node&&node.loc){diagnostic.loc={start:{line:node.loc.start.line,column:node.loc.start.column},end:{line:node.loc.end.line,column:node.loc.end.column}};diagnostic.source=(0,_utils.extractSourceSnippet)(ctx.template,diagnostic.loc)}if(details){diagnostic.details=details}ctx.diagnostics.push(diagnostic)}function schemaTypeLabel(schema){if(schema.type){return Array.isArray(schema.type)?schema.type.join(" | "):schema.type}if(schema.oneOf)return"oneOf(...)";if(schema.anyOf)return"anyOf(...)";if(schema.allOf)return"allOf(...)";if(schema.enum)return"enum";return"unknown"}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});function _export(target,all){for(var name in all)Object.defineProperty(target,name,{enumerable:true,get:Object.getOwnPropertyDescriptor(all,name).get})}_export(exports,{get analyze(){return analyze},get analyzeFromAst(){return analyzeFromAst},get inferBlockType(){return inferBlockType}});const _dispatchts=require("./dispatch.js");const _errors=require("./errors.js");const _parser=require("./parser.js");const _schemaresolver=require("./schema-resolver.js");const _utils=require("./utils.js");function analyze(template,inputSchema={},options){return(0,_dispatchts.dispatchAnalyze)(template,options,(tpl,coerceSchema)=>{const ast=(0,_parser.parse)(tpl);return analyzeFromAst(ast,tpl,inputSchema,{identifierSchemas:options?.identifierSchemas,coerceSchema})},(child,childOptions)=>analyze(child,inputSchema,childOptions))}function analyzeFromAst(ast,template,inputSchema={},options){(0,_schemaresolver.assertNoConditionalSchema)(inputSchema);if(options?.identifierSchemas){for(const[id,idSchema]of Object.entries(options.identifierSchemas)){(0,_schemaresolver.assertNoConditionalSchema)(idSchema,`/identifierSchemas/${id}`)}}const ctx={root:inputSchema,current:inputSchema,diagnostics:[],template,identifierSchemas:options?.identifierSchemas,helpers:options?.helpers,coerceSchema:options?.coerceSchema};const outputSchema=inferProgramType(ast,ctx);const hasErrors=ctx.diagnostics.some(d=>d.severity==="error");return{valid:!hasErrors,diagnostics:ctx.diagnostics,outputSchema:(0,_schemaresolver.simplifySchema)(outputSchema)}}function processStatement(stmt,ctx){switch(stmt.type){case"ContentStatement":case"CommentStatement":return undefined;case"MustacheStatement":return processMustache(stmt,ctx);case"BlockStatement":return inferBlockType(stmt,ctx);default:addDiagnostic(ctx,"UNANALYZABLE","warning",`Unsupported AST node type: "${stmt.type}"`,stmt);return undefined}}function processMustache(stmt,ctx){if(stmt.path.type==="SubExpression"){addDiagnostic(ctx,"UNANALYZABLE","warning","Sub-expressions are not statically analyzable",stmt);return{}}if(stmt.params.length>0||stmt.hash){const helperName=getExpressionName(stmt.path);const helper=ctx.helpers?.get(helperName);if(helper){const helperParams=helper.params;if(helperParams){const requiredCount=helperParams.filter(p=>!p.optional).length;if(stmt.params.length<requiredCount){addDiagnostic(ctx,"MISSING_ARGUMENT","error",`Helper "${helperName}" expects at least ${requiredCount} argument(s), but got ${stmt.params.length}`,stmt,{helperName,expected:`${requiredCount} argument(s)`,actual:`${stmt.params.length} argument(s)`})}}for(let i=0;i<stmt.params.length;i++){const resolvedSchema=resolveExpressionWithDiagnostics(stmt.params[i],ctx,stmt);const helperParam=helperParams?.[i];if(resolvedSchema&&helperParam?.type){const expectedType=helperParam.type;if(!isParamTypeCompatible(resolvedSchema,expectedType)){const paramName=helperParam.name;addDiagnostic(ctx,"TYPE_MISMATCH","error",`Helper "${helperName}" parameter "${paramName}" expects ${schemaTypeLabel(expectedType)}, but got ${schemaTypeLabel(resolvedSchema)}`,stmt,{helperName,expected:schemaTypeLabel(expectedType),actual:schemaTypeLabel(resolvedSchema)})}}}return helper.returnType??{type:"string"}}addDiagnostic(ctx,"UNKNOWN_HELPER","warning",`Unknown inline helper "${helperName}" — cannot analyze statically`,stmt,{helperName});return{type:"string"}}return resolveExpressionWithDiagnostics(stmt.path,ctx,stmt)??{}}function isParamTypeCompatible(resolved,expected){if(!expected.type||!resolved.type)return true;const expectedTypes=Array.isArray(expected.type)?expected.type:[expected.type];const resolvedTypes=Array.isArray(resolved.type)?resolved.type:[resolved.type];return resolvedTypes.some(rt=>expectedTypes.some(et=>rt===et||et==="number"&&rt==="integer"||et==="integer"&&rt==="number"))}function inferProgramType(program,ctx){const effective=(0,_parser.getEffectiveBody)(program);if(effective.length===0){return{type:"string"}}const singleExpr=(0,_parser.getEffectivelySingleExpression)(program);if(singleExpr){return processMustache(singleExpr,ctx)}const singleBlock=(0,_parser.getEffectivelySingleBlock)(program);if(singleBlock){return inferBlockType(singleBlock,ctx)}const allContent=effective.every(s=>s.type==="ContentStatement");if(allContent){const text=effective.map(s=>s.value).join("").trim();if(text==="")return{type:"string"};const coercedType=ctx.coerceSchema?.type;if(typeof coercedType==="string"&&(coercedType==="string"||coercedType==="number"||coercedType==="integer"||coercedType==="boolean"||coercedType==="null")){return{type:coercedType}}const literalType=(0,_parser.detectLiteralType)(text);if(literalType)return{type:literalType}}const allBlocks=effective.every(s=>s.type==="BlockStatement");if(allBlocks){const types=[];for(const stmt of effective){const t=inferBlockType(stmt,ctx);if(t)types.push(t)}if(types.length===1)return types[0];if(types.length>1)return(0,_schemaresolver.simplifySchema)({oneOf:types});return{type:"string"}}for(const stmt of program.body){processStatement(stmt,ctx)}return{type:"string"}}function inferBlockType(stmt,ctx){const helperName=getBlockHelperName(stmt);switch(helperName){case"if":case"unless":{const arg=getBlockArgument(stmt);if(arg){resolveExpressionWithDiagnostics(arg,ctx,stmt)}else{addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)(helperName),stmt,{helperName})}const thenType=inferProgramType(stmt.program,ctx);if(stmt.inverse){const elseType=inferProgramType(stmt.inverse,ctx);if((0,_utils.deepEqual)(thenType,elseType))return thenType;return(0,_schemaresolver.simplifySchema)({oneOf:[thenType,elseType]})}return thenType}case"each":{const arg=getBlockArgument(stmt);if(!arg){addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)("each"),stmt,{helperName:"each"});const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const collectionSchema=resolveExpressionWithDiagnostics(arg,ctx,stmt);if(!collectionSchema){const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const itemSchema=(0,_schemaresolver.resolveArrayItems)(collectionSchema,ctx.root);if(!itemSchema){addDiagnostic(ctx,"TYPE_MISMATCH","error",(0,_errors.createTypeMismatchMessage)("each","an array",schemaTypeLabel(collectionSchema)),stmt,{helperName:"each",expected:"array",actual:schemaTypeLabel(collectionSchema)});const saved=ctx.current;ctx.current={};inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}const saved=ctx.current;ctx.current=itemSchema;inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}case"with":{const arg=getBlockArgument(stmt);if(!arg){addDiagnostic(ctx,"MISSING_ARGUMENT","error",(0,_errors.createMissingArgumentMessage)("with"),stmt,{helperName:"with"});const saved=ctx.current;ctx.current={};const result=inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return result}const innerSchema=resolveExpressionWithDiagnostics(arg,ctx,stmt);const saved=ctx.current;ctx.current=innerSchema??{};const result=inferProgramType(stmt.program,ctx);ctx.current=saved;if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return result}default:{const helper=ctx.helpers?.get(helperName);if(helper){for(const param of stmt.params){resolveExpressionWithDiagnostics(param,ctx,stmt)}inferProgramType(stmt.program,ctx);if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return helper.returnType??{type:"string"}}addDiagnostic(ctx,"UNKNOWN_HELPER","warning",(0,_errors.createUnknownHelperMessage)(helperName),stmt,{helperName});inferProgramType(stmt.program,ctx);if(stmt.inverse)inferProgramType(stmt.inverse,ctx);return{type:"string"}}}}function resolveExpressionWithDiagnostics(expr,ctx,parentNode){if((0,_parser.isThisExpression)(expr)){return ctx.current}if(expr.type==="SubExpression"){return resolveSubExpression(expr,ctx,parentNode)}const segments=(0,_parser.extractPathSegments)(expr);if(segments.length===0){if(expr.type==="StringLiteral")return{type:"string"};if(expr.type==="NumberLiteral")return{type:"number"};if(expr.type==="BooleanLiteral")return{type:"boolean"};if(expr.type==="NullLiteral")return{type:"null"};if(expr.type==="UndefinedLiteral")return{};addDiagnostic(ctx,"UNANALYZABLE","warning",(0,_errors.createUnanalyzableMessage)(expr.type),parentNode??expr);return undefined}const{cleanSegments,identifier}=(0,_parser.extractExpressionIdentifier)(segments);if((0,_parser.isRootPathTraversal)(cleanSegments)){const fullPath=cleanSegments.join(".");addDiagnostic(ctx,"ROOT_PATH_TRAVERSAL","error",(0,_errors.createRootPathTraversalMessage)(fullPath),parentNode??expr,{path:fullPath});return undefined}if((0,_parser.isRootSegments)(cleanSegments)){if(identifier!==null){return resolveRootWithIdentifier(identifier,ctx,parentNode??expr)}return ctx.current}if(identifier!==null){return resolveWithIdentifier(cleanSegments,identifier,ctx,parentNode??expr)}const resolved=(0,_schemaresolver.resolveSchemaPath)(ctx.current,cleanSegments);if(resolved===undefined){const fullPath=cleanSegments.join(".");const availableProperties=(0,_utils.getSchemaPropertyNames)(ctx.current);addDiagnostic(ctx,"UNKNOWN_PROPERTY","error",(0,_errors.createPropertyNotFoundMessage)(fullPath,availableProperties),parentNode??expr,{path:fullPath,availableProperties});return undefined}return resolved}function resolveRootWithIdentifier(identifier,ctx,node){if(!ctx.identifierSchemas){addDiagnostic(ctx,"MISSING_IDENTIFIER_SCHEMAS","error",`Property "$root:${identifier}" uses an identifier but no identifier schemas were provided`,node,{path:`$root:${identifier}`,identifier});return undefined}const idSchema=ctx.identifierSchemas[identifier];if(!idSchema){addDiagnostic(ctx,"UNKNOWN_IDENTIFIER","error",`Property "$root:${identifier}" references identifier ${identifier} but no schema exists for this identifier`,node,{path:`$root:${identifier}`,identifier});return undefined}return idSchema}function resolveWithIdentifier(cleanSegments,identifier,ctx,node){const fullPath=cleanSegments.join(".");if(!ctx.identifierSchemas){addDiagnostic(ctx,"MISSING_IDENTIFIER_SCHEMAS","error",`Property "${fullPath}:${identifier}" uses an identifier but no identifier schemas were provided`,node,{path:`${fullPath}:${identifier}`,identifier});return undefined}const idSchema=ctx.identifierSchemas[identifier];if(!idSchema){addDiagnostic(ctx,"UNKNOWN_IDENTIFIER","error",`Property "${fullPath}:${identifier}" references identifier ${identifier} but no schema exists for this identifier`,node,{path:`${fullPath}:${identifier}`,identifier});return undefined}const resolved=(0,_schemaresolver.resolveSchemaPath)(idSchema,cleanSegments);if(resolved===undefined){const availableProperties=(0,_utils.getSchemaPropertyNames)(idSchema);addDiagnostic(ctx,"IDENTIFIER_PROPERTY_NOT_FOUND","error",`Property "${fullPath}" does not exist in the schema for identifier ${identifier}`,node,{path:fullPath,identifier,availableProperties});return undefined}return resolved}function resolveSubExpression(expr,ctx,parentNode){const helperName=getExpressionName(expr.path);const helper=ctx.helpers?.get(helperName);if(!helper){addDiagnostic(ctx,"UNKNOWN_HELPER","warning",`Unknown sub-expression helper "${helperName}" — cannot analyze statically`,parentNode??expr,{helperName});return{type:"string"}}const helperParams=helper.params;if(helperParams){const requiredCount=helperParams.filter(p=>!p.optional).length;if(expr.params.length<requiredCount){addDiagnostic(ctx,"MISSING_ARGUMENT","error",`Helper "${helperName}" expects at least ${requiredCount} argument(s), but got ${expr.params.length}`,parentNode??expr,{helperName,expected:`${requiredCount} argument(s)`,actual:`${expr.params.length} argument(s)`})}}for(let i=0;i<expr.params.length;i++){const resolvedSchema=resolveExpressionWithDiagnostics(expr.params[i],ctx,parentNode??expr);const helperParam=helperParams?.[i];if(resolvedSchema&&helperParam?.type){const expectedType=helperParam.type;if(!isParamTypeCompatible(resolvedSchema,expectedType)){const paramName=helperParam.name;addDiagnostic(ctx,"TYPE_MISMATCH","error",`Helper "${helperName}" parameter "${paramName}" expects ${schemaTypeLabel(expectedType)}, but got ${schemaTypeLabel(resolvedSchema)}`,parentNode??expr,{helperName,expected:schemaTypeLabel(expectedType),actual:schemaTypeLabel(resolvedSchema)})}}}return helper.returnType??{type:"string"}}function getBlockArgument(stmt){return stmt.params[0]}function getBlockHelperName(stmt){if(stmt.path.type==="PathExpression"){return stmt.path.original}return""}function getExpressionName(expr){if(expr.type==="PathExpression"){return expr.original}return""}function addDiagnostic(ctx,code,severity,message,node,details){const diagnostic={severity,code,message};if(node&&"loc"in node&&node.loc){diagnostic.loc={start:{line:node.loc.start.line,column:node.loc.start.column},end:{line:node.loc.end.line,column:node.loc.end.column}};diagnostic.source=(0,_utils.extractSourceSnippet)(ctx.template,diagnostic.loc)}if(details){diagnostic.details=details}ctx.diagnostics.push(diagnostic)}function schemaTypeLabel(schema){if(schema.type){return Array.isArray(schema.type)?schema.type.join(" | "):schema.type}if(schema.oneOf)return"oneOf(...)";if(schema.anyOf)return"anyOf(...)";if(schema.allOf)return"allOf(...)";if(schema.enum)return"enum";return"unknown"}
2
2
  //# sourceMappingURL=analyzer.js.map