diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb3add06..89f149886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +# [1.11.0-beta.6](https://github.com/analogjs/analog/compare/v1.11.0-beta.5...v1.11.0-beta.6) (2024-12-30) + +### Bug Fixes + +- **vite-plugin-nitro:** normalize outputPaths for app hosting ([09b1fa5](https://github.com/analogjs/analog/commit/09b1fa57a5dd038d5631febe5091311c0d9e1050)) + +# [1.11.0-beta.5](https://github.com/analogjs/analog/compare/v1.11.0-beta.4...v1.11.0-beta.5) (2024-12-30) + +### Features + +- **vite-plugin-nitro:** add support for Firebase App Hosting deployment ([#1529](https://github.com/analogjs/analog/issues/1529)) ([3657bf1](https://github.com/analogjs/analog/commit/3657bf17c1f2c3ee03bd008de008b6fdf80b2795)) + +# [1.11.0-beta.4](https://github.com/analogjs/analog/compare/v1.11.0-beta.3...v1.11.0-beta.4) (2024-12-28) + +### Bug Fixes + +- **vite-plugin-angular:** invalidation fixes for HMR/live reload ([#1526](https://github.com/analogjs/analog/issues/1526)) ([7b783d9](https://github.com/analogjs/analog/commit/7b783d9d114f5050a14786254aab6d3f198cc893)) + +# [1.11.0-beta.3](https://github.com/analogjs/analog/compare/v1.11.0-beta.2...v1.11.0-beta.3) (2024-12-27) + +### Features + +- **vite-plugin-angular:** introduce support for Angular v19 HMR/live reload ([#1523](https://github.com/analogjs/analog/issues/1523)) ([0602a8f](https://github.com/analogjs/analog/commit/0602a8f79ae3c16897c966f3defe7ac3309c32a6)) + +# [1.11.0-beta.2](https://github.com/analogjs/analog/compare/v1.11.0-beta.1...v1.11.0-beta.2) (2024-12-26) + +### Features + +- **vitest-angular:** add UI and coverage options to test builder ([#1521](https://github.com/analogjs/analog/issues/1521)) ([026b3dc](https://github.com/analogjs/analog/commit/026b3dce2f5cfe07da65922496b4c366642b3788)) + +# [1.11.0-beta.1](https://github.com/analogjs/analog/compare/v1.10.3...v1.11.0-beta.1) (2024-12-20) + +### Bug Fixes + +- **vitest-angular:** reuse vitest server in watch mode for build-test ([#1519](https://github.com/analogjs/analog/issues/1519)) ([724d1f1](https://github.com/analogjs/analog/commit/724d1f13caa55c6fc315321ef75d29eff9b96e41)) + +### Features + +- **router:** introduce support for Analog Server Components ([#1518](https://github.com/analogjs/analog/issues/1518)) ([44289b0](https://github.com/analogjs/analog/commit/44289b0008a9a62288d22866ec089f48fa502d80)) + ## [1.10.3](https://github.com/analogjs/analog/compare/v1.10.2...v1.10.3) (2024-12-17) ### Bug Fixes diff --git a/apps/analog-app/src/app/app.config.server.ts b/apps/analog-app/src/app/app.config.server.ts index 8b5a665d1..0da63b09b 100644 --- a/apps/analog-app/src/app/app.config.server.ts +++ b/apps/analog-app/src/app/app.config.server.ts @@ -1,15 +1,10 @@ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; -import { - provideServerRendering, - ɵSERVER_CONTEXT as SERVER_CONTEXT, -} from '@angular/platform-server'; +import { provideServerRendering } from '@angular/platform-server'; + import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { - providers: [ - provideServerRendering(), - { provide: SERVER_CONTEXT, useValue: 'ssr-analog' }, - ], + providers: [provideServerRendering()], }; export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/apps/analog-app/src/app/pages/client/(client).page.ts b/apps/analog-app/src/app/pages/client/(client).page.ts new file mode 100644 index 000000000..2e22540b0 --- /dev/null +++ b/apps/analog-app/src/app/pages/client/(client).page.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ServerOnly } from '@analogjs/router'; + +@Component({ + standalone: true, + imports: [ServerOnly], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Client Component

+ + + + + +

+ +

