Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vite-plugin-angular): add support for css preprocessing in component styles #88

Merged
merged 3 commits into from
Sep 22, 2022
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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$neon: powderblue;

@mixin background($color: #fff) {
background: $color;
}

h2 {
@include background($neon);
}
65 changes: 37 additions & 28 deletions apps/analog-app/src/app/product-list/product-list.component.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ProductAlertsComponent } from '../product-alerts/product-alerts.component';

import { products } from '../products';

@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule, ProductAlertsComponent, RouterModule],
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent {

products = [...products];

share() {
window.alert('The product has been shared!');
}

onNotify() {
window.alert('You will be notified when the product goes on sale');
}
}


import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ProductAlertsComponent } from '../product-alerts/product-alerts.component';

import { products } from '../products';

@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule, ProductAlertsComponent, RouterModule],
templateUrl: './product-list.component.html',
styles: [
`
$neon: lightblue;

@mixin background($color: #fff) {
background: $color;
}

h2 {
@include background($neon);
}
`,
],
})
export class ProductListComponent {
products = [...products];

share() {
window.alert('The product has been shared!');
}

onNotify() {
window.alert('You will be notified when the product goes on sale');
}
}
8 changes: 7 additions & 1 deletion apps/analog-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ export default defineConfig(({ mode }) => {
resolve: {
mainFields: ['module'],
},
plugins: [angular(), visualizer() as Plugin, splitVendorChunkPlugin()],
plugins: [
angular({
inlineStylesExtension: 'scss',
}),
visualizer() as Plugin,
splitVendorChunkPlugin(),
],
test: {
globals: true,
environment: 'jsdom',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: 'Using CSS Pre-processors'
---

The Vite Plugin supports CSS pre-processing using external `styleUrls` and inline `styles` in the Component decorator metadata.

External `styleUrls` can be used without any additional configuration.

An example with `styleUrls`:

```ts
import { Component } from '@angular/core';

@Component({
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {}
```

In order to support pre-processing of inline `styles`, the plugin must be configured to provide the extension of the type of styles being used.

An example of using `scss` with inline `styles`:

```ts
import { Component } from '@angular/core';

@Component({
standalone: true,
templateUrl: './app.component.html',
styles: [
`
$neon: #cf0;

@mixin background($color: #fff) {
background: $color;
}

h2 {
@include background($neon);
}
`,
],
})
export class AppComponent {}
```

In the `vite.config.ts`, provide and object to the `angular` plugin function with the `inlineStylesExtension` property set to the CSS pre-processing file extension.

```ts
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
// ... other config
plugins: [
angular({
inlineStylesExtension: 'scss',
}),
],
};
});
```

Support CSS pre-processor extensions include `scss`, `sass`, `less`, `styl`, and `stylus`. More information about CSS pre-processing can be found in the [Vite Docs](https://vitejs.dev/guide/features.html#css-pre-processors).
5 changes: 5 additions & 0 deletions apps/docs-app/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const sidebars = {
id: 'packages/vite-plugin-angular/overview',
label: 'Overview',
},
{
type: 'doc',
id: 'packages/vite-plugin-angular/css-preprocessors',
label: 'Using CSS Pre-processors',
},
],
},
{
Expand Down
45 changes: 34 additions & 11 deletions packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import {
resolveStyleUrls,
resolveTemplateUrl,
} from './component-resolvers';
import { inlineStylesPlugin } from './inline-styles-plugin';

interface PluginOptions {
tsconfig: string;
workspaceRoot: string;
tsconfig?: string;
workspaceRoot?: string;
inlineStylesExtension?: string;
}

interface EmitFileResult {
Expand All @@ -27,15 +29,20 @@ interface EmitFileResult {
}
type FileEmitter = (file: string) => Promise<EmitFileResult | undefined>;

export function angular(
pluginOptions: PluginOptions = {
export function angular(options: PluginOptions): Plugin[] {
/**
* Normalize plugin options so defaults
* are used for values not provided.
*/
const pluginOptions = {
tsconfig:
process.env['NODE_ENV'] === 'test'
options?.tsconfig ?? process.env['NODE_ENV'] === 'test'
? './tsconfig.spec.json'
: './tsconfig.app.json',
workspaceRoot: process.cwd(),
}
): Plugin[] {
workspaceRoot: options?.workspaceRoot ?? process.cwd(),
inlineStylesExtension: options?.inlineStylesExtension ?? '',
};

// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
let fileEmitter: FileEmitter | undefined;
let compilerOptions = {};
Expand All @@ -44,6 +51,9 @@ export function angular(
mergeTransformers,
replaceBootstrap,
} = require('@ngtools/webpack/src/ivy/transformation');
const {
replaceResources,
} = require('@ngtools/webpack/src/transformers/replace_resources');
const {
augmentProgramWithVersioning,
augmentHostWithCaching,
Expand Down Expand Up @@ -71,6 +81,7 @@ export function angular(
>('@angular/compiler-cli');

return {
assetsInclude: ['**/*.html'],
optimizeDeps: {
esbuildOptions: {
plugins: [
Expand Down Expand Up @@ -320,6 +331,7 @@ export function angular(
return;
},
},
inlineStylesPlugin(pluginOptions.inlineStylesExtension),
];

function setupCompilation() {
Expand Down Expand Up @@ -384,11 +396,22 @@ export function angular(

await angularCompiler.analyzeAsync();

const getTypeChecker = () => builder.getProgram().getTypeChecker();
fileEmitter = createFileEmitter(
builder,
mergeTransformers(angularCompiler.prepareEmit().transformers, {
before: [replaceBootstrap(() => builder.getProgram().getTypeChecker())],
}),
mergeTransformers(
{
before: [
replaceBootstrap(getTypeChecker),
replaceResources(
() => true,
getTypeChecker,
pluginOptions.inlineStylesExtension
),
],
},
angularCompiler.prepareEmit().transformers
),
() => []
);
}
Expand Down
40 changes: 40 additions & 0 deletions packages/vite-plugin-angular/src/lib/inline-styles-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Plugin } from 'vite';

const virtualModuleId = 'virtual:analog-inline-styles-module';

/**
* This plugin decodes and returns inline styles that were
* extracted from the Component decorator metadata and encoded
* into the import string. This allows Vite to intercept
* these imports, convert them into virtual modules, and
* return them as imported styles to pass through the CSS
* transform pipeline.
*/
export function inlineStylesPlugin(inlineStylesExtension = ''): Plugin {
return {
name: '@analogjs/vite-plugin-angular-inline-styles',
enforce: 'pre',
resolveId(id) {
if (id.includes(`.${inlineStylesExtension}?ngResource`)) {
return '\0' + virtualModuleId + id;
}

return undefined;
},
async load(id) {
if (!id.startsWith(`\0${virtualModuleId}`)) {
return undefined;
}

const encodedStyles = id.match(/data=(.*)\!/)![1];
const styles = Buffer.from(
decodeURIComponent(encodedStyles),
'base64'
).toString();

return {
code: styles,
};
},
};
}