Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 196 additions & 35 deletions packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ import * as realFs from 'node:fs';
import { SourceMap } from 'node:module';
import { tmpdir } from 'node:os';
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
import { normalizePath } from 'vite';
import { normalizePath, resolveConfig } from 'vite';
import { NgtscProgram } from '@angular/compiler-cli';

vi.mock('@angular/compiler-cli', async (importOriginal) => {
const actual = await importOriginal<typeof import('@angular/compiler-cli')>();
return {
...actual,
NgtscProgram: vi.fn(function (
...args: ConstructorParameters<typeof actual.NgtscProgram>
) {
return new actual.NgtscProgram(...args);
}),
};
});

import type ts from 'typescript';
import * as tsModule from 'typescript';
Expand All @@ -18,8 +31,10 @@ import {
mapTemplateUpdatesToFiles,
toAngularCompilationFileReplacements,
isTestWatchMode,
type PluginOptions,
} from './angular-vite-plugin';
import type { EmitFileResult } from './models';
import { releaseCssPreprocessorWorkers } from './utils/css-preprocessor-workers';

describe('angularVitePlugin', () => {
it('should work', () => {
Expand Down Expand Up @@ -1057,12 +1072,13 @@ export class AppComponent {}
});

afterEach(() => {
releaseCssPreprocessorWorkers();
realFs.rmSync(fixtureDir, { recursive: true, force: true });
});

// The plugin reads these at creation time to pick the AOT/JIT path, so an
// app build has to be simulated by clearing Vitest's own markers.
function createAppBuildPlugin() {
function createAppBuildPlugin(options: PluginOptions = {}) {
const { VITEST, NODE_ENV } = process.env;
delete process.env['VITEST'];
delete process.env['NODE_ENV'];
Expand All @@ -1071,6 +1087,7 @@ export class AppComponent {}
return angular({
tsconfig: path.join(fixtureDir, 'tsconfig.json'),
workspaceRoot: fixtureDir,
...options,
}).find((p) => p.name === '@analogjs/vite-plugin-angular') as any;
} finally {
process.env['VITEST'] = VITEST as string;
Expand All @@ -1080,21 +1097,173 @@ export class AppComponent {}
}
}

it.each([
{ command: 'build', jit: false },
{ command: 'serve', jit: false },
{ command: 'build', jit: true },
{ command: 'serve', jit: true },
] as const)(
'emits source-linked workspace packages in $command mode (jit=$jit)',
async ({ command, jit }) => {
const libDir = path.join(fixtureDir, 'lib');
realFs.mkdirSync(libDir, { recursive: true });
realFs.mkdirSync(path.join(fixtureDir, 'node_modules'), {
recursive: true,
});
realFs.writeFileSync(
path.join(libDir, 'package.json'),
JSON.stringify({
name: 'linked-lib',
type: 'module',
exports: {
'.': './index.ts',
'./types': './types.d.ts',
'./*': './*.ts',
},
}),
);
realFs.writeFileSync(
path.join(libDir, 'index.ts'),
"export { DemoDirective } from './directive';",
);
realFs.writeFileSync(
path.join(libDir, 'directive.ts'),
"import { Directive } from '@angular/core'; @Directive({ selector: '[demo]', standalone: true }) export class DemoDirective {}",
);
const declarationPath = path.join(libDir, 'types.d.ts');
realFs.writeFileSync(
declarationPath,
'export interface LinkedType { value: string; }',
);
const installedDir = path.join(fixtureDir, 'node_modules/installed-lib');
realFs.mkdirSync(installedDir, { recursive: true });
realFs.writeFileSync(
path.join(installedDir, 'package.json'),
JSON.stringify({ name: 'installed-lib', exports: './index.ts' }),
);
const installedPath = path.join(installedDir, 'index.ts');
realFs.writeFileSync(installedPath, 'export const installed = 42;');
realFs.symlinkSync(
libDir,
path.join(fixtureDir, 'node_modules/linked-lib'),
'junction',
);
realFs.appendFileSync(
componentPath,
"\nexport { DemoDirective } from 'linked-lib';\nexport type { LinkedType } from 'linked-lib/types';\nexport { installed } from 'installed-lib';",
);
const entries = Array.from({ length: 8 }, (_, index) => `entry${index}`);
for (const entry of entries) {
realFs.writeFileSync(
path.join(libDir, `${entry}.ts`),
`export const ${entry} = 42;`,
);
realFs.appendFileSync(
componentPath,
`\nexport { ${entry} } from 'linked-lib/${entry}';`,
);
}
vi.mocked(NgtscProgram).mockClear();
const mainPlugin = createAppBuildPlugin({
disableTypeChecking: false,
jit,
});
await mainPlugin.config({ root: fixtureDir, build: {} }, { command });
const resolvedConfig = await resolveConfig(
{ configFile: false, root: fixtureDir, mode: 'production' },
command,
);
mainPlugin.configResolved(resolvedConfig);
const ctx = {
environment: { config: resolvedConfig },
warn: vi.fn(),
error: vi.fn(),
addWatchFile: vi.fn(),
};
await mainPlugin.buildStart.call(ctx);
if (!jit) {
const program = vi
.mocked(NgtscProgram)
.mock.results[0].value.getTsProgram();
for (const externalPath of [declarationPath, installedPath]) {
const sourceFile = program.getSourceFile(normalizePath(externalPath));
expect(sourceFile).toBeDefined();
expect(program.isSourceFileFromExternalLibrary(sourceFile)).toBe(
true,
);
}
}
const transform = async (name: string) => {
const id = normalizePath(path.join(libDir, name));
return mainPlugin.transform.handler.call(
ctx,
realFs.readFileSync(id, 'utf8'),
id,
);
};
expect((await transform('index.ts'))?.code).toContain('DemoDirective');
expect((await transform('directive.ts'))?.code).toContain(
jit ? '__decorate' : 'ɵdir',
);
expect((await transform('index.ts'))?.code).toContain('DemoDirective');
const results = await Promise.all(
entries.map((entry) => transform(`${entry}.ts`)),
);
results.forEach((result, index) =>
expect(result?.code).toContain(`${entries[index]} = 42`),
);
expect(NgtscProgram).toHaveBeenCalledTimes(jit ? 0 : 1);
realFs.appendFileSync(
path.join(libDir, 'directive.ts'),
'\nexport const updated = 42;',
);
await mainPlugin.handleHotUpdate({
file: normalizePath(path.join(libDir, 'directive.ts')),
modules: [],
});
expect((await transform('directive.ts'))?.code).toContain('updated = 42');
expect(NgtscProgram).toHaveBeenCalledTimes(jit ? 0 : 2);
await mainPlugin.buildEnd.call(ctx);
expect(ctx.error).not.toHaveBeenCalled();
const watchers = new Map<string, (file: string) => void>();
mainPlugin.configureServer({
watcher: {
on: (event: string, handler: (file: string) => void) =>
watchers.set(event, handler),
},
});
const barrel = normalizePath(path.join(libDir, 'index.ts'));
realFs.rmSync(barrel);
vi.useFakeTimers();
try {
const unlink = watchers.get('unlink');
expect(unlink).toBeDefined();
unlink?.(barrel);
await vi.advanceTimersByTimeAsync(100);
await mainPlugin.buildStart.call(ctx);
expect(
await mainPlugin.transform.handler.call(ctx, '', barrel),
).toBeUndefined();
} finally {
vi.useRealTimers();
}
await mainPlugin.buildEnd.call(ctx);
expect(ctx.warn).not.toHaveBeenCalled();
},
60_000,
);

it('waits for the initial compilation before emitting a transform result', async () => {
const mainPlugin = createAppBuildPlugin();

await mainPlugin.config(
{ root: fixtureDir, build: {} },
{ command: 'build' },
);
const resolvedConfig = {
root: fixtureDir,
mode: 'production',
build: {},
server: { watch: {} },
safeModulePaths: new Set(),
css: {},
};
const resolvedConfig = await resolveConfig(
{ configFile: false, root: fixtureDir, mode: 'production' },
'build',
);
mainPlugin.configResolved(resolvedConfig);

const ctx = {
Expand Down Expand Up @@ -1138,14 +1307,15 @@ export class AppComponent {}
{ root: fixtureDir, build: { sourcemap: true } },
{ command: 'build' },
);
const resolvedConfig = {
root: fixtureDir,
mode: 'production',
build: { sourcemap: true },
server: { watch: {} },
safeModulePaths: new Set(),
css: {},
};
const resolvedConfig = await resolveConfig(
{
configFile: false,
root: fixtureDir,
mode: 'production',
build: { sourcemap: true },
},
'build',
);
mainPlugin.configResolved(resolvedConfig);

const ctx = {
Expand Down Expand Up @@ -1188,14 +1358,10 @@ export class AppComponent {}
{ root: fixtureDir, build: {} },
{ command: 'build' },
);
const resolvedConfig = {
root: fixtureDir,
mode: 'production',
build: {},
server: { watch: {} },
safeModulePaths: new Set(),
css: {},
};
const resolvedConfig = await resolveConfig(
{ configFile: false, root: fixtureDir, mode: 'production' },
'build',
);
mainPlugin.configResolved(resolvedConfig);
const ctx = {
environment: { config: resolvedConfig },
Expand Down Expand Up @@ -1229,15 +1395,10 @@ export class AppComponent {}
{ root: fixtureDir, build: {} },
{ command: 'build' },
);
const resolvedConfig = {
root: fixtureDir,
mode: 'production',
build: {},
server: { watch: {} },
safeModulePaths: new Set(),
css: {},
plugins: [],
};
const resolvedConfig = await resolveConfig(
{ configFile: false, root: fixtureDir, mode: 'production' },
'build',
);
mainPlugin.configResolved(resolvedConfig);

// Context without this.environment (e.g. older Vite or minimal test harness)
Expand Down
14 changes: 10 additions & 4 deletions packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from './component-resolvers.js';
import {
augmentHostWithCaching,
augmentHostWithModuleResolution,
augmentHostWithResources,
augmentProgramWithVersioning,
mergeTransformers,
Expand Down Expand Up @@ -77,6 +78,7 @@ import { fastCompilePlugin } from './fast-compile-plugin.js';
import { ANGULAR_DECORATOR_CALL_RE } from './compiler/index.js';
import {
TS_EXT_REGEX,
EXCLUDED_TS_EXT_REGEX,
createTsConfigGetter,
getTsConfigPath,
createDepOptimizerConfig,
Expand Down Expand Up @@ -410,7 +412,13 @@ export function angular(options?: PluginOptions): Plugin[] {
),
);
server.watcher.on('add', invalidateCompilationOnFsChange);
server.watcher.on('unlink', invalidateCompilationOnFsChange);
server.watcher.on('unlink', (file) => {
const id = normalizePath(file);
outputFiles.delete(id);
fileTransformMap.delete(id);
sourceFileCache.delete(id);
invalidateCompilationOnFsChange(file);
});
server.watcher.on('change', (file) => {
if (file.includes('tsconfig')) {
invalidateTsconfigCaches();
Expand Down Expand Up @@ -1381,6 +1389,7 @@ export function angular(options?: PluginOptions): Plugin[] {
return file;
},
});
augmentHostWithModuleResolution(host, tsCompilerOptions);
cachedHost = host;
cachedHostKey = hostKey;

Expand Down Expand Up @@ -1663,9 +1672,6 @@ export function angular(options?: PluginOptions): Plugin[] {
}

const COMPONENT_RESOURCE_EXT_REGEX = /\.(html|htm|css|scss|sass|less)$/;
// Spec files stay included — a newly added spec must join the program's
// root names in Vitest watch mode.
const EXCLUDED_TS_EXT_REGEX = /\.d\.[cm]?ts$/;

export function createFsWatcherCacheInvalidator(
invalidateFsCaches: () => void,
Expand Down
Loading
Loading