@fastang/core 0.1.0-alpha.1 → 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 +11 -0
- package/README.md +25 -1
- package/dist/api.d.ts +7 -7
- package/dist/api.js +0 -1
- package/dist/cli.js +3 -1
- package/dist/commands/build.js +2 -6
- 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/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/routes.d.ts +20 -0
- package/dist/routes.js +27 -0
- package/dist/vite-plugin.js +1 -1
- package/package.json +20 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 0.1.0-alpha.1
|
|
4
15
|
|
|
5
16
|
- Preserve separate Set-Cookie headers and allow explicit per-dispatch forwarding to HTML responses.
|
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.
|
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,15 +10,11 @@ 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
15
|
import { staticFile } from "../static-path.js";
|
|
17
|
-
import {
|
|
18
|
-
const
|
|
19
|
-
if (!loaded)
|
|
20
|
-
throw new Error('Missing fastang.config.ts');
|
|
21
|
-
const config = validateConfig(loaded.config);
|
|
16
|
+
import { loadFastangConfig } from "./load-config.js";
|
|
17
|
+
const config = await loadFastangConfig('build');
|
|
22
18
|
const vercel = process.argv.includes('--vercel');
|
|
23
19
|
const paths = [...new Set(config.prerender)];
|
|
24
20
|
const vercelPlan = vercel ? await prepareVercelPlan(paths, config.vercel) : undefined;
|
|
@@ -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
|
+
}
|
|
@@ -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/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/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
|
}
|