diff --git a/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/analog.spec.ts.snap b/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/analog.spec.ts.snap index 3bf8b07cf..a27738e2d 100644 --- a/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/analog.spec.ts.snap +++ b/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/analog.spec.ts.snap @@ -28,13 +28,17 @@ import nonameag from "./noname.ag"; }) export default class VirtualAnalogComponent { constructor() { - const [a, b, , c = 4] = [1, 2, 3]; - this.a = a; - this.b = b; - this.c = c; let divElement; let test; const counter = this.counter; + const a = this.a; + const b = this.b; + const c = this.c; + const restArr = this.restArr; + const foo = this.foo; + const renamedBar = this.renamedBar; + const nonExist = this.nonExist; + const restObj = this.restObj; const inputWithDefault = this.inputWithDefault; const inputWithoutDefault = this.inputWithoutDefault; const inputWithAlias = this.inputWithAlias; @@ -54,6 +58,8 @@ export default class VirtualAnalogComponent { const contentChildrenEl = this.contentChildrenEl; const route = this.route; const id = this.id; + const nativeElement = this.nativeElement; + const elementId = this.elementId; setTimeout(() => { test = 'test'; }, 1000) @@ -66,11 +72,25 @@ export default class VirtualAnalogComponent { }) } protected readonly Math = Math; - a; - b; - c; protected readonly routeMeta = routeMeta; counter = signal(0); + private readonly __destructured1 = [1, 2, 3]; + a = this.__destructured1.a; + b = this.__destructured1.b; + c = this.__destructured1.c === undefined ? 4 : this.__destructured1.c; + private readonly __destructured2 = [1, 2, 3, 4, 5]; + restArr = (() => { + const [, , ...restArr] = this.__destructured2; + return restArr; + })(); + private readonly __destructured3 = { foo: 1, bar: 2, baz: 3 }; + foo = this.__destructured3.foo; + renamedBar = this.__destructured3.bar; + nonExist = this.__destructured3.nonExist === undefined ? 4 : this.__destructured3.nonExist; + restObj = (() => { + const { foo, bar: renamedBar, nonExist = 4, ...restObj } = this.__destructured3; + return restObj; + })(); inputWithDefault = input(""); inputWithoutDefault = input(); inputWithAlias = input("", { alias: "theAlias" }); @@ -96,6 +116,9 @@ export default class VirtualAnalogComponent { contentChildrenEl = contentChildren('divElement'); route = inject(ActivatedRoute); id = this.route.snapshot.paramMap.get('id'); + private readonly __destructured4 = inject(ElementRef); + nativeElement = this.__destructured4.nativeElement; + elementId = this.nativeElement.id; protected readonly myFunc = myFunc; protected readonly ExternalEnum = ExternalEnum; } diff --git a/packages/vite-plugin-angular/src/lib/authoring/analog.spec.ts b/packages/vite-plugin-angular/src/lib/authoring/analog.spec.ts index afc6a54c8..800b4e418 100644 --- a/packages/vite-plugin-angular/src/lib/authoring/analog.spec.ts +++ b/packages/vite-plugin-angular/src/lib/authoring/analog.spec.ts @@ -47,6 +47,8 @@ setTimeout(() => { const counter = signal(0); const [a, b, , c = 4] = [1, 2, 3]; +const [, , ...restArr] = [1, 2, 3, 4, 5]; +const { foo, bar: renamedBar, nonExist = 4, ...restObj } = { foo: 1, bar: 2, baz: 3 }; const inputWithDefault = input(""); // InputSignal const inputWithoutDefault = input(); // InputSignal @@ -81,6 +83,9 @@ const contentChildrenEl = contentChildren('divElement'); const route = inject(ActivatedRoute); const id = route.snapshot.paramMap.get('id'); +const { nativeElement } = inject>(ElementRef); +const elementId = nativeElement.id; + afterNextRender(() => { console.log('the div', divElement); }) diff --git a/packages/vite-plugin-angular/src/lib/authoring/analog.ts b/packages/vite-plugin-angular/src/lib/authoring/analog.ts index 4cdbaaa62..cc2f30684 100644 --- a/packages/vite-plugin-angular/src/lib/authoring/analog.ts +++ b/packages/vite-plugin-angular/src/lib/authoring/analog.ts @@ -12,7 +12,6 @@ import { Scope, SourceFile, SyntaxKind, - VariableDeclaration, VariableDeclarationKind, } from 'ts-morph'; import { @@ -40,7 +39,7 @@ export function compileAnalogFile( throw new Error(`[Analog] Missing component name ${filePath}`); } - const [componentFileName, className] = [ + const [fileName, className] = [ toFileName(componentName), toClassName(componentName), ]; @@ -105,7 +104,7 @@ import { ${ngType}${ @${ngType}({ standalone: true, - selector: '${componentFileName},${className}', + selector: '${fileName},${className}', ${componentMetadata} }) export default class ${entityName} { @@ -130,10 +129,10 @@ export default class ${entityName} { interface ClassMember { name: string; - initializer?: Expression; - declaration: VariableDeclaration; + initializer?: Expression | ((writer: CodeBlockWriter) => void); hasExportKeyword: boolean; isLet: boolean; + isVirtual?: boolean; } function processAnalogScript( @@ -185,8 +184,9 @@ function processAnalogScript( exposes: [], }; - const classMembers = new Map(), - callsQueue: Array<() => void> = []; + let destructuredCount = 0; + const memberRegistry = new Map(), + pendingOperations: Array<() => void> = []; for (const node of sourceSyntaxList.getChildren()) { // Handle import statements @@ -279,26 +279,82 @@ function processAnalogScript( Node.isObjectBindingPattern(nameNode) ) { // destructuring - // NOTE/TODO (chau): not supporting destructured variables for input/output - targetConstructor.addStatements(nodeFullText); + // NOTE: we do not support nested destructuring - const bindingElements = nameNode - .getDescendantsOfKind(SyntaxKind.BindingElement) - .map((bindingElement) => bindingElement.getName()); + // this also gets rid of OmittedExpression (i.e: skipped elements in array destructuring) + const bindingElements = nameNode.getDescendantsOfKind( + SyntaxKind.BindingElement + ); + + // nothing to do here + if (bindingElements.length <= 0) continue; + + destructuredCount += 1; + + // add a virtual class property for the destructuring initializer + const destructuredVirtualName = `__destructured${destructuredCount}`; + memberRegistry.set(destructuredVirtualName, { + name: destructuredVirtualName, + isVirtual: true, + initializer, + isLet: false, + hasExportKeyword: false, + }); for (const bindingElement of bindingElements) { - targetClass.addProperty({ name: bindingElement }); - targetConstructor.addStatements( - `this.${bindingElement} = ${bindingElement};` - ); + const bindingName = bindingElement.getName(), + bindingPropertyName = bindingElement + .getPropertyNameNode() + ?.getText(), + bindingInitializer = bindingElement.getInitializer(), + bindingDotDotDot = bindingElement.getDotDotDotToken(); + + // rest element + if (bindingDotDotDot) { + memberRegistry.set(bindingName, { + name: bindingName, + isLet: false, + hasExportKeyword: false, + initializer: (writer) => { + // for rest, we'll write an iife + writer.write(`(() => { + const ${nameNode.getFullText()} = this.${destructuredVirtualName}; + return ${bindingName}; +})()`); + }, + }); + + continue; + } + + // regular destructured element + memberRegistry.set(bindingName, { + name: bindingName, + isLet: false, + hasExportKeyword: false, + initializer: (writer) => { + const propertyAccess = `this.${destructuredVirtualName}.${ + bindingPropertyName ?? bindingName + }`; + + // using `=== undefined` to strictly follow destructuring rule + if (bindingInitializer) { + writer.write( + `${propertyAccess} === undefined ? ${bindingInitializer.getText()} : ${propertyAccess}` + ); + } else { + writer.write(propertyAccess); + } + }, + }); } + continue; } - classMembers.set(name, { + memberRegistry.set(name, { name, initializer, - declaration, isLet, hasExportKeyword, }); @@ -316,7 +372,7 @@ function processAnalogScript( targetSourceFile.addStatements(nodeFullText); } - callsQueue.push(() => { + pendingOperations.push(() => { if (hasExportKeyword) { targetClass.addProperty({ name: functionName, @@ -370,7 +426,7 @@ function processAnalogScript( const initFunction = expression.getArguments()[0]; if (!Node.isFunctionLikeDeclaration(initFunction)) continue; - callsQueue.push(() => { + pendingOperations.push(() => { // add the function to constructor targetConstructor.addStatements( `this.${fnName} = ${initFunction.getText()}` @@ -387,16 +443,23 @@ function processAnalogScript( } // other function calls - callsQueue.push(() => targetConstructor.addStatements(nodeFullText)); + pendingOperations.push(() => + targetConstructor.addStatements(nodeFullText) + ); } } const gettersSetters: Array<{ propertyName: string; isFunction: boolean }> = []; - for (const classMember of classMembers.values()) { - const { isLet, hasExportKeyword, name, initializer, declaration } = - classMember; + for (const member of memberRegistry.values()) { + const { + isLet, + hasExportKeyword, + name, + initializer, + isVirtual = false, + } = member; if (isLet && hasExportKeyword) { console.warn(`[Analog] let variable cannot be exported: ${name}`); continue; @@ -405,29 +468,46 @@ function processAnalogScript( if (isLet) { targetConstructor.addStatements((writer) => { writer.write(`let ${name}`); - if (initializer) writer.write(`=${initializer.getText()}`); + if (initializer) { + if (typeof initializer === 'function') { + initializer(writer); + } else { + writer.write(`=${initializer.getText()}`); + } + } writer.write(';'); }); // push to gettersSetters gettersSetters.push({ propertyName: name, isFunction: - !!initializer && Node.isFunctionLikeDeclaration(initializer), + !!initializer && + typeof initializer !== 'function' && + Node.isFunctionLikeDeclaration(initializer), }); } else { targetClass.addProperty({ name, initializer: (writer) => { + if (typeof initializer === 'function') { + initializer(writer); + return; + } + if (hasExportKeyword) { writer.write(name); } else { - processInitializer(writer, declaration, classMembers, initializer); + processInitializer(writer, name, memberRegistry, initializer); } }, - isReadonly: hasExportKeyword, - scope: hasExportKeyword ? Scope.Protected : undefined, + isReadonly: hasExportKeyword || isVirtual, + scope: hasExportKeyword + ? Scope.Protected + : isVirtual + ? Scope.Private + : undefined, }); - if (name !== ROUTE_META) { + if (name !== ROUTE_META && !isVirtual) { targetConstructor.addStatements((writer) => { writer.write(`const ${name} = this.${name};`); }); @@ -435,7 +515,7 @@ function processAnalogScript( } } - for (const call of callsQueue) { + for (const call of pendingOperations) { call(); } @@ -502,14 +582,12 @@ ${gettersSetters.reduce((acc, { isFunction, propertyName }) => { function processInitializer( writer: CodeBlockWriter, - declaration: VariableDeclaration, - classMembers: Map, + name: string, + memberRegistry: Map, initializer?: Expression ) { if (!initializer) { - console.warn( - `[Analog] const variable must have an initializer: ${declaration.getName()}` - ); + console.warn(`[Analog] const variable must have an initializer: ${name}`); return; } @@ -522,7 +600,7 @@ function processInitializer( writer.write( processCallExpressionOrPropertyAccessExpressionForClassMember( initializer, - classMembers + memberRegistry ) ); return; @@ -536,14 +614,14 @@ function processInitializer( writer.write( processCallExpressionOrPropertyAccessExpressionForClassMember( initializer, - classMembers + memberRegistry ) ); } function processCallExpressionOrPropertyAccessExpressionForClassMember( initializer: Expression, - classMembers: Map + memberRegistry: Map ) { if (Node.isCallExpression(initializer)) { const currentExpression = initializer.getExpression(); @@ -558,7 +636,7 @@ function processCallExpressionOrPropertyAccessExpressionForClassMember( let fullExpressionText = currentExpression.getText(); if ( Node.isIdentifier(deepestExpression) && - classMembers.has(deepestExpression.getText()) + memberRegistry.has(deepestExpression.getText()) ) { // if it's part of the classMembers, add `this.` fullExpressionText = `this.${fullExpressionText}`; @@ -567,7 +645,7 @@ function processCallExpressionOrPropertyAccessExpressionForClassMember( // process arguments of the call expression const args: string[] = initializer.getArguments().map((arg) => { // if it's part of the classMembers, add `this.` - if (Node.isIdentifier(arg) && classMembers.has(arg.getText())) { + if (Node.isIdentifier(arg) && memberRegistry.has(arg.getText())) { return `this.${arg.getText()}`; } @@ -575,7 +653,7 @@ function processCallExpressionOrPropertyAccessExpressionForClassMember( if (Node.isPropertyAccessExpression(arg) || Node.isCallExpression(arg)) { return processCallExpressionOrPropertyAccessExpressionForClassMember( arg, - classMembers + memberRegistry ); } @@ -597,7 +675,7 @@ function processCallExpressionOrPropertyAccessExpressionForClassMember( if ( Node.isIdentifier(deepestExpression) && - classMembers.has(deepestExpression.getText()) + memberRegistry.has(deepestExpression.getText()) ) { return `this.${initializer.getText()}`; }