Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6ce1d36
fix(vite-plugin-angular): centralize stylesheet handling for hmr
benpsnyder Apr 4, 2026
eba6e08
feat(vite-plugin-angular): expand debug logging controls
benpsnyder Apr 4, 2026
2c49f0a
fix(content): split devtools into a dedicated entrypoint
benpsnyder Apr 4, 2026
8cbb8fd
fix(router): prefer app routes over shared duplicates
benpsnyder Apr 4, 2026
f23ce5a
fix(platform): restore analog-app action and content examples
benpsnyder Apr 4, 2026
8eec739
test(router): cover legacy PageServerAction compatibility
benpsnyder Apr 4, 2026
84002ee
fix(create-analog): scaffold Tailwind v4 PostCSS config
benpsnyder Apr 4, 2026
b9b60be
fix(router): preserve app route precedence in production
benpsnyder Apr 5, 2026
09482a9
feat(platform): add dev-time route idiom diagnostics
benpsnyder Apr 5, 2026
d8742e3
feat(vite-plugin-angular): improve component stylesheet hmr
benpsnyder Apr 5, 2026
87ad7d2
test: align fixtures with explicit selector enforcement
benpsnyder Apr 5, 2026
2f2f717
test(router): stabilize legacy page action fixture
benpsnyder Apr 5, 2026
162043d
fix(platform): emit debug reload reasons for content changes
benpsnyder Apr 5, 2026
605d215
build: adopt workspace catalogs for repo deps
benpsnyder Apr 5, 2026
5156354
fix(router): defer route metadata setup during bootstrap
benpsnyder Apr 5, 2026
7c9c2e9
chore: add tailwind debug coverage and stabilize e2e
benpsnyder Apr 5, 2026
677a4a6
fix(vite-plugin-angular): handle > inside quoted class bindings
benpsnyder Apr 5, 2026
1683e4e
fix(vite-plugin-angular): ignore [class.foo] in ngClass conflict guard
benpsnyder Apr 5, 2026
6a6efc4
fix(router): preserve last-wins behavior for equal-priority collisions
benpsnyder Apr 5, 2026
7eafecb
fix(router): register route metadata listeners during app init
benpsnyder Apr 5, 2026
b55a436
fix(vite-plugin-angular): restore rxjs optimizeDeps prebundling
benpsnyder Apr 5, 2026
8a93d0c
fix(vite-plugin-angular): evict stale metadata on file deletion
benpsnyder Apr 5, 2026
002de6c
fix(platform): cache page-route discovery for diagnostics
benpsnyder Apr 5, 2026
61807f3
refactor(vite-plugin-angular): dedupe vite-ignore hmr injection
benpsnyder Apr 5, 2026
ba044b0
chore: remove investigation snapshot artifacts
benpsnyder Apr 5, 2026
135c883
build: move all root package deps to workspace catalogs
benpsnyder Apr 5, 2026
fb701b8
docs: note Angular HMR version requirements
benpsnyder Apr 5, 2026
9c6a296
fix: harden tailwind debug app HMR diagnostics
benpsnyder Apr 5, 2026
779c9c8
fix(platform): refresh route diagnostics on route graph changes
benpsnyder Apr 5, 2026
c37ff35
fix(vite-plugin-angular): restrict metadata extraction to component d…
benpsnyder Apr 5, 2026
bcadc13
test(vite-plugin-angular): tighten resolver coverage docs
benpsnyder Apr 5, 2026
9e0be35
test(vite-plugin-angular): align tailwind debug app e2e with upstream…
benpsnyder Apr 5, 2026
c091cf7
fix(vite-plugin-nitro): avoid SSR entry lookup for ssr:false apps wit…
benpsnyder Apr 5, 2026
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
2 changes: 1 addition & 1 deletion .dagger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Container, Directory, Secret } from '@dagger.io/dagger';
import { argument, dag, func, object } from '@dagger.io/dagger';

