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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/bin/create-fastang.mjs +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +90 -0
- package/dist/create.d.ts +14 -0
- package/dist/create.js +56 -0
- package/package.json +44 -4
- package/template/README.md +31 -0
- package/template/_gitignore +9 -0
- package/template/fastang.config.ts +2 -0
- package/template/index.html +2 -0
- package/template/package.json +36 -0
- package/template/src/app/app.config.ts +8 -0
- package/template/src/app/app.ts +7 -0
- package/template/src/app/omitted.server.ts +14 -0
- package/template/src/app/pages/[...slug].page.ts +7 -0
- package/template/src/app/pages/[...slug].route.ts +1 -0
- package/template/src/app/pages/about.page.ts +3 -0
- package/template/src/app/pages/about.route.ts +1 -0
- package/template/src/app/pages/greet/[name].page.ts +3 -0
- package/template/src/app/pages/greet/[name].route.ts +5 -0
- package/template/src/app/pages/index.page.ts +12 -0
- package/template/src/app/pages/index.route.ts +1 -0
- package/template/src/main.server.ts +5 -0
- package/template/src/main.ts +6 -0
- package/template/src/server/api/greet/[name].get.ts +8 -0
- package/template/src/server/api/health.get.ts +2 -0
- package/template/src/server/api-entry.ts +7 -0
- package/template/src/server/index.ts +19 -0
- package/template/src/server/plugins.ts +2 -0
- package/template/src/server/start.ts +11 -0
- package/template/src/server/vercel-entry.ts +5 -0
- package/template/src/styles.css +7 -0
- package/template/tsconfig.app.json +14 -0
- package/template/tsconfig.json +21 -0
- package/template/tsconfig.node.json +16 -0
- package/template/vercel.json +5 -0
- 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
|
-
#
|
|
1
|
+
# create-fastang
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
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
|
+
}
|
package/dist/create.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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,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 @@
|
|
|
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,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,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,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
|
+
}
|