create-fastang 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -2
  4. package/bin/create-fastang.mjs +7 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +90 -0
  7. package/dist/create.d.ts +14 -0
  8. package/dist/create.js +56 -0
  9. package/package.json +44 -4
  10. package/template/README.md +31 -0
  11. package/template/_gitignore +9 -0
  12. package/template/fastang.config.ts +2 -0
  13. package/template/index.html +2 -0
  14. package/template/package.json +36 -0
  15. package/template/src/app/app.config.ts +8 -0
  16. package/template/src/app/app.ts +7 -0
  17. package/template/src/app/omitted.server.ts +14 -0
  18. package/template/src/app/pages/[...slug].page.ts +7 -0
  19. package/template/src/app/pages/[...slug].route.ts +1 -0
  20. package/template/src/app/pages/about.page.ts +3 -0
  21. package/template/src/app/pages/about.route.ts +1 -0
  22. package/template/src/app/pages/greet/[name].page.ts +3 -0
  23. package/template/src/app/pages/greet/[name].route.ts +5 -0
  24. package/template/src/app/pages/index.page.ts +12 -0
  25. package/template/src/app/pages/index.route.ts +1 -0
  26. package/template/src/main.server.ts +5 -0
  27. package/template/src/main.ts +6 -0
  28. package/template/src/server/api/greet/[name].get.ts +8 -0
  29. package/template/src/server/api/health.get.ts +2 -0
  30. package/template/src/server/api-entry.ts +7 -0
  31. package/template/src/server/index.ts +19 -0
  32. package/template/src/server/plugins.ts +2 -0
  33. package/template/src/server/start.ts +11 -0
  34. package/template/src/server/vercel-entry.ts +5 -0
  35. package/template/src/styles.css +7 -0
  36. package/template/tsconfig.app.json +14 -0
  37. package/template/tsconfig.json +21 -0
  38. package/template/tsconfig.node.json +16 -0
  39. package/template/vercel.json +5 -0
  40. package/template/vite.config.ts +2 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0-alpha.1
