@fastang/core 0.0.0-stage → 0.1.0-alpha.0

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 ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0-alpha.0
4
+
5
+ Initial experimental release.
6
+
7
+ - File-based Angular pages, nested layouts, route metadata and Fastify APIs.
8
+ - Angular SSR, hydration, HTTP transfer cache and per-request rendering state.
9
+ - Local API dispatch through Fastify with validation and serialization.
10
+ - Development CLI, Node production builds and explicit prerendering.
11
+ - Vercel Build Output API adapter with shared or per-route functions.
12
+ - Compiled ESM exports and TypeScript declarations, independently installable.
13
+
14
+ Targets Angular 22.2.3, Vite 8.3.4 and Fastify 5.12.7 or later in the 5.x line.
15
+ Node 22.22.3+, 24.15+ and 26+ are supported according to package.json.
16
+ The public API is experimental and may change between alpha releases.
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,54 @@
1
- # Temporary Holding Version
1
+ # @fastang/core
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
+ Experimental FastAng runtime and tooling for Angular + Fastify applications.
4
+ Version 0.1.0-alpha.0 is an experimental alpha. Public APIs may change between alpha releases.
5
+
6
+ The package contains compiled JavaScript and TypeScript declarations. Angular,
7
+ Fastify, Vite and the Angular compiler are explicit peer dependencies so the app
8
+ uses one instance of each runtime.
9
+
10
+ ## Installation
11
+
12
+ Create an app using the matching generator release:
13
+
14
+ ```sh
15
+ npm create fastang@alpha my-app -- --install
16
+ cd my-app
17
+ npm run dev
18
+ ```
19
+
20
+ Registry installation is available once both alpha packages have been published. The local tarball workflow below also works before publication.
21
+
22
+ ## Public entry points
23
+
24
+ - `@fastang/core`: API definitions, configuration, HTTP server factories, Node handler, listen and rendering context types.
25
+ - `@fastang/core/angular`: `createRenderer(RootComponent, createAppConfig)` with per-request providers, HTTP transfer cache and router redirects.
26
+ - `@fastang/core/vite`: `defineFastangConfig()` for Angular compilation, generated routes and server/client boundaries.
27
+ - `@fastang/core/config`, `/api`, `/server`, `/http-server`, `/node-handler`, `/listen`, `/render-context`: focused imports.
28
+ - `/routes`, `/adapters/vercel`, `/adapters/vercel-plan`, `/vite-plugin`: lower-level tooling APIs; still experimental.
29
+
30
+ `createServer()` and `createApiServer()` accept a `registerApi(app)` callback.
31
+ The framework never imports application files; the generated `.fastang/api.ts`
32
+ module belongs to the consuming app.
33
+
34
+ ## Commands
35
+
36
+ `fastang generate` generates routes in the current application. `fastang dev`
37
+ starts Vite and Fastify on port 3278 and restarts the backend after changes.
38
+ `fastang build` builds the client, Node server and explicit prerender URLs.
39
+ `fastang build --vercel` also creates the Vercel Build Output API directory.
40
+ Run Angular and TypeScript checks before a build, as shown in the starter.
41
+
42
+ The current conventions expect `fastang.config.ts`, `vite.config.ts`,
43
+ `src/main.server.ts`, `src/server/{index,start,api-entry,vercel-entry,plugins}.ts`
44
+ and `src/app/omitted.server.ts`. The starter supplies these application bindings.
45
+
46
+ To build and create a tarball from the repository root:
47
+
48
+ ```sh
49
+ npm ci
50
+ npm pack --workspace @fastang/core --pack-destination /tmp
51
+ ```
52
+
53
+ Install the tarball into a copy of `examples/starter`. No global CLI, repository
54
+ source files or website assets are required by the installed package.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
@@ -0,0 +1,22 @@
1
+ import type { VercelOptions } from '../config.ts';
2
+ import { type FileRoute } from '../routes.ts';
3
+ export interface FunctionPlan {
4
+ name: string;
5
+ kind: 'api' | 'ssr';
6
+ entry: string;
7
+ paths: string[];
8
+ sources: FileRoute[];
9
+ bundle?: string;
10
+ routesModule?: string;
11
+ renderedComponents?: string[];
12
+ }
13
+ export interface VercelPlan {
14
+ mode: 'shared' | 'routes';
15
+ functions: FunctionPlan[];
16
+ }
17
+ export declare const escapeRegex: (value: string) => string;
18
+ /** Match an entire route; parameter segments never consume a slash. */
19
+ export declare function routePattern(path: string): string;
20
+ export declare function planVercelFunctions(pages: FileRoute[], apis: FileRoute[], prerendered: string[], options?: VercelOptions): VercelPlan;
21
+ /** Generates API entries and component-pruned route graphs for page functions. */
22
+ export declare function prepareVercelPlan(prerendered: string[], options?: VercelOptions, root?: string): Promise<VercelPlan>;
@@ -0,0 +1,90 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { discover, discoverPageTree, pageRoutesSource, renderedPageFiles } from "../routes.js";
5
+ export const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
+ /** Match an entire route; parameter segments never consume a slash. */
7
+ export function routePattern(path) {
8
+ const segments = path.split('/').filter(Boolean);
9
+ return '^/' + segments.map((part) => part.startsWith(':') ? '[^/]+' : part === '*' || part === '**' ? '.*' : escapeRegex(part)).join('/') + (segments.length ? '/?' : '') + '$';
10
+ }
11
+ const signature = (path) => path.replace(/:[^/]+/g, ':param');
12
+ function identifier(path) {
13
+ const normalized = signature(path);
14
+ const slug = normalized.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 45) || 'index';
15
+ return slug + '-' + createHash('sha256').update(normalized).digest('hex').slice(0, 12);
16
+ }
17
+ export function planVercelFunctions(pages, apis, prerendered, options = {}) {
18
+ const mode = options.functions ?? 'shared';
19
+ if (mode !== 'shared' && mode !== 'routes')
20
+ throw new Error('vercel.functions must be shared or routes');
21
+ const prefixes = [...new Set(options.pagePrefixes ?? [''])];
22
+ if (!prefixes.length || prefixes.some((prefix) => prefix !== '' && !/^\/(?:[\w-]+)(?:\/[\w-]+)*$/.test(prefix))) {
23
+ throw new Error('vercel.pagePrefixes must contain empty or absolute static prefixes without a trailing slash');
24
+ }
25
+ if (prefixes.some((prefix) => /^\/(?:api|assets|__fastang)(?:\/|$)/.test(prefix)))
26
+ throw new Error('Reserved Vercel page prefix');
27
+ const functions = [
28
+ { name: mode === 'routes' ? '__fastang/ssr' : 'ssr', kind: 'ssr', entry: 'vercel', paths: [], sources: [] },
29
+ { name: mode === 'routes' ? '__fastang/api' : 'api', kind: 'api', entry: 'api', paths: [], sources: [] },
30
+ ];
31
+ if (mode === 'shared')
32
+ return { mode, functions };
33
+ // Discovery order is already specific-first. Catch-alls stay with the full router.
34
+ const seen = new Map();
35
+ const staticPaths = new Set(prerendered.map((path) => path.replace(/\/$/, '') || '/'));
36
+ for (const page of pages.filter((page) => !page.path.includes('**'))) {
37
+ const paths = prefixes.map((prefix) => prefix + (page.path ? '/' + page.path : '') || '/')
38
+ .filter((path) => !staticPaths.has(path));
39
+ for (const path of paths) {
40
+ if (/^\/(?:api|assets|__fastang)(?:\/|$)/.test(path))
41
+ throw new Error(`Reserved page path: ${path}`);
42
+ const key = signature(path);
43
+ if (seen.has(key))
44
+ throw new Error(`Overlapping Vercel page aliases: ${seen.get(key)} and ${page.file}`);
45
+ seen.set(key, page.file);
46
+ }
47
+ if (paths.length)
48
+ functions.push({ name: '__fastang/pages/' + identifier(page.path), kind: 'ssr', entry: 'vercel', paths, sources: [page] });
49
+ }
50
+ const groups = new Map();
51
+ for (const api of apis) {
52
+ const key = signature(api.path);
53
+ const group = groups.get(key) ?? [];
54
+ group.push(api);
55
+ groups.set(key, group);
56
+ }
57
+ for (const [path, sources] of groups) {
58
+ const id = identifier(path);
59
+ functions.push({ name: '__fastang/endpoints/' + id, kind: 'api', entry: 'endpoint-' + id, paths: ['/api' + (sources[0].path ? '/' + sources[0].path : '')], sources });
60
+ }
61
+ return { mode, functions };
62
+ }
63
+ /** Generates API entries and component-pruned route graphs for page functions. */
64
+ export async function prepareVercelPlan(prerendered, options, root = process.cwd()) {
65
+ const [pages, apis] = await Promise.all([
66
+ discover(resolve(root, 'src/app/pages'), 'page'),
67
+ discover(resolve(root, 'src/server/api'), 'api'),
68
+ ]);
69
+ const plan = planVercelFunctions(pages, apis, prerendered, options);
70
+ const directory = resolve(root, '.fastang/vercel');
71
+ await rm(directory, { recursive: true, force: true });
72
+ await mkdir(directory, { recursive: true });
73
+ const tree = await discoverPageTree(resolve(root, 'src/app/pages'));
74
+ for (const fn of plan.functions.filter((fn) => options?.prunePages !== false && fn.sources.length && fn.kind === 'ssr')) {
75
+ const id = fn.name.split('/').at(-1);
76
+ fn.bundle = `dist/pages/${id}/server/index.js`;
77
+ fn.routesModule = `.fastang/vercel/${id}.routes.ts`;
78
+ fn.renderedComponents = renderedPageFiles(tree, fn.sources[0].file);
79
+ await writeFile(resolve(root, fn.routesModule), pageRoutesSource(tree, {
80
+ pagesImport: '../../src/app/pages/', selectedFile: fn.sources[0].file,
81
+ omittedImport: '../../src/app/omitted.server',
82
+ }));
83
+ }
84
+ for (const fn of plan.functions.filter((fn) => fn.sources.length && fn.kind === 'api')) {
85
+ const imports = fn.sources.map((route, index) => `import route${index} from ${JSON.stringify('../../src/server/api/' + route.file.slice(0, -3))};`).join('\n');
86
+ const registrations = fn.sources.map((route, index) => ` route${index}(app, ${JSON.stringify(route.method)}, ${JSON.stringify('/api' + (route.path ? '/' + route.path : ''))});`).join('\n');
87
+ await writeFile(resolve(directory, fn.entry + '.ts'), `// Generated by FastAng.\nimport { createHttpServer } from '@fastang/core/http-server';\nimport { createNodeHandler } from '@fastang/core/node-handler';\nimport { configureServer } from '../../src/server/plugins.ts';\n${imports}\nexport default createNodeHandler(() => createHttpServer({\n configure: async (app) => {\n await configureServer(app);\n${registrations}\n },\n}));\n`);
88
+ }
89
+ return plan;
90
+ }
@@ -0,0 +1,3 @@
1
+ import { type VercelPlan } from './vercel-plan.ts';
2
+ /** Converts an existing Node build into Vercel Build Output API v3. */
3
+ export declare function buildVercelOutput(plan: VercelPlan, root?: string): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { copyFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { nodeFileTrace } from '@vercel/nft';
5
+ import { staticFile } from "../server.js";
6
+ import { sortAndValidate } from "../routes.js";
7
+ import { escapeRegex, routePattern } from "./vercel-plan.js";
8
+ /** Converts an existing Node build into Vercel Build Output API v3. */
9
+ export async function buildVercelOutput(plan, root = process.cwd()) {
10
+ const output = resolve(root, '.vercel/output');
11
+ const prerendered = JSON.parse(await readFile(resolve(root, 'dist/prerender.json'), 'utf8'));
12
+ // Validate before touching the previous output.
13
+ for (const path of prerendered)
14
+ staticFile(path);
15
+ await rm(output, { recursive: true, force: true });
16
+ const copy = async (file, destination) => {
17
+ await mkdir(dirname(destination), { recursive: true });
18
+ await copyFile(resolve(root, file), destination);
19
+ };
20
+ const { cp } = await import('node:fs/promises');
21
+ await cp(resolve(root, 'dist/client/assets'), resolve(output, 'static/assets'), { recursive: true });
22
+ for (const path of prerendered) {
23
+ const file = staticFile(path);
24
+ await copy(`dist/client/${file}`, resolve(output, 'static', file));
25
+ }
26
+ const report = [];
27
+ const traces = new Map();
28
+ for (const { name, entry, kind, bundle } of plan.functions) {
29
+ const directory = resolve(output, `functions/${name}.func`);
30
+ const entryFile = bundle ?? `dist/server/${entry}.js`;
31
+ // The bundled CommonJS runtime uses a require shim that tracing cannot
32
+ // always recognize. Trace Fastify's original graph for external helpers.
33
+ const fastifyEntry = createRequire(resolve(root, 'package.json')).resolve('fastify');
34
+ const trace = traces.get(entryFile) ?? await nodeFileTrace([entryFile, fastifyEntry], {
35
+ base: root, processCwd: root,
36
+ // Only the private SSR template is needed; public assets stay on the CDN.
37
+ ignore: ['dist/**/client/**', 'dist/client/**', 'dist/**/prerender.json', 'dist/prerender.json'],
38
+ });
39
+ traces.set(entryFile, trace);
40
+ for (const warning of trace.warnings)
41
+ console.warn(`[vercel:${name}] ${warning.message}`);
42
+ for (const file of trace.fileList) {
43
+ if (file.startsWith('../') || file.startsWith('/'))
44
+ throw new Error(`Dependency outside project: ${file}`);
45
+ await copy(file, resolve(directory, file));
46
+ }
47
+ if (kind === 'ssr')
48
+ await copy('dist/client/index.html', resolve(directory, dirname(entryFile), '../client/index.html'));
49
+ await mkdir(directory, { recursive: true });
50
+ await writeFile(resolve(directory, '.vc-config.json'), JSON.stringify({
51
+ runtime: 'nodejs24.x', launcherType: 'Nodejs', handler: entryFile,
52
+ shouldAddHelpers: false, supportsResponseStreaming: true,
53
+ }, null, 2) + '\n');
54
+ const sizes = await Promise.all([...trace.fileList].map(async (file) => ({ file, size: (await stat(resolve(root, file))).size })));
55
+ report.push({
56
+ name, tracedFiles: trace.fileList.size,
57
+ tracedBytes: sizes.reduce((sum, item) => sum + item.size, 0),
58
+ serverJavaScriptBytes: sizes.filter(({ file }) => file.startsWith('dist/') && file.endsWith('.js')).reduce((sum, item) => sum + item.size, 0),
59
+ pageJavaScriptBytes: sizes.filter(({ file }) => file.startsWith('dist/') && /(?:\.page-|\/layout-).*\.js$/.test(file)).reduce((sum, item) => sum + item.size, 0),
60
+ });
61
+ console.log(`Vercel ${name}: ${trace.fileList.size} traced files`);
62
+ }
63
+ await writeFile(resolve(output, 'config.json'), JSON.stringify({
64
+ version: 3,
65
+ routes: [
66
+ { src: '/(.*)', headers: { 'X-Content-Type-Options': 'nosniff' }, continue: true },
67
+ { src: '/assets/(.*)', headers: { 'Cache-Control': 'public, max-age=31536000, immutable' }, continue: true },
68
+ ...prerendered.map((path) => ({
69
+ src: `^${escapeRegex(path)}/?$`, methods: ['GET', 'HEAD'],
70
+ dest: '/' + staticFile(path), headers: { 'X-Fastang-Render': 'prerender', 'Cache-Control': 'public, max-age=0, must-revalidate' },
71
+ })),
72
+ { handle: 'filesystem' },
73
+ ...sortAndValidate(plan.functions.flatMap((fn) => fn.paths.map((path) => ({ file: fn.name, path: path.replace(/^\//, '') })))).map((route) => ({ src: routePattern('/' + route.path), dest: '/' + route.file })),
74
+ { src: '^/api(?:/.*)?$', dest: '/' + plan.functions.find((fn) => fn.kind === 'api' && !fn.paths.length).name },
75
+ { src: '/(.*)', dest: '/' + plan.functions.find((fn) => fn.kind === 'ssr' && !fn.paths.length).name },
76
+ ],
77
+ }, null, 2) + '\n');
78
+ await writeFile(resolve(root, 'dist/vercel-bundle-report.json'), JSON.stringify({ version: 1, functions: report }, null, 2) + '\n');
79
+ await writeFile(resolve(root, 'dist/vercel-manifest.json'), JSON.stringify(plan, null, 2) + '\n');
80
+ }
@@ -0,0 +1,3 @@
1
+ import type { ApplicationConfig, Type } from '@angular/core';
2
+ import type { RenderContext } from './render-context.ts';
3
+ export declare function createRenderer(App: Type<unknown>, createAppConfig: () => ApplicationConfig): (url: string, document: string, context: RenderContext) => Promise<string>;
@@ -0,0 +1,55 @@
1
+ import { REQUEST_CONTEXT, DestroyRef, inject, mergeApplicationConfig, provideAppInitializer } from '@angular/core';
2
+ import { NavigationEnd, Router } from '@angular/router';
3
+ import { bootstrapApplication } from '@angular/platform-browser';
4
+ import { provideServerRendering, renderApplication } from '@angular/platform-server';
5
+ import { HttpBackend, FetchBackend, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
6
+ import { from, map } from 'rxjs';
7
+ // Il backend opera DOPO gli interceptor Angular, inclusa la transfer cache nativa.
8
+ // Un interceptor che risponde prima di next() scavalcherebbe quella cache.
9
+ function localApiBackend() {
10
+ const upstream = inject(FetchBackend);
11
+ const context = inject(REQUEST_CONTEXT);
12
+ return {
13
+ handle(request) {
14
+ const target = new URL(request.urlWithParams, 'http://fastang.local');
15
+ if (target.origin !== 'http://fastang.local' || !target.pathname.startsWith('/api/'))
16
+ return upstream.handle(request);
17
+ const headers = Object.fromEntries(request.headers.keys().map((key) => [key, request.headers.get(key)]));
18
+ const contentType = request.detectContentTypeHeader();
19
+ if (contentType && !request.headers.has('content-type'))
20
+ headers['content-type'] = contentType;
21
+ return from(context.dispatchApi(target.pathname + target.search, request.method, request.body, headers)).pipe(map((response) => {
22
+ const responseHeaders = new HttpHeaders(response.headers);
23
+ if (response.status >= 400)
24
+ throw new HttpErrorResponse({ status: response.status, error: response.body, headers: responseHeaders, url: request.url });
25
+ return new HttpResponse({ status: response.status, body: response.body, headers: responseHeaders, url: request.url });
26
+ }));
27
+ },
28
+ };
29
+ }
30
+ export function createRenderer(App, createAppConfig) {
31
+ return async function render(url, document, context) {
32
+ // I provider con stato mutabile vengono ricreati per ogni applicazione SSR.
33
+ const config = mergeApplicationConfig(createAppConfig(), {
34
+ providers: [
35
+ provideServerRendering(),
36
+ { provide: HttpBackend, useFactory: localApiBackend },
37
+ provideAppInitializer(() => {
38
+ const router = inject(Router);
39
+ const target = new URL(url);
40
+ const incoming = router.serializeUrl(router.parseUrl(target.pathname + target.search + target.hash));
41
+ const subscription = router.events.subscribe((event) => {
42
+ if (event instanceof NavigationEnd) {
43
+ // Confronta URL canoniche Angular, non la codifica originale della query.
44
+ context.redirect = event.urlAfterRedirects === incoming
45
+ ? undefined
46
+ : { location: event.urlAfterRedirects, status: 302 };
47
+ }
48
+ });
49
+ inject(DestroyRef).onDestroy(() => subscription.unsubscribe());
50
+ }),
51
+ ],
52
+ });
53
+ return renderApplication((bootstrapContext) => bootstrapApplication(App, config, bootstrapContext), { url, document, allowedHosts: ['fastang.local'], platformProviders: [{ provide: REQUEST_CONTEXT, useValue: context }] });
54
+ };
55
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { FastifyInstance, FastifySchema, HTTPMethods, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteGenericInterface, RouteHandlerMethod } from 'fastify';
2
+ type Handler<T extends RouteGenericInterface> = RouteHandlerMethod<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, T>;
3
+ export declare function defineApiRoute<T extends RouteGenericInterface>(options: {
4
+ schema: FastifySchema;
5
+ handler: Handler<T>;
6
+ }): (app: FastifyInstance, method: HTTPMethods, url: string) => void;
7
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,4 @@
1
+ // I tipi rimangono nel closure: nessun cast quando si registra la rotta generata.
2
+ export function defineApiRoute(options) {
3
+ return (app, method, url) => { app.route({ ...options, method, url }); };
4
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ const command = process.argv[2];
3
+ try {
4
+ if (command === 'generate')
5
+ await (await import("./routes.js")).generate();
6
+ else if (command === 'dev')
7
+ await import("./commands/dev.js");
8
+ else if (command === 'build')
9
+ await import("./commands/build.js");
10
+ else if (!command || command === '--help' || command === '-h')
11
+ console.log('Usage: fastang <dev|generate|build> [--vercel]');
12
+ else
13
+ throw new Error(`Unknown command: ${command}`);
14
+ }
15
+ catch (error) {
16
+ console.error(error instanceof Error ? error.message : error);
17
+ process.exitCode = 1;
18
+ }
19
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
10
+ import { resolve, dirname } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ import { build } from 'vite';
13
+ import { loadConfigFromFile } from 'vite';
14
+ import { prepareVercelPlan } from "../adapters/vercel-plan.js";
15
+ import { buildVercelOutput } from "../adapters/vercel.js";
16
+ import { staticFile } from "../server.js";
17
+ const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, 'fastang.config.ts');
18
+ if (!loaded)
19
+ throw new Error('Missing fastang.config.ts');
20
+ const config = loaded.config;
21
+ const vercel = process.argv.includes('--vercel');
22
+ await rm('dist', { recursive: true, force: true });
23
+ const paths = [...new Set(config.prerender)];
24
+ for (const path of paths) {
25
+ staticFile(path);
26
+ if (path === '/' || path === '/api' || path.startsWith('/api/') || path.startsWith('/assets/')) {
27
+ throw new Error(`Percorso riservato al runtime: ${path}`);
28
+ }
29
+ }
30
+ const vercelPlan = vercel ? await prepareVercelPlan(paths, config.vercel) : undefined;
31
+ const endpointEntries = Object.fromEntries((vercelPlan?.functions ?? []).filter((fn) => fn.entry.startsWith('endpoint-')).map((fn) => [fn.entry, `.fastang/vercel/${fn.entry}.ts`]));
32
+ await build({ build: { outDir: 'dist/client' } });
33
+ await build({ build: {
34
+ outDir: 'dist/server', minify: true, ssr: vercel ? true : 'src/server/start.ts',
35
+ rollupOptions: {
36
+ ...(vercel ? { input: { index: 'src/server/start.ts', vercel: 'src/server/vercel-entry.ts', api: 'src/server/api-entry.ts', ...endpointEntries } } : {}),
37
+ output: { entryFileNames: vercel ? '[name].js' : 'index.js', ...(vercel ? { chunkFileNames: '[name]-[hash].js' } : {}) },
38
+ },
39
+ } });
40
+ // Each page build has its own module graph. A multi-entry build would pull
41
+ // unrelated lazy components back in through the full-router fallback entry.
42
+ for (const fn of vercelPlan?.functions ?? []) {
43
+ if (!fn.bundle || !fn.routesModule)
44
+ continue;
45
+ await build({
46
+ resolve: { alias: [{ find: /^\.\/fastang\.routes$/, replacement: resolve(fn.routesModule) }] },
47
+ build: {
48
+ outDir: dirname(fn.bundle), minify: true, ssr: 'src/server/vercel-entry.ts',
49
+ rollupOptions: { output: { entryFileNames: 'index.js', chunkFileNames: '[name]-[hash].js' } },
50
+ },
51
+ });
52
+ }
53
+ const { createProductionServer } = await import(__rewriteRelativeImportExtension(pathToFileURL(resolve('dist/server/index.js')).href));
54
+ const app = await createProductionServer({ prerender: false });
55
+ try {
56
+ for (const path of paths) {
57
+ const response = await app.inject({ url: path });
58
+ if (response.statusCode !== 200)
59
+ throw new Error(`Prerender ${path}: HTTP ${response.statusCode}`);
60
+ const target = resolve('dist/client', staticFile(path));
61
+ await mkdir(dirname(target), { recursive: true });
62
+ await writeFile(target, response.body);
63
+ console.log(`Prerendered ${path}`);
64
+ }
65
+ await writeFile('dist/prerender.json', JSON.stringify(paths, null, 2) + '\n');
66
+ }
67
+ finally {
68
+ await app.close();
69
+ }
70
+ if (vercelPlan)
71
+ await buildVercelOutput(vercelPlan);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createServer as createViteServer } from 'vite';
3
+ import middie from '@fastify/middie';
4
+ import { createServer } from "../server.js";
5
+ import { listen } from "../listen.js";
6
+ import { tsImport } from 'tsx/esm/api';
7
+ import { resolve } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ const { configureServer } = await tsImport(pathToFileURL(resolve('src/server/plugins.ts')).href, import.meta.url);
10
+ const { registerApi } = await tsImport(pathToFileURL(resolve('.fastang/api.ts')).href, import.meta.url);
11
+ let vite;
12
+ try {
13
+ const app = await createServer({
14
+ logger: true, registerApi,
15
+ async configure(server) {
16
+ vite = await createViteServer({ server: { middlewareMode: true, hmr: { server: server.server } }, appType: 'custom' });
17
+ server.addHook('onClose', async () => { await vite?.close(); });
18
+ await server.register(middie);
19
+ // Middie gestisce next(), risposte completate ed errori del middleware Connect.
20
+ server.use(vite.middlewares);
21
+ await configureServer(server);
22
+ },
23
+ async template(url) { return vite.transformIndexHtml(url, await readFile('index.html', 'utf8')); },
24
+ async render(url, document, context) {
25
+ try {
26
+ const module = await vite.ssrLoadModule('/src/main.server.ts');
27
+ return await module['render'](url, document, context);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof Error)
31
+ vite.ssrFixStacktrace(error);
32
+ throw error;
33
+ }
34
+ },
35
+ });
36
+ await listen(app);
37
+ }
38
+ catch (error) {
39
+ await vite?.close();
40
+ throw error;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { watch } from 'node:fs';
4
+ import { spawn } from 'node:child_process';
5
+ import { once } from 'node:events';
6
+ let child;
7
+ let stopping = false;
8
+ let timer;
9
+ let queue = Promise.resolve();
10
+ async function stopChild() {
11
+ if (child && child.exitCode === null && child.signalCode === null) {
12
+ const exited = once(child, 'exit');
13
+ child.kill('SIGTERM');
14
+ await exited;
15
+ }
16
+ }
17
+ async function restart() {
18
+ await stopChild();
19
+ if (stopping)
20
+ return;
21
+ // Processo nuovo: anche le modifiche al generatore sono applicate al riavvio.
22
+ const generator = spawn(process.execPath, [fileURLToPath(new URL('../cli.js', import.meta.url)), 'generate'], { stdio: 'inherit', env: process.env });
23
+ const [code] = await once(generator, 'exit');
24
+ if (code !== 0)
25
+ throw new Error('Generazione route fallita');
26
+ child = spawn(process.execPath, [fileURLToPath(new URL('./dev-server.js', import.meta.url))], { stdio: 'inherit', env: process.env });
27
+ child.on('error', (error) => { console.error(error); process.exitCode = 1; });
28
+ }
29
+ function changed() {
30
+ clearTimeout(timer);
31
+ timer = setTimeout(() => {
32
+ queue = queue.then(restart).catch((error) => console.error(error));
33
+ }, 150);
34
+ }
35
+ // Solo sorgenti backend e configurazione: i moduli temporanei di Vite non causano riavvii.
36
+ const watchers = ['src/server', 'src/shared', 'vite.config.ts', 'fastang.config.ts'].filter(existsSync).map(path => watch(path, { recursive: true }, changed));
37
+ async function shutdown() {
38
+ stopping = true;
39
+ clearTimeout(timer);
40
+ watchers.forEach((watcher) => watcher.close());
41
+ await queue;
42
+ await stopChild();
43
+ }
44
+ process.once('SIGINT', shutdown);
45
+ process.once('SIGTERM', shutdown);
46
+ await restart();
@@ -0,0 +1,13 @@
1
+ export interface VercelOptions {
2
+ /** Shared functions are the default; routes creates separate deployment functions. */
3
+ functions?: 'shared' | 'routes';
4
+ /** Public prefixes added by the application router, e.g. ['', '/en']. */
5
+ pagePrefixes?: string[];
6
+ /** Keep only the selected page/layout components in route functions. Default: true. */
7
+ prunePages?: boolean;
8
+ }
9
+ export interface FastangConfig {
10
+ prerender: string[];
11
+ vercel?: VercelOptions;
12
+ }
13
+ export declare function defineConfig(config: FastangConfig): FastangConfig;
package/dist/config.js ADDED
@@ -0,0 +1,3 @@
1
+ export function defineConfig(config) {
2
+ return config;
3
+ }
@@ -0,0 +1,6 @@
1
+ import { type FastifyInstance, type FastifyServerOptions } from 'fastify';
2
+ export interface HttpServerOptions {
3
+ logger?: FastifyServerOptions['logger'];
4
+ configure?: (app: FastifyInstance) => Promise<void> | void;
5
+ }
6
+ export declare function createHttpServer(options?: HttpServerOptions): Promise<FastifyInstance>;
@@ -0,0 +1,19 @@
1
+ import Fastify, {} from 'fastify';
2
+ export async function createHttpServer(options = {}) {
3
+ const app = Fastify({ logger: options.logger ?? false, bodyLimit: 1024 * 1024 });
4
+ try {
5
+ await options.configure?.(app);
6
+ app.setNotFoundHandler((_request, reply) => reply.code(404).send({ error: 'Not found' }));
7
+ app.setErrorHandler((error, request, reply) => {
8
+ request.log.error(error);
9
+ const statusCode = error && typeof error === 'object' && 'statusCode' in error ? error.statusCode : 500;
10
+ const status = typeof statusCode === 'number' && statusCode >= 400 && statusCode < 500 ? statusCode : 500;
11
+ reply.code(status).send({ error: status === 500 ? 'Internal server error' : error instanceof Error ? error.message : 'Bad request' });
12
+ });
13
+ return app;
14
+ }
15
+ catch (error) {
16
+ await app.close();
17
+ throw error;
18
+ }
19
+ }
@@ -0,0 +1,6 @@
1
+ export * from './api.ts';
2
+ export * from './config.ts';
3
+ export * from './server.ts';
4
+ export * from './node-handler.ts';
5
+ export * from './listen.ts';
6
+ export type * from './render-context.ts';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./api.js";
2
+ export * from "./config.js";
3
+ export * from "./server.js";
4
+ export * from "./node-handler.js";
5
+ export * from "./listen.js";
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ /** FAST on a telephone keypad. PORT can override this local convention. */
3
+ export declare const DEFAULT_PORT = 3278;
4
+ export declare function listen(app: FastifyInstance): Promise<void>;