diff --git a/MAINTAINER.md b/MAINTAINER.md index a39a3c93f..c3e417704 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -1,5 +1,9 @@ # Contributing (Maintainer) +## Debugging + +Repo-local debugging guidance for Analog package development lives in [docs/debugging.md](docs/debugging.md). + ## Adding and updating an package Adding or updating a package requires some extra step if its part of `dependencies` or `devDependencies`. diff --git a/apps/docs-app/docs/guides/debugging.md b/apps/docs-app/docs/guides/debugging.md index ca299ba53..9f1cf1267 100644 --- a/apps/docs-app/docs/guides/debugging.md +++ b/apps/docs-app/docs/guides/debugging.md @@ -4,7 +4,9 @@ sidebar_position: 4 # Debugging -Analog includes structured debug logging powered by [obug](https://www.npmjs.com/package/obug). Debug output can be enabled through the `debug` option in your Vite config or via the `DEBUG` environment variable. +Analog includes structured debug logging powered by [obug](https://www.npmjs.com/package/obug). You can enable debug output through the `debug` option in your Vite config or via the `DEBUG` environment variable. + +The examples below use `npm`, but the same `DEBUG` values work with any package manager. ## Enabling Debug Output @@ -91,20 +93,20 @@ analog({ }); ``` -You can mix immediate and deferred entries — entries without `mode` enable immediately for both commands: +You can mix immediate and deferred entries. Entries without `mode` enable immediately for both commands: ```ts analog({ debug: [ - { scopes: ['analog:platform'] }, // both commands - { scopes: ['analog:angular:hmr'], mode: 'dev' }, // dev only - { scopes: ['analog:platform:typed-router'], mode: 'build' }, // build only + { scopes: ['analog:platform'] }, + { scopes: ['analog:angular:hmr'], mode: 'dev' }, + { scopes: ['analog:platform:typed-router'], mode: 'build' }, ], }); ``` :::tip -To enable debug output for **both** build and dev, simply omit `mode`. Any form without `mode` — `true`, a `string[]`, or `{ scopes }` — outputs in both commands. +To enable debug output for both build and dev, omit `mode`. Any form without `mode` outputs in both commands. ::: ### Environment variable @@ -113,13 +115,13 @@ The `DEBUG` environment variable works independently of the config option and is ```bash # All Analog scopes -DEBUG=analog:* pnpm dev +DEBUG=analog:* npm run dev # Specific scopes -DEBUG=analog:platform:routes,analog:angular:compiler pnpm build +DEBUG=analog:platform:routes,analog:angular:compiler npm run build # All platform scopes -DEBUG=analog:platform:* pnpm dev +DEBUG=analog:platform:* npm run dev ``` ## Debugging a local Analog checkout from another pnpm workspace @@ -248,7 +250,7 @@ import angular from '@analogjs/vite-plugin-angular'; export default defineConfig({ plugins: [ angular({ - debug: true, // enables analog:angular:* scopes + debug: true, }), ], }); diff --git a/apps/docs-app/docs/guides/migrating-v2-to-v3.md b/apps/docs-app/docs/guides/migrating-v2-to-v3.md index b2cfbb54b..88b0c92e6 100644 --- a/apps/docs-app/docs/guides/migrating-v2-to-v3.md +++ b/apps/docs-app/docs/guides/migrating-v2-to-v3.md @@ -158,4 +158,4 @@ Keep automated migration tooling focused on the breaking changes above: - rewrite only the legacy `@analogjs/vite-plugin-angular/setup-vitest` setup import - flag `@analogjs/trpc` as a removed package that needs a manual migration plan - flag `experimental.useAnalogCompiler`, `analogCompilationMode`, and `@analogjs/angular-compiler` as unsupported on the current v3 alpha line rather than removed outright -- treat optional helpers such as `withTypedRouter`, `withRouteContext`, `withLoaderCaching`, `withDebugRoutes`, and compatibility aliases such as `liveReload` as opt-in rather than mandatory rewrites +- treat optional helpers such as `withTypedRouter`, `withRouteContext`, `withLoaderCaching`, `withDebugRoutes`, and `liveReload` as opt-in rather than mandatory rewrites diff --git a/apps/docs-app/docs/guides/migrating.md b/apps/docs-app/docs/guides/migrating.md index 02f71e57a..401adc758 100644 --- a/apps/docs-app/docs/guides/migrating.md +++ b/apps/docs-app/docs/guides/migrating.md @@ -190,8 +190,11 @@ export default defineConfig(({ mode }) => ({ ## Enabling HMR -Angular supports HMR where in most cases components can be updated without a full page reload. In Analog, prefer the `hmr` option. `liveReload` is still accepted as a compatibility alias, but `hmr` is the primary API. -Analog requires Angular v19 or newer for `hmr` / `liveReload` to work. On Angular v17-v18, `hmr` and its `liveReload` alias are forcibly disabled at runtime with a console warning, so HMR is unavailable on those versions. +Angular supports HMR where in most cases components can be updated without a full page reload. In Analog, use `liveReload` to control the Angular live-reload pipeline. + +This is separate from Vite's `server.hmr` option, which configures the HMR websocket transport. You can use `server.hmr` together with `liveReload` when you need custom host, port, or path settings. + +Analog requires Angular v19 or newer for `liveReload` to work. On Angular v17-v18, `liveReload` is forcibly disabled at runtime with a console warning, so HMR is unavailable on those versions. ```ts /// @@ -204,7 +207,7 @@ export default defineConfig(({ mode }) => ({ // .. other configuration plugins: [ analog({ - hmr: true, + liveReload: true, }), ], })); @@ -223,7 +226,7 @@ import tailwindcss from '@tailwindcss/vite'; export default defineConfig(() => ({ plugins: [ analog({ - hmr: true, + liveReload: true, vite: { tailwindCss: { rootStylesheet: resolve(import.meta.dirname, 'src/styles.css'), diff --git a/apps/docs-app/docs/integrations/storybook/index.md b/apps/docs-app/docs/integrations/storybook/index.md index 67532239a..448d78105 100644 --- a/apps/docs-app/docs/integrations/storybook/index.md +++ b/apps/docs-app/docs/integrations/storybook/index.md @@ -91,7 +91,9 @@ const config: StorybookConfig = { export default config; ``` -For current Analog projects, prefer `framework.options.hmr` if you need to configure Angular HMR. `liveReload` is still accepted as a compatibility alias, but `hmr` is the recommended option. +For current Analog projects, use `framework.options.liveReload` to control Analog's Angular live-reload behavior. + +This is separate from Vite's `server.hmr` option, which configures the HMR websocket transport. You can use `server.hmr` together with `framework.options.liveReload` when Storybook needs custom host, port, or path settings. Remove the existing `webpackFinal` config function if present. @@ -177,7 +179,7 @@ If your project uses Tailwind v4, keep Storybook aligned with the same opinionat - one root stylesheet such as `src/styles.css` - `@import 'tailwindcss';` in that stylesheet - `framework.options.tailwindCss.rootStylesheet` pointing at that stylesheet -- `framework.options.hmr` for Angular HMR behavior +- `framework.options.liveReload` for Angular reload behavior ```ts import { resolve } from 'node:path'; @@ -187,7 +189,7 @@ const config: StorybookConfig = { framework: { name: '@analogjs/storybook-angular', options: { - hmr: true, + liveReload: true, tailwindCss: { rootStylesheet: resolve(__dirname, '../src/styles.css'), }, diff --git a/apps/docs-app/docs/integrations/tailwind/index.md b/apps/docs-app/docs/integrations/tailwind/index.md new file mode 100644 index 000000000..198fdcadf --- /dev/null +++ b/apps/docs-app/docs/integrations/tailwind/index.md @@ -0,0 +1,146 @@ +# Tailwind CSS v4 + +Analog does not replace Tailwind's installation guides. Start with one Tailwind setup that matches your project: + +- [Install Tailwind with Vite](https://tailwindcss.com/docs/installation/using-vite) +- [Install Tailwind with PostCSS](https://tailwindcss.com/docs/installation/using-postcss) +- [Install Tailwind with Angular](https://tailwindcss.com/docs/installation/framework-guides/angular) + +Once Tailwind is installed, Analog adds the Angular-specific part: component stylesheet handling for `@apply` and Tailwind-aware `@reference` injection. + +## What Analog adds + +Use Analog's `tailwindCss.rootStylesheet` option when you want Tailwind utilities inside Angular component styles. + +That option lets Analog: + +- detect component stylesheets that use Tailwind utilities +- inject the correct `@reference` to your root stylesheet +- keep component styles aligned with your root Tailwind theme, prefixes, and plugins +- avoid manual `@reference` directives in every component stylesheet + +If you only use Tailwind utilities in templates and a global stylesheet, you can follow Tailwind's install docs and keep your generated scaffold defaults without adding extra Analog configuration. + +## Component Styles Setup + +When you enable `tailwindCss.rootStylesheet`, keep Tailwind wired through Vite for the component stylesheet path: + +```ts +/// + +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import analog from '@analogjs/platform'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig(() => ({ + plugins: [ + analog({ + vite: { + tailwindCss: { + rootStylesheet: resolve(__dirname, 'src/styles.css'), + }, + }, + }), + tailwindcss(), + ], +})); +``` + +If you are using `@analogjs/vite-plugin-angular` directly instead of `@analogjs/platform`, the same option lives on the Angular plugin: + +```ts +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import angular from '@analogjs/vite-plugin-angular'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig(() => ({ + plugins: [ + angular({ + tailwindCss: { + rootStylesheet: resolve(__dirname, 'src/styles.css'), + }, + }), + tailwindcss(), + ], +})); +``` + +List `analog()` before `tailwindcss()` in your Vite config. Current generators now scaffold that order. + +## Root Stylesheet + +In `src/styles.css`: + +```css +@import 'tailwindcss'; +``` + +You can keep your theme, `@source`, plugins, and prefixes there as well: + +```css +@import 'tailwindcss' prefix(tw); + +@source './src'; + +@theme { + --color-primary: #3b82f6; +} +``` + +Use an absolute `rootStylesheet` path. Analog may serve component styles through virtual stylesheet ids during dev, so relative `@reference` paths are not reliable there. + +## How Component Styles Work + +Angular compiles component styles in isolation. When a component stylesheet contains `@apply`, Tailwind still needs access to the root stylesheet that defines prefixes, theme values, and plugins. + +Analog handles that by: + +- detecting Tailwind usage in component CSS +- injecting `@reference` to the configured root stylesheet +- routing those component styles through the Vite CSS pipeline when needed + +That means you should not manually add `@reference` to every component stylesheet in the normal setup. + +## Prefixes + +If your component styles use custom-prefixed utilities, configure `prefixes` so Analog knows which stylesheets need Tailwind `@reference` injection: + +```ts +analog({ + vite: { + tailwindCss: { + rootStylesheet: resolve(__dirname, 'src/styles.css'), + prefixes: ['tw:'], + }, + }, +}); +``` + +Without `prefixes`, Analog falls back to its default Tailwind usage detection for component styles. + +## HMR + +Use `liveReload` when you need to configure Analog's Angular live-reload behavior explicitly. + +Vite's `server.hmr` option is separate. It controls the HMR websocket transport, so you can use `server.hmr` together with `liveReload` when your dev server needs custom host, port, or path settings. + +Angular HMR requires Angular v19 or newer. On Angular v17-v18, `liveReload` is intentionally disabled at runtime and emits a console warning, so HMR is unavailable on those versions. For broader migration guidance, see the [migration guide](/docs/guides/migrating). + +Tailwind support does not require you to enable HMR manually. The stylesheet pipeline is handled independently from whether Angular can produce a hot component update for a given edit. + +## Generated Apps + +Current `create-analog` and Nx app scaffolds already: + +- import Tailwind in `src/styles.css` +- register Tailwind in `vite.config.ts` +- keep the generated Vite plugin order aligned with the current Analog templates + +Some templates may also include additional Tailwind tooling config files. Treat the generated scaffold as your project default, and only diverge after validating your own dev and build behavior. + +## Related + +- [Using CSS Pre-processors](/docs/packages/vite-plugin-angular/css-preprocessors) +- [create-analog](/docs/packages/create-analog/overview) diff --git a/apps/docs-app/docs/packages/create-analog/overview.md b/apps/docs-app/docs/packages/create-analog/overview.md index efc62021d..1ac297658 100644 --- a/apps/docs-app/docs/packages/create-analog/overview.md +++ b/apps/docs-app/docs/packages/create-analog/overview.md @@ -47,14 +47,11 @@ pnpm create analog ### Tailwind v4 -`create-analog` scaffolds Tailwind v4 with the Vite plugin by default for the current Analog templates. Generated projects use `@tailwindcss/vite`, add `@import 'tailwindcss';` to `src/styles.css`, and also generate a `postcss.config.mjs` with `@tailwindcss/postcss` so the build path and tool integrations use the same Tailwind setup. +`create-analog` scaffolds Tailwind v4 for the current Analog templates. Generated projects import Tailwind in `src/styles.css` and wire the Tailwind-related Vite config the template expects. -This is the recommended Analog v3 direction: +If you only need Tailwind utilities in templates and global styles, keep the scaffold defaults. -- Keep one root stylesheet, usually `src/styles.css`, that contains `@import 'tailwindcss';` -- Keep `@tailwindcss/vite` enabled in `vite.config.ts` -- Let Analog handle component-level `@reference` injection through its Tailwind-aware stylesheet pipeline instead of adding `@reference` directives manually in every component stylesheet -- Prefer the `hmr` option over `liveReload` when you need to configure Angular HMR explicitly +If you also want `@apply` inside Angular component styles, add Analog's `tailwindCss.rootStylesheet` option and follow the [Tailwind CSS guide](/docs/integrations/tailwind). If you do not want Tailwind in the generated app, pass `--skipTailwind true`. The default Tailwind v4 flow expects a plain CSS entry file for global styles. diff --git a/apps/docs-app/docs/packages/vite-plugin-angular/css-preprocessors.md b/apps/docs-app/docs/packages/vite-plugin-angular/css-preprocessors.md index 9f6a49f9e..4a256b24b 100644 --- a/apps/docs-app/docs/packages/vite-plugin-angular/css-preprocessors.md +++ b/apps/docs-app/docs/packages/vite-plugin-angular/css-preprocessors.md @@ -4,9 +4,9 @@ title: 'Using CSS Pre-processors' The Vite Plugin supports CSS pre-processing using external `styleUrls` and inline `styles` in the Component decorator metadata. -## Recommended Tailwind v4 setup +## Tailwind v4 component styles -If your app uses Tailwind v4, the recommended Analog setup is opinionated: +Tailwind installation itself should follow Tailwind's docs. The Analog-specific configuration below is for Angular component styles that use Tailwind utilities such as `@apply`. - keep a single root stylesheet such as `src/styles.css` - put `@import 'tailwindcss';` in that root stylesheet @@ -15,6 +15,8 @@ If your app uses Tailwind v4, the recommended Analog setup is opinionated: This lets Analog preprocess component stylesheets and inject the correct `@reference` directive automatically for component CSS that uses Tailwind utilities. +For the broader Tailwind + Analog overview, see the [Tailwind CSS guide](/docs/integrations/tailwind). + ```ts /// @@ -29,7 +31,7 @@ export default defineConfig(() => ({ tailwindCss: { rootStylesheet: resolve(__dirname, 'src/styles.css'), }, - hmr: true, + liveReload: true, }), tailwindcss(), ], @@ -44,8 +46,12 @@ And in `src/styles.css`: Use an absolute path for `rootStylesheet`. Analog serves some component styles through virtual stylesheet ids during dev, so relative `@reference` paths are not reliable there. +Use `liveReload` to control Analog's Angular reload behavior. Vite's top-level `server.hmr` option remains available when you need to configure the HMR websocket transport separately. + You only need `tailwindCss.prefixes` when your component styles use custom-prefixed utilities and you want Analog to look for those prefixes instead of the default `@apply` detection. +If you only use Tailwind utilities in templates and a global stylesheet, you can keep your Tailwind install path and skip `tailwindCss.rootStylesheet`. + External `styleUrls` can be used without any additional configuration. An example with `styleUrls`: diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 000000000..439fbbf88 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,143 @@ +# Debugging + +This document is for maintainers and contributors working inside the Analog monorepo. + +For consumer-facing debug flags and scope reference, see the public guide in `apps/docs-app/docs/guides/debugging.md`. + +This repo-local file covers the monorepo-specific workflow that does not belong in the public docs site. + +## Repo Root Commands + +From this workspace, the common debug flow is to run commands from the repo root with `pnpm` or `pnpm nx`. + +```bash +# Serve the default dev app with all Analog scopes +DEBUG=analog:* pnpm dev + +# Build from the repo root with selected scopes +DEBUG=analog:platform:routes,analog:angular:compiler pnpm build + +# Serve a specific app target through Nx +DEBUG=analog:platform:* pnpm nx serve docs-app +``` + +## Package Development + +When debugging package changes in this monorepo, prefer the project-level Nx targets so you stay on the same workspace graph and dependency layout as CI: + +```bash +# Focus on Angular plugin HMR/style behavior +DEBUG=analog:angular:hmr,analog:angular:styles pnpm nx test vite-plugin-angular + +# Focus on platform routing output +DEBUG=analog:platform:routes pnpm nx build platform + +# Focus on the style-pipeline integration seam in a served app +DEBUG=analog:platform:style-pipeline,analog:angular:style-pipeline pnpm nx serve your-app +``` + +## Debugging a local Analog checkout from another pnpm workspace + +If you want to debug this checkout while serving a different app on your machine, +point that consumer workspace at the built Analog package outputs under +`/path/to/analog/packages/*/dist`. + +Use the built `dist` directories, not the raw package roots. Build the packages +first so each `dist` folder contains its generated `package.json`. The source +package manifests still contain `catalog:` and `workspace:*` references that are +only rewritten during Analog's release-style build pipeline. + +### Local checkout example + +`pnpm-workspace.yaml` + +```yaml +packages: + - 'apps/*' + - 'libs/**' + +overrides: + '@analogjs/platform': file:/path/to/analog/packages/platform/dist + '@analogjs/router': file:/path/to/analog/packages/router/dist + '@analogjs/vite-plugin-angular': file:/path/to/analog/packages/vite-plugin-angular/dist + '@analogjs/vite-plugin-nitro': file:/path/to/analog/packages/vite-plugin-nitro/dist + '@analogjs/vitest-angular': file:/path/to/analog/packages/vitest-angular/dist +``` + +Root `package.json` + +```json +{ + "dependencies": { + "@analogjs/platform": "file:/path/to/analog/packages/platform/dist" + }, + "overrides": { + "@analogjs/platform": "file:/path/to/analog/packages/platform/dist", + "@analogjs/router": "file:/path/to/analog/packages/router/dist", + "@analogjs/vite-plugin-angular": "file:/path/to/analog/packages/vite-plugin-angular/dist", + "@analogjs/vite-plugin-nitro": "file:/path/to/analog/packages/vite-plugin-nitro/dist", + "@analogjs/vitest-angular": "file:/path/to/analog/packages/vitest-angular/dist" + } +} +``` + +:::important +Keep the overrides in both places. If you only pin `@analogjs/platform`, pnpm +can still resolve transitive packages like `@analogjs/vite-plugin-angular` and +`@analogjs/vite-plugin-nitro` from npm instead of your local checkout. +::: + +:::note +pnpm currently does not allow `file:` entries in `catalog`, so local checkout +wiring needs direct `file:` overrides instead of `catalog:` indirection. +::: + +If your app also uses other published Analog packages such as +`@analogjs/content` or `@analogjs/storybook-angular`, pin those the same way. + +### GitHub branch example + +If you want the same pattern from a GitHub branch instead of a local path, pnpm +supports Git subdirectory specs via `#branch&path:...`. + +`pnpm-workspace.yaml` + +```yaml +catalog: + '@analogjs/platform': github:your-user/analog#your-branch&path:packages/platform/dist + '@analogjs/router': github:your-user/analog#your-branch&path:packages/router/dist + '@analogjs/vite-plugin-angular': github:your-user/analog#your-branch&path:packages/vite-plugin-angular/dist + '@analogjs/vite-plugin-nitro': github:your-user/analog#your-branch&path:packages/vite-plugin-nitro/dist + '@analogjs/vitest-angular': github:your-user/analog#your-branch&path:packages/vitest-angular/dist +``` + +Root `package.json` + +```json +{ + "dependencies": { + "@analogjs/platform": "catalog:" + }, + "overrides": { + "@analogjs/platform": "$@analogjs/platform", + "@analogjs/router": "$@analogjs/router", + "@analogjs/vite-plugin-angular": "$@analogjs/vite-plugin-angular", + "@analogjs/vite-plugin-nitro": "$@analogjs/vite-plugin-nitro", + "@analogjs/vitest-angular": "$@analogjs/vitest-angular" + } +} +``` + +:::caution +For Analog, the GitHub form only works when the branch exposes release-ready +`dist/package.json` files at those paths. Pointing pnpm at +`path:packages/platform` or any other raw source package path will fail because +those manifests still contain unresolved `catalog:` and `workspace:*` +specifiers. +::: + +## Notes + +- Use the repo root unless you have a specific reason to run inside a package subdirectory. +- Prefer `pnpm nx ` when you want task-graph behavior that matches CI. +- The scope names themselves are documented in the public guide so consumer and maintainer docs stay aligned. diff --git a/packages/create-analog/__tests__/cli.spec.ts b/packages/create-analog/__tests__/cli.spec.ts index fe90609f7..6bf4dbdc0 100644 --- a/packages/create-analog/__tests__/cli.spec.ts +++ b/packages/create-analog/__tests__/cli.spec.ts @@ -55,7 +55,7 @@ const expectTailwindScaffold = () => { expect(readGeneratedStyles()).toContain(`@import 'tailwindcss';`); expect(viteConfig).toContain(`import tailwindcss from '@tailwindcss/vite';`); expect(viteConfig).toMatch( - /plugins:\s*\[[\s\S]*tailwindcss\(\),[\s\S]*analog\(/, + /plugins:\s*\[[\s\S]*analog\(\),[\s\S]*tailwindcss\(\)/, ); expect(readFileSync(join(genPath, 'postcss.config.mjs'), 'utf-8')).toContain( `'@tailwindcss/postcss': {}`, diff --git a/packages/create-analog/template-latest/vite.config.ts b/packages/create-analog/template-latest/vite.config.ts index 997423525..8737bf35c 100644 --- a/packages/create-analog/template-latest/vite.config.ts +++ b/packages/create-analog/template-latest/vite.config.ts @@ -12,8 +12,8 @@ export default defineConfig(({ mode }) => ({ mainFields: ['module'], }, plugins: [ -__TAILWIND_PLUGIN__ analog(), - ], + analog(), +__TAILWIND_PLUGIN__ ], test: { globals: true, environment: 'jsdom', diff --git a/packages/nx-plugin/src/generators/app/files/template-angular-v17/vite.config.ts__template__ b/packages/nx-plugin/src/generators/app/files/template-angular-v17/vite.config.ts__template__ index a7c5e682b..3ca4b4e30 100644 --- a/packages/nx-plugin/src/generators/app/files/template-angular-v17/vite.config.ts__template__ +++ b/packages/nx-plugin/src/generators/app/files/template-angular-v17/vite.config.ts__template__ @@ -23,10 +23,10 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ + analog(), <% if (addTailwind) { %> tailwindcss(), <% } %> - analog(), ], test: { globals: true, diff --git a/packages/nx-plugin/src/generators/app/files/template-angular-v18/vite.config.ts__template__ b/packages/nx-plugin/src/generators/app/files/template-angular-v18/vite.config.ts__template__ index f8efa3634..8cd519adc 100644 --- a/packages/nx-plugin/src/generators/app/files/template-angular-v18/vite.config.ts__template__ +++ b/packages/nx-plugin/src/generators/app/files/template-angular-v18/vite.config.ts__template__ @@ -22,10 +22,10 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ - <% if (addTailwind) { %> + analog(), + <% if (addTailwind) { %> tailwindcss(), <% } %> - analog(), ], test: { globals: true, diff --git a/packages/nx-plugin/src/generators/app/files/template-angular-v19/vite.config.ts__template__ b/packages/nx-plugin/src/generators/app/files/template-angular-v19/vite.config.ts__template__ index d0fe48d9e..749ed2ccd 100644 --- a/packages/nx-plugin/src/generators/app/files/template-angular-v19/vite.config.ts__template__ +++ b/packages/nx-plugin/src/generators/app/files/template-angular-v19/vite.config.ts__template__ @@ -22,10 +22,10 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ + analog(), <% if (addTailwind) { %> tailwindcss(), <% } %> - analog(), ], test: { globals: true, diff --git a/packages/nx-plugin/src/generators/app/files/template-angular/vite.config.ts__template__ b/packages/nx-plugin/src/generators/app/files/template-angular/vite.config.ts__template__ index 2d4262240..bc85590a1 100644 --- a/packages/nx-plugin/src/generators/app/files/template-angular/vite.config.ts__template__ +++ b/packages/nx-plugin/src/generators/app/files/template-angular/vite.config.ts__template__ @@ -22,10 +22,10 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ + analog(), <% if (addTailwind) { %> tailwindcss(), <% } %> - analog(), ], test: { globals: true, diff --git a/packages/nx-plugin/src/generators/app/generator.spec.ts b/packages/nx-plugin/src/generators/app/generator.spec.ts index cc0321ae5..a92a78ff7 100644 --- a/packages/nx-plugin/src/generators/app/generator.spec.ts +++ b/packages/nx-plugin/src/generators/app/generator.spec.ts @@ -114,7 +114,7 @@ describe('nx-plugin generator', () => { `import tailwindcss from '@tailwindcss/vite';`, ); expect(viteConfig).toMatch( - /plugins:\s*\[[\s\S]*tailwindcss\(\),[\s\S]*analog\(/, + /plugins:\s*\[[\s\S]*analog\(\),[\s\S]*tailwindcss\(\)/, ); }; diff --git a/packages/nx-plugin/src/generators/app/templates.spec.ts b/packages/nx-plugin/src/generators/app/templates.spec.ts index 8e1d3ee30..4f67f5600 100644 --- a/packages/nx-plugin/src/generators/app/templates.spec.ts +++ b/packages/nx-plugin/src/generators/app/templates.spec.ts @@ -21,6 +21,17 @@ const versionedTemplates = readdirSync(templatesDir).filter((d) => ); describe('generator templates', () => { + describe.each(versionedTemplates)('%s vite.config.ts', (template) => { + const viteConfig = readFileSync( + join(templatesDir, template, 'vite.config.ts__template__'), + 'utf-8', + ); + + it('registers analog exactly once', () => { + expect(viteConfig.match(/analog\(\)/g)).toHaveLength(1); + }); + }); + describe.each(versionedTemplates)('%s tsconfig.json', (template) => { const tsconfig = readTemplateJson(template, 'tsconfig.json__template__'); diff --git a/packages/nx-plugin/src/generators/preset/__snapshots__/generator.spec.ts.snap b/packages/nx-plugin/src/generators/preset/__snapshots__/generator.spec.ts.snap index dcf7ec6f4..c71d7c385 100644 --- a/packages/nx-plugin/src/generators/preset/__snapshots__/generator.spec.ts.snap +++ b/packages/nx-plugin/src/generators/preset/__snapshots__/generator.spec.ts.snap @@ -119,7 +119,7 @@ export default defineConfig(({ mode }) => { allow: ['.'], }, }, - plugins: [tailwindcss(), analog()], + plugins: [analog(), tailwindcss()], test: { globals: true, environment: 'jsdom', diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index e780e33e4..a02697ce3 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -98,7 +98,7 @@ export interface Options { * * When `false`, the following top-level options are ignored because they * are only forwarded to the internal Angular plugin: `jit`, - * `disableTypeChecking`, `hmr`, `liveReload`, `inlineStylesExtension`, + * `disableTypeChecking`, `liveReload`, `inlineStylesExtension`, * `fileReplacements`, and `include`. * * Use this to configure the embedded Angular integration itself, not as the @@ -116,14 +116,13 @@ export interface Options { */ inlineStylesExtension?: string; /** - * Enables Angular's HMR during development/watch mode. + * Enables Analog's Angular live-reload/HMR pipeline during development/watch mode. + * + * This is separate from Vite's `server.hmr` option, which configures the + * HMR client transport. * * Defaults to `true` for watch mode. */ - hmr?: boolean; - /** - * @deprecated Use `hmr` instead. Kept as a compatibility alias. - */ liveReload?: boolean; /** * Enable debug logging for specific scopes. diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index a5312d550..054373596 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -30,6 +30,11 @@ export function platformPlugin(opts: Options = {}): Plugin[] { const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST']; const viteOptions = opts?.vite === false ? undefined : opts?.vite; + const { + experimental: viteExperimental, + hmr: _removedViteHmrOption, + ...forwardedViteOptions + } = viteOptions ?? {}; const { ...platformOptions } = { ssr: true, ...opts, @@ -56,7 +61,7 @@ export function platformPlugin(opts: Options = {}): Plugin[] { const useAngularCompilationAPI = platformOptions.experimental?.useAngularCompilationAPI ?? - viteOptions?.experimental?.useAngularCompilationAPI; + viteExperimental?.useAngularCompilationAPI; debugPlatform('experimental options resolved', { useAngularCompilationAPI: !!useAngularCompilationAPI, typedRouter: platformOptions.experimental?.typedRouter, @@ -112,7 +117,6 @@ export function platformPlugin(opts: Options = {}): Plugin[] { ), ], additionalContentDirs: platformOptions.additionalContentDirs, - hmr: platformOptions.hmr, liveReload: platformOptions.liveReload, inlineStylesExtension: platformOptions.inlineStylesExtension, fileReplacements: platformOptions.fileReplacements, @@ -124,9 +128,9 @@ export function platformPlugin(opts: Options = {}): Plugin[] { platformOptions.experimental.stylePipeline.angularPlugins, } : undefined, - ...(viteOptions ?? {}), + ...forwardedViteOptions, experimental: { - ...(viteOptions?.experimental ?? {}), + ...(viteExperimental ?? {}), useAngularCompilationAPI, }, }), diff --git a/packages/storybook-angular/README.md b/packages/storybook-angular/README.md index ba7f84b89..a08cfa0b8 100644 --- a/packages/storybook-angular/README.md +++ b/packages/storybook-angular/README.md @@ -35,7 +35,7 @@ const config: StorybookConfig = { framework: { name: '@analogjs/storybook-angular', options: { - hmr: true, + liveReload: true, }, }, }; @@ -152,7 +152,9 @@ In your global stylesheet, import Tailwind with: Storybook does not automatically infer the Tailwind plugin from your app's `vite.config.ts`, so add it in `viteFinal` when your stories depend on Tailwind utilities. -Angular HMR is controlled with `framework.options.hmr`. `liveReload` is still accepted as a compatibility alias, but `hmr` is the preferred option. +Angular reload behavior is controlled with `framework.options.liveReload`. + +This is separate from Vite's `server.hmr` option, which configures the HMR websocket transport. You can use `server.hmr` together with `framework.options.liveReload` when Storybook needs custom host, port, or path settings. ## Enabling Zoneless Change Detection diff --git a/packages/storybook-angular/src/lib/preset.spec.ts b/packages/storybook-angular/src/lib/preset.spec.ts index c54501dc4..7b0c58103 100644 --- a/packages/storybook-angular/src/lib/preset.spec.ts +++ b/packages/storybook-angular/src/lib/preset.spec.ts @@ -148,30 +148,24 @@ describe('viteFinal', () => { }; describe('Angular plugin options', () => { - it('prefers hmr over liveReload and keeps liveReload as compatibility input', async () => { - const options = makeOptions({ hmr: true, liveReload: false }); + it('forwards liveReload without a duplicate hmr flag', async () => { + const options = makeOptions({ liveReload: false }); await viteFinal(baseConfig, options); - expect(angularPluginMock).toHaveBeenCalledWith( - expect.objectContaining({ - hmr: true, - liveReload: false, - }), - ); + const [angularOptions] = angularPluginMock.mock.calls[0]; + expect(angularOptions.liveReload).toBe(false); + expect(angularOptions).not.toHaveProperty('hmr'); }); - it('falls back to liveReload when hmr is omitted', async () => { - const options = makeOptions({ liveReload: true }); + it('defaults liveReload to false when omitted', async () => { + const options = makeOptions(); await viteFinal(baseConfig, options); - expect(angularPluginMock).toHaveBeenCalledWith( - expect.objectContaining({ - hmr: true, - liveReload: true, - }), - ); + const [angularOptions] = angularPluginMock.mock.calls[0]; + expect(angularOptions.liveReload).toBe(false); + expect(angularOptions).not.toHaveProperty('hmr'); }); }); diff --git a/packages/storybook-angular/src/lib/preset.ts b/packages/storybook-angular/src/lib/preset.ts index 5f0555968..3cfc9d0d5 100644 --- a/packages/storybook-angular/src/lib/preset.ts +++ b/packages/storybook-angular/src/lib/preset.ts @@ -76,10 +76,16 @@ export const viteFinal = async (config: any, options: any): Promise => { // @ts-expect-error - untyped storybook presets API const framework = await options.presets.apply('framework'); + const { hmr: _removedHmrOption, ...frameworkOptions } = + framework.options ?? {}; const experimentalZoneless = await resolveExperimentalZoneless( - framework.options, + frameworkOptions, options?.angularBuilderOptions, ); + const liveReload = + typeof frameworkOptions.liveReload !== 'undefined' + ? frameworkOptions.liveReload + : false; return vite.mergeConfig(config, { // Add dependencies to pre-optimization optimizeDeps: { @@ -101,23 +107,17 @@ export const viteFinal = async (config: any, options: any): Promise => { plugins: [ angular({ jit: - typeof framework.options?.jit !== 'undefined' - ? framework.options?.jit + typeof frameworkOptions.jit !== 'undefined' + ? frameworkOptions.jit : true, - hmr: - typeof framework.options?.hmr !== 'undefined' - ? framework.options?.hmr - : typeof framework.options?.liveReload !== 'undefined' - ? framework.options?.liveReload - : false, - liveReload: framework.options?.liveReload, + liveReload, tsconfig: - typeof framework.options?.tsconfig !== 'undefined' - ? framework.options?.tsconfig + typeof frameworkOptions.tsconfig !== 'undefined' + ? frameworkOptions.tsconfig : (options?.tsConfig ?? './.storybook/tsconfig.json'), inlineStylesExtension: - typeof framework.options?.inlineStylesExtension !== 'undefined' - ? framework.options?.inlineStylesExtension + typeof frameworkOptions.inlineStylesExtension !== 'undefined' + ? frameworkOptions.inlineStylesExtension : 'css', }), angularOptionsPlugin(options, { diff --git a/packages/storybook-angular/src/types.ts b/packages/storybook-angular/src/types.ts index 1fa7f702c..232e32b9a 100644 --- a/packages/storybook-angular/src/types.ts +++ b/packages/storybook-angular/src/types.ts @@ -10,9 +10,11 @@ type BuilderName = CompatibleString<'@storybook/builder-vite'>; export type FrameworkOptions = { builder?: BuilderOptions; jit?: boolean; - hmr?: boolean; /** - * @deprecated Use `hmr` instead. Kept as a compatibility alias. + * Enables Analog's Angular live-reload/HMR pipeline for Storybook. + * + * This is separate from Vite's `server.hmr` option, which configures the + * HMR client transport. */ liveReload?: boolean; inlineStylesExtension?: string; diff --git a/packages/vite-plugin-angular/README.md b/packages/vite-plugin-angular/README.md index fe506e091..18b0c4c24 100644 --- a/packages/vite-plugin-angular/README.md +++ b/packages/vite-plugin-angular/README.md @@ -89,3 +89,9 @@ Create a `tsconfig.app.json` in the root of the project. "include": ["src/**/*.ts"] } ``` + +## Tailwind CSS v4 + +For Angular component styles that use Tailwind utilities like `@apply`, configure `tailwindCss.rootStylesheet` and follow the Tailwind guide for Analog: + +- https://analogjs.org/docs/integrations/tailwind diff --git a/packages/vite-plugin-angular/src/lib/angular-vite-plugin-live-reload.spec.ts b/packages/vite-plugin-angular/src/lib/angular-vite-plugin-live-reload.spec.ts index 345c8e0be..09a2458a0 100644 --- a/packages/vite-plugin-angular/src/lib/angular-vite-plugin-live-reload.spec.ts +++ b/packages/vite-plugin-angular/src/lib/angular-vite-plugin-live-reload.spec.ts @@ -125,9 +125,10 @@ async function setupLiveReloadPlugin(options: { const { angular } = await import('./angular-vite-plugin'); const plugin = angular({ + tsconfig: `${resolvedWorkspaceRoot}/tsconfig.base.json`, + liveReload: true, include: options.include, tsconfig: resolvedTsconfig, - hmr: true, inlineStylesExtension: 'css', stylePreprocessor: options.stylePreprocessor, stylePipeline: options.stylePipeline, diff --git a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts index 710385a09..f6fd6af1d 100644 --- a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts +++ b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.spec.ts @@ -87,7 +87,7 @@ describe('angularVitePlugin', () => { }); }); -describe('hmr option', () => { +describe('liveReload option', () => { beforeEach(() => { process.env['NODE_ENV'] = 'development'; delete process.env['VITEST']; @@ -107,8 +107,8 @@ describe('hmr option', () => { } }); - it('disables HMR helper plugins when hmr is false', () => { - const plugins = angular({ hmr: false }); + it('disables HMR helper plugins when liveReload is false', () => { + const plugins = angular({ liveReload: false }); const names = plugins.map((plugin) => plugin.name); expect(names).toEqual(expect.not.arrayContaining(hmrPluginNames)); @@ -119,20 +119,6 @@ describe('hmr option', () => { expect(names).toEqual(expect.arrayContaining(hmrPluginNames)); }); - - it('accepts liveReload as a compatibility alias for HMR', () => { - const plugins = angular({ liveReload: true }); - const names = plugins.map((plugin) => plugin.name); - - expect(names).toEqual(expect.arrayContaining(hmrPluginNames)); - }); - - it('prefers hmr over liveReload when both are provided', () => { - const plugins = angular({ hmr: false, liveReload: true }); - const names = plugins.map((plugin) => plugin.name); - - expect(names).toEqual(expect.not.arrayContaining(hmrPluginNames)); - }); }); describe('isTestWatchMode', () => { diff --git a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts index 984b3be57..8c6dffb66 100644 --- a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts +++ b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts @@ -160,14 +160,14 @@ export interface PluginOptions { include?: string[]; additionalContentDirs?: string[]; /** - * Enables Angular's HMR during development/watch mode. + * Enables Analog's Angular live-reload/HMR pipeline during development/watch mode. * - * Defaults to `true` for watch mode. Set to `false` to disable HMR while - * keeping other stylesheet externalization behavior available when needed. - */ - hmr?: boolean; - /** - * @deprecated Use `hmr` instead. Kept as a compatibility alias. + * This is separate from Vite's `server.hmr` option, which configures the + * HMR client transport. + * + * Defaults to `true` for watch mode. Set to `false` to disable Angular + * reload updates while keeping other stylesheet externalization behavior + * available when needed. */ liveReload?: boolean; disableTypeChecking?: boolean; @@ -442,6 +442,7 @@ export function buildStylePreprocessor( export function angular(options?: PluginOptions): Plugin[] { applyDebugOption(options?.debug, options?.workspaceRoot); + const liveReload = options?.liveReload ?? true; /** * Normalize plugin options so defaults @@ -463,7 +464,7 @@ export function angular(options?: PluginOptions): Plugin[] { jit: options?.jit, include: options?.include ?? [], additionalContentDirs: options?.additionalContentDirs ?? [], - hmr: options?.hmr ?? options?.liveReload ?? true, + liveReload, disableTypeChecking: options?.disableTypeChecking ?? true, fileReplacements: options?.fileReplacements ?? [], useAngularCompilationAPI: @@ -512,9 +513,17 @@ export function angular(options?: PluginOptions): Plugin[] { const transformedStyleOwnerMetadata = new Map(); const styleSourceOwners = new Map>(); - function shouldEnableHmr(): boolean { + function hasViteHmrTransport(): boolean { + return resolvedConfig ? resolvedConfig.server.hmr !== false : true; + } + + function shouldEnableLiveReload(): boolean { const effectiveWatchMode = isTest ? testWatchMode : watchMode; - return !!(effectiveWatchMode && pluginOptions.hmr); + return !!( + effectiveWatchMode && + pluginOptions.liveReload && + hasViteHmrTransport() + ); } /** @@ -536,7 +545,7 @@ export function angular(options?: PluginOptions): Plugin[] { function shouldExternalizeStyles(): boolean { const effectiveWatchMode = isTest ? testWatchMode : watchMode; if (!effectiveWatchMode) return false; - return !!(shouldEnableHmr() || pluginOptions.hasTailwindCss); + return !!(shouldEnableLiveReload() || pluginOptions.hasTailwindCss); } /** @@ -824,7 +833,7 @@ export function angular(options?: PluginOptions): Plugin[] { function angularPlugin(): Plugin { let isProd = false; - if (angularFullVersion < 190000 && pluginOptions.hmr) { + if (angularFullVersion < 190000 && pluginOptions.liveReload) { // Angular < 19 does not support externalRuntimeStyles or _enableHmr. debugHmr('hmr disabled: Angular version does not support HMR APIs', { angularVersion: angularFullVersion, @@ -834,7 +843,7 @@ export function angular(options?: PluginOptions): Plugin[] { '[@analogjs/vite-plugin-angular]: HMR was disabled because Angular v19+ is required for externalRuntimeStyles/_enableHmr support. Detected Angular version: %s.', angularFullVersion, ); - pluginOptions.hmr = false; + pluginOptions.liveReload = false; } if (isTest) { @@ -843,7 +852,7 @@ export function angular(options?: PluginOptions): Plugin[] { // This does NOT block style externalization — shouldExternalizeStyles() // independently checks hasTailwindCss, so Tailwind utilities in // component styles still work in unit tests. - pluginOptions.hmr = false; + pluginOptions.liveReload = false; debugHmr('hmr disabled', { angularVersion: angularFullVersion, isTest, @@ -1051,7 +1060,7 @@ export function angular(options?: PluginOptions): Plugin[] { let result; - if (shouldEnableHmr()) { + if (shouldEnableLiveReload()) { await pendingCompilation; pendingCompilation = null; result = fileEmitter(fileId); @@ -1079,7 +1088,7 @@ export function angular(options?: PluginOptions): Plugin[] { } if ( - shouldEnableHmr() && + shouldEnableLiveReload() && result?.hmrEligible && classNames.get(fileId) ) { @@ -1434,7 +1443,7 @@ export function angular(options?: PluginOptions): Plugin[] { } if ( - shouldEnableHmr() && + shouldEnableLiveReload() && /\.(html|htm)$/.test(ctx.file) && fileModules.length === 0 ) { @@ -2152,7 +2161,7 @@ export function angular(options?: PluginOptions): Plugin[] { }, } satisfies Plugin), angularPlugin(), - pluginOptions.hmr && liveReloadPlugin({ classNames, fileEmitter }), + pluginOptions.liveReload && liveReloadPlugin({ classNames, fileEmitter }), ...(isTest && !isStackBlitz ? angularVitestPlugins() : []), (jit && jitPlugin({ @@ -2405,7 +2414,7 @@ export function angular(options?: PluginOptions): Plugin[] { // Populate classNames during initial compilation so HMR for // HTML template changes can find the parent TS module. - if (shouldEnableHmr() && className && containingFile) { + if (shouldEnableLiveReload() && className && containingFile) { classNames.set(normalizePath(containingFile), className as string); } @@ -2483,14 +2492,15 @@ export function angular(options?: PluginOptions): Plugin[] { tsCompilerOptions['externalRuntimeStyles'] = true; } - if (shouldEnableHmr()) { + if (shouldEnableLiveReload()) { tsCompilerOptions['_enableHmr'] = true; // Workaround for https://github.com/angular/angular/issues/59310 tsCompilerOptions['supportTestBed'] = true; } debugCompiler('tsCompilerOptions (compilation API)', { - hmr: pluginOptions.hmr, + liveReload: pluginOptions.liveReload, + viteHmr: hasViteHmrTransport(), hasTailwindCss: pluginOptions.hasTailwindCss, watchMode, shouldExternalize: shouldExternalizeStyles(), @@ -2762,14 +2772,15 @@ export function angular(options?: PluginOptions): Plugin[] { tsCompilerOptions['externalRuntimeStyles'] = true; } - if (shouldEnableHmr()) { + if (shouldEnableLiveReload()) { tsCompilerOptions['_enableHmr'] = true; // Workaround for https://github.com/angular/angular/issues/59310 tsCompilerOptions['supportTestBed'] = true; } debugCompiler('tsCompilerOptions (NgtscProgram path)', { - hmr: pluginOptions.hmr, + liveReload: pluginOptions.liveReload, + viteHmr: hasViteHmrTransport(), shouldExternalize: shouldExternalizeStyles(), externalRuntimeStyles: !!tsCompilerOptions['externalRuntimeStyles'], hmrEnabled: !!tsCompilerOptions['_enableHmr'], @@ -2930,7 +2941,7 @@ export function angular(options?: PluginOptions): Plugin[] { const fileMetadata = getFileMetadata( builder, angularCompiler!, - pluginOptions.hmr, + pluginOptions.liveReload, pluginOptions.disableTypeChecking, ); diff --git a/packages/vite-plugin-angular/src/lib/live-reload-plugin.ts b/packages/vite-plugin-angular/src/lib/live-reload-plugin.ts index c3201cce6..dfcbed8af 100644 --- a/packages/vite-plugin-angular/src/lib/live-reload-plugin.ts +++ b/packages/vite-plugin-angular/src/lib/live-reload-plugin.ts @@ -19,6 +19,11 @@ export function liveReloadPlugin({ name: 'analogjs-live-reload-plugin', apply: 'serve', configureServer(server: ViteDevServer) { + if (server.config.server.hmr === false) { + debugHmr('middleware disabled: vite server.hmr is false'); + return; + } + const angularComponentMiddleware: Connect.HandleFunction = async ( req: Connect.IncomingMessage, res: ServerResponse,