@fastang/core 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +21 -0
- package/README.md +25 -1
- package/dist/adapters/vercel-plan.d.ts +1 -1
- package/dist/adapters/vercel-plan.js +2 -7
- package/dist/angular.js +6 -2
- package/dist/api.d.ts +7 -7
- package/dist/api.js +0 -1
- package/dist/cli.js +3 -1
- package/dist/commands/build.js +6 -13
- package/dist/commands/dev-server.js +2 -0
- package/dist/commands/inspect-routes.d.ts +1 -0
- package/dist/commands/inspect-routes.js +25 -0
- package/dist/commands/load-config.d.ts +3 -0
- package/dist/commands/load-config.js +9 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +51 -1
- package/dist/contract-route.d.ts +7 -0
- package/dist/contract-route.js +8 -0
- package/dist/contracts.d.ts +46 -0
- package/dist/contracts.js +51 -0
- package/dist/http-client.d.ts +28 -0
- package/dist/http-client.js +16 -0
- package/dist/render-context.d.ts +9 -2
- package/dist/routes.d.ts +20 -0
- package/dist/routes.js +27 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +20 -11
- package/dist/static-path.d.ts +1 -0
- package/dist/static-path.js +6 -0
- package/dist/vite-plugin.js +1 -1
- package/package.json +20 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.2
|
|
4
|
+
|
|
5
|
+
- Accept native Fastify per-route options in API and contract helpers, including typed hooks, body limits, route config and HEAD behavior. File routing and shared contract schemas remain authoritative.
|
|
6
|
+
|
|
7
|
+
- Add read-only `fastang routes` inspection with page/layout/metadata ancestry and explicit API methods, including versioned JSON output.
|
|
8
|
+
|
|
9
|
+
- Validate FastAng configuration before starting the development server, using the same loader and checks as production builds.
|
|
10
|
+
|
|
11
|
+
- Optional TypeBox API contracts infer Fastify handler types and Angular JSON client requests and successful responses.
|
|
12
|
+
- Shared contracts reuse runtime schemas, check file-route consistency and encode API URLs explicitly.
|
|
13
|
+
|
|
14
|
+
## 0.1.0-alpha.1
|
|
15
|
+
|
|
16
|
+
- Preserve separate Set-Cookie headers and allow explicit per-dispatch forwarding to HTML responses.
|
|
17
|
+
- Keep inherited credentials out of Angular's default HTTP transfer cache.
|
|
18
|
+
- Treat internal API redirects as HttpClient errors and normalize local dispatch paths.
|
|
19
|
+
- Reject Set-Cookie responses during public prerendering.
|
|
20
|
+
|
|
21
|
+
- Validate prerender and Vercel configuration before changing build output, including plain config exports.
|
|
22
|
+
- Reject unknown options, invalid types, reserved paths and conflicting prerender destinations.
|
|
23
|
+
|
|
3
24
|
## 0.1.0-alpha.0
|
|
4
25
|
|
|
5
26
|
Initial experimental release.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @fastang/core
|
|
2
2
|
|
|
3
3
|
Experimental FastAng runtime and tooling for Angular + Fastify applications.
|
|
4
|
-
Version 0.1.0-alpha.
|
|
4
|
+
Version 0.1.0-alpha.2 is an experimental alpha. Public APIs may change between alpha releases.
|
|
5
5
|
|
|
6
6
|
The package contains compiled JavaScript and TypeScript declarations. Angular,
|
|
7
7
|
Fastify, Vite and the Angular compiler are explicit peer dependencies so the app
|
|
@@ -25,6 +25,7 @@ Registry installation is available once both alpha packages have been published.
|
|
|
25
25
|
- `@fastang/core/angular`: `createRenderer(RootComponent, createAppConfig)` with per-request providers, HTTP transfer cache and router redirects.
|
|
26
26
|
- `@fastang/core/vite`: `defineFastangConfig()` for Angular compilation, generated routes and server/client boundaries.
|
|
27
27
|
- `@fastang/core/config`, `/api`, `/server`, `/http-server`, `/node-handler`, `/listen`, `/render-context`: focused imports.
|
|
28
|
+
- `/contracts`, `/contract-route`, `/http-client`: optional shared TypeBox contracts, Fastify registration and Angular JSON requests (from `0.1.0-alpha.2`). Install `@sinclair/typebox` to use contracts.
|
|
28
29
|
- `/routes`, `/adapters/vercel`, `/adapters/vercel-plan`, `/vite-plugin`: lower-level tooling APIs; still experimental.
|
|
29
30
|
|
|
30
31
|
`createServer()` and `createApiServer()` accept a `registerApi(app)` callback.
|
|
@@ -52,3 +53,26 @@ npm pack --workspace @fastang/core --pack-destination /tmp
|
|
|
52
53
|
|
|
53
54
|
Install the tarball into a copy of `examples/starter`. No global CLI, repository
|
|
54
55
|
source files or website assets are required by the installed package.
|
|
56
|
+
|
|
57
|
+
## Inspect file routes
|
|
58
|
+
|
|
59
|
+
From `0.1.0-alpha.2`, `fastang routes` lists page paths in matching order with their source
|
|
60
|
+
files, ancestor layouts and metadata files, followed by explicit API methods.
|
|
61
|
+
Use `fastang routes --json` for a JSON document with `schemaVersion: 1`, `pages`
|
|
62
|
+
and `api` arrays.
|
|
63
|
+
|
|
64
|
+
Inspection uses the same discovery and collision checks as generation. It does
|
|
65
|
+
not import application modules, evaluate configuration, write generated files,
|
|
66
|
+
build the app or start a server. Paths describe file conventions: custom Angular
|
|
67
|
+
router prefixes, routes registered by plugins and Fastify's implicit HEAD routes
|
|
68
|
+
are not included. `**` denotes an Angular catch-all; API catch-alls use `*`.
|
|
69
|
+
The output does not report prerendering or deployment-function assignment.
|
|
70
|
+
|
|
71
|
+
## Native API route options
|
|
72
|
+
|
|
73
|
+
From `0.1.0-alpha.2`, `defineApiRoute()` forwards native Fastify route options, including hooks,
|
|
74
|
+
`bodyLimit`, `config`, `exposeHeadRoute` and route error handlers. Method and URL
|
|
75
|
+
remain controlled by file routing. Hooks preserve the request/reply generic types;
|
|
76
|
+
a second generic describes custom route config. Schemas are optional, as in
|
|
77
|
+
Fastify. `defineContractRoute()` from `/contract-route` accepts the same options,
|
|
78
|
+
except that its schema comes from the shared contract.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { validateVercelOptions } from "../config.js";
|
|
4
5
|
import { discover, discoverPageTree, pageRoutesSource, renderedPageFiles } from "../routes.js";
|
|
5
6
|
export const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
7
|
/** Match an entire route; parameter segments never consume a slash. */
|
|
@@ -15,15 +16,9 @@ function identifier(path) {
|
|
|
15
16
|
return slug + '-' + createHash('sha256').update(normalized).digest('hex').slice(0, 12);
|
|
16
17
|
}
|
|
17
18
|
export function planVercelFunctions(pages, apis, prerendered, options = {}) {
|
|
19
|
+
validateVercelOptions(options);
|
|
18
20
|
const mode = options.functions ?? 'shared';
|
|
19
|
-
if (mode !== 'shared' && mode !== 'routes')
|
|
20
|
-
throw new Error('vercel.functions must be shared or routes');
|
|
21
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
22
|
const functions = [
|
|
28
23
|
{ name: mode === 'routes' ? '__fastang/ssr' : 'ssr', kind: 'ssr', entry: 'vercel', paths: [], sources: [] },
|
|
29
24
|
{ name: mode === 'routes' ? '__fastang/api' : 'api', kind: 'api', entry: 'api', paths: [], sources: [] },
|
package/dist/angular.js
CHANGED
|
@@ -19,8 +19,12 @@ function localApiBackend() {
|
|
|
19
19
|
if (contentType && !request.headers.has('content-type'))
|
|
20
20
|
headers['content-type'] = contentType;
|
|
21
21
|
return from(context.dispatchApi(target.pathname + target.search, request.method, request.body, headers)).pipe(map((response) => {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
let responseHeaders = new HttpHeaders(response.headers);
|
|
23
|
+
// Inherited credentials were invisible to the transfer-cache request check.
|
|
24
|
+
// Mark the response private so Angular's native response check excludes it.
|
|
25
|
+
if (response.credentialed)
|
|
26
|
+
responseHeaders = responseHeaders.set('cache-control', 'private, no-store');
|
|
27
|
+
if (response.status < 200 || response.status >= 300)
|
|
24
28
|
throw new HttpErrorResponse({ status: response.status, error: response.body, headers: responseHeaders, url: request.url });
|
|
25
29
|
return new HttpResponse({ status: response.status, body: response.body, headers: responseHeaders, url: request.url });
|
|
26
30
|
}));
|
package/dist/api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { FastifyInstance,
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export
|
|
1
|
+
import type { FastifyInstance, HTTPMethods, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteGenericInterface, RouteOptions } from 'fastify';
|
|
2
|
+
/** Native Fastify route options; the file convention owns method and URL. */
|
|
3
|
+
export type ApiRouteOptions<T extends RouteGenericInterface = RouteGenericInterface, ContextConfig = Record<string, unknown>> = Omit<RouteOptions<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, T, ContextConfig>, 'method' | 'url'> & {
|
|
4
|
+
method?: never;
|
|
5
|
+
url?: never;
|
|
6
|
+
};
|
|
7
|
+
export declare function defineApiRoute<T extends RouteGenericInterface, ContextConfig = Record<string, unknown>>(options: ApiRouteOptions<T, ContextConfig>): (app: FastifyInstance, method: HTTPMethods, url: string) => void;
|
package/dist/api.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,14 @@ const command = process.argv[2];
|
|
|
3
3
|
try {
|
|
4
4
|
if (command === 'generate')
|
|
5
5
|
await (await import("./routes.js")).generate();
|
|
6
|
+
else if (command === 'routes')
|
|
7
|
+
await import("./commands/inspect-routes.js");
|
|
6
8
|
else if (command === 'dev')
|
|
7
9
|
await import("./commands/dev.js");
|
|
8
10
|
else if (command === 'build')
|
|
9
11
|
await import("./commands/build.js");
|
|
10
12
|
else if (!command || command === '--help' || command === '-h')
|
|
11
|
-
console.log('Usage: fastang <dev|generate|build
|
|
13
|
+
console.log('Usage: fastang <dev|generate|build|routes>\n build [--vercel]\n routes [--json]');
|
|
12
14
|
else
|
|
13
15
|
throw new Error(`Unknown command: ${command}`);
|
|
14
16
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -10,25 +10,16 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
|
10
10
|
import { resolve, dirname } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { build } from 'vite';
|
|
13
|
-
import { loadConfigFromFile } from 'vite';
|
|
14
13
|
import { prepareVercelPlan } from "../adapters/vercel-plan.js";
|
|
15
14
|
import { buildVercelOutput } from "../adapters/vercel.js";
|
|
16
|
-
import { staticFile } from "../
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw new Error('Missing fastang.config.ts');
|
|
20
|
-
const config = loaded.config;
|
|
15
|
+
import { staticFile } from "../static-path.js";
|
|
16
|
+
import { loadFastangConfig } from "./load-config.js";
|
|
17
|
+
const config = await loadFastangConfig('build');
|
|
21
18
|
const vercel = process.argv.includes('--vercel');
|
|
22
|
-
await rm('dist', { recursive: true, force: true });
|
|
23
19
|
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
20
|
const vercelPlan = vercel ? await prepareVercelPlan(paths, config.vercel) : undefined;
|
|
31
21
|
const endpointEntries = Object.fromEntries((vercelPlan?.functions ?? []).filter((fn) => fn.entry.startsWith('endpoint-')).map((fn) => [fn.entry, `.fastang/vercel/${fn.entry}.ts`]));
|
|
22
|
+
await rm('dist', { recursive: true, force: true });
|
|
32
23
|
await build({ build: { outDir: 'dist/client' } });
|
|
33
24
|
await build({ build: {
|
|
34
25
|
outDir: 'dist/server', minify: true, ssr: vercel ? true : 'src/server/start.ts',
|
|
@@ -57,6 +48,8 @@ try {
|
|
|
57
48
|
const response = await app.inject({ url: path });
|
|
58
49
|
if (response.statusCode !== 200)
|
|
59
50
|
throw new Error(`Prerender ${path}: HTTP ${response.statusCode}`);
|
|
51
|
+
if (response.headers['set-cookie'])
|
|
52
|
+
throw new Error(`Prerender ${path}: Set-Cookie is not allowed in public HTML`);
|
|
60
53
|
const target = resolve('dist/client', staticFile(path));
|
|
61
54
|
await mkdir(dirname(target), { recursive: true });
|
|
62
55
|
await writeFile(target, response.body);
|
|
@@ -6,6 +6,8 @@ import { listen } from "../listen.js";
|
|
|
6
6
|
import { tsImport } from 'tsx/esm/api';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { loadFastangConfig } from "./load-config.js";
|
|
10
|
+
await loadFastangConfig('serve');
|
|
9
11
|
const { configureServer } = await tsImport(pathToFileURL(resolve('src/server/plugins.ts')).href, import.meta.url);
|
|
10
12
|
const { registerApi } = await tsImport(pathToFileURL(resolve('.fastang/api.ts')).href, import.meta.url);
|
|
11
13
|
let vite;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { inspectRoutes } from "../routes.js";
|
|
2
|
+
const args = process.argv.slice(3);
|
|
3
|
+
if (args.length > 1 || args.some(argument => argument !== '--json'))
|
|
4
|
+
throw new Error('Usage: fastang routes [--json]');
|
|
5
|
+
const manifest = await inspectRoutes();
|
|
6
|
+
if (args.includes('--json'))
|
|
7
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
8
|
+
else {
|
|
9
|
+
console.log('File-based page routes (matching order)');
|
|
10
|
+
for (const page of manifest.pages) {
|
|
11
|
+
console.log(`PAGE ${page.path} ${page.file}`);
|
|
12
|
+
for (const layout of page.layouts)
|
|
13
|
+
console.log(` layout ${layout.path} ${layout.file}${layout.metadata ? ` metadata: ${layout.metadata}` : ''}`);
|
|
14
|
+
if (page.metadata)
|
|
15
|
+
console.log(` metadata ${page.metadata}`);
|
|
16
|
+
}
|
|
17
|
+
if (!manifest.pages.length)
|
|
18
|
+
console.log('(none)');
|
|
19
|
+
console.log('\nFile-based API routes');
|
|
20
|
+
for (const route of manifest.api)
|
|
21
|
+
console.log(`${route.method.padEnd(7)} ${route.path} ${route.file}`);
|
|
22
|
+
if (!manifest.api.length)
|
|
23
|
+
console.log('(none)');
|
|
24
|
+
console.log('\nFile conventions only: application router prefixes, plugin routes and implicit HEAD routes are not included.');
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { loadConfigFromFile } from 'vite';
|
|
2
|
+
import { validateConfig } from "../config.js";
|
|
3
|
+
/** Use the same loader and validation before initializing either server or build. */
|
|
4
|
+
export async function loadFastangConfig(command) {
|
|
5
|
+
const loaded = await loadConfigFromFile({ command, mode: command === 'build' ? 'production' : 'development' }, 'fastang.config.ts');
|
|
6
|
+
if (!loaded)
|
|
7
|
+
throw new Error('Missing fastang.config.ts');
|
|
8
|
+
return validateConfig(loaded.config);
|
|
9
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -11,3 +11,7 @@ export interface FastangConfig {
|
|
|
11
11
|
vercel?: VercelOptions;
|
|
12
12
|
}
|
|
13
13
|
export declare function defineConfig(config: FastangConfig): FastangConfig;
|
|
14
|
+
/** Also used by the adapter when invoked without defineConfig. */
|
|
15
|
+
export declare function validateVercelOptions(value: unknown): asserts value is VercelOptions;
|
|
16
|
+
/** Validate plain exports too, before the build touches existing output. */
|
|
17
|
+
export declare function validateConfig(value: unknown): FastangConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
import { staticFile } from "./static-path.js";
|
|
1
2
|
export function defineConfig(config) {
|
|
2
|
-
return config;
|
|
3
|
+
return validateConfig(config);
|
|
4
|
+
}
|
|
5
|
+
function object(value, field, keys) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
throw new Error(`${field} must be an object`);
|
|
8
|
+
for (const key of Object.keys(value))
|
|
9
|
+
if (!keys.includes(key))
|
|
10
|
+
throw new Error(`Unknown ${field} option: ${key}`);
|
|
11
|
+
}
|
|
12
|
+
/** Also used by the adapter when invoked without defineConfig. */
|
|
13
|
+
export function validateVercelOptions(value) {
|
|
14
|
+
object(value, 'vercel', ['functions', 'pagePrefixes', 'prunePages']);
|
|
15
|
+
if (value.functions !== undefined && value.functions !== 'shared' && value.functions !== 'routes')
|
|
16
|
+
throw new Error('vercel.functions must be shared or routes');
|
|
17
|
+
if (value.prunePages !== undefined && typeof value.prunePages !== 'boolean')
|
|
18
|
+
throw new Error('vercel.prunePages must be a boolean');
|
|
19
|
+
if (value.pagePrefixes !== undefined) {
|
|
20
|
+
if (!Array.isArray(value.pagePrefixes) || !value.pagePrefixes.length || value.pagePrefixes.some(prefix => typeof prefix !== 'string' || (prefix !== '' && !/^\/(?:[\w-]+)(?:\/[\w-]+)*$/.test(prefix)))) {
|
|
21
|
+
throw new Error('vercel.pagePrefixes must contain empty or absolute static prefixes without a trailing slash');
|
|
22
|
+
}
|
|
23
|
+
if (value.pagePrefixes.some(prefix => /^\/(?:api|assets|__fastang)(?:\/|$)/.test(prefix)))
|
|
24
|
+
throw new Error('Reserved Vercel page prefix');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Validate plain exports too, before the build touches existing output. */
|
|
28
|
+
export function validateConfig(value) {
|
|
29
|
+
object(value, 'FastAng', ['prerender', 'vercel']);
|
|
30
|
+
if (!Array.isArray(value.prerender))
|
|
31
|
+
throw new Error('prerender must be an array of URL paths');
|
|
32
|
+
const targets = new Map();
|
|
33
|
+
for (const [index, path] of value.prerender.entries()) {
|
|
34
|
+
if (typeof path !== 'string')
|
|
35
|
+
throw new Error(`prerender[${index}] must be a URL path string`);
|
|
36
|
+
let target;
|
|
37
|
+
try {
|
|
38
|
+
target = staticFile(path);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`Invalid prerender[${index}] path: ${JSON.stringify(path)}`);
|
|
42
|
+
}
|
|
43
|
+
if (path === '/' || /^\/(?:api|assets|__fastang)(?:\/|$)/.test(path))
|
|
44
|
+
throw new Error(`Reserved prerender path: ${path}`);
|
|
45
|
+
const previous = targets.get(target);
|
|
46
|
+
if (previous !== undefined && previous !== path)
|
|
47
|
+
throw new Error(`Conflicting prerender paths: ${previous} and ${path}`);
|
|
48
|
+
targets.set(target, path);
|
|
49
|
+
}
|
|
50
|
+
if (value.vercel !== undefined)
|
|
51
|
+
validateVercelOptions(value.vercel);
|
|
52
|
+
return value;
|
|
3
53
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyInstance, HTTPMethods } from 'fastify';
|
|
2
|
+
import type { ApiRouteOptions } from './api.ts';
|
|
3
|
+
import type { ApiContract, ApiRouteTypes } from './contracts.ts';
|
|
4
|
+
/** Infer handler types and reuse validation/serialization from the shared contract. */
|
|
5
|
+
export declare function defineContractRoute<C extends ApiContract, ContextConfig = Record<string, unknown>>(contract: C, options: Omit<ApiRouteOptions<ApiRouteTypes<C>, ContextConfig>, 'schema'> & {
|
|
6
|
+
schema?: never;
|
|
7
|
+
}): (app: FastifyInstance, method: HTTPMethods, url: string) => void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Infer handler types and reuse validation/serialization from the shared contract. */
|
|
2
|
+
export function defineContractRoute(contract, options) {
|
|
3
|
+
return (app, method, url) => {
|
|
4
|
+
if (method !== contract.method || url !== contract.path)
|
|
5
|
+
throw new Error(`API contract mismatch: file route ${method} ${url}, contract ${contract.method} ${contract.path}`);
|
|
6
|
+
app.route({ ...options, method, url, schema: contract.schema });
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Static, TSchema } from '@sinclair/typebox';
|
|
2
|
+
export interface ApiSchemas {
|
|
3
|
+
params?: TSchema;
|
|
4
|
+
querystring?: TSchema;
|
|
5
|
+
body?: TSchema;
|
|
6
|
+
response: Record<number, TSchema>;
|
|
7
|
+
}
|
|
8
|
+
export interface ApiContract<S extends ApiSchemas = ApiSchemas> {
|
|
9
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
10
|
+
path: string;
|
|
11
|
+
schema: S;
|
|
12
|
+
}
|
|
13
|
+
type Field<S, K extends PropertyKey> = S extends Record<K, infer T extends TSchema> ? Static<T> : never;
|
|
14
|
+
type InputFields<C extends ApiContract> = (C['schema'] extends {
|
|
15
|
+
params: TSchema;
|
|
16
|
+
} ? {
|
|
17
|
+
params: Field<C['schema'], 'params'>;
|
|
18
|
+
} : {}) & (C['schema'] extends {
|
|
19
|
+
querystring: TSchema;
|
|
20
|
+
} ? {
|
|
21
|
+
query: Field<C['schema'], 'querystring'>;
|
|
22
|
+
} : {}) & (C['schema'] extends {
|
|
23
|
+
body: TSchema;
|
|
24
|
+
} ? {
|
|
25
|
+
body: Field<C['schema'], 'body'>;
|
|
26
|
+
} : {});
|
|
27
|
+
export type ApiInput<C extends ApiContract> = keyof InputFields<C> extends never ? Record<string, never> : InputFields<C>;
|
|
28
|
+
type UrlFields<C extends ApiContract> = Pick<InputFields<C>, Extract<keyof InputFields<C>, 'params' | 'query'>>;
|
|
29
|
+
export type ApiUrlInput<C extends ApiContract> = keyof UrlFields<C> extends never ? Record<string, never> : UrlFields<C>;
|
|
30
|
+
export type ApiReply<C extends ApiContract> = {
|
|
31
|
+
[K in keyof C['schema']['response']]: C['schema']['response'][K] extends TSchema ? Static<C['schema']['response'][K]> : never;
|
|
32
|
+
};
|
|
33
|
+
export type ApiSuccess<C extends ApiContract> = {
|
|
34
|
+
[K in keyof ApiReply<C>]: K extends string | number ? `${K}` extends `2${string}` ? ApiReply<C>[K] : never : never;
|
|
35
|
+
}[keyof ApiReply<C>];
|
|
36
|
+
export type ApiRouteTypes<C extends ApiContract> = {
|
|
37
|
+
Params: Field<C['schema'], 'params'>;
|
|
38
|
+
Querystring: Field<C['schema'], 'querystring'>;
|
|
39
|
+
Body: Field<C['schema'], 'body'>;
|
|
40
|
+
Reply: ApiReply<C>;
|
|
41
|
+
};
|
|
42
|
+
/** Public JSON schemas only: do not import a server handler into a contract module. */
|
|
43
|
+
export declare function defineApiContract<const S extends ApiSchemas>(contract: ApiContract<S>): ApiContract<S>;
|
|
44
|
+
/** Encodes scalar path values and flat query values; never stringifies objects. */
|
|
45
|
+
export declare function apiUrl<C extends ApiContract>(contract: C, input: ApiUrlInput<C>): string;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const pathPattern = /^\/api\/(?:[\w-]+|:[a-zA-Z_]\w*)(?:\/(?:[\w-]+|:[a-zA-Z_]\w*))*$/;
|
|
2
|
+
/** Public JSON schemas only: do not import a server handler into a contract module. */
|
|
3
|
+
export function defineApiContract(contract) {
|
|
4
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(contract.method))
|
|
5
|
+
throw new Error('Unsupported contract method');
|
|
6
|
+
if (!pathPattern.test(contract.path))
|
|
7
|
+
throw new Error('Contract path must be a local /api/... path with static or named segments');
|
|
8
|
+
const parameters = contract.path.split('/').filter(part => part.startsWith(':')).map(part => part.slice(1));
|
|
9
|
+
const schema = contract.schema.params;
|
|
10
|
+
if (schema && schema.type !== 'object')
|
|
11
|
+
throw new Error('schema.params must be an object schema');
|
|
12
|
+
const properties = Object.keys(schema?.properties ?? {});
|
|
13
|
+
if (new Set(parameters).size !== parameters.length || parameters.length !== properties.length || parameters.some(name => !properties.includes(name) || !schema?.required?.includes(name))) {
|
|
14
|
+
throw new Error('Contract path parameters must match required schema.params properties');
|
|
15
|
+
}
|
|
16
|
+
if (contract.method === 'GET' && contract.schema.body)
|
|
17
|
+
throw new Error('GET contracts do not support bodies');
|
|
18
|
+
if (!Object.keys(contract.schema.response).some(status => /^2\d\d$/.test(status)) || Object.keys(contract.schema.response).some(status => !/^[1-5]\d\d$/.test(status)))
|
|
19
|
+
throw new Error('Contract response schemas need HTTP statuses and at least one 2xx response');
|
|
20
|
+
return contract;
|
|
21
|
+
}
|
|
22
|
+
/** Encodes scalar path values and flat query values; never stringifies objects. */
|
|
23
|
+
export function apiUrl(contract, input) {
|
|
24
|
+
if (!pathPattern.test(contract.path))
|
|
25
|
+
throw new Error('Invalid local API contract path');
|
|
26
|
+
const values = input;
|
|
27
|
+
const names = contract.path.split('/').filter(part => part.startsWith(':')).map(part => part.slice(1));
|
|
28
|
+
if (Object.keys(values.params ?? {}).some(name => !names.includes(name)))
|
|
29
|
+
throw new Error('Unknown API path parameter');
|
|
30
|
+
const scalar = (value) => {
|
|
31
|
+
if (typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)))
|
|
32
|
+
return String(value);
|
|
33
|
+
throw new Error('API URL values must be strings, finite numbers or booleans');
|
|
34
|
+
};
|
|
35
|
+
const path = contract.path.split('/').map(part => {
|
|
36
|
+
if (!part.startsWith(':'))
|
|
37
|
+
return part;
|
|
38
|
+
const value = scalar(values.params?.[part.slice(1)]);
|
|
39
|
+
if (!value || value === '.' || value === '..')
|
|
40
|
+
throw new Error('Empty or dot path parameters are not supported');
|
|
41
|
+
return encodeURIComponent(value);
|
|
42
|
+
}).join('/');
|
|
43
|
+
const query = new URLSearchParams();
|
|
44
|
+
for (const [key, value] of Object.entries(values.query ?? {})) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
48
|
+
query.append(key, scalar(item));
|
|
49
|
+
}
|
|
50
|
+
return path + (query.size ? '?' + query.toString() : '');
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpClient, type HttpContext, type HttpHeaders } from '@angular/common/http';
|
|
2
|
+
import type { Observable } from 'rxjs';
|
|
3
|
+
import { type ApiContract, type ApiInput, type ApiSuccess } from './contracts.ts';
|
|
4
|
+
export interface ApiRequestOptions {
|
|
5
|
+
headers?: HttpHeaders | Record<string, string | string[]>;
|
|
6
|
+
context?: HttpContext;
|
|
7
|
+
withCredentials?: boolean;
|
|
8
|
+
credentials?: RequestCredentials;
|
|
9
|
+
transferCache?: boolean | {
|
|
10
|
+
includeHeaders?: string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
type Arguments<C extends ApiContract> = C['schema'] extends {
|
|
14
|
+
params: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
querystring: unknown;
|
|
17
|
+
} | {
|
|
18
|
+
body: unknown;
|
|
19
|
+
} ? [input: ApiInput<C>, options?: ApiRequestOptions] : [input?: ApiInput<C>, options?: ApiRequestOptions];
|
|
20
|
+
/** Thin JSON client: native HttpClient interceptors, errors and transfer cache apply. */
|
|
21
|
+
export declare function createApiClient(http: HttpClient): {
|
|
22
|
+
request<C extends ApiContract>(contract: C, ...args: Arguments<C>): Observable<ApiSuccess<C>>;
|
|
23
|
+
};
|
|
24
|
+
/** Call in an Angular injection context, just like inject(HttpClient). */
|
|
25
|
+
export declare function injectApiClient(): {
|
|
26
|
+
request<C extends ApiContract>(contract: C, ...args: Arguments<C>): Observable<ApiSuccess<C>>;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { inject } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { apiUrl } from "./contracts.js";
|
|
4
|
+
/** Thin JSON client: native HttpClient interceptors, errors and transfer cache apply. */
|
|
5
|
+
export function createApiClient(http) {
|
|
6
|
+
return {
|
|
7
|
+
request(contract, ...args) {
|
|
8
|
+
const [input = {}, options = {}] = args;
|
|
9
|
+
return http.request(contract.method, apiUrl(contract, input), {
|
|
10
|
+
...options, body: input.body, observe: 'body', responseType: 'json',
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Call in an Angular injection context, just like inject(HttpClient). */
|
|
16
|
+
export function injectApiClient() { return createApiClient(inject(HttpClient)); }
|
package/dist/render-context.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export interface ApiDispatchResult {
|
|
2
2
|
status: number;
|
|
3
3
|
body: unknown;
|
|
4
|
-
|
|
4
|
+
/** Set-Cookie values remain separate, including Expires commas. */
|
|
5
|
+
headers: Record<string, string | string[]>;
|
|
6
|
+
/** Credentials actually sent to the internal endpoint, including inherited ones. */
|
|
7
|
+
credentialed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ApiDispatchOptions {
|
|
10
|
+
/** Append internal Set-Cookie headers to this HTML response only. Default: false. */
|
|
11
|
+
forwardSetCookie?: boolean;
|
|
5
12
|
}
|
|
6
13
|
export interface RenderContext {
|
|
7
14
|
status: number;
|
|
@@ -9,5 +16,5 @@ export interface RenderContext {
|
|
|
9
16
|
location: string;
|
|
10
17
|
status?: 301 | 302 | 303 | 307 | 308;
|
|
11
18
|
};
|
|
12
|
-
dispatchApi(path: string, method: string, body: unknown, headers: Record<string, string
|
|
19
|
+
dispatchApi(path: string, method: string, body: unknown, headers: Record<string, string>, options?: ApiDispatchOptions): Promise<ApiDispatchResult>;
|
|
13
20
|
}
|
package/dist/routes.d.ts
CHANGED
|
@@ -16,6 +16,26 @@ export interface PageRouteNode {
|
|
|
16
16
|
}
|
|
17
17
|
export declare function buildPageTree(files: string[]): PageRouteNode[];
|
|
18
18
|
export declare function discoverPageTree(directory: string): Promise<PageRouteNode[]>;
|
|
19
|
+
export interface RouteInspection {
|
|
20
|
+
schemaVersion: 1;
|
|
21
|
+
pages: {
|
|
22
|
+
path: string;
|
|
23
|
+
file: string;
|
|
24
|
+
metadata?: string;
|
|
25
|
+
layouts: {
|
|
26
|
+
path: string;
|
|
27
|
+
file: string;
|
|
28
|
+
metadata?: string;
|
|
29
|
+
}[];
|
|
30
|
+
}[];
|
|
31
|
+
api: {
|
|
32
|
+
method: string;
|
|
33
|
+
path: string;
|
|
34
|
+
file: string;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
/** Inspect file conventions without evaluating application modules or writing files. */
|
|
38
|
+
export declare function inspectRoutes(root?: string): Promise<RouteInspection>;
|
|
19
39
|
export interface PageRoutesSourceOptions {
|
|
20
40
|
pagesImport: string;
|
|
21
41
|
selectedFile?: string;
|
package/dist/routes.js
CHANGED
|
@@ -177,6 +177,33 @@ export function buildPageTree(files) {
|
|
|
177
177
|
export async function discoverPageTree(directory) {
|
|
178
178
|
return buildPageTree(await listFiles(directory));
|
|
179
179
|
}
|
|
180
|
+
/** Inspect file conventions without evaluating application modules or writing files. */
|
|
181
|
+
export async function inspectRoutes(root = process.cwd()) {
|
|
182
|
+
const [tree, endpoints] = await Promise.all([
|
|
183
|
+
discoverPageTree(resolve(root, 'src/app/pages')),
|
|
184
|
+
discover(resolve(root, 'src/server/api'), 'api'),
|
|
185
|
+
]);
|
|
186
|
+
const pages = [];
|
|
187
|
+
const describe = (node) => ({
|
|
188
|
+
path: '/' + node.fullPath,
|
|
189
|
+
file: 'src/app/pages/' + node.file,
|
|
190
|
+
...(node.metadata ? { metadata: 'src/app/pages/' + node.metadata } : {}),
|
|
191
|
+
});
|
|
192
|
+
function visit(nodes, layouts) {
|
|
193
|
+
for (const node of nodes) {
|
|
194
|
+
if (node.kind === 'layout')
|
|
195
|
+
visit(node.children ?? [], [...layouts, describe(node)]);
|
|
196
|
+
else
|
|
197
|
+
pages.push({ ...describe(node), layouts });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
visit(tree, []);
|
|
201
|
+
return {
|
|
202
|
+
schemaVersion: 1,
|
|
203
|
+
pages,
|
|
204
|
+
api: endpoints.map(route => ({ method: route.method, path: '/api' + (route.path ? '/' + route.path : ''), file: 'src/server/api/' + route.file })),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
180
207
|
async function writeChanged(file, content) {
|
|
181
208
|
await mkdir(dirname(file), { recursive: true });
|
|
182
209
|
if (await readFile(file, 'utf8').catch(() => '') !== content)
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { staticFile } from './static-path.ts';
|
|
1
2
|
import { type FastifyInstance, type FastifyServerOptions } from 'fastify';
|
|
2
3
|
import type { RenderContext } from './render-context.ts';
|
|
3
4
|
export type Renderer = (url: string, document: string, context: RenderContext) => Promise<string>;
|
|
@@ -10,6 +11,5 @@ export interface ServerOptions {
|
|
|
10
11
|
prerendered?: string[];
|
|
11
12
|
configure?: (app: FastifyInstance) => Promise<void> | void;
|
|
12
13
|
}
|
|
13
|
-
export declare function staticFile(path: string): string;
|
|
14
14
|
export declare function createApiServer(options?: Pick<ServerOptions, 'logger' | 'configure' | 'registerApi'>): Promise<FastifyInstance>;
|
|
15
15
|
export declare function createServer(options: ServerOptions): Promise<FastifyInstance>;
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import { staticFile } from "./static-path.js";
|
|
2
|
+
export { staticFile } from "./static-path.js";
|
|
1
3
|
import { createHttpServer } from "./http-server.js";
|
|
2
4
|
import {} from 'fastify';
|
|
3
5
|
import { readFile } from 'node:fs/promises';
|
|
4
6
|
import { extname, resolve } from 'node:path';
|
|
5
7
|
import { normalizeRedirectLocation } from "./redirect.js";
|
|
6
|
-
export function staticFile(path) {
|
|
7
|
-
if (!path.startsWith('/') || path.includes('//') || !/^\/[a-zA-Z0-9_/-]*$/.test(path) || path.split('/').includes('..')) {
|
|
8
|
-
throw new Error(`Percorso di prerendering non supportato: ${path}`);
|
|
9
|
-
}
|
|
10
|
-
return `${path.replace(/^\/|\/$/g, '')}${path === '/' ? '' : '/'}index.html`;
|
|
11
|
-
}
|
|
12
8
|
export async function createApiServer(options = {}) {
|
|
13
9
|
return createHttpServer({
|
|
14
10
|
...options,
|
|
@@ -42,9 +38,11 @@ export async function createServer(options) {
|
|
|
42
38
|
const html = await readFile(resolve(options.clientDir, staticPath), 'utf8');
|
|
43
39
|
return reply.header('x-fastang-render', 'prerender').header('cache-control', 'public, max-age=0, must-revalidate').type('text/html; charset=utf-8').send(html);
|
|
44
40
|
}
|
|
41
|
+
const responseCookies = [];
|
|
45
42
|
const context = {
|
|
46
43
|
status: 200,
|
|
47
|
-
async dispatchApi(path, method, body, headers) {
|
|
44
|
+
async dispatchApi(path, method, body, headers, dispatchOptions = {}) {
|
|
45
|
+
path = normalizeRedirectLocation(path);
|
|
48
46
|
if (!path.startsWith('/api/'))
|
|
49
47
|
throw new Error('Solo API locali consentite');
|
|
50
48
|
const forwarded = {};
|
|
@@ -53,18 +51,24 @@ export async function createServer(options) {
|
|
|
53
51
|
if (typeof value === 'string')
|
|
54
52
|
forwarded[name] = value;
|
|
55
53
|
}
|
|
54
|
+
const outgoing = { ...forwarded, ...(body == null ? {} : { 'content-type': 'application/json' }), ...Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])) };
|
|
56
55
|
const response = await app.inject({
|
|
57
56
|
method: method, url: path,
|
|
58
|
-
headers:
|
|
57
|
+
headers: outgoing,
|
|
59
58
|
...(body == null ? {} : { payload: typeof body === 'string' ? body : JSON.stringify(body) }),
|
|
60
59
|
});
|
|
61
60
|
const responseHeaders = {};
|
|
62
61
|
for (const [name, value] of Object.entries(response.headers)) {
|
|
63
62
|
if (value !== undefined)
|
|
64
|
-
responseHeaders[name] = Array.isArray(value) ? value
|
|
63
|
+
responseHeaders[name] = Array.isArray(value) ? [...value] : String(value);
|
|
64
|
+
}
|
|
65
|
+
if (dispatchOptions.forwardSetCookie === true) {
|
|
66
|
+
const cookies = responseHeaders['set-cookie'];
|
|
67
|
+
if (cookies)
|
|
68
|
+
responseCookies.push(...(Array.isArray(cookies) ? cookies : [cookies]));
|
|
65
69
|
}
|
|
66
70
|
const contentType = String(response.headers['content-type'] ?? '');
|
|
67
|
-
return { status: response.statusCode, body: contentType.includes('json') ? response.json() : response.body, headers: responseHeaders };
|
|
71
|
+
return { status: response.statusCode, body: contentType.includes('json') ? response.json() : response.body, headers: responseHeaders, credentialed: !!(outgoing['cookie'] || outgoing['authorization']) };
|
|
68
72
|
},
|
|
69
73
|
};
|
|
70
74
|
const html = await options.render(`http://fastang.local${request.url}`, await options.template(request.url), context);
|
|
@@ -72,9 +76,14 @@ export async function createServer(options) {
|
|
|
72
76
|
const status = context.redirect.status ?? 302;
|
|
73
77
|
if (![301, 302, 303, 307, 308].includes(status))
|
|
74
78
|
throw new Error('Stato redirect non valido');
|
|
79
|
+
const location = normalizeRedirectLocation(context.redirect.location);
|
|
80
|
+
if (responseCookies.length)
|
|
81
|
+
reply.header('set-cookie', responseCookies);
|
|
75
82
|
return reply.header('x-fastang-render', 'redirect').header('cache-control', 'private, no-store')
|
|
76
|
-
.redirect(
|
|
83
|
+
.redirect(location, status);
|
|
77
84
|
}
|
|
85
|
+
if (responseCookies.length)
|
|
86
|
+
reply.header('set-cookie', responseCookies);
|
|
78
87
|
return reply.code(context.status).header('x-fastang-render', 'ssr').header('cache-control', 'private, no-store').type('text/html; charset=utf-8').send(html);
|
|
79
88
|
});
|
|
80
89
|
return app;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function staticFile(path: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function staticFile(path) {
|
|
2
|
+
if (!path.startsWith('/') || path.includes('//') || !/^\/[a-zA-Z0-9_/-]*$/.test(path) || path.split('/').includes('..')) {
|
|
3
|
+
throw new Error(`Percorso di prerendering non supportato: ${path}`);
|
|
4
|
+
}
|
|
5
|
+
return `${path.replace(/^\/|\/$/g, '')}${path === '/' ? '' : '/'}index.html`;
|
|
6
|
+
}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -3,7 +3,7 @@ export function isServerModule(id) {
|
|
|
3
3
|
const path = id.replaceAll('\\', '/').split('?')[0];
|
|
4
4
|
return /\/(src\/server|framework|\.fastang)\//.test(path)
|
|
5
5
|
|| /\.server\.[cm]?[jt]s$/.test(path)
|
|
6
|
-
|| /\/(?:@fastang\/core|packages\/core)\/(?:src|dist)\/(?:index|server|http-server|node-handler|listen|angular|api|routes|vite|vite-plugin|cli|commands\/.*|adapters\/.*)\.[cm]?[jt]s$/.test(path);
|
|
6
|
+
|| /\/(?:@fastang\/core|packages\/core)\/(?:src|dist)\/(?:index|server|http-server|node-handler|listen|angular|api|contract-route|routes|vite|vite-plugin|cli|commands\/.*|adapters\/.*)\.[cm]?[jt]s$/.test(path);
|
|
7
7
|
}
|
|
8
8
|
export function fastang() {
|
|
9
9
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastang/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Angular rendering, Fastify APIs and file-based routing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -78,6 +78,18 @@
|
|
|
78
78
|
"./vite": {
|
|
79
79
|
"types": "./dist/vite.d.ts",
|
|
80
80
|
"import": "./dist/vite.js"
|
|
81
|
+
},
|
|
82
|
+
"./contracts": {
|
|
83
|
+
"types": "./dist/contracts.d.ts",
|
|
84
|
+
"import": "./dist/contracts.js"
|
|
85
|
+
},
|
|
86
|
+
"./contract-route": {
|
|
87
|
+
"types": "./dist/contract-route.d.ts",
|
|
88
|
+
"import": "./dist/contract-route.js"
|
|
89
|
+
},
|
|
90
|
+
"./http-client": {
|
|
91
|
+
"types": "./dist/http-client.d.ts",
|
|
92
|
+
"import": "./dist/http-client.js"
|
|
81
93
|
}
|
|
82
94
|
},
|
|
83
95
|
"scripts": {
|
|
@@ -104,7 +116,8 @@
|
|
|
104
116
|
"vite": "8.3.4",
|
|
105
117
|
"typescript": "~6.0.2",
|
|
106
118
|
"@angular/compiler-cli": "22.2.3",
|
|
107
|
-
"@angular/build": "22.2.3"
|
|
119
|
+
"@angular/build": "22.2.3",
|
|
120
|
+
"@sinclair/typebox": "^0.34.41"
|
|
108
121
|
},
|
|
109
122
|
"repository": {
|
|
110
123
|
"type": "git",
|
|
@@ -119,5 +132,10 @@
|
|
|
119
132
|
"homepage": "https://fastang.dev",
|
|
120
133
|
"bugs": {
|
|
121
134
|
"url": "https://github.com/daclab-dev/fastang/issues"
|
|
135
|
+
},
|
|
136
|
+
"peerDependenciesMeta": {
|
|
137
|
+
"@sinclair/typebox": {
|
|
138
|
+
"optional": true
|
|
139
|
+
}
|
|
122
140
|
}
|
|
123
141
|
}
|