Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add initial support to pages for server-side data fetching #446

Merged
merged 23 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c10339e
feat: add initial support for server-side data fetching
brandonroberts May 30, 2023
f700c06
chore: fix unit tests
brandonroberts May 30, 2023
76ca6a8
chore: move example data-fetching usage to analog-app
brandonroberts May 30, 2023
f594e57
chore: fix trpc-app
brandonroberts May 30, 2023
1c1c865
chore: cleanup
brandonroberts May 30, 2023
1be5f61
chore: update .node-version
brandonroberts May 30, 2023
c75d2af
feat: add support for (index) routes, added useLoad function, types
brandonroberts May 31, 2023
5760385
chore: suppress warnings about empty server chunks
brandonroberts May 31, 2023
c164f32
feat: add utility types for loaded data
brandonroberts May 31, 2023
fc33312
chore: update workflows
brandonroberts May 31, 2023
9bc40cc
chore: add platform to cypress e2e implicit deps
brandonroberts May 31, 2023
5f30f06
chore: add e2e to depend on build
brandonroberts May 31, 2023
1c9505b
ci: add Vite environment variable
brandonroberts May 31, 2023
199e646
ci: set vite URL
brandonroberts May 31, 2023
5e74e9a
chore: update ci key
brandonroberts May 31, 2023
c5308eb
ci: use localhost for base URL
brandonroberts May 31, 2023
c2a6815
ci: use environment vars
brandonroberts May 31, 2023
6fced2d
feat: add data fetching to route discovery
brandonroberts Jun 7, 2023
f2bf143
chore: cleanup
brandonroberts Jun 7, 2023
ff53677
fix: add support for multiple pathless routes
brandonroberts Jun 7, 2023
7ed26fb
refactor: rename helper functions and types
brandonroberts Jun 7, 2023
700712a
refactor: rename metadata key
brandonroberts Jun 7, 2023
bd41e1e
fix: use Symbol for analog metadata key
brandonroberts Jun 7, 2023
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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ env:
DOCS_APP_ARTIFACT_NAME: docs-app
DOCS_APP_PATH: dist/apps/docs-app/
NODE_OPTIONS: --max-old-space-size=6144
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
VITE_ANALOG_PUBLIC_BASE_URL: ${{ vars.VITE_ANALOG_PUBLIC_BASE_URL }}

jobs:
commitlint:
Expand Down Expand Up @@ -73,6 +73,8 @@ jobs:

unit:
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand All @@ -91,6 +93,8 @@ jobs:

e2e:
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ stats.html

# Nitro
.nitro
/migrations.json
/migrations.json

.env
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.16.0
18.13.0
3 changes: 2 additions & 1 deletion apps/analog-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideFileRouter } from '@analogjs/router';
import { withNavigationErrorHandler } from '@angular/router';

export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(),
provideFileRouter(withNavigationErrorHandler(console.error)),
provideHttpClient(),
provideClientHydration(),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { RouteMeta } from '@analogjs/router';
import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouteMeta, useLoad } from '@analogjs/router';
import { NgForOf, NgIf } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { RouterLinkWithHref } from '@angular/router';
import { catchError, of } from 'rxjs';

import { ProductAlertsComponent } from '../product-alerts/product-alerts.component';
import { Product } from '../products';
import { load } from './(home).server';