const DEFAULT_E2E_PROJECTS =
'analog-app-e2e,blog-app-e2e,tanstack-query-app-e2e';
'analog-app-e2e,blog-app-e2e,tailwind-debug-app-e2e,tanstack-query-app-e2e';

@object()
export class AnalogCi {
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
project:
- analog-app-e2e
- blog-app-e2e
- tailwind-debug-app-e2e
- tanstack-query-app-e2e
steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ node_modules
npm-debug.log
yarn-error.log
testem.log
debug.analog.log
/typings

# System Files
Expand Down
29 changes: 29 additions & 0 deletions apps/analog-app-e2e/tests/legacy-server-action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from '@playwright/test';

test.describe('legacy PageServerAction - /legacy-action', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/legacy-action');
await page.locator('form').waitFor({ timeout: 15_000 });
});

test('submits successfully through the legacy action handler', async ({
page,
}) => {
await page.locator('input[name="email"]').fill('legacy@example.com');
await page.getByRole('button', { name: /submit/i }).click();

await expect(page.locator('#legacy-action-success')).toContainText(
'legacy@example.com',
);
});

test('returns validation errors through the legacy action handler', async ({
page,
}) => {
await page.getByRole('button', { name: /submit/i }).click();

await expect(page.locator('#legacy-action-error')).toContainText(
'Email is required',
);
});
});
7 changes: 7 additions & 0 deletions apps/analog-app/oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ export default defineConfig({
style: 'kebab-case',
},
],
'@angular-eslint/use-component-selector': 'error',
'@angular-eslint/prefer-standalone': 'error',
},
},
{
files: ['src/**/pages/**/*.ts', 'src/**/*.page.ts', 'src/**/*.page.tsx'],
rules: {
'@angular-eslint/use-component-selector': 'off',
},
},
{
files: ['src/stories/**/*.ts', 'src/stories/**/*.tsx'],
rules: {
Expand Down
1 change: 1 addition & 0 deletions apps/analog-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"@analogjs/content": "workspace:*",
"@analogjs/router": "workspace:*",
"es-toolkit": "catalog:"
},
Expand Down
4 changes: 4 additions & 0 deletions apps/analog-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
withFetch,
withInterceptors,
} from '@angular/common/http';
import { provideContent, withMarkdownRenderer } from '@analogjs/content';
import type { ApplicationConfig } from '@angular/core';
import {
provideClientHydration,
Expand All @@ -17,6 +18,7 @@ import {
withLoaderCaching,
requestContextInterceptor,
} from '@analogjs/router';
import { withContentRoutes } from '@analogjs/router/content';
import { withNavigationErrorHandler } from '@angular/router';

const fallbackRoutes = [
Expand All @@ -28,6 +30,7 @@ export const appConfig: ApplicationConfig = {
provideFileRouter(
withNavigationErrorHandler(console.error),
withDebugRoutes(),
withContentRoutes(),
withExtraRoutes(fallbackRoutes),
// Experimental: TanStack Router-inspired features
withTypedRouter({ strictRouteParams: true }),
Expand All @@ -42,6 +45,7 @@ export const appConfig: ApplicationConfig = {
withFetch(),
withInterceptors([requestContextInterceptor]),
),
provideContent(withMarkdownRenderer()),
// Hydration must be configured for both server and client bootstraps so
// SSR can serialize the metadata the browser uses to hydrate.
provideClientHydration(withEventReplay()),
Expand Down
18 changes: 7 additions & 11 deletions apps/analog-app/src/app/pages/client/(client).page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { ServerOnly, type RouteMeta } from '@analogjs/router';
import { type RouteMeta } from '@analogjs/router';

export const routeMeta: RouteMeta = {
title: 'Client Component',
Expand All @@ -12,29 +12,25 @@ export const routeMeta: RouteMeta = {
};

@Component({
imports: [ServerOnly],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>Client Component</h2>

<ServerOnly component="hello" [props]="props()" (outputs)="log($event)" />
<p id="client-message">
This route renders entirely on the client and updates without SSR.
</p>

<ServerOnly component="hello" [props]="props2()" (outputs)="log($event)" />
<p>Current count: {{ count() }}</p>

<p>
<button (click)="update()">Update</button>
</p>
`,
})
export default class ClientComponent {
props = signal({ name: 'Brandon', count: 0 });
props2 = signal({ name: 'Brandon', count: 4 });
readonly count = signal(0);

update() {
this.props.update((data) => ({ ...data, count: ++data.count }));
}

log($event: object) {
console.log({ outputs: $event });
this.count.update((value) => value + 1);
}
}
52 changes: 52 additions & 0 deletions apps/analog-app/src/app/pages/legacy-action.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Component, signal } from '@angular/core';
import { FormAction } from '@analogjs/router';

import {
type LegacyActionError,
type LegacyActionSuccess,
} from './legacy-action.server';

@Component({
selector: 'analogjs-legacy-action-page',
standalone: true,
imports: [FormAction],
template: `
<h3>Legacy Server Action</h3>

@if (submittedEmail()) {
<div id="legacy-action-success">
Legacy action submitted for {{ submittedEmail() }}.
</div>
} @else {
<form
method="post"
(onSuccess)="handleSuccess($any($event))"
(onError)="handleError($any($event))"
>
<div>
<label for="email"> Email </label>
<input id="email" type="email" name="email" />
</div>

<button class="button" type="submit">Submit</button>
</form>

@if (errors()?.email) {
<p id="legacy-action-error">{{ errors()?.email }}</p>
}
}
`,
})
export default class LegacyActionComponent {
readonly submittedEmail = signal('');
readonly errors = signal<LegacyActionError | undefined>(undefined);

handleSuccess(result: LegacyActionSuccess) {
this.errors.set(undefined);
this.submittedEmail.set(result.email);
}

handleError(result?: LegacyActionError) {
this.errors.set(result);
}
}
32 changes: 32 additions & 0 deletions apps/analog-app/src/app/pages/legacy-action.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
type PageServerAction,
fail,
json,
} from '@analogjs/router/server/actions';

export type LegacyActionSuccess = {
type: 'success';
email: string;
};

export type LegacyActionError =
| {
email?: string;
}
| undefined;

export async function action({ event }: PageServerAction) {
const body = await event.req.formData();
const email = body.get('email');

if (typeof email !== 'string' || email.length === 0) {
return fail<Exclude<LegacyActionError, undefined>>(422, {
email: 'Email is required',
});
}

return json<LegacyActionSuccess>({
type: 'success',
email,
});
}
1 change: 0 additions & 1 deletion apps/analog-app/src/app/pages/newsletter.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ type FormErrors =
method="post"
(onSuccess)="onSuccess($any($event))"
(onError)="onError($any($event))"
(onStateChange)="errors.set(undefined)"
>
<div>
<label for="email"> Email </label>
Expand Down
35 changes: 19 additions & 16 deletions apps/analog-app/src/app/pages/newsletter.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
type PageServerAction,
redirect,
json,
defineAction,
fail,
json,
redirect,
} from '@analogjs/router/server/actions';
import { readFormData } from 'nitro/h3';
import * as v from 'valibot';

export type NewsletterSubmitResponse = {
type: 'success';
Expand All @@ -17,17 +17,20 @@ export function load() {
};
}

export async function action({ event }: PageServerAction) {
const body = await readFormData(event);
const email = body.get('email') as string;

if (!email) {
return fail(422, { email: 'Email is required' });
}
const NewsletterSchema = v.object({
email: v.pipe(v.string(), v.nonEmpty('Email is required')),
});

if (email.length < 10) {
return redirect('/');
}
export const action = defineAction({
schema: NewsletterSchema,
handler: async ({ data }) => {
if (data.email.length < 10) {
return redirect('/');
}

return json<NewsletterSubmitResponse>({ type: 'success', email });
}
return json<NewsletterSubmitResponse>({
type: 'success',
email: data.email,
});
},
});
Loading
Loading