@xaendar/compiler 0.7.23 → 0.7.25

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.
@@ -11,7 +11,7 @@
11
11
  * into the generated `adoptedStyleSheets` assignment.
12
12
  * @returns A string containing the compiled Javascript render method body.
13
13
  */
14
- export declare function compile(input: string, className: string, cssVariableName?: string): {
14
+ export declare function compile(input: string, cssVariableName?: string): {
15
15
  javascript: string;
16
16
  typescript: string;
17
17
  };
@@ -907,7 +907,7 @@ var Generator = class {
907
907
  }
908
908
  _processNode(node, parentNode, index, compilerContext, anchor) {
909
909
  const state = this._states[node.type];
910
- if (!state) throw new Error(`[Parser] No transition function for token type ${ASTNodeType[node.type]}`);
910
+ if (!state) throw new Error(`[Generator] No transition function for token type ${ASTNodeType[node.type]}`);
911
911
  return state(node, parentNode, index, compilerContext, anchor);
912
912
  }
913
913
  };
@@ -2901,46 +2901,43 @@ var Parser = class {
2901
2901
  };
2902
2902
  //#endregion
2903
2903
  //#region ../packages/compiler/src/type-checker/states/type-check-element.state.ts
2904
- function typeCheckElement(node, parentNode, index, context) {
2905
- const nodeName = getElementIdentifier(node, parentNode.identifier, index);
2906
- const retVal = {
2907
- code: [`let ${nodeName}!: HTMLElement`],
2908
- functionsToProcess: /* @__PURE__ */ new Map()
2909
- };
2910
- node.attributes.forEach(({ name, value }) => {
2911
- if (!(typeof value === "string")) retVal.code.push(`const ${nodeName}_${name} = ${resolveExpression(value.expression, context, { resolver: "root" })};`);
2904
+ /**
2905
+ * Type-checks an element node: its attribute/property bindings, its event
2906
+ * handlers, and recursively its children.
2907
+ *
2908
+ * No variable is declared for the element itself (see the module doc on
2909
+ * `TypeChecker` for why) — attribute expressions and event calls are
2910
+ * emitted as bare statements, validated in place.
2911
+ */
2912
+ function typeCheckElement(node, context, index, processNode) {
2913
+ const lines = [];
2914
+ node.attributes.forEach(({ value }) => {
2915
+ if (typeof value !== "string") lines.push(`${resolveExpression(value.expression, context, { resolver: "root" })};`);
2912
2916
  });
2913
2917
  context.addUnresolvableIdentifier("$event");
2914
- node.events.forEach(({ name, handler, parameters }) => {
2915
- let parsedEventParameter = false;
2916
- const mappedParameters = parameters.map((parameter) => {
2917
- const resolvedParameter = resolveExpression(parameter, context, { resolver: "root" });
2918
- if (!parsedEventParameter && resolvedParameter === "$event") {
2919
- parsedEventParameter = true;
2920
- return `$event`;
2921
- } else return `${resolvedParameter}`;
2922
- }).join(", ");
2923
- const beginning = parsedEventParameter ? "($event)" : "()";
2924
- retVal.code.push(`const ${nodeName}_${name} = ${beginning} => root.${handler}(${mappedParameters});`);
2918
+ node.events.forEach(({ handler, parameters }) => {
2919
+ const args = parameters.map((parameter) => resolveExpression(parameter, context, { resolver: "root" })).join(", ");
2920
+ lines.push(`root.${handler}(${args});`);
2925
2921
  });
2926
- if (node.children.length) retVal.functionsToProcess.set(`${nodeName}Children`, { fn: {
2927
- node,
2928
- parentNode,
2929
- context
2930
- } });
2931
- return retVal;
2922
+ node.children.forEach((child, i) => {
2923
+ lines.push(...processNode(child, context, i.toString()));
2924
+ });
2925
+ return lines;
2932
2926
  }
2933
2927
  //#endregion
2934
2928
  //#region ../packages/compiler/src/type-checker/states/type-check-for.state.ts
2935
- function typeCheckFor(node, parentNode, index, compilerContext) {
2936
- const retVal = {
2937
- code: [],
2938
- functionsToProcess: /* @__PURE__ */ new Map()
2939
- };
2940
- const iterableSource = node.iterableSource;
2941
- const iterableExpr = compilerContext.hasIdentifier(iterableSource) ? iterableSource : `root.${iterableSource}`;
2942
- const itemsName = getTextIdentifier("items", parentNode.identifier, index);
2943
- const counterName = getTextIdentifier("i", parentNode.identifier, index);
2929
+ /**
2930
+ * Type-checks an `@for` block using a real `for...of` loop.
2931
+ *
2932
+ * This replaces the previous "synthetic function with a `typeof array`
2933
+ * parameter" trick, which mistyped the loop variable as the *whole array*
2934
+ * rather than a single element. A real `for (const item of array)` lets
2935
+ * TypeScript infer `item`'s type correctly as the array's element type —
2936
+ * exactly like it would for a loop written by hand — with no synthetic
2937
+ * function boundary needed.
2938
+ */
2939
+ function typeCheckFor(node, context, index, processNode) {
2940
+ const iterableExpr = context.hasIdentifier(node.iterableSource) ? node.iterableSource : `root.${node.iterableSource}`;
2944
2941
  const indexName = resolveImplicit(node, "$index");
2945
2942
  const firstName = resolveImplicit(node, "$first");
2946
2943
  const lastName = resolveImplicit(node, "$last");
@@ -2953,22 +2950,20 @@ function typeCheckFor(node, parentNode, index, compilerContext) {
2953
2950
  [lastName, "signal"],
2954
2951
  [evenName, "signal"],
2955
2952
  [oddName, "signal"]
2956
- ], compilerContext);
2957
- const forKey = getBlockIdentifier("for", parentNode.identifier, index);
2958
- retVal.functionsToProcess.set(forKey, {
2959
- fn: {
2960
- node,
2961
- parentNode,
2962
- context: forContext
2963
- },
2964
- args: [`${itemsName}: typeof ${iterableExpr}`, `${counterName}: number`]
2965
- });
2966
- retVal.code.push(`const ${forKey}_${itemsName} = ${iterableExpr};`);
2967
- retVal.code.push(`const ${forKey}_${node.itemAlias} = ${resolveExpression(node.trackExpression, forContext, {
2968
- skipResolution: true,
2969
- resolver: `${iterableExpr}`
2970
- })};`);
2971
- return retVal;
2953
+ ], context);
2954
+ const lines = [];
2955
+ lines.push(`for (const ${node.itemAlias} of ${iterableExpr}) {`);
2956
+ lines.push(...indent([
2957
+ `let ${indexName}!: number;`,
2958
+ `let ${firstName}!: boolean;`,
2959
+ `let ${lastName}!: boolean;`,
2960
+ `let ${evenName}!: boolean;`,
2961
+ `let ${oddName}!: boolean;`,
2962
+ `${resolveExpression(node.trackExpression, forContext, { resolver: "root" })};`,
2963
+ ...node.children.flatMap((child, i) => processNode(child, forContext, i.toString()))
2964
+ ]));
2965
+ lines.push("}");
2966
+ return lines;
2972
2967
  }
2973
2968
  /**
2974
2969
  * Resolves the name that should be used in generated code for a given
@@ -2987,77 +2982,109 @@ function resolveImplicit(node, implicit) {
2987
2982
  }
2988
2983
  //#endregion
2989
2984
  //#region ../packages/compiler/src/type-checker/states/type-check-if.state.ts
2990
- function typeCheckIf(node, parentNode, index, compilerContext) {
2991
- const ifContext = new CompilerContext([], compilerContext);
2992
- const retVal = {
2993
- code: [],
2994
- functionsToProcess: /* @__PURE__ */ new Map()
2995
- };
2996
- const ifKey = getBlockIdentifier("if", parentNode.identifier, index);
2997
- retVal.code.push(`const ${ifKey} = ${resolveExpression(node.conditionNode, compilerContext, { resolver: "root" })};`);
2998
- retVal.functionsToProcess.set(ifKey, { fn: {
2999
- node,
3000
- parentNode,
3001
- context: ifContext
3002
- } });
2985
+ /**
2986
+ * Type-checks an `@if`/`@else if`/`@else` chain using real TypeScript
2987
+ * `if` / `else if` / `else` blocks.
2988
+ *
2989
+ * This is a genuine correctness improvement over the previous
2990
+ * sibling-functions approach, not just a simplification: a real
2991
+ * `if`/`else if` chain gives the TS compiler's control-flow analysis the
2992
+ * negated narrowing of every preceding condition for free (e.g. inside an
2993
+ * `else if`, TS already knows the first condition was false) — something
2994
+ * flat sibling functions could never express.
2995
+ */
2996
+ function typeCheckIf(node, context, index, processNode) {
2997
+ const lines = [];
2998
+ const condition = resolveExpression(node.conditionNode, context, { resolver: "root" });
2999
+ const ifContext = new CompilerContext([], context);
3000
+ lines.push(`if (${condition}) {`);
3001
+ lines.push(...indent(node.children.flatMap((child, i) => processNode(child, ifContext, i.toString()))));
3002
+ lines.push("}");
3003
3003
  let alt = node.alternate;
3004
- let i = 0;
3005
3004
  while (alt?.type === ASTNodeType.ElseIf) {
3006
- const elseIfContext = new CompilerContext([], compilerContext);
3007
- const keyElseIf = getBlockIdentifier("elseIf", parentNode.identifier, `${index}_${i}`);
3008
- const conditionNode = alt.conditionNode;
3009
- retVal.code.push(`const ${keyElseIf} = ${resolveExpression(conditionNode, compilerContext, { resolver: "root" })};`);
3010
- retVal.functionsToProcess.set(keyElseIf, { fn: {
3011
- node: alt,
3012
- parentNode,
3013
- context: elseIfContext
3014
- } });
3005
+ const elseIfCondition = resolveExpression(alt.conditionNode, context, { resolver: "root" });
3006
+ const elseIfContext = new CompilerContext([], context);
3007
+ lines.push(`else if (${elseIfCondition}) {`);
3008
+ lines.push(...indent(alt.children.flatMap((child, i) => processNode(child, elseIfContext, i.toString()))));
3009
+ lines.push("}");
3015
3010
  alt = alt.alternate;
3016
- i++;
3017
3011
  }
3018
3012
  if (alt) {
3019
- const elseContext = new CompilerContext([], compilerContext);
3020
- const keyElse = getBlockIdentifier("else", parentNode.identifier, index);
3021
- retVal.functionsToProcess.set(keyElse, { fn: {
3022
- node: alt,
3023
- parentNode,
3024
- context: elseContext
3025
- } });
3013
+ const elseContext = new CompilerContext([], context);
3014
+ lines.push("else {");
3015
+ lines.push(...indent(alt.children.flatMap((child, i) => processNode(child, elseContext, i.toString()))));
3016
+ lines.push("}");
3026
3017
  }
3027
- return retVal;
3018
+ return lines;
3028
3019
  }
3029
3020
  //#endregion
3030
3021
  //#region ../packages/compiler/src/type-checker/states/type-check-switch.state.ts
3031
- function typeCheckSwitch(node, parentNode, index, compilerContext) {
3032
- const retVal = {
3033
- code: [],
3034
- functionsToProcess: /* @__PURE__ */ new Map()
3035
- };
3036
- const expression = resolveExpression(node.expression, compilerContext, { resolver: "root" });
3037
- const keySwitch = getBlockIdentifier("switch", parentNode.identifier, index);
3038
- retVal.code.push(`const ${keySwitch} = ${expression};`);
3022
+ /**
3023
+ * Type-checks a `@switch` block using a real TypeScript `switch` statement.
3024
+ *
3025
+ * This drops the previous `const case_0: typeof switchExpr = 'literal';`
3026
+ * trick entirely: a real `switch (expr) { case 'literal': ... }` already
3027
+ * makes TS validate that each case value is assignable to the switch
3028
+ * expression's type as part of ordinary switch-statement semantics — and,
3029
+ * as a bonus, narrows the switch expression's type inside each case block
3030
+ * (e.g. from a `'loading' | 'error' | 'idle'` union down to just
3031
+ * `'loading'`), which the previous approach never provided.
3032
+ *
3033
+ * Multiple case labels that shared one body in the AST (fallthrough) are
3034
+ * emitted as stacked `case` labels sharing that same body, matching real
3035
+ * JS/TS fallthrough syntax directly.
3036
+ */
3037
+ function typeCheckSwitch(node, context, index, processNode) {
3038
+ const lines = [`switch (${resolveExpression(node.expression, context, { resolver: "root" })}) {`];
3039
3039
  node.children.forEach((caseNode, i) => {
3040
- const caseContext = new CompilerContext([], compilerContext);
3041
- const caseKey = caseNode.condition ? getBlockIdentifier("case", parentNode.identifier, `${index}_${i}`) : getBlockIdentifier("default", parentNode.identifier, index);
3042
- retVal.functionsToProcess.set(caseKey, { fn: {
3043
- node: caseNode,
3044
- parentNode,
3045
- context: caseContext
3046
- } });
3047
- caseNode.condition?.forEach((condition, i) => retVal.code.push(`const ${caseKey}_${i}: typeof ${expression} = ${condition};`));
3040
+ const caseContext = new CompilerContext([], context);
3041
+ if (caseNode.condition?.length) caseNode.condition.forEach((conditionValue) => {
3042
+ lines.push(` case ${conditionValue}:`);
3043
+ });
3044
+ else lines.push(" default:");
3045
+ lines.push(...indent(indent([...caseNode.children.flatMap((child, ci) => processNode(child, caseContext, ci.toString())), "break;"])));
3048
3046
  });
3049
- return retVal;
3047
+ lines.push("}");
3048
+ return lines;
3050
3049
  }
3051
3050
  //#endregion
3052
3051
  //#region ../packages/compiler/src/type-checker/states/type-check-text-and-interpolation.state.ts
3053
- function typeCheckTextAndInterpolation(node, parentNode, index, compilerContext) {
3054
- return { code: [`const ${getTextIdentifier("text", parentNode.identifier, index)} = ${node.type === ASTNodeType.Text ? `'${node.value}'` : resolveExpression(node.expression, compilerContext, { resolver: "root" })};`] };
3052
+ /**
3053
+ * Type-checks a text or interpolation node.
3054
+ *
3055
+ * Plain text nodes carry no expression, so they produce no lines. An
3056
+ * interpolation's expression is emitted as a bare statement — enough for
3057
+ * TS to validate it, with no name needing to be bound to the result.
3058
+ */
3059
+ function typeCheckTextAndInterpolation(node, context, index, processNode) {
3060
+ return node.type === ASTNodeType.Interpolation ? [`${resolveExpression(node.expression, context, { resolver: "root" })};`] : [];
3055
3061
  }
3056
3062
  //#endregion
3057
3063
  //#region ../packages/compiler/src/type-checker/type-checker.ts
3064
+ /**
3065
+ * Generates a single, flat TypeScript function body ("shim") from a
3066
+ * template AST, meant only to be fed to the TS compiler / LanguageService
3067
+ * for diagnostics — it is never executed and never emitted as real output.
3068
+ *
3069
+ * This deliberately does NOT mirror the JS code generator's structure:
3070
+ *
3071
+ * - No variable is declared per HTML element. Element identifiers exist in
3072
+ * the JS output purely so runtime code can create/reference the actual
3073
+ * DOM node; a type-check expression never references "the element
3074
+ * itself" (the DSL has no template-ref syntax), so an `HTMLElement`
3075
+ * local would add zero type-checking value.
3076
+ * - No control-flow block gets its own function. In the JS output, each
3077
+ * `@if`/`@for`/`@switch` becomes a separate function because it needs
3078
+ * its own runtime closure over the `Context` chain. The type checker has
3079
+ * no runtime at all, so real, nested TypeScript blocks — `if`, `for`,
3080
+ * `switch` — give correct scoping and (as a bonus) real control-flow
3081
+ * narrowing, for free, with no synthetic machinery.
3082
+ *
3083
+ * Every AST node turns directly into TypeScript lines, recursively, inside
3084
+ * one single `typeCheck()` function.
3085
+ */
3058
3086
  var TypeChecker = class {
3059
3087
  _ast;
3060
- _nodeToProcess = /* @__PURE__ */ new Map();
3061
3088
  _states = {
3062
3089
  [ASTNodeType.Text]: typeCheckTextAndInterpolation,
3063
3090
  [ASTNodeType.Interpolation]: typeCheckTextAndInterpolation,
@@ -3070,43 +3097,34 @@ var TypeChecker = class {
3070
3097
  constructor(_ast) {
3071
3098
  this._ast = _ast;
3072
3099
  }
3073
- generate(className) {
3074
- this._nodeToProcess.clear();
3100
+ /**
3101
+ * Generates the full `function typeCheck() { ... }` shim body for the
3102
+ * component's template.
3103
+ *
3104
+ * A `let $event!: Event;` declaration is prepended only if the generated
3105
+ * body actually references `$event` (event handler bindings), so shims
3106
+ * for templates with no event bindings don't carry an unused local —
3107
+ * relevant if the consuming project has `noUnusedLocals` enabled.
3108
+ */
3109
+ generate() {
3075
3110
  const context = new CompilerContext();
3076
- const generatedCode = ["function typeCheck() {"];
3077
- for (let i = 0; i < this._ast.length; i++) {
3078
- const result = this._processNode(this._ast[i], {
3079
- identifier: ROOT_NODE,
3080
- type: className
3081
- }, i.toString(), context);
3082
- if (result) {
3083
- const { code, functionsToProcess } = result;
3084
- functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
3085
- generatedCode.push(...indent(code));
3086
- }
3087
- }
3088
- generatedCode.push("}");
3089
- for (const [key, fnData] of this._nodeToProcess.entries()) {
3090
- const { node, parentNode, precode, context } = fnData.fn;
3091
- generatedCode.push(`\nfunction ${key}(${fnData.args?.join(", ") ?? ""}): void {`);
3092
- if (precode) generatedCode.push(indent(precode));
3093
- generatedCode.push(...indent([...node.children.map((child, i) => {
3094
- const result = this._processNode(child, parentNode, i.toString(), context);
3095
- if (result) {
3096
- const { code, functionsToProcess } = result;
3097
- functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
3098
- return code;
3099
- }
3100
- return "";
3101
- }).flat()]), "}");
3102
- }
3103
- return generatedCode.join("\n");
3111
+ const body = this._ast.flatMap((node, i) => this._processNode(node, context, i.toString()));
3112
+ return [
3113
+ "function typeCheck() {",
3114
+ ...indent(body.some((line) => line.includes("$event")) ? ["let $event!: Event;", ...body] : body),
3115
+ "}"
3116
+ ].join("\n");
3104
3117
  }
3105
- _processNode(node, parentNode, index, context) {
3118
+ /**
3119
+ * Dispatches a single AST node to its state function, passing itself
3120
+ * back down as `processNode` so state functions can recurse into their
3121
+ * own children inline.
3122
+ */
3123
+ _processNode = (node, context, index) => {
3106
3124
  const state = this._states[node.type];
3107
- if (!state) throw new Error(`[Parser] No transition function for token type ${ASTNodeType[node.type]}`);
3108
- return state(node, parentNode, index, context);
3109
- }
3125
+ if (!state) throw new Error(`[Type Checker] No transition function for token type ${ASTNodeType[node.type]}`);
3126
+ return state(node, context, index, this._processNode);
3127
+ };
3110
3128
  };
3111
3129
  //#endregion
3112
3130
  //#region ../packages/compiler/src/compile.ts
@@ -3123,11 +3141,11 @@ var TypeChecker = class {
3123
3141
  * into the generated `adoptedStyleSheets` assignment.
3124
3142
  * @returns A string containing the compiled Javascript render method body.
3125
3143
  */
3126
- function compile(input, className, cssVariableName) {
3144
+ function compile(input, cssVariableName) {
3127
3145
  const nodes = new Parser(new Lexer(input).tokenize()).parse();
3128
3146
  return {
3129
3147
  javascript: new Generator(nodes).generate(cssVariableName),
3130
- typescript: new TypeChecker(nodes).generate(className)
3148
+ typescript: new TypeChecker(nodes).generate()
3131
3149
  };
3132
3150
  }
3133
3151
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaendar/compiler",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
4
4
  "description": "A library for transpiling Xaendar Templates into JavaScript code",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -16,8 +16,8 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@xaendar/common": "0.7.23",
20
- "@xaendar/types": "0.7.23",
19
+ "@xaendar/common": "0.7.25",
20
+ "@xaendar/types": "0.7.25",
21
21
  "typescript": "^6.0.3"
22
22
  }
23
23
  }