Skip to content

Commit

Permalink
fix(vite-plugin-angular): extract styleUrls using AST (#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlmestre authored Feb 12, 2024
1 parent 180f358 commit a7d5ff0
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 74 deletions.
21 changes: 4 additions & 17 deletions packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ import { jitPlugin } from './angular-jit-plugin';
import { angularVitestPlugin } from './angular-vitest-plugin';

import { createCompilerPlugin } from './compiler-plugin';
import {
hasStyleUrls,
hasTemplateUrl,
StyleUrlsResolver,
TemplateUrlsResolver,
} from './component-resolvers';
import { StyleUrlsResolver, TemplateUrlsResolver } from './component-resolvers';
import { augmentHostWithResources } from './host';
import {
angularApplicationPreset,
Expand Down Expand Up @@ -293,16 +288,8 @@ export function angular(options?: PluginOptions): Plugin[] {
}
}

let templateUrls: string[] = [];
let styleUrls: string[] = [];

if (hasTemplateUrl(code)) {
templateUrls = templateUrlsResolver.resolve(code, id);
}

if (hasStyleUrls(code)) {
styleUrls = styleUrlsResolver.resolve(code, id);
}
const templateUrls = templateUrlsResolver.resolve(code, id);
const styleUrls = styleUrlsResolver.resolve(code, id);

if (watchMode) {
for (const urlSet of [...templateUrls, ...styleUrls]) {
Expand All @@ -314,7 +301,7 @@ export function angular(options?: PluginOptions): Plugin[] {
}
}

const typescriptResult = fileEmitter && (await fileEmitter!(id));
const typescriptResult = await fileEmitter?.(id);

// return fileEmitter
let data = typescriptResult?.content ?? '';
Expand Down
44 changes: 26 additions & 18 deletions packages/vite-plugin-angular/src/lib/component-resolvers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { describe, it, expect } from 'vitest';

import {
hasTemplateUrl,
StyleUrlsResolver,
TemplateUrlsResolver,
} from './component-resolvers';
import { StyleUrlsResolver, TemplateUrlsResolver } from './component-resolvers';
import { normalizePath } from 'vite';
import { relative } from 'path';

Expand Down Expand Up @@ -49,10 +45,10 @@ describe('component-resolvers', () => {
const actualPaths = [
'./app.component.css|/path/to/src/app.component.css',
];
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedPaths = templateUrlsResolver.resolve(code, id);
const styleUrlsResolver = new StyleUrlsResolver();
const resolvedPaths = styleUrlsResolver.resolve(code, id);

expect(thePathsAreEqual(resolvedPaths, actualPaths));
expect(thePathsAreEqual(resolvedPaths, actualPaths)).toBe(true);
});

it('should handle single line styleUrl', () => {
Expand All @@ -66,10 +62,10 @@ describe('component-resolvers', () => {
const actualPaths = [
'./app.component.css|/path/to/src/app.component.css',
];
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedPaths = templateUrlsResolver.resolve(code, id);
const styleUrlsResolver = new StyleUrlsResolver();
const resolvedPaths = styleUrlsResolver.resolve(code, id);

expect(thePathsAreEqual(resolvedPaths, actualPaths));
expect(thePathsAreEqual(resolvedPaths, actualPaths)).toBe(true);
});

it('should handle multi-line styleUrls', () => {
Expand All @@ -91,7 +87,7 @@ describe('component-resolvers', () => {
const styleUrlsResolver = new StyleUrlsResolver();
const resolvedPaths = styleUrlsResolver.resolve(code, id);

expect(thePathsAreEqual(resolvedPaths, actualPaths));
expect(thePathsAreEqual(resolvedPaths, actualPaths)).toBe(true);
});

it('should handle wrapped multi-line styleUrls', () => {
Expand All @@ -114,7 +110,24 @@ describe('component-resolvers', () => {
const styleUrlsResolver = new StyleUrlsResolver();
const resolvedPaths = styleUrlsResolver.resolve(code, id);

expect(thePathsAreEqual(resolvedPaths, actualPaths));
expect(thePathsAreEqual(resolvedPaths, actualPaths)).toBe(true);
});

it('should handle styleUrls with route params in filename', () => {
const code = `
@Component({
styleUrls: ['./[param].component.css']
})
export class MyComponent {}
`;

const actualPaths = [
'./[param].component.css|/path/to/src/[param].component.css',
];
const styleUrlsResolver = new StyleUrlsResolver();
const resolvedPaths = styleUrlsResolver.resolve(code, id);

expect(thePathsAreEqual(resolvedPaths, actualPaths)).toBe(true);
});
});

Expand All @@ -135,7 +148,6 @@ describe('component-resolvers', () => {
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedTemplateUrls = templateUrlsResolver.resolve(code, id);

expect(hasTemplateUrl(code)).toBeTruthy();
expect(thePathsAreEqual(resolvedTemplateUrls, [actualUrl])).toBe(true);
});

Expand All @@ -152,7 +164,6 @@ describe('component-resolvers', () => {
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedTemplateUrls = templateUrlsResolver.resolve(code, id);

expect(hasTemplateUrl(code)).toBeTruthy();
expect(thePathsAreEqual(resolvedTemplateUrls, [actualUrl])).toBe(true);
});

Expand All @@ -169,7 +180,6 @@ describe('component-resolvers', () => {
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedTemplateUrls = templateUrlsResolver.resolve(code, id);

expect(hasTemplateUrl(code)).toBeTruthy();
expect(thePathsAreEqual(resolvedTemplateUrls, [actualUrl])).toBe(true);
});

Expand All @@ -186,7 +196,6 @@ describe('component-resolvers', () => {
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedTemplateUrls = templateUrlsResolver.resolve(code, id);

expect(hasTemplateUrl(code)).toBeTruthy();
expect(thePathsAreEqual(resolvedTemplateUrls, [actualUrl])).toBe(true);
});

Expand All @@ -210,7 +219,6 @@ describe('component-resolvers', () => {
const templateUrlsResolver = new TemplateUrlsResolver();
const resolvedTemplateUrls = templateUrlsResolver.resolve(code, id);

expect(hasTemplateUrl(code)).toBeTruthy();
expect(
thePathsAreEqual(resolvedTemplateUrls, [actualUrl1, actualUrl2])
).toBe(true);
Expand Down
78 changes: 39 additions & 39 deletions packages/vite-plugin-angular/src/lib/component-resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { dirname, resolve } from 'path';
import { Project, SyntaxKind } from 'ts-morph';
import {
ArrayLiteralExpression,
Project,
PropertyAssignment,
SyntaxKind,
} from 'ts-morph';
import { normalizePath } from 'vite';

const styleUrlsRE = /styleUrls\s*:\s*\[([^\[]*?)\]|styleUrl:\s*["'](.*?)["']/;

export function hasStyleUrls(code: string) {
return styleUrlsRE.test(code);
}

interface StyleUrlsCacheEntry {
matchedStyleUrls: string;
matchedStyleUrls: string[];
styleUrls: string[];
}

const EMPTY_ARRAY: any[] = [];

export class StyleUrlsResolver {
// These resolvers may be called multiple times during the same
// compilation for the same files. Caching is required because these
Expand All @@ -23,64 +20,67 @@ export class StyleUrlsResolver {
private readonly styleUrlsCache = new Map<string, StyleUrlsCacheEntry>();

resolve(code: string, id: string): string[] {
const styleUrlsExecArray = styleUrlsRE.exec(code);

if (styleUrlsExecArray === null) {
return EMPTY_ARRAY;
}

// Given the code is the following:
// @Component({
// styleUrls: [
// './app.component.scss'
// ]
// })
// The `matchedStyleUrls` would result in: `styleUrls: [\n './app.component.scss'\n ]`.
const [matchedStyleUrls] = styleUrlsExecArray;
const matchedStyleUrls = getStyleUrls(code);
const entry = this.styleUrlsCache.get(id);
// We're using `matchedStyleUrls` as a key because the code may be changing continuously,
// resulting in the resolver being called multiple times. While the code changes, the
// `styleUrls` may remain constant, which means we should always return the previously
// resolved style URLs.
if (entry?.matchedStyleUrls === matchedStyleUrls) {
if (entry && entry.matchedStyleUrls === matchedStyleUrls) {
return entry.styleUrls;
}

// The `styleUrls` property is an array, which means we may have a list of
// CSS files provided there. Let `matchedStyleUrls` be equal to the following:
// "styleUrls: [\n './app.component.scss',\n '../global.scss'\n ]"
const styleUrlPaths = matchedStyleUrls
.replace(/(styleUrls|\:|\s|\[|\]|"|')/g, '')
.replace(/(styleUrl|:\s*["'](.*?)["'])/g, '')
// The above replace will result in the following:
// "./app.component.scss,../global.scss"
.split(',');

const styleUrls = styleUrlPaths.map((styleUrlPath) => {
const styleUrls = matchedStyleUrls.map((styleUrlPath) => {
return `${styleUrlPath}|${normalizePath(
resolve(dirname(id), styleUrlPath)
)}`;
});

this.styleUrlsCache.set(matchedStyleUrls, { styleUrls, matchedStyleUrls });
this.styleUrlsCache.set(id, { styleUrls, matchedStyleUrls });
return styleUrls;
}
}

export function hasTemplateUrl(code: string) {
return code.includes('templateUrl:');
function getTextByProperty(name: string, properties: PropertyAssignment[]) {
return properties
.filter((property) => property.getName() === name)
.map((property) =>
property.getInitializer()?.getText().replace(/['"]/g, '')
)
.filter((url): url is string => url !== undefined);
}

export function getStyleUrls(code: string) {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('cmp.ts', code);
const properties = sourceFile.getDescendantsOfKind(
SyntaxKind.PropertyAssignment
);
const styleUrl = getTextByProperty('styleUrl', properties);
const styleUrls = properties
.filter((property) => property.getName() === 'styleUrls')
.map((property) => property.getInitializer() as ArrayLiteralExpression)
.flatMap((array) =>
array.getElements().map((el) => el.getText().replace(/['"]/g, ''))
);

return [...styleUrls, ...styleUrl];
}

export function getTemplateUrls(code: string) {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('cmp.ts', code);
return sourceFile
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.filter((property) => property.getName() === 'templateUrl')
.map((property) =>
property.getInitializer()?.getText().replace(/['"]/g, '')
)
.filter((url): url is string => url !== undefined);
const properties = sourceFile.getDescendantsOfKind(
SyntaxKind.PropertyAssignment
);
return getTextByProperty('templateUrl', properties);
}

interface TemplateUrlsCacheEntry {
Expand Down

0 comments on commit a7d5ff0

Please sign in to comment.