Skip to content

Commit e7e1652

Browse files
feat(vite-plugin-angular): introduce support for Angular Compilation API
1 parent 61cd4a0 commit e7e1652

File tree

7 files changed

+177
-15
lines changed

7 files changed

+177
-15
lines changed

apps/analog-app/tsconfig.app.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
"target": "ES2022",
77
"useDefineForClassFields": false
88
},
9-
"files": ["src/main.ts", "src/main.server.ts"],
9+
"files": [
10+
"src/main.ts",
11+
"src/main.server.ts",
12+
"src/environments/environment.prod.ts"
13+
],
1014
"include": [
1115
"src/**/*.d.ts",
1216
"src/app/pages/**/*.page.ts",

apps/analog-app/tsconfig.spec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"outDir": "../../dist/out-tsc",
55
"types": ["node", "vitest/globals"]
66
},
7-
"files": ["src/test-setup.ts", "src/polyfills.ts"],
7+
"files": ["src/test-setup.ts"],
88
"include": ["src/**/*.spec.ts", "**/*.d.ts"]
99
}

apps/analog-app/vite.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ export default defineConfig(({ mode }) => {
7272
},
7373
vite: {
7474
inlineStylesExtension: 'scss',
75+
experimental: {
76+
useAngularCompilationAPI: true,
77+
},
7578
},
76-
liveReload: true,
79+
liveReload: false,
7780
nitro: {
7881
routeRules: {
7982
'/cart/**': {

libs/top-bar/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import angular from '@analogjs/vite-plugin-angular';
77
export default defineConfig(({ mode }) => {
88
return {
99
root: __dirname,
10-
plugins: [angular()],
10+
plugins: [angular({ experimental: { useAngularCompilationAPI: true } })],
1111
test: {
1212
reporters: ['default'],
1313
globals: true,

packages/vite-plugin-angular/src/lib/angular-pending-tasks.plugin.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Plugin } from 'vite';
22

3-
import { angularMajor, angularMinor, angularPatch } from './utils/devkit.js';
3+
import { angularFullVersion } from './utils/devkit.js';
44

55
/**
66
* This plugin is a workaround for the ɵPendingTasks symbol being renamed
@@ -13,14 +13,7 @@ export function pendingTasksPlugin(): Plugin {
1313
return {
1414
name: 'analogjs-pending-tasks-plugin',
1515
transform(code, id) {
16-
const padVersion = (version: number) => String(version).padStart(2, '0');
17-
18-
if (
19-
Number(
20-
`${angularMajor}${padVersion(angularMinor)}${padVersion(angularPatch)}`,
21-
) < 190004 &&
22-
id.includes('analogjs-content.mjs')
23-
) {
16+
if (angularFullVersion < 190004 && id.includes('analogjs-content.mjs')) {
2417
return {
2518
code: code.replace('ɵPendingTasksInternal', 'ɵPendingTasks'),
2619
};

packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import * as compilerCli from '@angular/compiler-cli';
1313
import { createRequire } from 'node:module';
1414
import * as ts from 'typescript';
15+
import { type createAngularCompilation as createAngularCompilationType } from '@angular/build/private';
1516

1617
import * as ngCompiler from '@angular/compiler';
1718
import { globSync } from 'tinyglobby';
@@ -40,9 +41,10 @@ import {
4041

4142
import { angularVitestPlugins } from './angular-vitest-plugin.js';
4243
import {
43-
angularMajor,
44+
createAngularCompilation,
4445
createJitResourceTransformer,
4546
SourceFileCache,
47+
angularFullVersion,
4648
} from './utils/devkit.js';
4749

4850
const require = createRequire(import.meta.url);
@@ -58,6 +60,15 @@ import {
5860
replaceFiles,
5961
} from './plugins/file-replacements.plugin.js';
6062
import { routerPlugin } from './router-plugin.js';
63+
import { createHash } from 'node:crypto';
64+
65+
export enum DiagnosticModes {
66+
None = 0,
67+
Option = 1 << 0,
68+
Syntactic = 1 << 1,
69+
Semantic = 1 << 2,
70+
All = Option | Syntactic | Semantic,
71+
}
6172

6273
export interface PluginOptions {
6374
tsconfig?: string | (() => string);
@@ -80,6 +91,9 @@ export interface PluginOptions {
8091
liveReload?: boolean;
8192
disableTypeChecking?: boolean;
8293
fileReplacements?: FileReplacement[];
94+
experimental?: {
95+
useAngularCompilationAPI?: boolean;
96+
};
8397
}
8498

8599
/**
@@ -120,6 +134,8 @@ export function angular(options?: PluginOptions): Plugin[] {
120134
liveReload: options?.liveReload ?? false,
121135
disableTypeChecking: options?.disableTypeChecking ?? true,
122136
fileReplacements: options?.fileReplacements ?? [],
137+
useAngularCompilationAPI:
138+
options?.experimental?.useAngularCompilationAPI ?? false,
123139
};
124140

125141
let resolvedConfig: ResolvedConfig;
@@ -156,14 +172,38 @@ export function angular(options?: PluginOptions): Plugin[] {
156172
};
157173
let initialCompilation = false;
158174
const declarationFiles: DeclarationFile[] = [];
175+
let compilation: Awaited<ReturnType<typeof createAngularCompilationType>>;
159176

160177
function angularPlugin(): Plugin {
161178
let isProd = false;
162179

163-
if (angularMajor < 19 || isTest) {
180+
if (angularFullVersion < 190000 || isTest) {
164181
pluginOptions.liveReload = false;
165182
}
166183

184+
if (pluginOptions.useAngularCompilationAPI) {
185+
if (angularFullVersion < 200100) {
186+
pluginOptions.useAngularCompilationAPI = false;
187+
console.warn(
188+
'[@analogjs/vite-plugin-angular]: The Angular Compilation API is only available with Angular v20.1 and later',
189+
);
190+
}
191+
192+
if (pluginOptions.liveReload) {
193+
pluginOptions.liveReload = false;
194+
console.warn(
195+
'[@analogjs-vite-plugin-angular]: Live reload is currently not compatible with the Angular Compilation API option',
196+
);
197+
}
198+
199+
if (pluginOptions.fileReplacements) {
200+
pluginOptions.fileReplacements = [];
201+
console.warn(
202+
'[@analogjs-vite-plugin-angular]: File replacements are currently not compatible with the Angular Compilation API option',
203+
);
204+
}
205+
}
206+
167207
return {
168208
name: '@analogjs/vite-plugin-angular',
169209
async config(config, { command }) {
@@ -219,6 +259,11 @@ export function angular(options?: PluginOptions): Plugin[] {
219259
configResolved(config) {
220260
resolvedConfig = config;
221261

262+
if (pluginOptions.useAngularCompilationAPI) {
263+
externalComponentStyles = new Map();
264+
inlineComponentStyles = new Map();
265+
}
266+
222267
if (isTest) {
223268
// set test watch mode
224269
// - vite override from vitest-angular
@@ -665,7 +710,115 @@ export function angular(options?: PluginOptions): Plugin[] {
665710
);
666711
}
667712

713+
async function performAngularCompilation(config: ResolvedConfig) {
714+
compilation = await (
715+
createAngularCompilation as typeof createAngularCompilationType
716+
)(!!pluginOptions.jit, false);
717+
718+
const resolvedTsConfigPath = resolveTsConfigPath();
719+
const compilationResult = await compilation.initialize(
720+
resolvedTsConfigPath,
721+
{
722+
async transformStylesheet(
723+
data,
724+
containingFile,
725+
resourceFile,
726+
order,
727+
className,
728+
) {
729+
if (pluginOptions.liveReload) {
730+
const id = createHash('sha256')
731+
.update(containingFile)
732+
.update(className as string)
733+
.update(String(order))
734+
.update(data)
735+
.digest('hex');
736+
const filename = id + '.' + pluginOptions.inlineStylesExtension;
737+
inlineComponentStyles!.set(filename, data);
738+
return filename;
739+
}
740+
741+
const filename =
742+
resourceFile ??
743+
containingFile.replace('.ts', `.${options?.inlineStylesExtension}`);
744+
745+
let stylesheetResult;
746+
747+
try {
748+
stylesheetResult = await preprocessCSS(
749+
data,
750+
`${filename}?direct`,
751+
resolvedConfig,
752+
);
753+
} catch (e) {
754+
console.error(`${e}`);
755+
}
756+
757+
return stylesheetResult?.code || '';
758+
},
759+
processWebWorker(workerFile, containingFile) {
760+
return '';
761+
},
762+
},
763+
(tsCompilerOptions) => {
764+
if (pluginOptions.liveReload && watchMode) {
765+
tsCompilerOptions['_enableHmr'] = true;
766+
tsCompilerOptions['externalRuntimeStyles'] = true;
767+
// Workaround for https://github.com/angular/angular/issues/59310
768+
// Force extra instructions to be generated for HMR w/defer
769+
tsCompilerOptions['supportTestBed'] = true;
770+
}
771+
772+
if (tsCompilerOptions.compilationMode === 'partial') {
773+
// These options can't be false in partial mode
774+
tsCompilerOptions['supportTestBed'] = true;
775+
tsCompilerOptions['supportJitMode'] = true;
776+
}
777+
778+
if (!isTest && config.build?.lib) {
779+
tsCompilerOptions['declaration'] = true;
780+
tsCompilerOptions['declarationMap'] = watchMode;
781+
tsCompilerOptions['inlineSources'] = true;
782+
}
783+
784+
if (isTest) {
785+
// Allow `TestBed.overrideXXX()` APIs.
786+
tsCompilerOptions['supportTestBed'] = true;
787+
}
788+
789+
return tsCompilerOptions;
790+
},
791+
);
792+
793+
compilationResult.externalStylesheets?.forEach((value, key) => {
794+
externalComponentStyles?.set(`${value}.css`, key);
795+
});
796+
797+
const diagnostics = await compilation.diagnoseFiles(
798+
pluginOptions.disableTypeChecking
799+
? DiagnosticModes.All & ~DiagnosticModes.Semantic
800+
: DiagnosticModes.All,
801+
);
802+
803+
const errors = diagnostics.errors?.length ? diagnostics.errors : [];
804+
const warnings = diagnostics.warnings?.length ? diagnostics.warnings : [];
805+
806+
for (const file of await compilation.emitAffectedFiles()) {
807+
outputFiles.set(file.filename, {
808+
content: file.contents,
809+
dependencies: [],
810+
errors: errors.map((error) => error.text || ''),
811+
warnings: warnings.map((warning) => warning.text || ''),
812+
});
813+
}
814+
}
815+
668816
async function performCompilation(config: ResolvedConfig, ids?: string[]) {
817+
if (pluginOptions.useAngularCompilationAPI) {
818+
await performAngularCompilation(config);
819+
return { host: {} };
820+
}
821+
669822
const isProd = config.mode === 'production';
670823
const includeFiles = findIncludes();
671824

packages/vite-plugin-angular/src/lib/utils/devkit.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ const require = createRequire(import.meta.url);
88
const angularMajor = Number(VERSION.major);
99
const angularMinor = Number(VERSION.minor);
1010
const angularPatch = Number(VERSION.patch);
11+
const padVersion = (version: number) => String(version).padStart(2, '0');
12+
const angularFullVersion = Number(
13+
`${angularMajor}${padVersion(angularMinor)}${padVersion(angularPatch)}`,
14+
);
1115
let sourceFileCache: any;
1216
let cjt: Function;
1317
let jt: any;
18+
let createAngularCompilation: Function;
1419

1520
if (angularMajor < 17) {
1621
throw new Error('AnalogJS is not compatible with Angular v16 and lower');
@@ -39,11 +44,13 @@ if (angularMajor < 17) {
3944
createJitResourceTransformer,
4045
JavaScriptTransformer,
4146
SourceFileCache,
47+
createAngularCompilation: createAngularCompilationFn,
4248
} = require('@angular/build/private');
4349

4450
sourceFileCache = SourceFileCache;
4551
cjt = createJitResourceTransformer;
4652
jt = JavaScriptTransformer;
53+
createAngularCompilation = createAngularCompilationFn;
4754
}
4855

4956
export {
@@ -54,4 +61,6 @@ export {
5461
angularMajor,
5562
angularMinor,
5663
angularPatch,
64+
createAngularCompilation,
65+
angularFullVersion,
5766
};

0 commit comments

Comments
 (0)