diff --git a/.prettierrc b/.prettierrc
index 544138be4..3e099db38 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,3 +1,11 @@
{
- "singleQuote": true
+ "singleQuote": true,
+ "overrides": [
+ {
+ "files": "*.ng",
+ "options": {
+ "parser": "html"
+ }
+ }
+ ]
}
diff --git a/apps/ng-app/.eslintrc.json b/apps/ng-app/.eslintrc.json
new file mode 100644
index 000000000..7eef2ce74
--- /dev/null
+++ b/apps/ng-app/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/ng-app/index.html b/apps/ng-app/index.html
new file mode 100644
index 000000000..578fcad18
--- /dev/null
+++ b/apps/ng-app/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ ng-app
+
+
+
+
+
+
+
+
+
diff --git a/apps/ng-app/project.json b/apps/ng-app/project.json
new file mode 100644
index 000000000..af5693334
--- /dev/null
+++ b/apps/ng-app/project.json
@@ -0,0 +1,63 @@
+{
+ "name": "ng-app",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/ng-app/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@nx/vite:build",
+ "outputs": [
+ "{options.outputPath}",
+ "{workspaceRoot}/dist/apps/ng-app/.nitro",
+ "{workspaceRoot}/dist/apps/ng-app/ssr",
+ "{workspaceRoot}/dist/apps/ng-app/analog"
+ ],
+ "options": {
+ "configFile": "apps/ng-app/vite.config.ts",
+ "outputPath": "dist/apps/ng-app/client"
+ },
+ "defaultConfiguration": "production",
+ "configurations": {
+ "development": {
+ "mode": "development"
+ },
+ "production": {
+ "sourcemap": false,
+ "mode": "production"
+ }
+ }
+ },
+ "serve": {
+ "executor": "@nx/vite:dev-server",
+ "defaultConfiguration": "development",
+ "options": {
+ "buildTarget": "ng-app:build",
+ "port": 3000
+ },
+ "configurations": {
+ "development": {
+ "buildTarget": "ng-app:build:development",
+ "hmr": true
+ },
+ "production": {
+ "buildTarget": "ng-app:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "ng-app:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["apps/ng-app/**/*.ts", "apps/ng-app/**/*.html"]
+ }
+ }
+ }
+}
diff --git a/apps/ng-app/src/app/another-one.ng b/apps/ng-app/src/app/another-one.ng
new file mode 100644
index 000000000..7876f42af
--- /dev/null
+++ b/apps/ng-app/src/app/another-one.ng
@@ -0,0 +1,15 @@
+
+
+
+ DJ KHALED!!!
+
+
+
diff --git a/apps/ng-app/src/app/app.component.ng b/apps/ng-app/src/app/app.component.ng
new file mode 100644
index 000000000..55429ccde
--- /dev/null
+++ b/apps/ng-app/src/app/app.component.ng
@@ -0,0 +1,78 @@
+
+
+
+ @if (counter() > 5) {
+
+
+ }
+
+ Counter: {{ counter() }}
+ Doubled: {{ doubled() }}
+ Doubled with Pipe: {{ counter() | doubled }}
+
+
+
+ @if (todo(); as todo) {
+ {{todo | json }}
+ } @else {
+ Loading todo...
+ }
+
+
+
diff --git a/apps/ng-app/src/app/app.config.ts b/apps/ng-app/src/app/app.config.ts
new file mode 100644
index 000000000..1c0c9422f
--- /dev/null
+++ b/apps/ng-app/src/app/app.config.ts
@@ -0,0 +1,6 @@
+import { provideHttpClient } from '@angular/common/http';
+import { ApplicationConfig } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideHttpClient()],
+};
diff --git a/apps/ng-app/src/app/doubled.ng b/apps/ng-app/src/app/doubled.ng
new file mode 100644
index 000000000..4b0fd05d0
--- /dev/null
+++ b/apps/ng-app/src/app/doubled.ng
@@ -0,0 +1,9 @@
+
diff --git a/apps/ng-app/src/app/hello.ng b/apps/ng-app/src/app/hello.ng
new file mode 100644
index 000000000..1d07e802d
--- /dev/null
+++ b/apps/ng-app/src/app/hello.ng
@@ -0,0 +1,11 @@
+
+
+
+ Hello.ng again
+
diff --git a/apps/ng-app/src/app/highlight.ng b/apps/ng-app/src/app/highlight.ng
new file mode 100644
index 000000000..0140902b9
--- /dev/null
+++ b/apps/ng-app/src/app/highlight.ng
@@ -0,0 +1,13 @@
+
diff --git a/apps/ng-app/src/assets/.gitkeep b/apps/ng-app/src/assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/ng-app/src/favicon.ico b/apps/ng-app/src/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/ng-app/src/favicon.ico differ
diff --git a/apps/ng-app/src/main.ts b/apps/ng-app/src/main.ts
new file mode 100644
index 000000000..b57b508a7
--- /dev/null
+++ b/apps/ng-app/src/main.ts
@@ -0,0 +1,8 @@
+import 'zone.js';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import AppComponent from './app/app.component.ng';
+
+bootstrapApplication(AppComponent, appConfig).catch((err) =>
+ console.error(err)
+);
diff --git a/apps/ng-app/src/styles.css b/apps/ng-app/src/styles.css
new file mode 100644
index 000000000..90d4ee007
--- /dev/null
+++ b/apps/ng-app/src/styles.css
@@ -0,0 +1 @@
+/* You can add global styles to this file, and also import other style files */
diff --git a/apps/ng-app/src/vite-env.d.ts b/apps/ng-app/src/vite-env.d.ts
new file mode 100644
index 000000000..dc307357c
--- /dev/null
+++ b/apps/ng-app/src/vite-env.d.ts
@@ -0,0 +1,38 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_ANALOG_PUBLIC_BASE_URL: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
+
+declare global {
+ import type { Component, Directive, Pipe } from '@angular/core';
+
+ interface Window {
+ defineComponentMetadata: (
+ metadata: Omit<
+ Component,
+ | 'template'
+ | 'templateUrl'
+ | 'host'
+ | 'standalone'
+ | 'changeDetection'
+ | 'styleUrls'
+ | 'styleUrl'
+ | 'styles'
+ >
+ ) => void;
+ defineDirectiveMetadata: (
+ metadata: Omit
+ ) => void;
+ definePipeMetadata: (metadata: Omit) => void;
+ }
+}
+
+declare module '*.ng' {
+ const cmp = any;
+ export default cmp;
+}
diff --git a/apps/ng-app/tsconfig.app.json b/apps/ng-app/tsconfig.app.json
new file mode 100644
index 000000000..fff4a41d4
--- /dev/null
+++ b/apps/ng-app/tsconfig.app.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/ng-app/tsconfig.editor.json b/apps/ng-app/tsconfig.editor.json
new file mode 100644
index 000000000..4ee639340
--- /dev/null
+++ b/apps/ng-app/tsconfig.editor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "compilerOptions": {
+ "types": []
+ }
+}
diff --git a/apps/ng-app/tsconfig.json b/apps/ng-app/tsconfig.json
new file mode 100644
index 000000000..cd3727d6f
--- /dev/null
+++ b/apps/ng-app/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.editor.json"
+ }
+ ],
+ "extends": "../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/ng-app/vite.config.ts b/apps/ng-app/vite.config.ts
new file mode 100644
index 000000000..659492c97
--- /dev/null
+++ b/apps/ng-app/vite.config.ts
@@ -0,0 +1,31 @@
+///
+
+import { defineConfig } from 'vite';
+import analog from '@analogjs/platform';
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => ({
+ publicDir: 'src/assets',
+ build: {
+ target: ['es2020'],
+ },
+ resolve: {
+ mainFields: ['module'],
+ },
+ plugins: [
+ analog({
+ ssr: false,
+ vite: { experimental: { dangerouslySupportNgFormat: true } },
+ }),
+ ],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['src/test.ts'],
+ include: ['**/*.spec.ts'],
+ reporters: ['default'],
+ },
+ define: {
+ 'import.meta.vitest': mode !== 'production',
+ },
+}));
diff --git a/package.json b/package.json
index 5703683be..0ee8ebb65 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
- "prettify": "prettier --write ."
+ "prettify": "prettier --write .",
+ "build:test": "nx build ng-app --skip-nx-cache"
},
"engines": {
"node": "^18.13.0",
@@ -103,12 +104,14 @@
"@nx/plugin": "17.1.3",
"@nx/vite": "17.1.3",
"@nx/web": "17.1.3",
+ "@phenomnomnominal/tsquery": "^6.1.3",
"@schematics/angular": "^17.0.0",
"@swc-node/register": "~1.6.7",
"@swc/cli": "0.1.62",
"@swc/core": "~1.3.85",
"@swc/helpers": "0.5.1",
"@types/babel__core": "^7.20.0",
+ "@types/hast": "^3.0.3",
"@types/jest": "29.4.4",
"@types/marked": "^5.0.0",
"@types/node": "18.11.18",
@@ -132,6 +135,9 @@
"fs-extra": "^11.1.1",
"h3": "^1.8.2",
"happy-dom": "^12.10.3",
+ "hast-util-from-html": "^2.0.1",
+ "hast-util-from-parse5": "^8.0.1",
+ "hast-util-to-html": "^9.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jsdom": "22.1.0",
@@ -142,6 +148,7 @@
"ng-packagr": "^17.0.0",
"nitropack": "^2.6.0",
"nx": "17.1.3",
+ "parse5": "^7.1.2",
"playwright": "^1.30.0",
"postcss": "^8.4.21",
"postcss-import": "~15.1.0",
@@ -154,6 +161,7 @@
"start-server-and-test": "^1.15.4",
"tailwindcss": "^3.0.2",
"ts-jest": "29.1.0",
+ "ts-morph": "^21.0.1",
"ts-node": "10.9.1",
"typescript": "~5.2.0",
"vite": "5.0.0",
diff --git a/packages/create-analog/template-angular-v17/src/vite-env.d.ts b/packages/create-analog/template-angular-v17/src/vite-env.d.ts
index 11f02fe2a..a73290570 100644
--- a/packages/create-analog/template-angular-v17/src/vite-env.d.ts
+++ b/packages/create-analog/template-angular-v17/src/vite-env.d.ts
@@ -1 +1,31 @@
///
+
+// Uncomment the lines below to enable types for experimental .ng format support
+// declare global {
+// import type { Component, Directive, Pipe } from '@angular/core';
+
+// interface Window {
+// defineComponentMetadata: (
+// metadata: Omit<
+// Component,
+// | 'template'
+// | 'templateUrl'
+// | 'host'
+// | 'standalone'
+// | 'changeDetection'
+// | 'styleUrls'
+// | 'styleUrl'
+// | 'styles'
+// >
+// ) => void;
+// defineDirectiveMetadata: (
+// metadata: Omit
+// ) => void;
+// definePipeMetadata: (metadata: Omit) => void;
+// }
+// }
+
+// declare module '*.ng' {
+// const cmp = any;
+// export default cmp;
+// }
diff --git a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
index e6c90e589..49f178d98 100644
--- a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
+++ b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
@@ -1,4 +1,11 @@
-import { ModuleNode, Plugin, PluginContainer, ViteDevServer } from 'vite';
+import {
+ ModuleNode,
+ normalizePath,
+ Plugin,
+ PluginContainer,
+ UserConfig,
+ ViteDevServer,
+} from 'vite';
import { CompilerHost, NgtscProgram } from '@angular/compiler-cli';
import { transformAsync } from '@babel/core';
@@ -34,6 +41,12 @@ export interface PluginOptions {
*/
tsTransformers?: ts.CustomTransformers;
};
+ experimental?: {
+ /**
+ * Enable experimental support for .ng file format! Use as your own risk!
+ */
+ dangerouslySupportNgFormat?: boolean;
+ };
supportedBrowsers?: string[];
transformFilter?: (code: string, id: string) => boolean;
}
@@ -51,7 +64,7 @@ type FileEmitter = (file: string) => Promise;
* Match .(c or m)ts, .ts extensions with an optional ? for query params
* Ignore .tsx extensions
*/
-const TS_EXT_REGEX = /\.[cm]?ts[^x]?\??/;
+const TS_EXT_REGEX = /\.[cm]?(ts|ng)[^x]?\??/;
export function angular(options?: PluginOptions): Plugin[] {
/**
@@ -76,6 +89,7 @@ export function angular(options?: PluginOptions): Plugin[] {
},
supportedBrowsers: options?.supportedBrowsers ?? ['safari 15'],
jit: options?.jit,
+ supportNgFormat: options?.experimental?.dangerouslySupportNgFormat,
};
// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
@@ -92,6 +106,7 @@ export function angular(options?: PluginOptions): Plugin[] {
} = require('@ngtools/webpack/src/ivy/host');
let compilerCli: typeof import('@angular/compiler-cli');
+ let userConfig: UserConfig;
let rootNames: string[];
let host: ts.CompilerHost;
let nextProgram: NgtscProgram | undefined | ts.Program;
@@ -114,6 +129,7 @@ export function angular(options?: PluginOptions): Plugin[] {
name: '@analogjs/vite-plugin-angular',
async config(config, { command }) {
watchMode = command === 'serve';
+ userConfig = config;
pluginOptions.tsconfig =
options?.tsconfig ??
@@ -167,7 +183,7 @@ export function angular(options?: PluginOptions): Plugin[] {
cssPlugin = plugins.find((plugin) => plugin.name === 'vite:css');
}
- setupCompilation();
+ setupCompilation(userConfig);
// Only store cache if in watch mode
if (watchMode) {
@@ -285,7 +301,7 @@ export function angular(options?: PluginOptions): Plugin[] {
}
}
- const typescriptResult = await fileEmitter!(id);
+ const typescriptResult = fileEmitter && (await fileEmitter!(id));
// return fileEmitter
let data = typescriptResult?.content ?? '';
@@ -327,6 +343,16 @@ export function angular(options?: PluginOptions): Plugin[] {
/for\s+await\s*\(|async\s+function\s*\*/.test(data);
const useInputSourcemap = (!isProd ? undefined : false) as undefined;
+ if (
+ id.includes('.ng') &&
+ pluginOptions.supportNgFormat &&
+ fileEmitter
+ ) {
+ sourceFileCache.invalidate([`${id}.ts`]);
+ const ngFileResult = await fileEmitter!(`${id}.ts`);
+ data = ngFileResult?.content || '';
+ }
+
if (!forceAsyncTransformation && !isProd) {
return {
code: data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
@@ -383,7 +409,26 @@ export function angular(options?: PluginOptions): Plugin[] {
}),
].filter(Boolean) as Plugin[];
- function setupCompilation() {
+ function findNgFiles(config: UserConfig) {
+ if (!pluginOptions.supportNgFormat) {
+ return [];
+ }
+
+ const fg = require('fast-glob');
+ const root = normalizePath(
+ path.resolve(pluginOptions.workspaceRoot, config.root || '.')
+ );
+ const ngFiles: string[] = fg
+ .sync([`${root}/**/*.ng`], {
+ dot: true,
+ })
+ .map((file: string) => `${file}.ts`);
+
+ return ngFiles;
+ }
+
+ function setupCompilation(config: UserConfig) {
+ const ngFiles = findNgFiles(config);
const { options: tsCompilerOptions, rootNames: rn } =
compilerCli.readConfiguration(pluginOptions.tsconfig, {
suppressOutputPathCheck: true,
@@ -398,7 +443,14 @@ export function angular(options?: PluginOptions): Plugin[] {
supportTestBed: false,
});
- rootNames = rn;
+ if (pluginOptions.supportNgFormat) {
+ // Experimental Local Compilation is necessary
+ // for the Angular compiler to work with
+ // AOT and virtually compiled .ng files.
+ tsCompilerOptions.compilationMode = 'experimental-local';
+ }
+
+ rootNames = rn.concat(ngFiles);
compilerOptions = tsCompilerOptions;
host = ts.createIncrementalCompilerHost(compilerOptions);
@@ -409,6 +461,8 @@ export function angular(options?: PluginOptions): Plugin[] {
if (!jit) {
augmentHostWithResources(host, styleTransform, {
inlineStylesExtension: pluginOptions.inlineStylesExtension,
+ supportNgFormat: pluginOptions.supportNgFormat,
+ isProd: isProd,
});
}
}
diff --git a/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/ng.spec.ts.snap b/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/ng.spec.ts.snap
new file mode 100644
index 000000000..a9796b38d
--- /dev/null
+++ b/packages/vite-plugin-angular/src/lib/authoring/__snapshots__/ng.spec.ts.snap
@@ -0,0 +1,137 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`authoring ng file > should process component as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { signal } from "@angular/core";
+
+@Component({
+ standalone: true,
+ selector: 'virtual,Virtual,VIRTUAL',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: \`
+Component
+ {{ counter() }}
\`,
+ imports: []
+})
+export default class AnalogNgEntity {
+ constructor() {
+ let counter = signal(0);
+ this.counter = counter;
+ }
+
+ protected counter;
+}
+"
+`;
+
+exports[`authoring ng file > should process directive as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { inject, ElementRef, afterNextRender } from "@angular/core";
+import { Directive } from "@angular/core";
+
+@Directive({
+ standalone: true,
+ selector: 'input[directive]'
+})
+export default class AnalogNgEntity {
+ constructor() {
+ let elRef = inject(ElementRef);
+ this.elRef = elRef;
+ afterNextRender(() => {
+ elRef.nativeElement.focus();
+ });
+ }
+
+ protected elRef;
+}
+"
+`;
+
+exports[`authoring ng file > should process pipe as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { Pipe } from "@angular/core";
+
+@Pipe({
+ standalone: true,
+ name: 'doubled'
+})
+export default class AnalogNgEntity {
+ constructor() {
+ function transform(value: number) {
+ return value * 2;
+ }
+ this.transform = transform.bind(this);
+ }
+
+ protected transform;
+}
+"
+`;
+
+exports[`authoring ng file should process component as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { signal } from \\"@angular/core\\";
+
+@Component({
+ standalone: true,
+ selector: 'virtual,Virtual,VIRTUAL',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: \`
+Component
+ {{ counter() }}
\`,
+ imports: []
+})
+export default class AnalogNgEntity {
+ constructor() {
+ let counter = signal(0);
+ this.counter = counter;
+ }
+
+ protected counter;
+}
+"
+`;
+
+exports[`authoring ng file should process directive as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { inject, ElementRef, afterNextRender } from \\"@angular/core\\";
+import { Directive } from \\"@angular/core\\";
+
+@Directive({
+ standalone: true,
+ selector: 'input[directive]'
+})
+export default class AnalogNgEntity {
+ constructor() {
+ let elRef = inject(ElementRef);
+ this.elRef = elRef;
+ afterNextRender(() => {
+ elRef.nativeElement.focus();
+ });
+ }
+
+ protected elRef;
+}
+"
+`;
+
+exports[`authoring ng file should process pipe as ng file 1`] = `
+"import { Component, ChangeDetectionStrategy } from '@angular/core';
+import { Pipe } from \\"@angular/core\\";
+
+@Pipe({
+ standalone: true,
+ name: 'doubled'
+})
+export default class AnalogNgEntity {
+ constructor() {
+ function transform(value: number) {
+ return value * 2;
+ }
+ this.transform = transform.bind(this);
+ }
+
+ protected transform;
+}
+"
+`;
diff --git a/packages/vite-plugin-angular/src/lib/authoring/ng.spec.ts b/packages/vite-plugin-angular/src/lib/authoring/ng.spec.ts
new file mode 100644
index 000000000..f561ca713
--- /dev/null
+++ b/packages/vite-plugin-angular/src/lib/authoring/ng.spec.ts
@@ -0,0 +1,72 @@
+import { processNgFile } from './ng';
+
+const COMPONENT_CONTENT = `
+
+
+
+ Component
+ {{ counter() }}
+
+
+
+`;
+
+const DIRECTIVE_CONTENT = `
+
+`;
+
+const PIPE_CONTENT = `
+
+`;
+
+describe('authoring ng file', () => {
+ it('should process component as ng file', () => {
+ const source = processNgFile('virtual.ng.ts', COMPONENT_CONTENT);
+ expect(source).toContain('Component');
+ expect(source).toMatchSnapshot();
+ });
+
+ it('should process directive as ng file', () => {
+ const source = processNgFile('virtual.ng.ts', DIRECTIVE_CONTENT);
+ expect(source).toContain('Directive');
+ expect(source).toMatchSnapshot();
+ });
+
+ it('should process pipe as ng file', () => {
+ const source = processNgFile('virtual.ng.ts', PIPE_CONTENT);
+ expect(source).toContain('Pipe');
+ expect(source).toMatchSnapshot();
+ });
+});
diff --git a/packages/vite-plugin-angular/src/lib/authoring/ng.ts b/packages/vite-plugin-angular/src/lib/authoring/ng.ts
new file mode 100644
index 000000000..2bf6bff94
--- /dev/null
+++ b/packages/vite-plugin-angular/src/lib/authoring/ng.ts
@@ -0,0 +1,316 @@
+import {
+ ClassDeclaration,
+ ConstructorDeclaration,
+ Expression,
+ FunctionDeclaration,
+ Node,
+ ObjectLiteralExpression,
+ Project,
+ Scope,
+ StructureKind,
+} from 'ts-morph';
+
+const SCRIPT_TAG_REGEX = /