+ `, +}) +export default class ClientComponent { + props = signal({ name: 'Brandon', count: 0 }); + props2 = signal({ name: 'Brandon', count: 4 }); + + update() { + this.props.update((data) => ({ ...data, count: ++data.count })); + } + + log($event: object) { + console.log({ outputs: $event }); + } +} diff --git a/apps/analog-app/src/app/pages/goodbye.page.analog b/apps/analog-app/src/app/pages/goodbye.page.analog new file mode 100644 index 000000000..e8fcf7f42 --- /dev/null +++ b/apps/analog-app/src/app/pages/goodbye.page.analog @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/apps/analog-app/src/app/pages/server/(server).page.ts b/apps/analog-app/src/app/pages/server/(server).page.ts new file mode 100644 index 000000000..1aef0a8ea --- /dev/null +++ b/apps/analog-app/src/app/pages/server/(server).page.ts @@ -0,0 +1,11 @@ +import { RouteMeta } from '@analogjs/router'; + +import { ServerOnly } from '@analogjs/router'; + +export const routeMeta: RouteMeta = { + data: { + component: 'hello', + }, +}; + +export default ServerOnly; diff --git a/apps/analog-app/src/main.server.ts b/apps/analog-app/src/main.server.ts index 8e3db29a7..3715f0e1e 100644 --- a/apps/analog-app/src/main.server.ts +++ b/apps/analog-app/src/main.server.ts @@ -1,31 +1,8 @@ import 'zone.js/node'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import type { ServerContext } from '@analogjs/router/tokens'; +import '@angular/platform-server/init'; +import { render } from '@analogjs/router/server'; import { config } from './app/app.config.server'; import { AppComponent } from './app/app.component'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(AppComponent, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(AppComponent, config); diff --git a/apps/analog-app/src/server/components/goodbye.ag b/apps/analog-app/src/server/components/goodbye.ag new file mode 100644 index 000000000..e7584039d --- /dev/null +++ b/apps/analog-app/src/server/components/goodbye.ag @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/apps/analog-app/src/server/components/hello.ts b/apps/analog-app/src/server/components/hello.ts new file mode 100644 index 000000000..bb6f9f1d6 --- /dev/null +++ b/apps/analog-app/src/server/components/hello.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; + +import { + injectStaticOutputs, + injectStaticProps, +} from '@analogjs/router/server'; + +@Component({ + selector: 'app-hello', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Hello From the Server

+ +

Props: {{ json() }}

+ +

Time: {{ Date.now().toString() }}

+ `, + styles: ` + h3 { + color: blue; + } + `, +}) +export default class HelloComponent { + Date = Date; + props = injectStaticProps(); + outputs = injectStaticOutputs<{ loaded: boolean }>(); + json = computed(() => JSON.stringify(this.props)); + + ngOnInit() { + this.outputs.set({ loaded: true }); + } +} diff --git a/apps/analog-app/tsconfig.app.json b/apps/analog-app/tsconfig.app.json index b3bc45583..c0b4e6bcf 100644 --- a/apps/analog-app/tsconfig.app.json +++ b/apps/analog-app/tsconfig.app.json @@ -10,6 +10,7 @@ "include": [ "src/**/*.d.ts", "src/app/pages/**/*.page.ts", + "src/server/components/**/*.ts", "src/server/middleware/**/*.ts" ], "exclude": ["**/*.test.ts", "**/*.spec.ts"] diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index 35eab6b95..920c32039 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -32,14 +32,18 @@ export default defineConfig(({ mode }) => { additionalPagesDirs: ['/libs/shared/feature'], additionalAPIDirs: ['/libs/shared/feature/src/api'], prerender: { - routes: ['/', '/cart', '/shipping'], + routes: ['/', '/cart', '/shipping', '/client'], sitemap: { host: base, }, }, vite: { inlineStylesExtension: 'scss', + experimental: { + supportAnalogFormat: true, + }, }, + liveReload: true, }), nxViteTsPaths(), visualizer() as Plugin, diff --git a/apps/blog-app/src/main.server.ts b/apps/blog-app/src/main.server.ts index e2ce90d90..3715f0e1e 100644 --- a/apps/blog-app/src/main.server.ts +++ b/apps/blog-app/src/main.server.ts @@ -1,31 +1,8 @@ import 'zone.js/node'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; +import '@angular/platform-server/init'; +import { render } from '@analogjs/router/server'; import { config } from './app/app.config.server'; import { AppComponent } from './app/app.component'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(AppComponent, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(AppComponent, config); diff --git a/apps/docs-app/docs/features/deployment/providers.md b/apps/docs-app/docs/features/deployment/providers.md index f199c0b97..c499eedf6 100644 --- a/apps/docs-app/docs/features/deployment/providers.md +++ b/apps/docs-app/docs/features/deployment/providers.md @@ -5,6 +5,137 @@ import TabItem from '@theme/TabItem'; Analog supports deployment to many providers with little or no additional configuration using [Nitro](https://nitro.unjs.io) as its underlying server engine. You can find more providers in the [Nitro deployment docs](https://nitro.unjs.io/deploy). +## Zerops + +:::info +[Zerops](https://zerops.io) is the **official** deployment partner for AnalogJS. +::: + +Analog supports deploying both static and server-side rendered apps to [Zerops](https://zerops.io) with a simple configuration file. + +> One Zerops project can contain multiple Analog projects. See example repositories for [static](https://github.com/zeropsio/recipe-analog-static) and [server-side rendered](https://github.com/zeropsio/recipe-analog-nodejs) Analog apps for a quick start. + +### Static (SSG) Analog app + +If your project is not SSG Ready, set up your project for [Static Site Generation](/docs/features/server/static-site-generation). + +#### 1. Create a project in Zerops + +Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: + +```yml +project: + name: recipe-analog +services: + - hostname: app + type: static +``` + +This creates a project called `recipe-analog` with a Zerops Static service called `app`. + +#### 2. Add zerops.yml configuration + +To tell Zerops how to build and run your site, add a `zerops.yml` to your repository: + +```yml +zerops: + - setup: app + build: + base: nodejs@20 + buildCommands: + - pnpm i + - pnpm build + deployFiles: + - public + - dist/analog/public/~ + run: + base: static +``` + +#### 3. [Trigger the build & deploy pipeline](#build--deploy-your-code) + +### Server-side rendered (SSR) Analog app + +If your project is not SSR Ready, set up your project for [Server Side Rendering](/docs/features/server/server-side-rendering). + +#### 1. Create a project in Zerops + +Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: + +```yml +project: + name: recipe-analog +services: + - hostname: app + type: nodejs@20 +``` + +This creates a project called `recipe-analog` with a Zerops Node.js service called `app`. + +#### 2. Add zerops.yml configuration + +To tell Zerops how to build and run your site, add a `zerops.yml` to your repository: + +```yml +zerops: + - setup: app + build: + base: nodejs@20 + buildCommands: + - pnpm i + - pnpm build + deployFiles: + - public + - node_modules + - dist + run: + base: nodejs@20 + ports: + - port: 3000 + httpSupport: true + start: node dist/analog/server/index.mjs +``` + +#### 3. [Trigger the build & deploy pipeline](#build-deploy-your-code) + +--- + +### Build & deploy your code + +#### Trigger the pipeline by connecting the service with your GitHub / GitLab repository + +Your code can be deployed automatically on each commit or a new tag by connecting the service with your GitHub / GitLab repository. This connection can be set up in the service detail. + +#### Trigger the pipeline using Zerops CLI (zcli) + +You can also trigger the pipeline manually from your terminal or your existing CI/CD by using Zerops CLI. + +1. Install the Zerops CLI. + +```bash +# To download the zcli binary directly, +# use https://github.com/zeropsio/zcli/releases +npm i -g @zerops/zcli +``` + +2. Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management) in the Zerops app and generate a new access token. + +3. Log in using your access token with the following command: + +```bash +zcli login +``` + +4. Navigate to the root of your app (where `zerops.yml` is located) and run the following command to trigger the deploy: + +```bash +zcli push +``` + +#### Trigger the pipeline using GitHub / Gitlab + +You can also check out [Github Integration](https://docs.zerops.io/references/github-integration) / [Gitlab Integration](https://docs.zerops.io/references/gitlab-integration) in [Zerops Docs](https://docs.zerops.io/) for git integration. + ## Netlify Analog supports deploying on [Netlify](https://netlify.com/) with minimal configuration. @@ -161,9 +292,17 @@ BUILD_PRESET=cloudflare-pages npm run build npx wrangler pages dev ./dist/analog/public ``` -## Firebase +## Firebase App Hosting + +Analog supports [Firebase App Hosting](https://firebase.google.com/docs/app-hosting) with no additional configuration out of the box. + +**Note**: You need to be on the **Blaze plan** to deploy Analog applications with Firebase App Hosting. -Analog supports [Firebase Hosting](https://firebase.google.com/docs/hosting) with Cloud Functions out of the box. +Follow the [Getting Started instructions](https://firebase.google.com/docs/app-hosting/get-started#step-1:) to connect your GitHub repository to Firebase App Hosting. + +## Firebase Hosting + +Analog supports [Firebase Hosting](https://firebase.google.com/docs/hosting) with Cloud Functions and [Firebase App Hosting](https://firebase.google.com/docs/app-hosting) out of the box. See a [Sample Repo](https://github.com/brandonroberts/analog-angular-firebase-example) with Firebase configured @@ -462,132 +601,3 @@ jobs: echo "DRY_RUN_OPTION=$DRY_RUN_OPTION" npx angular-cli-ghpages --no-silent --dir="${{env.TARGET_DIR}}" $CNAME_OPTION $DRY_RUN_OPTION ``` - -## Zerops - -Analog supports deploying both static and server-side rendered apps to [Zerops](https://zerops.io) with a simple configuration file. - -:::info -One Zerops project can contain multiple Analog projects. See example repositories for [static](https://github.com/zeropsio/recipe-analog-static) and [server-side rendered](https://github.com/zeropsio/recipe-analog-nodejs) Analog apps for a quick start. -::: - -### Static (SSG) Analog app - -If your project is not SSG Ready, set up your project for [Static Site Generation](/docs/features/server/static-site-generation). - -#### 1. Create a project in Zerops - -Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: - -```yml -project: - name: recipe-analog -services: - - hostname: app - type: static -``` - -This creates a project called `recipe-analog` with a Zerops Static service called `app`. - -#### 2. Add zerops.yml configuration - -To tell Zerops how to build and run your site, add a `zerops.yml` to your repository: - -```yml -zerops: - - setup: app - build: - base: nodejs@20 - buildCommands: - - pnpm i - - pnpm build - deployFiles: - - public - - dist/analog/public/~ - run: - base: static -``` - -#### 3. [Trigger the build & deploy pipeline](#build--deploy-your-code) - -### Server-side rendered (SSR) Analog app - -If your project is not SSR Ready, set up your project for [Server Side Rendering](/docs/features/server/server-side-rendering). - -#### 1. Create a project in Zerops - -Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: - -```yml -project: - name: recipe-analog -services: - - hostname: app - type: nodejs@20 -``` - -This creates a project called `recipe-analog` with a Zerops Node.js service called `app`. - -#### 2. Add zerops.yml configuration - -To tell Zerops how to build and run your site, add a `zerops.yml` to your repository: - -```yml -zerops: - - setup: app - build: - base: nodejs@20 - buildCommands: - - pnpm i - - pnpm build - deployFiles: - - public - - node_modules - - dist - run: - base: nodejs@20 - ports: - - port: 3000 - httpSupport: true - start: node dist/analog/server/index.mjs -``` - -#### 3. [Trigger the build & deploy pipeline](#build-deploy-your-code) - ---- - -### Build & deploy your code - -#### Trigger the pipeline by connecting the service with your GitHub / GitLab repository - -Your code can be deployed automatically on each commit or a new tag by connecting the service with your GitHub / GitLab repository. This connection can be set up in the service detail. - -#### Trigger the pipeline using Zerops CLI (zcli) - -You can also trigger the pipeline manually from your terminal or your existing CI/CD by using Zerops CLI. - -1. Install the Zerops CLI. - -```bash -# To download the zcli binary directly, -# use https://github.com/zeropsio/zcli/releases -npm i -g @zerops/zcli -``` - -2. Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management) in the Zerops app and generate a new access token. - -3. Log in using your access token with the following command: - -```bash -zcli login -``` - -4. Navigate to the root of your app (where `zerops.yml` is located) and run the following command to trigger the deploy: - -```bash -zcli push -``` - -#### Trigger the pipeline using Github / Gitlab - -You can also check out [Github Integration](https://docs.zerops.io/references/github-integration) / [Gitlab Integration](https://docs.zerops.io/references/gitlab-integration) in [Zerops Docs](https://docs.zerops.io/) for git integration. diff --git a/apps/ng-app/vite.config.ts b/apps/ng-app/vite.config.ts index 1bfd46716..4c6292004 100644 --- a/apps/ng-app/vite.config.ts +++ b/apps/ng-app/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig(({ mode }) => ({ analog({ ssr: false, static: true, + liveReload: true, vite: { experimental: { supportAnalogFormat: true, diff --git a/apps/trpc-app/src/main.server.ts b/apps/trpc-app/src/main.server.ts index 29ad24c06..4db4761f6 100644 --- a/apps/trpc-app/src/main.server.ts +++ b/apps/trpc-app/src/main.server.ts @@ -1,20 +1,8 @@ import 'zone.js/node'; -import { enableProdMode } from '@angular/core'; -import { renderApplication } from '@angular/platform-server'; -import { AppComponent } from './app/app.component'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { config } from './app.config.server'; - -if (import.meta.env.PROD) { - enableProdMode(); -} +import '@angular/platform-server/init'; +import { render } from '@analogjs/router/server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +import { config } from './app.config.server'; +import { AppComponent } from './app/app.component'; -export default async function render(url: string, document: string) { - const html = await renderApplication(bootstrap, { - document, - url, - }); - return html; -} +export default render(AppComponent, config); diff --git a/package.json b/package.json index 1f0daf544..fc4530aa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "analogjs-platform", - "version": "1.10.3", + "version": "1.11.0-beta.6", "license": "MIT", "type": "module", "scripts": { diff --git a/packages/astro-angular/package.json b/packages/astro-angular/package.json index 96a395ee0..05920d1cf 100644 --- a/packages/astro-angular/package.json +++ b/packages/astro-angular/package.json @@ -1,6 +1,6 @@ { "name": "@analogjs/astro-angular", - "version": "1.10.3", + "version": "1.11.0-beta.6", "description": "Use Angular components within Astro", "type": "module", "author": "Brandon Roberts ", @@ -32,7 +32,7 @@ "url": "https://github.com/sponsors/brandonroberts" }, "dependencies": { - "@analogjs/vite-plugin-angular": "^1.10.3" + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6" }, "peerDependencies": { "@angular-devkit/build-angular": ">=16.0.0", diff --git a/packages/content/package.json b/packages/content/package.json index 1ae10f49c..4cc6b8bb7 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -1,6 +1,6 @@ { "name": "@analogjs/content", - "version": "1.10.3", + "version": "1.11.0-beta.6", "description": "Content Rendering for Analog", "type": "module", "author": "Brandon Roberts ", diff --git a/packages/create-analog/package.json b/packages/create-analog/package.json index 52e144afb..910f2f1b7 100644 --- a/packages/create-analog/package.json +++ b/packages/create-analog/package.json @@ -1,6 +1,6 @@ { "name": "create-analog", - "version": "1.10.3", + "version": "1.11.0-beta.6", "type": "module", "license": "MIT", "author": "Brandon Roberts", diff --git a/packages/create-analog/template-angular-v16/package.json b/packages/create-analog/template-angular-v16/package.json index 01728b623..6b128b7b3 100644 --- a/packages/create-analog/template-angular-v16/package.json +++ b/packages/create-analog/template-angular-v16/package.json @@ -15,8 +15,8 @@ "test": "ng test" }, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^16.2.0", "@angular/common": "^16.2.0", "@angular/compiler": "^16.2.0", @@ -38,9 +38,9 @@ "zone.js": "~0.13.0" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular-devkit/build-angular": "^16.2.0", "@angular/cli": "^16.2.0", "@angular/compiler-cli": "^16.2.0", diff --git a/packages/create-analog/template-angular-v17/package.json b/packages/create-analog/template-angular-v17/package.json index 699877393..a2a53a459 100644 --- a/packages/create-analog/template-angular-v17/package.json +++ b/packages/create-analog/template-angular-v17/package.json @@ -15,8 +15,8 @@ "test": "ng test" }, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^17.2.0", "@angular/common": "^17.2.0", "@angular/compiler": "^17.2.0", @@ -38,9 +38,9 @@ "zone.js": "~0.14.0" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular-devkit/build-angular": "^17.2.0", "@angular/cli": "^17.2.0", "@angular/compiler-cli": "^17.2.0", diff --git a/packages/create-analog/template-angular-v18/package.json b/packages/create-analog/template-angular-v18/package.json index 2aed2b40c..b2bd10ca7 100644 --- a/packages/create-analog/template-angular-v18/package.json +++ b/packages/create-analog/template-angular-v18/package.json @@ -15,8 +15,8 @@ }, "private": true, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^18.0.0", "@angular/build": "^18.0.0", "@angular/common": "^18.0.0", @@ -38,9 +38,9 @@ "zone.js": "~0.14.3" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular/cli": "^18.0.0", "@angular/compiler-cli": "^18.0.0", "jsdom": "^22.0.0", diff --git a/packages/create-analog/template-blog/package.json b/packages/create-analog/template-blog/package.json index e05c5bf9a..42d5b440d 100644 --- a/packages/create-analog/template-blog/package.json +++ b/packages/create-analog/template-blog/package.json @@ -15,8 +15,8 @@ }, "private": true, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", "@angular/compiler": "^19.0.0", @@ -36,9 +36,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular-devkit/build-angular": "^19.0.0", "@angular/build": "^19.0.0", "@angular/cli": "^19.0.0", diff --git a/packages/create-analog/template-blog/src/main.server.ts b/packages/create-analog/template-blog/src/main.server.ts index f365e2b63..d3f23684e 100644 --- a/packages/create-analog/template-blog/src/main.server.ts +++ b/packages/create-analog/template-blog/src/main.server.ts @@ -1,32 +1,8 @@ import 'zone.js/node'; import '@angular/platform-server/init'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; +import { render } from '@analogjs/router/server'; __APP_COMPONENT_IMPORT__ import { config } from './app/app.config.server'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(__APP_COMPONENT__, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(__APP_COMPONENT__, config); diff --git a/packages/create-analog/template-latest/package.json b/packages/create-analog/template-latest/package.json index 8420656a6..1f25e68e4 100644 --- a/packages/create-analog/template-latest/package.json +++ b/packages/create-analog/template-latest/package.json @@ -15,8 +15,8 @@ }, "private": true, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", "@angular/compiler": "^19.0.0", @@ -37,9 +37,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular-devkit/build-angular": "^19.0.0", "@angular/build": "^19.0.0", "@angular/cli": "^19.0.0", diff --git a/packages/create-analog/template-latest/src/main.server.ts b/packages/create-analog/template-latest/src/main.server.ts index f365e2b63..d3f23684e 100644 --- a/packages/create-analog/template-latest/src/main.server.ts +++ b/packages/create-analog/template-latest/src/main.server.ts @@ -1,32 +1,8 @@ import 'zone.js/node'; import '@angular/platform-server/init'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; +import { render } from '@analogjs/router/server'; __APP_COMPONENT_IMPORT__ import { config } from './app/app.config.server'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(__APP_COMPONENT__, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(__APP_COMPONENT__, config); diff --git a/packages/create-analog/template-minimal/package.json b/packages/create-analog/template-minimal/package.json index 8420656a6..1f25e68e4 100644 --- a/packages/create-analog/template-minimal/package.json +++ b/packages/create-analog/template-minimal/package.json @@ -15,8 +15,8 @@ }, "private": true, "dependencies": { - "@analogjs/content": "^1.10.3", - "@analogjs/router": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", + "@analogjs/router": "^1.11.0-beta.6", "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", "@angular/compiler": "^19.0.0", @@ -37,9 +37,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@analogjs/platform": "^1.10.3", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vitest-angular": "^1.10.3", + "@analogjs/platform": "^1.11.0-beta.6", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vitest-angular": "^1.11.0-beta.6", "@angular-devkit/build-angular": "^19.0.0", "@angular/build": "^19.0.0", "@angular/cli": "^19.0.0", diff --git a/packages/create-analog/template-minimal/src/main.server.ts b/packages/create-analog/template-minimal/src/main.server.ts index f365e2b63..d3f23684e 100644 --- a/packages/create-analog/template-minimal/src/main.server.ts +++ b/packages/create-analog/template-minimal/src/main.server.ts @@ -1,32 +1,8 @@ import 'zone.js/node'; import '@angular/platform-server/init'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; +import { render } from '@analogjs/router/server'; __APP_COMPONENT_IMPORT__ import { config } from './app/app.config.server'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(__APP_COMPONENT__, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(__APP_COMPONENT__, config); diff --git a/packages/nx-plugin/src/generators/app/files/template-angular-v18/src/main.server.ts__template__ b/packages/nx-plugin/src/generators/app/files/template-angular-v18/src/main.server.ts__template__ index 2b6d4d14b..3715f0e1e 100644 --- a/packages/nx-plugin/src/generators/app/files/template-angular-v18/src/main.server.ts__template__ +++ b/packages/nx-plugin/src/generators/app/files/template-angular-v18/src/main.server.ts__template__ @@ -1,32 +1,8 @@ import 'zone.js/node'; import '@angular/platform-server/init'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; +import { render } from '@analogjs/router/server'; import { config } from './app/app.config.server'; import { AppComponent } from './app/app.component'; -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(AppComponent, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} +export default render(AppComponent, config); diff --git a/packages/platform/package.json b/packages/platform/package.json index cca3a04ab..bf785c0ed 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -1,6 +1,6 @@ { "name": "@analogjs/platform", - "version": "1.10.3", + "version": "1.11.0-beta.6", "description": "The fullstack meta-framework for Angular", "type": "module", "author": "Brandon Roberts ", @@ -29,8 +29,8 @@ }, "dependencies": { "nitropack": "^2.10.0", - "@analogjs/vite-plugin-angular": "^1.10.3", - "@analogjs/vite-plugin-nitro": "^1.10.3", + "@analogjs/vite-plugin-angular": "^1.11.0-beta.6", + "@analogjs/vite-plugin-nitro": "^1.11.0-beta.6", "vitefu": "^0.2.5" }, "peerDependencies": { diff --git a/packages/platform/src/lib/deps-plugin.ts b/packages/platform/src/lib/deps-plugin.ts index cea9f4a9d..1d913f110 100644 --- a/packages/platform/src/lib/deps-plugin.ts +++ b/packages/platform/src/lib/deps-plugin.ts @@ -69,7 +69,6 @@ export function depsPlugin(options?: Options): Plugin[] { return pkgJson['module'] && pkgJson['module'].includes('fesm'); }, }); - return pkgConfig; }, }, diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index 129909309..c1ce56665 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -37,6 +37,12 @@ export interface Options { index?: string; workspaceRoot?: string; content?: ContentPluginOptions; + + /** + * Enables Angular's HMR during development + */ + liveReload?: boolean; + /** * Additional page paths to include */ diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index 5417f3cd6..e5130c954 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -36,6 +36,7 @@ export function platformPlugin(opts: Options = {}): Plugin[] { ), ], additionalContentDirs: platformOptions.additionalContentDirs, + liveReload: platformOptions.liveReload, ...(opts?.vite ?? {}), }), serverModePlugin(), diff --git a/packages/platform/src/lib/router-plugin.ts b/packages/platform/src/lib/router-plugin.ts index fbcd52d29..c31c291c2 100644 --- a/packages/platform/src/lib/router-plugin.ts +++ b/packages/platform/src/lib/router-plugin.ts @@ -87,9 +87,9 @@ export function routerPlugin(options?: Options): Plugin[] { ); let result = code.replace( - 'let ANALOG_ROUTE_FILES = {};', + 'ANALOG_ROUTE_FILES = {};', ` - let ANALOG_ROUTE_FILES = {${routeFiles.map( + ANALOG_ROUTE_FILES = {${routeFiles.map( (module) => `"${module.replace(root, '')}": () => import('${module}')` )}}; @@ -97,9 +97,9 @@ export function routerPlugin(options?: Options): Plugin[] { ); result = result.replace( - 'let ANALOG_CONTENT_ROUTE_FILES = {};', + 'ANALOG_CONTENT_ROUTE_FILES = {};', ` - let ANALOG_CONTENT_ROUTE_FILES = {${contentRouteFiles.map( + ANALOG_CONTENT_ROUTE_FILES = {${contentRouteFiles.map( (module) => `"${module.replace( root, @@ -133,9 +133,9 @@ export function routerPlugin(options?: Options): Plugin[] { ); const result = code.replace( - 'let ANALOG_PAGE_ENDPOINTS = {};', + 'ANALOG_PAGE_ENDPOINTS = {};', ` - let ANALOG_PAGE_ENDPOINTS = {${endpointFiles.map( + ANALOG_PAGE_ENDPOINTS = {${endpointFiles.map( (module) => `"${module.replace(root, '')}": () => import('${module}')` )}}; diff --git a/packages/router/package.json b/packages/router/package.json index f33095b71..bfb5fd11a 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@analogjs/router", - "version": "1.10.3", + "version": "1.11.0-beta.6", "description": "Filesystem-based routing for Angular", "type": "module", "author": "Brandon Roberts ", @@ -24,7 +24,7 @@ "url": "https://github.com/sponsors/brandonroberts" }, "peerDependencies": { - "@analogjs/content": "^1.10.3", + "@analogjs/content": "^1.11.0-beta.6", "@angular/core": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/router": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, diff --git a/packages/router/server/src/index.ts b/packages/router/server/src/index.ts index c875fc0a3..0eb71ead4 100644 --- a/packages/router/server/src/index.ts +++ b/packages/router/server/src/index.ts @@ -1 +1,7 @@ export { provideServerContext } from './provide-server-context'; +export { injectStaticProps, injectStaticOutputs } from './tokens'; +export { + serverComponentRequest, + renderServerComponent, +} from './server-component-render'; +export { render } from './render'; diff --git a/packages/router/server/src/render.ts b/packages/router/server/src/render.ts new file mode 100644 index 000000000..d051196b2 --- /dev/null +++ b/packages/router/server/src/render.ts @@ -0,0 +1,59 @@ +import { + ApplicationConfig, + Provider, + Type, + enableProdMode, +} from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { renderApplication } from '@angular/platform-server'; +import type { ServerContext } from '@analogjs/router/tokens'; + +import { provideServerContext } from './provide-server-context'; +import { + serverComponentRequest, + renderServerComponent, +} from './server-component-render'; + +if (import.meta.env.PROD) { + enableProdMode(); +} + +/** + * Returns a function that accepts the navigation URL, + * the root HTML, and server context. + * + * @param rootComponent + * @param config + * @param platformProviders + * @returns Promise + */ +export function render( + rootComponent: Type, + config: ApplicationConfig, + platformProviders: Provider[] = [] +) { + function bootstrap() { + return bootstrapApplication(rootComponent, config); + } + + return async function render( + url: string, + document: string, + serverContext: ServerContext + ) { + if (serverComponentRequest(serverContext)) { + return await renderServerComponent(url, serverContext); + } + + const html = await renderApplication(bootstrap, { + document, + url, + platformProviders: [ + provideServerContext(serverContext), + platformProviders, + ], + }); + + return html; + }; +} diff --git a/packages/router/server/src/server-component-render.ts b/packages/router/server/src/server-component-render.ts new file mode 100644 index 000000000..94016f034 --- /dev/null +++ b/packages/router/server/src/server-component-render.ts @@ -0,0 +1,167 @@ +import { ApplicationConfig, Type } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { + reflectComponentType, + ɵConsole as Console, + APP_ID, +} from '@angular/core'; +import { + provideServerRendering, + renderApplication, + ɵSERVER_CONTEXT as SERVER_CONTEXT, +} from '@angular/platform-server'; +import { ServerContext } from '@analogjs/router/tokens'; +import { createEvent, readBody, getHeader } from 'h3'; + +import { provideStaticProps } from './tokens'; + +type ComponentLoader = () => Promise>; + +export function serverComponentRequest(serverContext: ServerContext) { + const serverComponentId = getHeader( + createEvent(serverContext.req, serverContext.res), + 'X-Analog-Component' + ); + + if ( + !serverComponentId && + serverContext.req.url && + serverContext.req.url.startsWith('/_analog/components') + ) { + const componentId = serverContext.req.url.split('/')?.[3]; + + return componentId; + } + + return serverComponentId; +} + +const components = import.meta.glob([ + '/src/server/components/**/*.{ts,analog,ag}', +]); + +export async function renderServerComponent( + url: string, + serverContext: ServerContext, + config?: ApplicationConfig +) { + const componentReqId = serverComponentRequest(serverContext) as string; + const { componentLoader, componentId } = getComponentLoader(componentReqId); + + if (!componentLoader) { + return new Response(`Server Component Not Found ${componentId}`, { + status: 404, + }); + } + + const component = ((await componentLoader()) as any)[ + 'default' + ] as Type; + + if (!component) { + return new Response(`No default export for ${componentId}`, { + status: 422, + }); + } + + const mirror = reflectComponentType(component); + const selector = mirror?.selector.split(',')?.[0] || 'server-component'; + const event = createEvent(serverContext.req, serverContext.res); + const body = (await readBody(event)) || {}; + const appId = `analog-server-${selector.toLowerCase()}-${new Date().getTime()}`; + + const bootstrap = () => + bootstrapApplication(component, { + providers: [ + provideServerRendering(), + provideStaticProps(body), + { provide: SERVER_CONTEXT, useValue: 'analog-server-component' }, + { + provide: APP_ID, + useFactory() { + return appId; + }, + }, + ...(config?.providers || []), + ], + }); + + const html = await renderApplication(bootstrap, { + url, + document: `<${selector}>`, + platformProviders: [ + { + provide: Console, + useFactory() { + return { + warn: () => {}, + log: () => {}, + }; + }, + }, + ], + }); + + const outputs = retrieveTransferredState(html, appId); + const responseData: { html: string; outputs: Record } = { + html, + outputs, + }; + + return new Response(JSON.stringify(responseData), { + headers: { + 'X-Analog-Component': 'true', + }, + }); +} + +function getComponentLoader(componentReqId: string): { + componentLoader: ComponentLoader | undefined; + componentId: string; +} { + let _componentId = `/src/server/components/${componentReqId.toLowerCase()}`; + let componentLoader: ComponentLoader | undefined = undefined; + let componentId = _componentId; + + if (components[`${_componentId}.ts`]) { + componentId = `${_componentId}.ts`; + componentLoader = components[componentId] as ComponentLoader; + } else if (components[`${componentId}.analog`]) { + componentId = `${_componentId}.analog`; + componentLoader = components[componentId] as ComponentLoader; + } else if (components[`${componentId}.ag`]) { + componentId = `${_componentId}.ag`; + componentLoader = components[componentId] as ComponentLoader; + } + + return { componentLoader, componentId }; +} + +function retrieveTransferredState( + html: string, + appId: string +): Record { + const regex = new RegExp( + `