Skip to content

Commit

Permalink
feat(vite-plugin-angular): add support for import attributes (#905)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuamorony authored Feb 27, 2024
1 parent 49f8518 commit 8f65c23
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 21 deletions.
21 changes: 13 additions & 8 deletions apps/ng-app/src/app/app.component.analog
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
<script lang="ts">
import { inject, signal, effect, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { JsonPipe } from '@angular/common';
import { RouterOutlet, RouterLink } from '@angular/router';
import { JsonPipe } from '@angular/common' with { analog: 'imports'};
import { RouterOutlet, RouterLink } from '@angular/router' with { analog: 'imports'};
import { delay } from 'rxjs';

import Hello from './hello.analog';
import AnotherOne from './another-one.analog';
import Highlight from './highlight.analog';
import External from './external/external.analog';
import { HelloOriginal } from './hello';
import Hello from './hello.analog' with { analog: 'imports'};
import AnotherOne from './another-one.analog' with { analog: 'imports' };
import Highlight from './highlight.analog' with { analog: 'imports' };
import External from './external/external.analog' with { analog: 'imports' };
import { Goodbye } from './my-components' with { analog: 'imports' };
import { HelloOriginal } from './hello' with { analog: 'imports'};
import { MyService } from './my.service' with { analog: 'providers'};

defineMetadata({
selector: 'app-root',
imports: [JsonPipe, HelloOriginal, RouterOutlet, RouterLink],
exposes: [Math],
});

const title = 'Angular Analog';

const http = inject(HttpClient);
const myService = inject(MyService);

const counter = signal(1);
const doubled = computed(() => counter() * 2);
Expand All @@ -44,6 +46,7 @@

onInit(() => {
console.log('App init');
console.log(myService.sayHello());
http
.get('https://jsonplaceholder.typicode.com/todos/1')
.pipe(delay(2000))
Expand Down Expand Up @@ -82,6 +85,8 @@

<br />

<Goodbye />

<router-outlet />
</template>

Expand Down
3 changes: 3 additions & 0 deletions apps/ng-app/src/app/goodbye.analog
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<h2>Goodbye</h2>
</template>
3 changes: 1 addition & 2 deletions apps/ng-app/src/app/hello.analog
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
afterNextRender,
} from '@angular/core';

import { myFunc } from './export-stuff.analog';
import { myFunc } from './export-stuff.analog' with { analog: 'exposes'};

defineMetadata({
queries: {
divElement: new ViewChild('divElement'),
},
exposes: [myFunc],
});

let divElement: ElementRef<HTMLDivElement>;
Expand Down
3 changes: 3 additions & 0 deletions apps/ng-app/src/app/my-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Goodbye from './goodbye.analog';

export { Goodbye };
8 changes: 8 additions & 0 deletions apps/ng-app/src/app/my.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@angular/core';

@Injectable()
export class MyService {
sayHello() {
return 'hello';
}
}
2 changes: 1 addition & 1 deletion apps/ng-app/src/app/pages/about.page.analog
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { RouteMeta } from '@analogjs/router';
import Hello from '../hello.analog';
import Hello from '../hello.analog' with { analog: 'imports'};

export const routeMeta: RouteMeta = {
title: 'My page',
Expand Down
4 changes: 4 additions & 0 deletions apps/ng-app/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ interface ImportMeta {
readonly env: ImportMetaEnv;
}

interface ImportAttributes {
analog: 'imports' | 'providers' | 'viewProviders' | 'exposes';
}

declare global {
import type { Component } from '@angular/core';

Expand Down
4 changes: 4 additions & 0 deletions packages/create-analog/template-angular-v17/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/// <reference types="vite/client" />

// Uncomment the lines below to enable types for experimental .analog format support
// interface ImportAttributes {
// analog: 'imports' | 'providers' | 'viewProviders' | 'exposes';
// }
//
// declare global {
// import type { Component } from '@angular/core';
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
exports[`authoring ng file > should process component as an analog file 1`] = `
"import { Component, ChangeDetectionStrategy } from '@angular/core';
import { signal, input, ViewChild, afterNextRender, ElementRef, viewChild, viewChildren, contentChild, contentChildren } from "@angular/core";
import External from "./external.analog";
import { ExternalService } from "./external";
import { ExternalEnum } from "./external.model";
@Component({
standalone: true,
Expand All @@ -18,6 +21,8 @@ import { signal, input, ViewChild, afterNextRender, ElementRef, viewChild, viewC
queries: {
divElement: new ViewChild('divElement')
},
imports: [External],
providers: [ExternalService],
outputs: ['output', 'outputWithType']
})
export default class VirtualAnalogComponent {
Expand Down Expand Up @@ -87,6 +92,7 @@ export default class VirtualAnalogComponent {
protected contentChildEl = contentChild<ElementRef>('divElement');
protected contentChildRequiredEl = contentChild.required<ElementRef>('divElement');
protected contentChildrenEl = contentChildren<HTMLDivElement[]>('divElement');
protected ExternalEnum = ExternalEnum;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/vite-plugin-angular/src/lib/authoring/analog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { compileAnalogFile } from './analog';
const COMPONENT_CONTENT = `
<script lang="ts">
import { signal, input, ViewChild, afterNextRender, ElementRef, viewChild, viewChildren, contentChild, contentChildren } from '@angular/core';
import External from './external.analog' with { analog: 'imports' };
import { ExternalService } from './external' with { analog: 'providers' };
import { ExternalEnum } from './external.model' with { analog: 'exposes' };
defineMetadata({
exposes: [Math],
Expand Down
97 changes: 87 additions & 10 deletions packages/vite-plugin-angular/src/lib/authoring/analog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ConstructorDeclaration,
FunctionDeclaration,
FunctionExpression,
ImportAttributeStructure,
ImportSpecifierStructure,
Node,
ObjectLiteralExpression,
OptionalKind,
Expand Down Expand Up @@ -149,8 +151,15 @@ function processAnalogScript(
throw new Error(`[Analog] invalid constructor body ${fileName}`);
}

const declarations: Array<string> = [],
gettersSetters: Array<{ propertyName: string; isFunction: boolean }> = [],
const importAttributes: { [key: string]: Array<string> } = {
imports: [],
viewProviders: [],
providers: [],
exposes: [],
};

const gettersSetters: Array<{ propertyName: string; isFunction: boolean }> =
[],
outputs: Array<string> = [],
sourceSyntaxList = ngSourceFile.getChildren()[0]; // SyntaxList

Expand All @@ -160,14 +169,54 @@ function processAnalogScript(

for (const node of sourceSyntaxList.getChildren()) {
if (Node.isImportDeclaration(node)) {
const moduleSpecifier = node.getModuleSpecifierValue();
if (moduleSpecifier.endsWith('.analog')) {
// other .ng files
declarations.push(node.getDefaultImport()?.getText() || '');
const structure = node.getStructure();
const attributes = structure.attributes;
const passThroughAttributes: OptionalKind<ImportAttributeStructure>[] =
[];
let foundAttribute = '';

for (const attribute of attributes || []) {
if (attribute.name === 'analog') {
const value = attribute.value.replaceAll("'", '');
if (!(value in importAttributes)) {
throw new Error(
`[Analog] Invalid Analog import attribute ${value} in ${fileName}`
);
}
foundAttribute = value;
continue;
}

passThroughAttributes.push(attribute);
}

if (foundAttribute) {
const { defaultImport, namedImports } = structure;
if (defaultImport) {
importAttributes[foundAttribute].push(defaultImport);
}

if (namedImports && Array.isArray(namedImports)) {
const namedImportStructures = namedImports.filter(
(
namedImport
): namedImport is OptionalKind<ImportSpecifierStructure> =>
typeof namedImport === 'object'
);
const importNames = namedImportStructures.map(
(namedImport) => namedImport.alias ?? namedImport.name
);
importAttributes[foundAttribute].push(...importNames);
}
}

// copy the import to the target `.analog.ts` file
targetSourceFile.addImportDeclaration(node.getStructure());
targetSourceFile.addImportDeclaration({
...structure,
attributes: passThroughAttributes.length
? passThroughAttributes
: undefined,
});
continue;
}

Expand Down Expand Up @@ -364,11 +413,39 @@ function processAnalogScript(
}
}

if (ngType === 'Component' && declarations.length) {
if (ngType === 'Component') {
if (importAttributes['viewProviders'].length) {
processArrayLiteralMetadata(
targetMetadataArguments,
'viewProviders',
importAttributes['viewProviders']
);
}

if (importAttributes['imports'].length) {
processArrayLiteralMetadata(
targetMetadataArguments,
'imports',
importAttributes['imports']
);
}

if (importAttributes['exposes'].length) {
const exposes = importAttributes['exposes'].map((item) => ({
name: item.trim(),
initializer: item.trim(),
scope: Scope.Protected,
}));

targetClass.addProperties(exposes);
}
}

if (importAttributes['providers'].length) {
processArrayLiteralMetadata(
targetMetadataArguments,
'imports',
declarations
'providers',
importAttributes['providers']
);
}

Expand Down

0 comments on commit 8f65c23

Please sign in to comment.