@xaendar/compiler 0.7.24 → 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,49 +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
- identifier: nodeName,
2930
- type: "HTMLElement"
2931
- },
2932
- context
2933
- } });
2934
- return retVal;
2922
+ node.children.forEach((child, i) => {
2923
+ lines.push(...processNode(child, context, i.toString()));
2924
+ });
2925
+ return lines;
2935
2926
  }
2936
2927
  //#endregion
2937
2928
  //#region ../packages/compiler/src/type-checker/states/type-check-for.state.ts
2938
- function typeCheckFor(node, parentNode, index, compilerContext) {
2939
- const retVal = {
2940
- code: [],
2941
- functionsToProcess: /* @__PURE__ */ new Map()
2942
- };
2943
- const iterableSource = node.iterableSource;
2944
- const iterableExpr = compilerContext.hasIdentifier(iterableSource) ? iterableSource : `root.${iterableSource}`;
2945
- const itemsName = getTextIdentifier("items", parentNode.identifier, index);
2946
- 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}`;
2947
2941
  const indexName = resolveImplicit(node, "$index");
2948
2942
  const firstName = resolveImplicit(node, "$first");
2949
2943
  const lastName = resolveImplicit(node, "$last");
@@ -2956,25 +2950,20 @@ function typeCheckFor(node, parentNode, index, compilerContext) {
2956
2950
  [lastName, "signal"],
2957
2951
  [evenName, "signal"],
2958
2952
  [oddName, "signal"]
2959
- ], compilerContext);
2960
- const forKey = getBlockIdentifier("for", parentNode.identifier, index);
2961
- retVal.functionsToProcess.set(forKey, {
2962
- fn: {
2963
- node,
2964
- parentNode: {
2965
- identifier: forKey,
2966
- type: "HTMLElement"
2967
- },
2968
- context: forContext
2969
- },
2970
- args: [`${itemsName}: typeof ${iterableExpr}`, `${counterName}: number`]
2971
- });
2972
- retVal.code.push(`const ${forKey}_${itemsName} = ${iterableExpr};`);
2973
- retVal.code.push(`const ${forKey}_${node.itemAlias} = ${resolveExpression(node.trackExpression, forContext, {
2974
- skipResolution: true,
2975
- resolver: `${iterableExpr}`
2976
- })};`);
2977
- 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;
2978
2967
  }
2979
2968
  /**
2980
2969
  * Resolves the name that should be used in generated code for a given
@@ -2993,89 +2982,109 @@ function resolveImplicit(node, implicit) {
2993
2982
  }
2994
2983
  //#endregion
2995
2984
  //#region ../packages/compiler/src/type-checker/states/type-check-if.state.ts
2996
- function typeCheckIf(node, parentNode, index, compilerContext) {
2997
- const ifContext = new CompilerContext([], compilerContext);
2998
- const retVal = {
2999
- code: [],
3000
- functionsToProcess: /* @__PURE__ */ new Map()
3001
- };
3002
- const ifKey = getBlockIdentifier("if", parentNode.identifier, index);
3003
- retVal.code.push(`const ${ifKey} = ${resolveExpression(node.conditionNode, compilerContext, { resolver: "root" })};`);
3004
- retVal.functionsToProcess.set(ifKey, { fn: {
3005
- node,
3006
- parentNode: {
3007
- identifier: ifKey,
3008
- type: "HTMLElement"
3009
- },
3010
- context: ifContext
3011
- } });
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("}");
3012
3003
  let alt = node.alternate;
3013
- let i = 0;
3014
3004
  while (alt?.type === ASTNodeType.ElseIf) {
3015
- const elseIfContext = new CompilerContext([], compilerContext);
3016
- const keyElseIf = getBlockIdentifier("elseIf", parentNode.identifier, `${index}_${i}`);
3017
- const conditionNode = alt.conditionNode;
3018
- retVal.code.push(`const ${keyElseIf} = ${resolveExpression(conditionNode, compilerContext, { resolver: "root" })};`);
3019
- retVal.functionsToProcess.set(keyElseIf, { fn: {
3020
- node: alt,
3021
- parentNode: {
3022
- identifier: keyElseIf,
3023
- type: "HTMLElement"
3024
- },
3025
- context: elseIfContext
3026
- } });
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("}");
3027
3010
  alt = alt.alternate;
3028
- i++;
3029
3011
  }
3030
3012
  if (alt) {
3031
- const elseContext = new CompilerContext([], compilerContext);
3032
- const keyElse = getBlockIdentifier("else", parentNode.identifier, index);
3033
- retVal.functionsToProcess.set(keyElse, { fn: {
3034
- node: alt,
3035
- parentNode: {
3036
- identifier: getBlockIdentifier("else", parentNode.identifier, index),
3037
- type: "HTMLElement"
3038
- },
3039
- context: elseContext
3040
- } });
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("}");
3041
3017
  }
3042
- return retVal;
3018
+ return lines;
3043
3019
  }
3044
3020
  //#endregion
3045
3021
  //#region ../packages/compiler/src/type-checker/states/type-check-switch.state.ts
3046
- function typeCheckSwitch(node, parentNode, index, compilerContext) {
3047
- const retVal = {
3048
- code: [],
3049
- functionsToProcess: /* @__PURE__ */ new Map()
3050
- };
3051
- const expression = resolveExpression(node.expression, compilerContext, { resolver: "root" });
3052
- const keySwitch = getBlockIdentifier("switch", parentNode.identifier, index);
3053
- 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" })}) {`];
3054
3039
  node.children.forEach((caseNode, i) => {
3055
- const caseContext = new CompilerContext([], compilerContext);
3056
- const caseKey = caseNode.condition ? getBlockIdentifier("case", parentNode.identifier, `${index}_${i}`) : getBlockIdentifier("default", parentNode.identifier, index);
3057
- retVal.functionsToProcess.set(caseKey, { fn: {
3058
- node: caseNode,
3059
- parentNode: {
3060
- identifier: caseKey,
3061
- type: "HTMLElement"
3062
- },
3063
- context: caseContext
3064
- } });
3065
- 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;"])));
3066
3046
  });
