Skip to content

Commit 755a0f2

Browse files
feat(vite-plugin-angular): introduce support for Angular Compilation API
1 parent 25edd36 commit 755a0f2

File tree

6 files changed

+174
-14
lines changed

6 files changed

+174
-14
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/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: 153 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 {
@@ -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;
@@ -157,14 +173,38 @@ export function angular(options?: PluginOptions): Plugin[] {
157173
};
158174
let initialCompilation = false;
159175
const declarationFiles: DeclarationFile[] = [];
176+
let compilation: Awaited<ReturnType<typeof createAngularCompilationType>>;
160177

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

164-
if (angularMajor < 19 || isTest) {
181+
if (angularFullVersion < 190000 || isTest) {
165182
pluginOptions.liveReload = false;
166183
}
167184

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

269+
if (pluginOptions.useAngularCompilationAPI) {
270+
externalComponentStyles = new Map();
271+
inlineComponentStyles = new Map();
272+
}
273+
229274
// resolve the tsconfig path after config is fully resolved
230275
if (tsConfigResolutionContext) {
231276
const tsconfigValue = pluginOptions.tsconfigGetter();
@@ -672,7 +717,113 @@ export function angular(options?: PluginOptions): Plugin[] {
672717
return resolvedPath;
673718
}
674719

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

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)