4
+
5
+ - Pin the generated starter to @fastang/core 0.1.0-alpha.1, with runtime configuration validation and the explicit SSR HTTP contract.
6
+
7
+ ## 0.1.0-alpha.0
8
+
9
+ Initial experimental release.
10
+
11
+ - Generate an independent Angular + Fastify application.
12
+ - Include a home page, prerendered page, SSR API example and HTTP 404 page.
13
+ - Use the same starter source maintained in the repository.
14
+ - Customize the npm package name and optionally install dependencies.
15
+ - Support local core tarballs before registry publication.
16
+ - Reject existing destinations and exclude local/generated files from templates.
17
+
18
+ The generator pins the matching @fastang/core alpha release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dario Chiapperini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # create-fastang
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generate a minimal Angular + Fastify application with FastAng.
4
+
5
+ This is an experimental alpha package. Use the explicit `alpha` tag for the prerelease:
6
+
7
+ ```sh
8
+ npm create fastang@alpha my-app -- --install
9
+ cd my-app
10
+ npm run dev
11
+ ```
12
+
13
+ Registry installation is available once the generator and matching core release have been published. For local release testing, use the tarball workflow below.
14
+
15
+ ## Use from this repository
16
+
17
+ At the repository root:
18
+
19
+ ```sh
20
+ npm ci
21
+ npm pack --workspace @fastang/core --pack-destination /tmp
22
+ npm run create -- /tmp/my-fastang-app --core-package /tmp/fastang-core-0.1.0-alpha.1.tgz
23
+ cd /tmp/my-fastang-app
24
+ npm install
25
+ npm run dev
26
+ ```
27
+
28
+ Open http://localhost:3278. Use PORT and HOST to override the defaults.
29
+ The core tarball must remain available for future clean dependency installations.
30
+
31
+ ## Test the packaged generator
32
+
33
+ ```sh
34
+ npm pack --workspace create-fastang --pack-destination /tmp
35
+ npm exec --package=/tmp/create-fastang-0.1.0-alpha.1.tgz -- create-fastang /tmp/another-app --core-package /tmp/fastang-core-0.1.0-alpha.1.tgz
36
+ ```
37
+
38
+ No checkout files are required by either installed package. The generated app
39
+ includes its own configuration, Node and Vercel entries, pages and API endpoints.
40
+
41
+ ## Options
42
+
43
+ - `<directory>`: new project directory. Existing files, folders and symlinks are rejected, including an existing empty directory. Without an argument, an interactive terminal asks for the directory; automation must provide one explicitly.
44
+ - `--name <name>`: npm package name, including scoped names. Otherwise a lowercase name is derived from the directory.
45
+ - `--core-package <file.tgz>`: local core package for use before registry publication.
46
+ - `--install`: run npm install after generating the project. On failure, the generated files remain available for retrying installation.
47
+ - `--no-install`: generate files only; this is the default.
48
+ - `--help`, `--version`: show usage or generator version.
49
+
50
+ There are no git operations, global installations or deployments. Use the
51
+ project's `npm run build:vercel` command when preparing a Vercel deployment.
52
+
53
+ ## Maintain the template
54
+
55
+ `examples/starter` is the single source of application files. `npm run build
56
+ --workspace create-fastang` compiles the generator and prepares its distributable
57
+ template. `prepack` runs this build automatically. Build output, dependencies,
58
+ local environment files and generated routes are excluded. `.gitignore` is
59
+ shipped as `_gitignore` and restored when creating an app.
60
+
61
+ `npm run test:starter` installs both tarballs outside the workspace, invokes the
62
+ installed generator and verifies the generated application, including development
63
+ restarts and an actual packaged Vercel function.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/cli.js';
3
+ try { await main(); }
4
+ catch (error) {
5
+ console.error(`create-fastang: ${error instanceof Error ? error.message : error}`);
6
+ process.exitCode = 1;
7
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(args?: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,90 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { relative } from 'node:path';
5
+ import { createProject } from "./create.js";
6
+ const help = `Usage: create-fastang <directory> [options]
7
+
8
+ Create a minimal Angular + Fastify project in a new directory.
9
+
10
+ --name <name> Set the npm package name
11
+ --install Install dependencies with npm
12
+ --no-install Generate files only (default)
13
+ --core-package <file> Use a local @fastang/core .tgz before npm publication
14
+ -h, --help Show this help
15
+ -v, --version Show generator version
16
+
17
+ Example: create-fastang my-app --core-package /tmp/fastang-core-0.1.0-alpha.1.tgz
18
+ `;
19
+ const quote = (value) => /^[a-zA-Z0-9_./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\"'\"'")}'`;
20
+ export async function main(args = process.argv.slice(2)) {
21
+ if (args.includes('--help') || args.includes('-h')) {
22
+ console.log(help);
23
+ return;
24
+ }
25
+ if (args.includes('--version') || args.includes('-v')) {
26
+ console.log(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version);
27
+ return;
28
+ }
29
+ let directory;
30
+ let name;
31
+ let corePackage;
32
+ let install = false;
33
+ let installationOption;
34
+ let positional = false;
35
+ for (let index = 0; index < args.length; index++) {
36
+ const arg = args[index];
37
+ if (!positional && arg === '--') {
38
+ positional = true;
39
+ continue;
40
+ }
41
+ if (!positional && ['--name', '--core-package'].includes(arg)) {
42
+ const value = args[++index];
43
+ if (!value || value.startsWith('--'))
44
+ throw new Error(`Missing value for ${arg}.`);
45
+ if (arg === '--name')
46
+ name = value;
47
+ else
48
+ corePackage = value;
49
+ }
50
+ else if (!positional && ['--install', '--no-install'].includes(arg)) {
51
+ if (installationOption && installationOption !== arg)
52
+ throw new Error('Choose either --install or --no-install.');
53
+ installationOption = arg;
54
+ install = arg === '--install';
55
+ }
56
+ else if (!positional && arg.startsWith('-'))
57
+ throw new Error(`Unknown option: ${arg}. Run create-fastang --help.`);
58
+ else if (directory !== undefined)
59
+ throw new Error('Provide exactly one project directory.');
60
+ else
61
+ directory = arg;
62
+ }
63
+ if (directory === undefined) {
64
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
65
+ throw new Error('Provide a project directory. Run create-fastang --help.');
66
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
67
+ try {
68
+ directory = (await prompt.question('Project directory (fastang-app): ')).trim() || 'fastang-app';
69
+ }
70
+ finally {
71
+ prompt.close();
72
+ }
73
+ }
74
+ const project = await createProject({ directory, name, corePackage });
75
+ console.log(`\nCreated ${project.name} in ${project.directory}`);
76
+ if (install) {
77
+ console.log('\nInstalling dependencies with npm…');
78
+ await new Promise((resolve, reject) => {
79
+ const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install'], { cwd: project.directory, stdio: 'inherit', shell: process.platform === 'win32' });
80
+ child.once('error', reject);
81
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error('npm install failed. Project files are preserved; run npm install in the project to retry.')));
82
+ });
83
+ }
84
+ const path = relative(process.cwd(), project.directory) || '.';
85
+ console.log('\nNext steps:');
86
+ console.log(process.platform === 'win32' ? ` Open ${project.directory}` : ` cd ${quote(path.startsWith('-') ? './' + path : path)}`);
87
+ if (!install)
88
+ console.log(' npm install');
89
+ console.log(' npm run dev\n\nhttp://localhost:3278');
90
+ }
@@ -0,0 +1,14 @@
1
+ export interface CreateOptions {
2
+ directory: string;
3
+ cwd?: string;
4
+ name?: string;
5
+ corePackage?: string;
6
+ }
7
+ export declare function packageName(directory: string): string;
8
+ export declare function validatePackageName(name: string): void;
9
+ /** Generate atomically into a new directory; existing paths are never replaced. */
10
+ export declare function createProject(options: CreateOptions): Promise<{
11
+ directory: string;
12
+ name: string;
13
+ localCore: boolean;
14
+ }>;
package/dist/create.js ADDED
@@ -0,0 +1,56 @@
1
+ import { cp, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { basename, dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function packageName(directory) {
5
+ return basename(resolve(directory)).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '') || 'fastang-app';
6
+ }
7
+ export function validatePackageName(name) {
8
+ if (name.length > 214 || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name) || ['node_modules', 'favicon.ico'].includes(name)) {
9
+ throw new Error('Invalid npm package name. Use lowercase letters, numbers, dots, hyphens or underscores.');
10
+ }
11
+ }
12
+ /** Generate atomically into a new directory; existing paths are never replaced. */
13
+ export async function createProject(options) {
14
+ if (!options.directory.trim())
15
+ throw new Error('Provide a new project directory.');
16
+ const cwd = options.cwd ?? process.cwd();
17
+ const directory = resolve(cwd, options.directory);
18
+ const name = options.name ?? packageName(directory);
19
+ validatePackageName(name);
20
+ if (await lstat(directory).then(() => true, (error) => {
21
+ if (error.code === 'ENOENT')
22
+ return false;
23
+ throw error;
24
+ }))
25
+ throw new Error(`Destination already exists: ${directory}. Choose a new directory.`);
26
+ let corePackage;
27
+ if (options.corePackage) {
28
+ corePackage = resolve(cwd, options.corePackage);
29
+ if (!(await lstat(corePackage)).isFile() || !corePackage.endsWith('.tgz'))
30
+ throw new Error('--core-package must point to a local npm .tgz package.');
31
+ }
32
+ const template = fileURLToPath(new URL('../template/', import.meta.url));
33
+ const manifest = JSON.parse(await readFile(resolve(template, 'package.json'), 'utf8'));
34
+ manifest.name = name;
35
+ if (corePackage)
36
+ manifest.dependencies['@fastang/core'] = `file:${corePackage}`;
37
+ await mkdir(dirname(directory), { recursive: true });
38
+ const staging = await mkdtemp(resolve(dirname(directory), '.fastang-create-'));
39
+ try {
40
+ await cp(template, resolve(staging, 'project'), { recursive: true, force: false, errorOnExist: true });
41
+ await rename(resolve(staging, 'project/_gitignore'), resolve(staging, 'project/.gitignore'));
42
+ await writeFile(resolve(staging, 'project/package.json'), JSON.stringify(manifest, null, 2) + '\n');
43
+ // Recheck before rename: do not replace a directory created while copying.
44
+ if (await lstat(directory).then(() => true, (error) => {
45
+ if (error.code === 'ENOENT')
46
+ return false;
47
+ throw error;
48
+ }))
49
+ throw new Error(`Destination already exists: ${directory}. Choose a new directory.`);
50
+ await rename(resolve(staging, 'project'), directory);
51
+ }
52
+ finally {
53
+ await rm(staging, { recursive: true, force: true });
54
+ }
55
+ return { directory, name, localCore: Boolean(corePackage) };
56
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "create-fastang",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Create an Angular + Fastify project with FastAng.",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist",
13
+ "template",
14
+ "README.md",
15
+ "LICENSE",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "bin": {
19
+ "create-fastang": "./bin/create-fastang.mjs"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/create.d.ts",
24
+ "import": "./dist/create.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json && node scripts/build-template.mjs",
29
+ "prepack": "npm run build"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/daclab-dev/fastang.git",
34
+ "directory": "packages/create-fastang"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "tag": "alpha",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "homepage": "https://fastang.dev",
42
+ "bugs": {
43
+ "url": "https://github.com/daclab-dev/fastang/issues"
44
+ },
45
+ "author": "Dario Chiapperini"
46
+ }
@@ -0,0 +1,31 @@
1
+ # FastAng app
2
+
3
+ Angular pages, Fastify APIs and server rendering.
4
+
5
+ ```sh
6
+ npm install
7
+ npm run dev
8
+ ```
9
+
10
+ Open http://localhost:3278. Use PORT and HOST to override the defaults.
11
+
12
+ ## Commands
13
+
14
+ - `npm run check`: generate routes and validate types and Angular templates.
15
+ - `npm run build`: build the Node server, client assets and prerendered pages.
16
+ - `npm start`: run the production server.
17
+ - `npm run build:vercel`: generate the Vercel Build Output API directory.
18
+
19
+ ## Application files
20
+
21
+ - `src/app/pages`: Angular pages and adjacent route metadata.
22
+ - `src/server/api`: Fastify endpoints; the filename selects the HTTP method.
23
+ - `src/server/plugins.ts`: Fastify hooks and plugins.
24
+ - `fastang.config.ts`: explicit prerender URLs and Vercel function configuration.
25
+
26
+ The home page includes an interactive counter. `/greet/world` resolves data from
27
+ `/api/greet/world` during SSR and uses Angular's HTTP transfer cache during
28
+ hydration. `/about` is prerendered. Unknown pages return HTTP 404.
29
+
30
+ For Vercel use the Other preset, build command `npm run build:vercel`, and no
31
+ output-directory override. `vercel.json` already contains the build settings.
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ dist/
3
+ .fastang/
4
+ .angular/
5
+ .vercel/
6
+ src/app/fastang.routes.ts
7
+ .env
8
+ .env.*
9
+ !.env.example
@@ -0,0 +1,2 @@
1
+ import { defineConfig } from '@fastang/core';
2
+ export default defineConfig({ prerender: ['/about'], vercel: { functions: 'routes' } });
@@ -0,0 +1,2 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><base href="/"><meta name="viewport" content="width=device-width, initial-scale=1"><title>FastAng starter</title></head><body><app-root></app-root><script type="module" src="/src/main.ts"></script></body></html>
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "fastang-starter",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "fastang dev",
11
+ "generate": "fastang generate",
12
+ "check": "fastang generate && ngc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit",
13
+ "build": "npm run check && fastang build",
14
+ "build:vercel": "npm run check && fastang build --vercel",
15
+ "start": "node dist/server/index.js"
16
+ },
17
+ "dependencies": {
18
+ "@angular/common": "22.2.3",
19
+ "@angular/compiler": "22.2.3",
20
+ "@angular/core": "22.2.3",
21
+ "@angular/platform-browser": "22.2.3",
22
+ "@angular/platform-server": "22.2.3",
23
+ "@angular/router": "22.2.3",
24
+ "fastify": "^5.12.7",
25
+ "rxjs": "^7.8.2",
26
+ "tslib": "^2.8.1",
27
+ "@fastang/core": "0.1.0-alpha.1"
28
+ },
29
+ "devDependencies": {
30
+ "@angular/build": "22.2.3",
31
+ "@angular/compiler-cli": "22.2.3",
32
+ "@types/node": "^22.19.0",
33
+ "typescript": "~6.0.2",
34
+ "vite": "8.3.4"
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { ApplicationConfig } from '@angular/core';
2
+ import { provideHttpClient, withFetch } from '@angular/common/http';
3
+ import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
4
+ import { provideRouter, withComponentInputBinding } from '@angular/router';
5
+ import { createRoutes } from './fastang.routes';
6
+ export function createAppConfig(): ApplicationConfig {
7
+ return { providers: [provideRouter(createRoutes(), withComponentInputBinding()), provideHttpClient(withFetch()), provideClientHydration(withEventReplay())] };
8
+ }
@@ -0,0 +1,7 @@
1
+ import { Component } from '@angular/core';
2
+ import { RouterLink, RouterOutlet } from '@angular/router';
3
+ @Component({ selector: 'app-root', imports: [RouterLink, RouterOutlet], template: `
4
+ <header><a routerLink="/">FastAng</a><nav><a routerLink="/about">About</a><a routerLink="/greet/world">SSR + API</a></nav></header>
5
+ <main><router-outlet /></main>
6
+ ` })
7
+ export class App {}
@@ -0,0 +1,14 @@
1
+ import { Component, REQUEST_CONTEXT, inject } from '@angular/core';
2
+ import { RouterOutlet } from '@angular/router';
3
+ import type { RenderContext } from '@fastang/core';
4
+
5
+ /** Redirect destinations can be resolved without bundling their page templates. */
6
+ @Component({ selector: 'fastang-omitted-route', imports: [RouterOutlet], template: '<router-outlet />' })
7
+ export default class OmittedRoute {
8
+ constructor() {
9
+ const context = inject(REQUEST_CONTEXT) as RenderContext | null;
10
+ // A redirect takes precedence over status in the HTTP bridge. An unexpected
11
+ // direct invocation must never turn an omitted page into a blank HTTP 200.
12
+ if (context) context.status = 404;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import { Component, REQUEST_CONTEXT, inject } from '@angular/core';
2
+ import { RouterLink } from '@angular/router';
3
+ import type { RenderContext } from '@fastang/core';
4
+ @Component({ imports: [RouterLink], template: `<h1>Page not found</h1><a routerLink="/">Back home</a>` })
5
+ export default class NotFoundPage {
6
+ constructor() { const context = inject(REQUEST_CONTEXT) as RenderContext | null; if (context) context.status = 404; }
7
+ }
@@ -0,0 +1 @@
1
+ export default { title: 'Page not found · FastAng' };
@@ -0,0 +1,3 @@
1
+ import { Component } from '@angular/core';
2
+ @Component({ template: `<p class="eyebrow">PRERENDERED</p><h1>Built ahead of time.</h1><p>This page is generated during the build. Choose public URLs in fastang.config.ts.</p>` })
3
+ export default class AboutPage {}
@@ -0,0 +1 @@
1
+ export default { title: 'About · FastAng' };
@@ -0,0 +1,3 @@
1
+ import { Component, input } from '@angular/core';
2
+ @Component({ template: `<p class="eyebrow">SERVER RENDERED</p><h1>{{ greeting().message }}</h1><p>The Angular resolver calls a validated Fastify endpoint. Hydration reuses its response.</p>` })
3
+ export default class GreetingPage { readonly greeting = input.required<{ message: string }>(); }
@@ -0,0 +1,5 @@
1
+ import { inject } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import type { ResolveFn } from '@angular/router';
4
+ const greeting: ResolveFn<{ message: string }> = route => inject(HttpClient).get<{ message: string }>('/api/greet/' + encodeURIComponent(route.paramMap.get('name')!));
5
+ export default { title: 'Greeting · FastAng', resolve: { greeting } };
@@ -0,0 +1,12 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { RouterLink } from '@angular/router';
3
+ @Component({ imports: [RouterLink], template: `
4
+ <p class="eyebrow">ANGULAR + FASTIFY</p><h1>Your next idea starts here.</h1>
5
+ <p>Angular pages, Fastify APIs and server rendering in one project.</p>
6
+ <p><a routerLink="/greet/world">Try a server-rendered API response →</a></p>
7
+ <button (click)="count.update(increment)">Clicked {{ count() }} {{ count() === 1 ? 'time' : 'times' }}</button>
8
+ ` })
9
+ export default class HomePage {
10
+ readonly count = signal(0);
11
+ readonly increment = (value: number) => value + 1;
12
+ }
@@ -0,0 +1 @@
1
+ export default { title: 'Home · FastAng' };
@@ -0,0 +1,5 @@
1
+ import { createRenderer } from '@fastang/core/angular';
2
+ import { App } from './app/app';
3
+ import { createAppConfig } from './app/app.config';
4
+
5
+ export const render = createRenderer(App, createAppConfig);
@@ -0,0 +1,6 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { App } from './app/app';
3
+ import { createAppConfig } from './app/app.config';
4
+ import './styles.css';
5
+
6
+ bootstrapApplication(App, createAppConfig()).catch(console.error);
@@ -0,0 +1,8 @@
1
+ import { defineApiRoute } from '@fastang/core';
2
+ export default defineApiRoute<{ Params: { name: string } }>({
3
+ schema: {
4
+ params: { type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 1, maxLength: 80 } } },
5
+ response: { 200: { type: 'object', required: ['message'], properties: { message: { type: 'string' } } } },
6
+ },
7
+ handler: async request => ({ message: `Hello, ${request.params.name}!` }),
8
+ });
@@ -0,0 +1,2 @@
1
+ import { defineApiRoute } from '@fastang/core';
2
+ export default defineApiRoute({ schema: { response: { 200: { type: 'object', required: ['ok'], properties: { ok: { type: 'boolean' } } } } }, handler: async () => ({ ok: true }) });
@@ -0,0 +1,7 @@
1
+ import { createApiServer } from '@fastang/core/server';
2
+ import { createNodeHandler } from '@fastang/core/node-handler';
3
+ import { configureServer } from './plugins';
4
+ import { registerApi } from '../../.fastang/api';
5
+
6
+ // This entry point deliberately has no Angular renderer dependency.
7
+ export default createNodeHandler(() => createApiServer({ configure: configureServer, registerApi }));
@@ -0,0 +1,19 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createServer } from '@fastang/core/server';
5
+ import { render } from '../main.server';
6
+ import { configureServer } from './plugins';
7
+ import { registerApi } from '../../.fastang/api';
8
+
9
+ const directory = dirname(fileURLToPath(import.meta.url));
10
+ export async function createProductionServer(options: { prerender?: boolean; logger?: boolean; serveAssets?: boolean } = {}) {
11
+ const clientDir = resolve(directory, '../client');
12
+ const document = await readFile(resolve(clientDir, 'index.html'), 'utf8');
13
+ const prerendered: string[] = options.prerender === false ? [] : JSON.parse(await readFile(resolve(directory, '../prerender.json'), 'utf8'));
14
+ return createServer({
15
+ logger: options.logger ?? false,
16
+ clientDir: options.serveAssets === false ? undefined : clientDir, prerendered,
17
+ template: async () => document, render, configure: configureServer, registerApi,
18
+ });
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export async function configureServer(_app: FastifyInstance): Promise<void> {}
@@ -0,0 +1,11 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { listen } from '@fastang/core/listen';
4
+ import { createProductionServer } from './index';
5
+
6
+ export { createProductionServer };
7
+
8
+ // Kept in the executable entry, so code splitting cannot move this check to a shared chunk.
9
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
10
+ await listen(await createProductionServer({ logger: true }));
11
+ }
@@ -0,0 +1,5 @@
1
+ import { createNodeHandler } from '@fastang/core/node-handler';
2
+ import { createProductionServer } from './index';
3
+
4
+ // Static files and prerendered pages are handled by Vercel's CDN.
5
+ export default createNodeHandler(() => createProductionServer({ prerender: false, serveAssets: false }));
@@ -0,0 +1,7 @@
1
+ :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #11131a; color: #f3f4f8; }
2
+ body { margin: 0; } header, main { max-width: 58rem; margin: auto; padding: 2rem; }
3
+ header { display: flex; justify-content: space-between; border-bottom: 1px solid #34394b; }
4
+ nav { display: flex; gap: 1.5rem; } main { padding-top: 5rem; } a { color: #a6b9ff; }
5
+ h1 { font-size: clamp(2rem, 6vw, 4rem); letter-spacing: -.04em; line-height: 1.1; }
6
+ p { max-width: 42rem; line-height: 1.7; } .eyebrow { font-size: .8rem; letter-spacing: .16em; color: #a6b9ff; }
7
+ button { margin-top: 1rem; padding: .8rem 1.1rem; font: inherit; border-radius: .5rem; border: 1px solid #879deb; color: #fff; background: #334b9d; cursor: pointer; }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./.fastang/tsc",
5
+ "types": ["vite/client"],
6
+ "declaration": false
7
+ },
8
+ "angularCompilerOptions": {
9
+ "strictTemplates": true,
10
+ "strictInjectionParameters": true,
11
+ "strictInputAccessModifiers": true
12
+ },
13
+ "include": ["src/app/**/*.ts", "src/shared/**/*.ts", "src/main.ts", "src/main.server.ts"]
14
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM"
9
+ ],
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "experimentalDecorators": true,
13
+ "importHelpers": true,
14
+ "isolatedModules": true,
15
+ "verbatimModuleSyntax": true,
16
+ "esModuleInterop": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "allowImportingTsExtensions": true
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": [
5
+ "node",
6
+ "vite/client"
7
+ ],
8
+ "noEmit": true
9
+ },
10
+ "include": [
11
+ "src/server/**/*.ts",
12
+ "src/shared/**/*.ts",
13
+ "fastang.config.ts",
14
+ "vite.config.ts"
15
+ ]
16
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "framework": null,
4
+ "buildCommand": "npm run build:vercel"
5
+ }
@@ -0,0 +1,2 @@
1
+ import { defineFastangConfig } from '@fastang/core/vite';
2
+ export default defineFastangConfig();