3067
- return retVal;
3047
+ lines.push("}");
3048
+ return lines;
3068
3049
  }
3069
3050
  //#endregion
3070
3051
  //#region ../packages/compiler/src/type-checker/states/type-check-text-and-interpolation.state.ts
3071
- function typeCheckTextAndInterpolation(node, parentNode, index, compilerContext) {
3072
- 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" })};`] : [];
3073
3061
  }
3074
3062
  //#endregion
3075
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
+ */
3076
3086
  var TypeChecker = class {
3077
3087
  _ast;
3078
- _nodeToProcess = /* @__PURE__ */ new Map();
3079
3088
  _states = {
3080
3089
  [ASTNodeType.Text]: typeCheckTextAndInterpolation,
3081
3090
  [ASTNodeType.Interpolation]: typeCheckTextAndInterpolation,
@@ -3088,43 +3097,34 @@ var TypeChecker = class {
3088
3097
  constructor(_ast) {
3089
3098
  this._ast = _ast;
3090
3099
  }
3091
- generate(className) {
3092
- 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() {
3093
3110
  const context = new CompilerContext();
3094
- const generatedCode = ["function typeCheck() {"];
3095
- for (let i = 0; i < this._ast.length; i++) {
3096
- const result = this._processNode(this._ast[i], {
3097
- identifier: ROOT_NODE,
3098
- type: className
3099
- }, i.toString(), context);
3100
- if (result) {
3101
- const { code, functionsToProcess } = result;
3102
- functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
3103
- generatedCode.push(...indent(code));
3104
- }
3105
- }
3106
- generatedCode.push("}");
3107
- for (const [key, fnData] of this._nodeToProcess.entries()) {
3108
- const { node, parentNode, precode, context } = fnData.fn;
3109
- generatedCode.push(`\nfunction ${key}(${fnData.args?.join(", ") ?? ""}): void {`);
3110
- if (precode) generatedCode.push(indent(precode));
3111
- generatedCode.push(...indent([...node.children.map((child, i) => {
3112
- const result = this._processNode(child, parentNode, i.toString(), context);
3113
- if (result) {
3114
- const { code, functionsToProcess } = result;
3115
- functionsToProcess?.forEach((value, key) => this._nodeToProcess.set(key, value));
3116
- return code;
3117
- }
3118
- return "";
3119
- }).flat()]), "}");
3120
- }
3121
- 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");
3122
3117
  }
3123
- _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) => {
3124
3124
  const state = this._states[node.type];
3125
- if (!state) throw new Error(`[Parser] No transition function for token type ${ASTNodeType[node.type]}`);
3126
- return state(node, parentNode, index, context);
3127
- }
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
+ };
3128
3128
  };
3129
3129
  //#endregion
3130
3130
  //#region ../packages/compiler/src/compile.ts
@@ -3141,11 +3141,11 @@ var TypeChecker = class {
3141
3141
  * into the generated `adoptedStyleSheets` assignment.
3142
3142
  * @returns A string containing the compiled Javascript render method body.
3143
3143
  */
3144
- function compile(input, className, cssVariableName) {
3144
+ function compile(input, cssVariableName) {
3145
3145
  const nodes = new Parser(new Lexer(input).tokenize()).parse();
3146
3146
  return {
3147
3147
  javascript: new Generator(nodes).generate(cssVariableName),
3148
- typescript: new TypeChecker(nodes).generate(className)
3148
+ typescript: new TypeChecker(nodes).generate()
3149
3149
  };
3150
3150
  }
3151
3151
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaendar/compiler",
3
- "version": "0.7.24",
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.24",
20
- "@xaendar/types": "0.7.24",
19
+ "@xaendar/common": "0.7.25",
20
+ "@xaendar/types": "0.7.25",
21
21
  "typescript": "^6.0.3"
22
22
  }
23
23
  }