export const routeMeta: RouteMeta = {
title: 'Product List',
Expand All @@ -18,7 +18,7 @@ export const routeMeta: RouteMeta = {
template: `
<h2>Products</h2>

<div *ngFor="let product of products">
<div *ngFor="let product of data().products">
<h3>
<a
[title]="product.name + ' details'"
Expand Down Expand Up @@ -51,17 +51,7 @@ export const routeMeta: RouteMeta = {
],
})
export default class ProductListComponent {
products!: Product[];
http = inject(HttpClient);

ngOnInit() {
this.http
.get<Product[]>('/api/v1/products')
.pipe(catchError(() => of([])))
.subscribe((products) => {
this.products = products;
});
}
data = toSignal(useLoad<typeof load>(), { requireSync: true });

share() {
window.alert('The product has been shared!');
Expand Down
11 changes: 11 additions & 0 deletions apps/analog-app/src/app/pages/(home).server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PageServerLoad } from '@analogjs/router';

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

export const load = async ({ fetch }: PageServerLoad) => {
const products = await fetch<Product[]>('/api/v1/products');

return {
products: products,
};
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Component, inject, OnInit } from '@angular/core';
import { CurrencyPipe, NgIf } from '@angular/common';
import { injectActivatedRoute } from '@analogjs/router';
import { HttpClient } from '@angular/common/http';
import { catchError, of } from 'rxjs';

import { Product } from '../products';
import { CartService } from '../cart.service';
import { HttpClient } from '@angular/common/http';
import { catchError, of } from 'rxjs';

@Component({
selector: 'app-product-details',
Expand Down
9 changes: 9 additions & 0 deletions apps/analog-app/src/app/pages/products.[productId].server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PageServerLoad } from '@analogjs/router';

export const load = async ({ params, fetch }: PageServerLoad) => {
return {
slug: true,
};
};

export type LoadType = Awaited<ReturnType<typeof load>>;
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions apps/analog-app/src/app/pages/shipping/index.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const load = async () => {
console.log('shipping');
return {
shipping: true,
};
};
2 changes: 1 addition & 1 deletion apps/analog-app/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"useDefineForClassFields": false
},
"files": ["src/main.ts", "src/main.server.ts"],
"include": ["src/**/*.d.ts", "src/app/routes/**/*.ts"],
"include": ["src/**/*.d.ts", "src/app/pages/**/*.page.ts"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
2 changes: 1 addition & 1 deletion apps/analog-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default defineConfig(({ mode }) => {
analog({
apiPrefix: 'api',
prerender: {
routes: ['/'],
routes: ['/', '/cart'],
},
vite: {
inlineStylesExtension: 'scss',
Expand Down
7 changes: 7 additions & 0 deletions apps/blog-app/src/app/pages/blog/index.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from '@angular/core';

@Component({
standalone: true,
template: ` <h2>Home</h2> `,
})
export default class BlogComponent {}
3 changes: 3 additions & 0 deletions apps/blog-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { defineConfig } from 'vite';
export default defineConfig(() => {
return {
publicDir: 'src/assets',
optimizeDeps: {
include: ['@angular/common'],
},
build: {
target: ['es2020'],
},
Expand Down
3 changes: 3 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"test": {
"dependsOn": ["^build"]
},
"e2e": {
"dependsOn": ["^build"]
},
"serve": {
"dependsOn": ["^build"]
},
Expand Down
37 changes: 37 additions & 0 deletions packages/platform/src/lib/clear-client-page-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Plugin, normalizePath } from 'vite';

export function clearClientPageEndpointsPlugin(): Plugin {
return {
name: 'analogjs-platform-clear-client-page-endpoint',
config() {
return {
build: {
rollupOptions: {
onwarn(warning) {
if (
warning.message.includes('empty chunk') &&
warning.message.endsWith('.server')
) {
return;
}
},
},
},
};
},
transform(_code, id, options) {
if (
!options?.ssr &&
id.includes(normalizePath('src/app/pages')) &&
id.endsWith('.server.ts')
) {
return {
code: 'export default undefined;',
map: null,
};
}

return;
},
};
}
2 changes: 2 additions & 0 deletions packages/platform/src/lib/platform-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ssrBuildPlugin } from './ssr/ssr-build-plugin';
import { contentPlugin } from './content-plugin';
import viteNitroPlugin from '@analogjs/vite-plugin-nitro';
import angular from '@analogjs/vite-plugin-angular';
import { clearClientPageEndpointsPlugin } from './clear-client-page-endpoint';

export function platformPlugin(opts: Options = {}): Plugin[] {
const { apiPrefix, ...platformOptions } = {
Expand All @@ -32,5 +33,6 @@ export function platformPlugin(opts: Options = {}): Plugin[] {
? devServerPlugin({ entryServer: opts.entryServer })
: false) as Plugin,
...angular({ jit: platformOptions.jit, ...(opts?.vite ?? {}) }),
clearClientPageEndpointsPlugin(),
];
}
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export {
export { RouteMeta } from './lib/models';
export { provideFileRouter } from './lib/provide-file-router';
export { MetaTag } from './lib/meta-tags';
export { PageServerLoad, LoadReturn } from './lib/route-types';
export { useLoad } from './lib/use-load';
2 changes: 2 additions & 0 deletions packages/router/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ENDPOINT_EXTENSION = '.server.ts';
export const APP_DIR = 'src/app';
3 changes: 3 additions & 0 deletions packages/router/src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const PAGE_ENDPOINTS = import.meta.glob([
'/src/app/pages/**/*.server.ts',
]);
48 changes: 42 additions & 6 deletions packages/router/src/lib/route-config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import type { Route } from '@angular/router';
import { firstValueFrom } from 'rxjs';

import { RedirectRouteMeta, RouteConfig, RouteMeta } from './models';
import { ROUTE_META_TAGS_KEY } from './meta-tags';
import { PAGE_ENDPOINTS } from './endpoints';

export function toRouteConfig(routeMeta: RouteMeta | undefined): RouteConfig {
if (!routeMeta) {
return {};
}

if (isRedirectRouteMeta(routeMeta)) {
if (routeMeta && isRedirectRouteMeta(routeMeta)) {
return routeMeta;
}

const { meta, ...routeConfig } = routeMeta;
let { meta, ...routeConfig } = routeMeta ?? {};

if (Array.isArray(meta)) {
routeConfig.data = { ...routeConfig.data, [ROUTE_META_TAGS_KEY]: meta };
Expand All @@ -21,6 +23,40 @@ export function toRouteConfig(routeMeta: RouteMeta | undefined): RouteConfig {
};
}

if (!routeConfig) {
routeConfig = {};
}

routeConfig.resolve = {
...routeConfig.resolve,
load: async (route) => {
const routeConfig = route.routeConfig as Route & {
meta: { endpoint: string; endpointKey: string };
};

if (PAGE_ENDPOINTS[routeConfig.meta.endpointKey]) {
const { queryParams, fragment: hash, params } = route;
const url = new URL('', import.meta.env['VITE_ANALOG_PUBLIC_BASE_URL']);
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
url.pathname = `/api/_analog${routeConfig.meta.endpoint}`;
url.search = `${new URLSearchParams(queryParams).toString()}`;
url.hash = hash ?? '';

Object.keys(params).forEach((param) => {
url.pathname = url.pathname.replace(`[${param}]`, params[param]);
});

if ((globalThis as any).$fetch) {
return (globalThis as any).$fetch(url.pathname);
}

const http = inject(HttpClient);
return firstValueFrom(http.get(`${url.href}`));
}

return {};
},
};

return routeConfig;
}

Expand Down
13 changes: 13 additions & 0 deletions packages/router/src/lib/route-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { H3Event, H3EventContext } from 'h3';
import { $Fetch } from 'nitropack';

export type PageServerLoad = {
params: H3EventContext['params'];
req: H3Event['node']['req'];
res: H3Event['node']['res'];
fetch: $Fetch;
};

export type LoadReturn<
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
A extends (pageServerLoad: PageServerLoad) => Promise<any>
> = Awaited<ReturnType<A>>;
5 changes: 3 additions & 2 deletions packages/router/src/lib/routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ describe('routes', () => {
it('should add meta tags to data dictionary when they are defined as array', async () => {
const routeMeta: RouteMeta = {
data: { foo: 'bar' },
resolve: { x: () => of('y') },
resolve: { x: () => of('y'), load: expect.anything() },
meta: [
{ charset: 'utf-8' },
{
Expand All @@ -533,7 +533,7 @@ describe('routes', () => {
// routeMeta.data should not be mutated
expect(routeMeta.data).not.toBe(resolvedRoute.data);
// routeMeta.resolve should not be changed
expect(resolvedRoute.resolve).toBe(routeMeta.resolve);
expect(resolvedRoute.resolve).toStrictEqual(routeMeta.resolve);
});

it('should add meta tags to resolve dictionary when they are defined as resolver', async () => {
Expand All @@ -554,6 +554,7 @@ describe('routes', () => {
expect(resolvedRoute.resolve).toEqual({
...routeMeta.resolve,
[ROUTE_META_TAGS_KEY]: routeMeta.meta,
load: expect.anything(),
});
// routeMeta.resolve should not be mutated
expect(routeMeta.resolve).not.toBe(resolvedRoute.resolve);
Expand Down
Loading