@xaendar/compiler 0.7.30 → 0.7.31
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/xaendar-compiler.es.js +402 -15
- package/package.json +3 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Stack, indent } from "@xaendar/common";
|
|
2
|
-
import { ScriptTarget, SyntaxKind, createSourceFile, forEachChild, isExpressionStatement, isIdentifier, isPropertyAccessExpression, isPropertyAssignment } from "typescript";
|
|
2
|
+
import { ScriptTarget, SyntaxKind, createSourceFile, forEachChild, getNameOfDeclaration, isAccessor, isArrayLiteralExpression, isCallExpression, isClassDeclaration, isExpressionStatement, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral } from "typescript";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
5
|
+
//#endregion
|
|
3
6
|
//#region ../packages/compiler/src/parser/types/node.enum.ts
|
|
4
7
|
/**
|
|
5
8
|
* Discriminant values that identify the type of each AST node produced by the parser.
|
|
@@ -154,7 +157,7 @@ var CompilerContext = class {
|
|
|
154
157
|
* @returns `true` if the identifier exists in the scope chain, `false` otherwise.
|
|
155
158
|
*/
|
|
156
159
|
hasUnresolvableIdentifier(name) {
|
|
157
|
-
return this._unresolvableIdentifiers.includes(name);
|
|
160
|
+
return this._unresolvableIdentifiers.includes(name) || (this._parent?.hasUnresolvableIdentifier(name) ?? false);
|
|
158
161
|
}
|
|
159
162
|
};
|
|
160
163
|
//#endregion
|
|
@@ -1514,6 +1517,7 @@ function lexImport(cursor, _context) {
|
|
|
1514
1517
|
while (read) switch (cursor.peek()) {
|
|
1515
1518
|
case 32:
|
|
1516
1519
|
cursor.skipSpaces();
|
|
1520
|
+
if (importValue && cursor.peek() !== 44 && cursor.peek() !== 125) importValue = `${importValue} `;
|
|
1517
1521
|
break;
|
|
1518
1522
|
case 44:
|
|
1519
1523
|
addImport(retVal, cursor, importValue);
|
|
@@ -1812,7 +1816,7 @@ function lexTagOpenName(cursor, _context) {
|
|
|
1812
1816
|
return retVal;
|
|
1813
1817
|
}
|
|
1814
1818
|
//#endregion
|
|
1815
|
-
//#region ../packages/compiler/src/utils/chars.utils.ts
|
|
1819
|
+
//#region ../packages/compiler/src/lexer/utils/chars.utils.ts
|
|
1816
1820
|
/**
|
|
1817
1821
|
* Checks whether a string contains at least one non-whitespace character.
|
|
1818
1822
|
*
|
|
@@ -2752,19 +2756,57 @@ function parseIfOrElseIf(cursor, parseNode, token) {
|
|
|
2752
2756
|
}
|
|
2753
2757
|
//#endregion
|
|
2754
2758
|
//#region ../packages/compiler/src/parser/states/parse-import.state.ts
|
|
2759
|
+
/**
|
|
2760
|
+
* Parses an `@import` statement.
|
|
2761
|
+
*
|
|
2762
|
+
* Extracts specifiers with optional aliases and maps them to structured
|
|
2763
|
+
* `ImportSpecifier` objects. Supports:
|
|
2764
|
+
* - `foo` → imported: 'foo', local: 'foo'
|
|
2765
|
+
* - `foo as bar` → imported: 'foo', local: 'bar'
|
|
2766
|
+
* - `default as D` → imported: 'default', local: 'D'
|
|
2767
|
+
* - `* as ns` → imported: '*', local: 'ns'
|
|
2768
|
+
*/
|
|
2755
2769
|
function parseImport(cursor, _parseNode, _token) {
|
|
2756
|
-
const
|
|
2770
|
+
const specifiers = new Array();
|
|
2757
2771
|
while (cursor.peek().type === TokenType.IMPORT) {
|
|
2758
2772
|
cursor.advance();
|
|
2759
|
-
|
|
2773
|
+
const rawSpecifier = cursor.getCurrentToken().value.parts[0];
|
|
2774
|
+
const specifier = parseSpecifier(rawSpecifier);
|
|
2775
|
+
specifiers.push(specifier);
|
|
2760
2776
|
}
|
|
2761
2777
|
cursor.advance();
|
|
2762
2778
|
return {
|
|
2763
2779
|
type: ASTNodeType.Import,
|
|
2764
|
-
|
|
2780
|
+
specifiers,
|
|
2765
2781
|
path: cursor.getCurrentToken().value.parts[0]
|
|
2766
2782
|
};
|
|
2767
2783
|
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Parses a single import specifier string.
|
|
2786
|
+
*
|
|
2787
|
+
* @param raw - The raw specifier string (may include 'as' alias).
|
|
2788
|
+
* @returns An `ImportSpecifier` with `imported` and `local` fields.
|
|
2789
|
+
* @throws If the specifier format is invalid.
|
|
2790
|
+
*/
|
|
2791
|
+
function parseSpecifier(raw) {
|
|
2792
|
+
const trimmed = raw.trim();
|
|
2793
|
+
const namespaceMatch = trimmed.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
2794
|
+
if (namespaceMatch) return {
|
|
2795
|
+
imported: "*",
|
|
2796
|
+
local: namespaceMatch[1]
|
|
2797
|
+
};
|
|
2798
|
+
const defaultMatch = trimmed.match(/^default\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
2799
|
+
if (defaultMatch) return {
|
|
2800
|
+
imported: "default",
|
|
2801
|
+
local: defaultMatch[1]
|
|
2802
|
+
};
|
|
2803
|
+
const namedMatch = trimmed.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
|
|
2804
|
+
if (namedMatch) return {
|
|
2805
|
+
imported: namedMatch[1],
|
|
2806
|
+
local: namedMatch[2] ?? namedMatch[1]
|
|
2807
|
+
};
|
|
2808
|
+
throw new Error(`[Parser] Invalid import specifier "${raw}".`);
|
|
2809
|
+
}
|
|
2768
2810
|
//#endregion
|
|
2769
2811
|
//#region ../packages/compiler/src/parser/states/parse-switch.state.ts
|
|
2770
2812
|
/**
|
|
@@ -2902,29 +2944,148 @@ var Parser = class {
|
|
|
2902
2944
|
}
|
|
2903
2945
|
};
|
|
2904
2946
|
//#endregion
|
|
2947
|
+
//#region ../packages/compiler/src/type-checker/models/type-checker-context.ts
|
|
2948
|
+
/**
|
|
2949
|
+
* Type checker context that manages imported component and directive metadata
|
|
2950
|
+
* for the compilation process. Extends the base CompilerContext to provide
|
|
2951
|
+
* import-specific functionality.
|
|
2952
|
+
*/
|
|
2953
|
+
var TypeCheckContext = class extends CompilerContext {
|
|
2954
|
+
/**
|
|
2955
|
+
* Array of component and directive imports to be tracked during type checking.
|
|
2956
|
+
* Stores metadata extracted from @WebComponent and @Directive decorators.
|
|
2957
|
+
*/
|
|
2958
|
+
_imports = new Array();
|
|
2959
|
+
/**
|
|
2960
|
+
* Adds a new component or directive import to the type checker context.
|
|
2961
|
+
*
|
|
2962
|
+
* @param value - The component or directive import metadata to be added
|
|
2963
|
+
*/
|
|
2964
|
+
addImport(value) {
|
|
2965
|
+
this._imports.push(value);
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Retrieves all registered component and directive imports.
|
|
2969
|
+
*
|
|
2970
|
+
* @returns Array of all tracked imports
|
|
2971
|
+
*/
|
|
2972
|
+
getImportBySelector(tagName) {
|
|
2973
|
+
return this._imports.find((importValue) => importValue.type === "component" && importValue.selectors.includes(tagName));
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
//#endregion
|
|
2905
2977
|
//#region ../packages/compiler/src/type-checker/states/type-check-element.state.ts
|
|
2906
2978
|
/**
|
|
2907
2979
|
* Type-checks an element node: its attribute/property bindings, its event
|
|
2908
2980
|
* handlers, and recursively its children.
|
|
2909
2981
|
*
|
|
2982
|
+
* Two completely different validation paths, depending on the tag:
|
|
2983
|
+
*
|
|
2984
|
+
* - **Native tags** (`div`, `button`, ...): bindings are just resolved
|
|
2985
|
+
* expressions/handler calls, exactly as before — there's no component
|
|
2986
|
+
* contract to check them against.
|
|
2987
|
+
* - **Custom elements** (tag contains a hyphen): every binding is checked
|
|
2988
|
+
* by NAME against the `@Property`/`@Event` metadata extracted from the
|
|
2989
|
+
* imported component class. An unknown name is a hard error (the
|
|
2990
|
+
* template is binding to something the component doesn't expose) — not
|
|
2991
|
+
* a warning, consistent with the "no matching @import" check already in
|
|
2992
|
+
* place for the tag itself.
|
|
2993
|
+
*
|
|
2910
2994
|
* No variable is declared for the element itself (see the module doc on
|
|
2911
2995
|
* `TypeChecker` for why) — attribute expressions and event calls are
|
|
2912
2996
|
* emitted as bare statements, validated in place.
|
|
2913
2997
|
*/
|
|
2914
2998
|
function typeCheckElement(node, processNode, context) {
|
|
2915
2999
|
const lines = new Array();
|
|
2916
|
-
node.
|
|
2917
|
-
|
|
3000
|
+
if (isCustomElementTag(node.tagName)) {
|
|
3001
|
+
const metadata = context.getImportBySelector(node.tagName);
|
|
3002
|
+
if (!metadata) throw new Error(`[Type Checker] ${node.tagName} selector is not associated to any WebComponent imported in the template`);
|
|
3003
|
+
lines.push(...typeCheckComponentBindings(node, metadata, context));
|
|
3004
|
+
} else {
|
|
3005
|
+
node.attributes.forEach(({ value }) => {
|
|
3006
|
+
if (typeof value !== "string") lines.push(`${resolveExpression(value.expression, context, { resolver: "root" })};`);
|
|
3007
|
+
});
|
|
3008
|
+
node.events.forEach(({ handler, parameters }) => {
|
|
3009
|
+
const eventContext = new CompilerContext([], context);
|
|
3010
|
+
eventContext.addUnresolvableIdentifier("$event");
|
|
3011
|
+
const args = parameters.map((parameter) => resolveExpression(parameter, eventContext, { resolver: "root" })).join(", ");
|
|
3012
|
+
lines.push(`root.${handler}(${args});`);
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
node.children.forEach((child) => lines.push(...processNode(child, context)));
|
|
3016
|
+
return lines;
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Validates every attribute/property and event binding on a custom-element
|
|
3020
|
+
* node against the `@Property`/`@Event` metadata of the resolved component.
|
|
3021
|
+
*
|
|
3022
|
+
* Property values are checked with `satisfies` against `property.type` —
|
|
3023
|
+
* chosen over a typed `const` so an unused local never shows up if the
|
|
3024
|
+
* consuming project has `noUnusedLocals` enabled. Event handlers get a
|
|
3025
|
+
* block-scoped `$event` typed from `event.detailType`, wrapped in its own
|
|
3026
|
+
* `{ }` block so multiple event bindings on sibling elements never clash
|
|
3027
|
+
* on the `$event` name.
|
|
3028
|
+
*
|
|
3029
|
+
* ⚠️ Assumptions I couldn't verify against your actual types — please
|
|
3030
|
+
* confirm/adjust:
|
|
3031
|
+
* - `node.events` entries carry a `name` field (the template-facing event
|
|
3032
|
+
* name used to look up `@Event` metadata), the same way attributes carry
|
|
3033
|
+
* `name`. The snippets I've seen only destructured `handler`/`parameters`
|
|
3034
|
+
* for events, never `name` — if the field is called something else,
|
|
3035
|
+
* swap it in `findEvent`'s call site below.
|
|
3036
|
+
* - Literal string attribute values (`value` is a plain `string`, no
|
|
3037
|
+
* `{ expression }`) are only checked for NAME existence, not type — I
|
|
3038
|
+
* don't validate e.g. `collapsed="true"` against a `boolean` property,
|
|
3039
|
+
* since I don't know whether your DSL treats un-bound string attributes
|
|
3040
|
+
* as always-string or allows some literal coercion syntax. Flagging
|
|
3041
|
+
* rather than guessing.
|
|
3042
|
+
* - `property.type`/`event.detailType` are text that must resolve as a
|
|
3043
|
+
* standalone type expression in the shim's scope (e.g. `'NavItem[]'`
|
|
3044
|
+
* requires `NavItem` to be importable there too, not just the component
|
|
3045
|
+
* class). If your shim only imports the component class today, custom
|
|
3046
|
+
* exported types referenced by `type`/`detailType` will fail to resolve
|
|
3047
|
+
* as "cannot find name" — unrelated to the actual binding being right or
|
|
3048
|
+
* wrong. Worth double-checking against a component that has a non-
|
|
3049
|
+
* primitive `@Property` type, like `NavItem[]` in your own example.
|
|
3050
|
+
*/
|
|
3051
|
+
function typeCheckComponentBindings(node, metadata, context) {
|
|
3052
|
+
const lines = new Array();
|
|
3053
|
+
node.attributes.forEach(({ name, value }) => {
|
|
3054
|
+
const property = findProperty(metadata, name);
|
|
3055
|
+
if (!property) throw new Error(`[Type Checker] Unknown property "${name}" on <${node.tagName}> (${metadata.className} has no @Property with this name or alias).`);
|
|
3056
|
+
if (typeof value === "string") return;
|
|
3057
|
+
const expression = resolveExpression(value.expression, context, { resolver: "root" });
|
|
3058
|
+
lines.push(property.type ? `(${expression}) satisfies ${property.type};` : `${expression};`);
|
|
2918
3059
|
});
|
|
2919
|
-
node.events.forEach(({ handler, parameters }) => {
|
|
3060
|
+
node.events.forEach(({ name, handler, parameters }) => {
|
|
3061
|
+
const event = findEvent(metadata, name);
|
|
3062
|
+
if (!event) throw new Error(`[Type Checker] Unknown event "${name}" on <${node.tagName}> (${metadata.className} has no @Event with this name).`);
|
|
2920
3063
|
const eventContext = new CompilerContext([], context);
|
|
2921
3064
|
eventContext.addUnresolvableIdentifier("$event");
|
|
2922
3065
|
const args = parameters.map((parameter) => resolveExpression(parameter, eventContext, { resolver: "root" })).join(", ");
|
|
2923
|
-
lines.push(
|
|
3066
|
+
lines.push("{");
|
|
3067
|
+
lines.push(` let $event!: ${event.detailType ?? "unknown"};`);
|
|
3068
|
+
lines.push(` root.${handler}(${args});`);
|
|
3069
|
+
lines.push("}");
|
|
2924
3070
|
});
|
|
2925
|
-
node.children.forEach((child) => lines.push(...processNode(child, context)));
|
|
2926
3071
|
return lines;
|
|
2927
3072
|
}
|
|
3073
|
+
function findProperty(metadata, externalName) {
|
|
3074
|
+
return metadata.properties.find((property) => (property.alias ?? property.name) === externalName);
|
|
3075
|
+
}
|
|
3076
|
+
function findEvent(metadata, externalName) {
|
|
3077
|
+
return metadata.events.find((event) => event.name === externalName);
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* Returns true if `tagName` has the shape of a custom element (per the
|
|
3081
|
+
* Custom Elements spec: contains a hyphen). This is a pure classification
|
|
3082
|
+
* check — it does NOT validate whether the name is actually usable as a
|
|
3083
|
+
* new custom element (reserved names, etc.); use `isValidCustomElementName`
|
|
3084
|
+
* for that, at declaration time.
|
|
3085
|
+
*/
|
|
3086
|
+
function isCustomElementTag(tagName) {
|
|
3087
|
+
return /^[a-z][a-z0-9._\-]*-[a-z0-9._\-]*$/.test(tagName);
|
|
3088
|
+
}
|
|
2928
3089
|
//#endregion
|
|
2929
3090
|
//#region ../packages/compiler/src/type-checker/states/type-check-for.state.ts
|
|
2930
3091
|
/**
|
|
@@ -2938,7 +3099,7 @@ function typeCheckElement(node, processNode, context) {
|
|
|
2938
3099
|
* function boundary needed.
|
|
2939
3100
|
*/
|
|
2940
3101
|
function typeCheckFor(node, processNode, context) {
|
|
2941
|
-
const forContext = new
|
|
3102
|
+
const forContext = new TypeCheckContext([], context);
|
|
2942
3103
|
const indexName = resolveImplicit(node, "$index");
|
|
2943
3104
|
const firstName = resolveImplicit(node, "$first");
|
|
2944
3105
|
const lastName = resolveImplicit(node, "$last");
|
|
@@ -3016,6 +3177,43 @@ function typeCheckIf(node, processNode, context) {
|
|
|
3016
3177
|
return lines;
|
|
3017
3178
|
}
|
|
3018
3179
|
//#endregion
|
|
3180
|
+
//#region ../packages/compiler/src/type-checker/states/type-check-import.state.ts
|
|
3181
|
+
/**
|
|
3182
|
+
* Type-checks an `@import` node.
|
|
3183
|
+
*
|
|
3184
|
+
* Emits a typed variable declaration for each specifier so symbols are
|
|
3185
|
+
* available in template expressions. Metadata extraction (component/directive
|
|
3186
|
+
* decorator analysis) happens in a separate async phase via
|
|
3187
|
+
* `TypeChecker.populateImportMetadata()`.
|
|
3188
|
+
*/
|
|
3189
|
+
function typeCheckImport(node, _processNode, _context) {
|
|
3190
|
+
const source = node.path;
|
|
3191
|
+
const escapedSource = escapeTypeString(source);
|
|
3192
|
+
const lines = new Array();
|
|
3193
|
+
for (const specifier of node.specifiers) {
|
|
3194
|
+
const { imported, local } = specifier;
|
|
3195
|
+
switch (imported) {
|
|
3196
|
+
case "*":
|
|
3197
|
+
lines.push(`let ${local}!: typeof import('${escapedSource}');`);
|
|
3198
|
+
break;
|
|
3199
|
+
case "default":
|
|
3200
|
+
lines.push(`let ${local}!: typeof import('${escapedSource}')['default'];`);
|
|
3201
|
+
break;
|
|
3202
|
+
default:
|
|
3203
|
+
const escapedImported = escapeTypeString(imported);
|
|
3204
|
+
lines.push(`let ${local}!: typeof import('${escapedSource}')['${escapedImported}'];`);
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
return lines;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Escapes path/specifier text for safe use in TS single-quoted type strings.
|
|
3212
|
+
*/
|
|
3213
|
+
function escapeTypeString(value) {
|
|
3214
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3215
|
+
}
|
|
3216
|
+
//#endregion
|
|
3019
3217
|
//#region ../packages/compiler/src/type-checker/states/type-check-switch.state.ts
|
|
3020
3218
|
/**
|
|
3021
3219
|
* Type-checks a `@switch` block using a real TypeScript `switch` statement.
|
|
@@ -3054,6 +3252,174 @@ function typeCheckTextAndInterpolation(node, _processNode, context) {
|
|
|
3054
3252
|
return node.type === ASTNodeType.Interpolation ? [`${resolveExpression(node.expression, context, { resolver: "root" })};`] : [];
|
|
3055
3253
|
}
|
|
3056
3254
|
//#endregion
|
|
3255
|
+
//#region ../packages/compiler/src/type-checker/utils/component-metadata-extractor.ts
|
|
3256
|
+
var import___vite_browser_external = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
3257
|
+
module.exports = {};
|
|
3258
|
+
})))();
|
|
3259
|
+
/**
|
|
3260
|
+
* Extracts component metadata from a source file by parsing decorators.
|
|
3261
|
+
*
|
|
3262
|
+
* @param modulePath - The import module path (e.g., './button.component')
|
|
3263
|
+
* @param symbolName - The exported symbol name to look for
|
|
3264
|
+
* @param baseDir - The directory context for resolving relative paths
|
|
3265
|
+
* @returns Component metadata if found, undefined otherwise
|
|
3266
|
+
*/
|
|
3267
|
+
async function extractComponentMetadata(modulePath, symbolName, baseDir = process.cwd()) {
|
|
3268
|
+
try {
|
|
3269
|
+
const filePath = resolveModulePath(modulePath, baseDir);
|
|
3270
|
+
if (!filePath) return;
|
|
3271
|
+
const sourceFile = createSourceFile(filePath, await (0, import___vite_browser_external.readFile)(filePath, "utf8"), ScriptTarget.Latest, true);
|
|
3272
|
+
let componentMetadata;
|
|
3273
|
+
forEachChild(sourceFile, (node) => {
|
|
3274
|
+
if (componentMetadata) return;
|
|
3275
|
+
if (isClassDeclaration(node)) {
|
|
3276
|
+
const className = node.name?.text;
|
|
3277
|
+
if (className !== symbolName) return;
|
|
3278
|
+
const modifiers = node.modifiers ?? [];
|
|
3279
|
+
const webComponentDecorator = Array.from(modifiers).find((member) => isWebComponentDecorator(member));
|
|
3280
|
+
if (!webComponentDecorator) return;
|
|
3281
|
+
const selectors = extractSelectorsFromDecorator(webComponentDecorator);
|
|
3282
|
+
if (!selectors?.length) return;
|
|
3283
|
+
const properties = new Array();
|
|
3284
|
+
const events = new Array();
|
|
3285
|
+
node.members.forEach((member) => {
|
|
3286
|
+
if (!isAccessor(member)) return;
|
|
3287
|
+
const memberModifiers = member.modifiers ?? [];
|
|
3288
|
+
const propDecorator = Array.from(memberModifiers).find((member) => isPropertyDecorator(member));
|
|
3289
|
+
if (propDecorator) {
|
|
3290
|
+
const nameNode = getNameOfDeclaration(member);
|
|
3291
|
+
const propName = nameNode && isIdentifier(nameNode) ? nameNode.text : void 0;
|
|
3292
|
+
if (propName) {
|
|
3293
|
+
const metadata = extractPropertyMetadata(propName, propDecorator);
|
|
3294
|
+
if (metadata) properties.push(metadata);
|
|
3295
|
+
}
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const eventDecorator = Array.from(memberModifiers).find((member) => isEventDecorator(member));
|
|
3299
|
+
if (eventDecorator) {
|
|
3300
|
+
const nameNode = getNameOfDeclaration(member);
|
|
3301
|
+
const eventName = nameNode && isIdentifier(nameNode) ? nameNode.text : void 0;
|
|
3302
|
+
if (eventName) {
|
|
3303
|
+
const metadata = extractEventMetadata(eventName, eventDecorator);
|
|
3304
|
+
if (metadata) events.push(metadata);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
componentMetadata = {
|
|
3309
|
+
type: "component",
|
|
3310
|
+
className,
|
|
3311
|
+
selectors,
|
|
3312
|
+
properties,
|
|
3313
|
+
events
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
return componentMetadata;
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
console.error(`Failed to extract component metadata from ${modulePath}:`, error);
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Resolves a module import path to an actual file system path.
|
|
3325
|
+
* Handles both relative paths (./button.component) and package paths (@scope/pkg).
|
|
3326
|
+
*
|
|
3327
|
+
* @param modulePath - The import module path
|
|
3328
|
+
* @param baseDir - The directory to resolve relative imports from
|
|
3329
|
+
* @returns The resolved file path, or undefined if not found
|
|
3330
|
+
*/
|
|
3331
|
+
function resolveModulePath(modulePath, baseDir) {
|
|
3332
|
+
if (modulePath.startsWith(".")) {
|
|
3333
|
+
const resolvedPath = (0, import___vite_browser_external.resolve)(baseDir, modulePath);
|
|
3334
|
+
if ((0, import___vite_browser_external.existsSync)(`${resolvedPath}.ts`)) return resolvedPath + ".ts";
|
|
3335
|
+
if ((0, import___vite_browser_external.existsSync)(`${resolvedPath}/index.ts`)) return `${resolvedPath}/index.ts`;
|
|
3336
|
+
if ((0, import___vite_browser_external.existsSync)(resolvedPath)) return resolvedPath;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Checks if a modifier is a @WebComponent decorator.
|
|
3341
|
+
*/
|
|
3342
|
+
function isWebComponentDecorator(modifier) {
|
|
3343
|
+
if (modifier.kind !== SyntaxKind.Decorator) return false;
|
|
3344
|
+
const expr = isCallExpression(modifier.expression) ? modifier.expression.expression : modifier.expression;
|
|
3345
|
+
return isIdentifier(expr) && expr.text === "WebComponent";
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Checks if a modifier is a @Property decorator.
|
|
3349
|
+
*/
|
|
3350
|
+
function isPropertyDecorator(modifier) {
|
|
3351
|
+
if (modifier.kind !== SyntaxKind.Decorator) return false;
|
|
3352
|
+
const expr = modifier.expression;
|
|
3353
|
+
if (isCallExpression(expr)) return isIdentifier(expr.expression) && expr.expression.text === "Property";
|
|
3354
|
+
if (isPropertyAccessExpression(expr)) return isIdentifier(expr.expression) && expr.expression.text === "Property" && isIdentifier(expr.name) && expr.name.text === "required";
|
|
3355
|
+
return isIdentifier(expr) && expr.text === "Property";
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Checks if a modifier is an @Event decorator.
|
|
3359
|
+
*/
|
|
3360
|
+
function isEventDecorator(modifier) {
|
|
3361
|
+
if (modifier.kind !== SyntaxKind.Decorator) return false;
|
|
3362
|
+
const expr = isCallExpression(modifier.expression) ? modifier.expression.expression : modifier.expression;
|
|
3363
|
+
return isIdentifier(expr) && expr.text === "Event";
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Extracts selector(s) from @WebComponent decorator arguments.
|
|
3367
|
+
*
|
|
3368
|
+
* @example
|
|
3369
|
+
* @WebComponent({ selector: 'my-button' })
|
|
3370
|
+
* @WebComponent({ selector: ['my-btn', 'button-el'] })
|
|
3371
|
+
*/
|
|
3372
|
+
function extractSelectorsFromDecorator(modifier) {
|
|
3373
|
+
try {
|
|
3374
|
+
const expr = modifier.expression;
|
|
3375
|
+
if (!isCallExpression(expr)) return [];
|
|
3376
|
+
const args = expr.arguments;
|
|
3377
|
+
if (args.length === 0) return [];
|
|
3378
|
+
const arg = args[0];
|
|
3379
|
+
if (!arg || !isObjectLiteralExpression(arg)) return [];
|
|
3380
|
+
return extractStringOrStringArray(Array.from(arg.properties).find((prop) => isPropertyAssignment(prop) && isIdentifier(prop.name) && prop.name.text === "selector").initializer);
|
|
3381
|
+
} catch {
|
|
3382
|
+
return [];
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Extracts a string or string array value from a TypeScript node.
|
|
3387
|
+
*/
|
|
3388
|
+
function extractStringOrStringArray(node) {
|
|
3389
|
+
if (isStringLiteral(node)) return [node.text];
|
|
3390
|
+
return isArrayLiteralExpression(node) ? node.elements?.filter((node) => isStringLiteral(node)).map((node) => node.text) : [];
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Extracts property metadata from @Property or @Property.required decorator.
|
|
3394
|
+
*/
|
|
3395
|
+
function extractPropertyMetadata(propName, modifier) {
|
|
3396
|
+
const metadata = {
|
|
3397
|
+
name: propName,
|
|
3398
|
+
required: false
|
|
3399
|
+
};
|
|
3400
|
+
const expr = modifier.expression;
|
|
3401
|
+
if (isPropertyAccessExpression(expr)) {
|
|
3402
|
+
if (isIdentifier(expr.name) && expr.name.text === "required") metadata.required = true;
|
|
3403
|
+
}
|
|
3404
|
+
if (isCallExpression(expr)) {
|
|
3405
|
+
const args = expr.arguments;
|
|
3406
|
+
if (args.length) {
|
|
3407
|
+
const arg = args[0];
|
|
3408
|
+
if (isObjectLiteralExpression(arg)) {
|
|
3409
|
+
const aliasNode = arg.properties?.find((prop) => isPropertyAssignment(prop) && isIdentifier(prop.name) && prop.name.text === "alias");
|
|
3410
|
+
if (aliasNode && isStringLiteral(aliasNode.initializer)) metadata.alias = aliasNode.initializer.text;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return metadata;
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Extracts event metadata from @Event decorator.
|
|
3418
|
+
*/
|
|
3419
|
+
function extractEventMetadata(eventName, modifier) {
|
|
3420
|
+
return { name: eventName };
|
|
3421
|
+
}
|
|
3422
|
+
//#endregion
|
|
3057
3423
|
//#region ../packages/compiler/src/type-checker/type-checker.ts
|
|
3058
3424
|
/**
|
|
3059
3425
|
* Generates a single, flat TypeScript function body ("shim") from a
|
|
@@ -3079,6 +3445,7 @@ function typeCheckTextAndInterpolation(node, _processNode, context) {
|
|
|
3079
3445
|
*/
|
|
3080
3446
|
var TypeChecker = class {
|
|
3081
3447
|
_ast;
|
|
3448
|
+
_context = new TypeCheckContext();
|
|
3082
3449
|
_states = {
|
|
3083
3450
|
[ASTNodeType.Text]: typeCheckTextAndInterpolation,
|
|
3084
3451
|
[ASTNodeType.Interpolation]: typeCheckTextAndInterpolation,
|
|
@@ -3086,12 +3453,29 @@ var TypeChecker = class {
|
|
|
3086
3453
|
[ASTNodeType.If]: typeCheckIf,
|
|
3087
3454
|
[ASTNodeType.For]: typeCheckFor,
|
|
3088
3455
|
[ASTNodeType.Switch]: typeCheckSwitch,
|
|
3089
|
-
[ASTNodeType.Import]:
|
|
3456
|
+
[ASTNodeType.Import]: typeCheckImport
|
|
3090
3457
|
};
|
|
3091
3458
|
constructor(_ast) {
|
|
3092
3459
|
this._ast = _ast;
|
|
3093
3460
|
}
|
|
3094
3461
|
/**
|
|
3462
|
+
* Pre-populates the shared context with component and directive metadata
|
|
3463
|
+
* by parsing source files for all `@import` nodes in the AST.
|
|
3464
|
+
*
|
|
3465
|
+
* Must be awaited before calling `generate()` if metadata-driven
|
|
3466
|
+
* validation (e.g. unknown component inputs) is desired.
|
|
3467
|
+
*
|
|
3468
|
+
* @param baseDir - Absolute path used to resolve relative import paths.
|
|
3469
|
+
*/
|
|
3470
|
+
async populateImportMetadata(baseDir) {
|
|
3471
|
+
const importNodes = this._ast.filter((node) => node.type === ASTNodeType.Import);
|
|
3472
|
+
await Promise.all(importNodes.flatMap((node) => node.specifiers.filter(({ imported }) => imported !== "*").map(async ({ imported, local }) => {
|
|
3473
|
+
const symbolName = imported === "default" ? local : imported;
|
|
3474
|
+
const metadata = await extractComponentMetadata(node.path, symbolName, baseDir);
|
|
3475
|
+
if (metadata) this._context.addImport(metadata);
|
|
3476
|
+
})));
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3095
3479
|
* Generates the full `function typeCheck() { ... }` shim body for the
|
|
3096
3480
|
* component's template.
|
|
3097
3481
|
*
|
|
@@ -3099,11 +3483,14 @@ var TypeChecker = class {
|
|
|
3099
3483
|
* body actually references `$event` (event handler bindings), so shims
|
|
3100
3484
|
* for templates with no event bindings don't carry an unused local —
|
|
3101
3485
|
* relevant if the consuming project has `noUnusedLocals` enabled.
|
|
3486
|
+
*
|
|
3487
|
+
* @param baseDir - Absolute path used to resolve relative import paths
|
|
3102
3488
|
*/
|
|
3103
|
-
generate() {
|
|
3489
|
+
generate(baseDir) {
|
|
3490
|
+
this.populateImportMetadata(baseDir);
|
|
3104
3491
|
return [
|
|
3105
3492
|
"function typeCheck() {",
|
|
3106
|
-
...indent(this._ast.flatMap((node) => this._processNode(node))),
|
|
3493
|
+
...indent(this._ast.flatMap((node) => this._processNode(node, this._context))),
|
|
3107
3494
|
"}"
|
|
3108
3495
|
].join("\n");
|
|
3109
3496
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xaendar/compiler",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.31",
|
|
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.
|
|
20
|
-
"@xaendar/types": "0.7.
|
|
19
|
+
"@xaendar/common": "0.7.31",
|
|
20
|
+
"@xaendar/types": "0.7.31",
|
|
21
21
|
"typescript": "^6.0.3"
|
|
22
22
|
}
|
|
23
23
|
}
|