@fastang/core 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +14 -0
- package/README.md +57 -2
- package/dist/adapters/vercel.js +25 -5
- package/dist/check.d.ts +29 -0
- package/dist/check.js +159 -0
- package/dist/cli.js +3 -1
- package/dist/commands/build.js +6 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +16 -0
- package/dist/commands/dev-logging.d.ts +10 -0
- package/dist/commands/dev-logging.js +13 -0
- package/dist/commands/dev-server.js +11 -2
- package/dist/commands/dev.js +14 -12
- package/dist/commands/stop-child.d.ts +6 -0
- package/dist/commands/stop-child.js +22 -0
- package/dist/listen.js +1 -1
- package/dist/redirect.js +3 -3
- package/dist/routes.js +12 -12
- package/dist/server.js +2 -2
- package/dist/static-path.js +1 -1
- package/dist/vite-plugin.js +53 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.5
|
|
4
|
+
|
|
5
|
+
- Default development Fastify/Vite logging to warn, retain the startup address and allow FASTANG_LOG_LEVEL overrides. Suppress successful dev check summaries with check --quiet.
|
|
6
|
+
|
|
7
|
+
- Preserve relative dependency symlinks in Vercel functions for pnpm installations.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-alpha.4
|
|
10
|
+
|
|
11
|
+
- Add source-only `fastang check` diagnostics for API route/contract correspondence, with tsconfig resolution, JSON and strict mode.
|
|
12
|
+
- Run checks before builds and development restarts; share diagnostics with the Vite overlay.
|
|
13
|
+
|
|
14
|
+
- Bound development server shutdown to three seconds before forced termination.
|
|
15
|
+
- Use English for framework diagnostics.
|
|
16
|
+
|
|
3
17
|
## 0.1.0-alpha.3
|
|
4
18
|
|
|
5
19
|
- Coordinate the standalone CLI release and starter while preserving runtime APIs.
|
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.5 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
|
|
@@ -82,7 +82,62 @@ See the [public API and support reference](https://fastang.dev/en/docs/reference
|
|
|
82
82
|
## Standalone CLI
|
|
83
83
|
|
|
84
84
|
Project creation and upgrades belong to the separate lightweight `@fastang/cli`
|
|
85
|
-
package introduced in alpha.3. Core provides `dev`, `build`, `generate` and `
|
|
85
|
+
package introduced in alpha.3. Core provides `dev`, `build`, `generate`, `routes` and (from alpha.4) `check`.
|
|
86
86
|
The global CLI forwards these commands to the app's installed core.
|
|
87
87
|
|
|
88
88
|
See [CLI behavior and limitations](../../docs/cli.md).
|
|
89
|
+
|
|
90
|
+
## Static route checks
|
|
91
|
+
|
|
92
|
+
From `0.1.0-alpha.4`, FastAng includes a route/contract diagnostic command:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
npx fastang check
|
|
96
|
+
npx fastang check --json
|
|
97
|
+
npx fastang check --strict --project tsconfig.node.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The checker defaults to `tsconfig.node.json`, falling back to `tsconfig.json`.
|
|
101
|
+
It uses TypeScript module resolution, including inherited compiler settings,
|
|
102
|
+
`paths`, aliases and re-exports. It starts from file-based API routes and checks
|
|
103
|
+
only the contract actually passed to `defineContractRoute`; unused contracts
|
|
104
|
+
are not treated as missing routes. It never imports application modules, loads
|
|
105
|
+
FastAng/Vite configuration, emits files or connects to application services.
|
|
106
|
+
|
|
107
|
+
Literal strings, const references, object literals and resolvable spreads are
|
|
108
|
+
supported. Function calls that construct contracts, mutable variables, wrappers
|
|
109
|
+
and unresolved imports produce `FA1003` (unverified), rather than a successful
|
|
110
|
+
verification. `--strict` makes these warnings errors. A known method/path mismatch
|
|
111
|
+
is `FA1002`; configuration/syntax errors are `FA1000`, and route discovery errors
|
|
112
|
+
are `FA1001`. Errors exit with code 1. JSON output has `schemaVersion: 1`, counts
|
|
113
|
+
and diagnostics with source positions and a related contract location for
|
|
114
|
+
mismatches. `defineApiRoute` needs no contract correspondence check.
|
|
115
|
+
|
|
116
|
+
This is a source-convention check, not general constant evaluation or a replacement
|
|
117
|
+
for TypeScript/Angular checks. Keep method/path definitions declarative; mutations
|
|
118
|
+
or side effects elsewhere are not evaluated. Resolving a TS alias here does not
|
|
119
|
+
configure runtime aliases. Registration-time validation remains in place.
|
|
120
|
+
|
|
121
|
+
`fastang build` checks before loading application configuration or clearing build
|
|
122
|
+
output. The Vite plugin reuses the checker and reports errors in its terminal and
|
|
123
|
+
overlay on route/contract edits; fixing them triggers a full reload. `fastang dev`
|
|
124
|
+
checks before restarting the backend and keeps an existing server during an invalid
|
|
125
|
+
edit. The standalone CLI delegates `check` to the app's installed core: both need
|
|
126
|
+
a release containing this command. Use `0.1.0-alpha.4` or later for both packages.
|
|
127
|
+
|
|
128
|
+
## Development logging
|
|
129
|
+
|
|
130
|
+
From `0.1.0-alpha.5`, development defaults to `warn` for both Fastify and Vite during `fastang dev`.
|
|
131
|
+
The listening address remains visible. Routine HTTP request/completion logs and
|
|
132
|
+
successful route-check summaries are omitted; warnings, errors and failed checks
|
|
133
|
+
remain visible. Production logger configuration is unchanged.
|
|
134
|
+
|
|
135
|
+
Set `FASTANG_LOG_LEVEL=info` or `FASTANG_LOG_LEVEL=debug` in the process environment
|
|
136
|
+
when investigating requests. Accepted values are `trace`, `debug`, `info`, `warn`,
|
|
137
|
+
`error`, `fatal` and `silent`. Vite maps trace/debug to info and fatal to error.
|
|
138
|
+
This controls framework loggers, not application `console.log` calls or package
|
|
139
|
+
manager output. The startup address is retained even with `silent`.
|
|
140
|
+
|
|
141
|
+
`fastang check --quiet` suppresses only a successful text summary; diagnostics
|
|
142
|
+
still print, and `--json` always returns the JSON report. The dev supervisor uses
|
|
143
|
+
this option for checks before backend restarts. These options are available from `0.1.0-alpha.5`.
|
package/dist/adapters/vercel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { copyFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { copyFile, mkdir, readFile, rm, stat, lstat, readlink, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { nodeFileTrace } from '@vercel/nft';
|
|
5
5
|
import { staticFile } from "../server.js";
|
|
@@ -15,7 +15,22 @@ export async function buildVercelOutput(plan, root = process.cwd()) {
|
|
|
15
15
|
await rm(output, { recursive: true, force: true });
|
|
16
16
|
const copy = async (file, destination) => {
|
|
17
17
|
await mkdir(dirname(destination), { recursive: true });
|
|
18
|
-
|
|
18
|
+
const source = resolve(root, file);
|
|
19
|
+
const info = await lstat(source);
|
|
20
|
+
if (info.isSymbolicLink()) {
|
|
21
|
+
const target = await readlink(source);
|
|
22
|
+
const sourceTarget = relative(root, resolve(dirname(source), target));
|
|
23
|
+
if (isAbsolute(target) || sourceTarget === '..' || sourceTarget.startsWith('../') || sourceTarget.startsWith('..\\'))
|
|
24
|
+
throw new Error(`Dependency symlink outside project: ${file}`);
|
|
25
|
+
const directory = (await stat(source)).isDirectory();
|
|
26
|
+
if (directory)
|
|
27
|
+
await mkdir(resolve(dirname(destination), target), { recursive: true });
|
|
28
|
+
await symlink(target, destination, directory ? 'dir' : 'file');
|
|
29
|
+
}
|
|
30
|
+
else if (info.isDirectory())
|
|
31
|
+
await mkdir(destination, { recursive: true });
|
|
32
|
+
else
|
|
33
|
+
await copyFile(source, destination);
|
|
19
34
|
};
|
|
20
35
|
const { cp } = await import('node:fs/promises');
|
|
21
36
|
await cp(resolve(root, 'dist/client/assets'), resolve(output, 'static/assets'), { recursive: true });
|
|
@@ -39,11 +54,16 @@ export async function buildVercelOutput(plan, root = process.cwd()) {
|
|
|
39
54
|
traces.set(entryFile, trace);
|
|
40
55
|
for (const warning of trace.warnings)
|
|
41
56
|
console.warn(`[vercel:${name}] ${warning.message}`);
|
|
42
|
-
|
|
57
|
+
const files = await Promise.all([...trace.fileList].map(async (file) => {
|
|
43
58
|
if (file.startsWith('../') || file.startsWith('/'))
|
|
44
59
|
throw new Error(`Dependency outside project: ${file}`);
|
|
60
|
+
return { file, link: (await lstat(resolve(root, file))).isSymbolicLink() };
|
|
61
|
+
}));
|
|
62
|
+
// pnpm exposes dependencies through relative directory symlinks. Keep that
|
|
63
|
+
// topology in the function, creating links before copying traced files.
|
|
64
|
+
files.sort((a, b) => Number(b.link) - Number(a.link) || a.file.length - b.file.length);
|
|
65
|
+
for (const { file } of files)
|
|
45
66
|
await copy(file, resolve(directory, file));
|
|
46
|
-
}
|
|
47
67
|
if (kind === 'ssr')
|
|
48
68
|
await copy('dist/client/index.html', resolve(directory, dirname(entryFile), '../client/index.html'));
|
|
49
69
|
await mkdir(directory, { recursive: true });
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CheckDiagnostic {
|
|
2
|
+
code: string;
|
|
3
|
+
severity: 'error' | 'warning';
|
|
4
|
+
message: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
column?: number;
|
|
8
|
+
contract?: {
|
|
9
|
+
file: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface CheckResult {
|
|
15
|
+
schemaVersion: 1;
|
|
16
|
+
project: string;
|
|
17
|
+
routes: number;
|
|
18
|
+
verified: number;
|
|
19
|
+
diagnostics: CheckDiagnostic[];
|
|
20
|
+
}
|
|
21
|
+
export interface CheckOptions {
|
|
22
|
+
root?: string;
|
|
23
|
+
project?: string;
|
|
24
|
+
strict?: boolean;
|
|
25
|
+
onFiles?: (files: string[]) => void;
|
|
26
|
+
}
|
|
27
|
+
/** Analyze source only. No application imports, config execution, generation or emit. */
|
|
28
|
+
export declare function checkRoutes(options?: CheckOptions): Promise<CheckResult>;
|
|
29
|
+
export declare function formatCheck(result: CheckResult): string;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, relative, resolve } from 'node:path';
|
|
4
|
+
import { discover } from "./routes.js";
|
|
5
|
+
/** Analyze source only. No application imports, config execution, generation or emit. */
|
|
6
|
+
export async function checkRoutes(options = {}) {
|
|
7
|
+
const root = resolve(options.root ?? process.cwd());
|
|
8
|
+
const project = resolve(root, options.project ?? (existsSync(resolve(root, 'tsconfig.node.json')) ? 'tsconfig.node.json' : 'tsconfig.json'));
|
|
9
|
+
const result = { schemaVersion: 1, project: relative(root, project).replaceAll('\\', '/'), routes: 0, verified: 0, diagnostics: [] };
|
|
10
|
+
const fileName = (file) => relative(root, file).replaceAll('\\', '/');
|
|
11
|
+
const position = (node) => {
|
|
12
|
+
const source = node.getSourceFile();
|
|
13
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart());
|
|
14
|
+
return { file: fileName(source.fileName), line: line + 1, column: character + 1 };
|
|
15
|
+
};
|
|
16
|
+
const configError = (error) => {
|
|
17
|
+
const location = error.file && error.start !== undefined ? error.file.getLineAndCharacterOfPosition(error.start) : undefined;
|
|
18
|
+
result.diagnostics.push({ code: 'FA1000', severity: 'error', message: ts.flattenDiagnosticMessageText(error.messageText, '\n'), ...(error.file ? { file: fileName(error.file.fileName) } : {}), ...(location ? { line: location.line + 1, column: location.character + 1 } : {}) });
|
|
19
|
+
};
|
|
20
|
+
const config = ts.readConfigFile(project, ts.sys.readFile);
|
|
21
|
+
if (config.error) {
|
|
22
|
+
configError(config.error);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, resolve(project, '..'), undefined, project);
|
|
26
|
+
if (parsed.errors.length) {
|
|
27
|
+
parsed.errors.forEach(configError);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
let routes;
|
|
31
|
+
try {
|
|
32
|
+
routes = await discover(resolve(root, 'src/server/api'), 'api');
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
result.diagnostics.push({ code: 'FA1001', severity: 'error', message: String(error instanceof Error ? error.message : error) });
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
result.routes = routes.length;
|
|
39
|
+
const program = ts.createProgram({ rootNames: [...new Set([...parsed.fileNames, ...routes.map(route => resolve(root, 'src/server/api', route.file))])], options: { ...parsed.options, noEmit: true } });
|
|
40
|
+
options.onFiles?.(program.getSourceFiles().filter(file => !file.isDeclarationFile && !file.fileName.replaceAll('\\', '/').includes('/node_modules/')).map(file => file.fileName));
|
|
41
|
+
const checker = program.getTypeChecker();
|
|
42
|
+
function symbol(node) {
|
|
43
|
+
const value = checker.getSymbolAtLocation(node);
|
|
44
|
+
return value && value.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(value) : value;
|
|
45
|
+
}
|
|
46
|
+
function unwrap(node) {
|
|
47
|
+
while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))
|
|
48
|
+
node = node.expression;
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
function value(node, seen = new Set()) {
|
|
52
|
+
if (!node || seen.has(node) || seen.size > 100)
|
|
53
|
+
return undefined;
|
|
54
|
+
seen.add(node);
|
|
55
|
+
node = unwrap(node);
|
|
56
|
+
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
|
|
57
|
+
const declaration = symbol(ts.isPropertyAccessExpression(node) ? node.name : node)?.valueDeclaration;
|
|
58
|
+
if (declaration && ts.isVariableDeclaration(declaration) && declaration.parent.flags & ts.NodeFlags.Const)
|
|
59
|
+
return value(declaration.initializer, seen);
|
|
60
|
+
if (declaration && ts.isExportAssignment(declaration))
|
|
61
|
+
return value(declaration.expression, seen);
|
|
62
|
+
if (declaration && ts.isPropertyAssignment(declaration))
|
|
63
|
+
return value(declaration.initializer, seen);
|
|
64
|
+
if (ts.isPropertyAccessExpression(node))
|
|
65
|
+
return property(value(node.expression, seen), node.name.text, seen);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return node;
|
|
69
|
+
}
|
|
70
|
+
function property(node, key, seen = new Set()) {
|
|
71
|
+
if (!node || !ts.isObjectLiteralExpression(node))
|
|
72
|
+
return undefined;
|
|
73
|
+
for (const field of [...node.properties].reverse()) {
|
|
74
|
+
if (ts.isSpreadAssignment(field)) {
|
|
75
|
+
const spread = value(field.expression, new Set(seen));
|
|
76
|
+
if (!spread || !ts.isObjectLiteralExpression(spread))
|
|
77
|
+
return undefined;
|
|
78
|
+
const found = property(spread, key, new Set(seen));
|
|
79
|
+
if (found)
|
|
80
|
+
return found;
|
|
81
|
+
// An unresolved nested spread could override earlier fields.
|
|
82
|
+
if (spread.properties.some(field => ts.isSpreadAssignment(field) || field.name && (ts.isComputedPropertyName(field.name) || (ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) && field.name.text === key)))
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (field.name && ts.isComputedPropertyName(field.name))
|
|
87
|
+
return undefined;
|
|
88
|
+
if (field.name && (ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) && field.name.text === key) {
|
|
89
|
+
if (ts.isPropertyAssignment(field))
|
|
90
|
+
return value(field.initializer, new Set(seen));
|
|
91
|
+
if (ts.isShorthandPropertyAssignment(field)) {
|
|
92
|
+
const declaration = checker.getShorthandAssignmentValueSymbol(field)?.valueDeclaration;
|
|
93
|
+
return declaration && ts.isVariableDeclaration(declaration) && declaration.parent.flags & ts.NodeFlags.Const ? value(declaration.initializer, new Set(seen)) : undefined;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function apiCall(node, name, module) {
|
|
102
|
+
if (!node || !ts.isCallExpression(node))
|
|
103
|
+
return false;
|
|
104
|
+
const target = symbol(ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
|
|
105
|
+
return target?.name === name && !!target.declarations?.some(declaration => {
|
|
106
|
+
const path = declaration.getSourceFile().fileName.replaceAll('\\', '/');
|
|
107
|
+
return /\/(?:@fastang\/core|packages\/core)\/(?:src|dist)\//.test(path) && [module + '.ts', module + '.d.ts'].includes(basename(path));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const syntaxFiles = new Set();
|
|
111
|
+
for (const route of routes) {
|
|
112
|
+
const source = program.getSourceFile(resolve(root, 'src/server/api', route.file));
|
|
113
|
+
if (!source)
|
|
114
|
+
continue;
|
|
115
|
+
const expected = `${route.method} /api${route.path ? '/' + route.path : ''}`;
|
|
116
|
+
const module = checker.getSymbolAtLocation(source);
|
|
117
|
+
const exported = module && checker.getExportsOfModule(module).find(item => item.name === 'default');
|
|
118
|
+
const declaration = exported && (exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported).valueDeclaration;
|
|
119
|
+
const expression = declaration && (ts.isExportAssignment(declaration) ? value(declaration.expression) : ts.isVariableDeclaration(declaration) ? value(declaration.initializer) : declaration);
|
|
120
|
+
if (apiCall(expression, 'defineApiRoute', 'api'))
|
|
121
|
+
continue;
|
|
122
|
+
const warn = (message) => result.diagnostics.push({ code: 'FA1003', severity: options.strict ? 'error' : 'warning', message: `${expected}: ${message}`, ...position(declaration ?? source) });
|
|
123
|
+
if (!apiCall(expression, 'defineContractRoute', 'contract-route')) {
|
|
124
|
+
warn('Route export is not a statically resolvable defineContractRoute or defineApiRoute call; registration-time validation still applies to contract routes.');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const contract = value(expression.arguments[0]);
|
|
128
|
+
if (!apiCall(contract, 'defineApiContract', 'contracts')) {
|
|
129
|
+
warn('Contract is dynamic or unresolved; static correspondence cannot be verified.');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const definition = value(contract.arguments[0]);
|
|
133
|
+
const method = property(definition, 'method');
|
|
134
|
+
const path = property(definition, 'path');
|
|
135
|
+
if (!method || !path || !ts.isStringLiteralLike(method) || !ts.isStringLiteralLike(path)) {
|
|
136
|
+
warn('Contract method/path must resolve to string literals or const references.');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const actual = `${method.text} ${path.text}`;
|
|
140
|
+
if (actual !== expected)
|
|
141
|
+
result.diagnostics.push({ code: 'FA1002', severity: 'error', message: `File route ${expected}, contract ${actual}. Update the contract or rename/move the route file.`, ...position(expression.getSourceFile() === source ? expression : source), contract: position(contract) });
|
|
142
|
+
else
|
|
143
|
+
result.verified++;
|
|
144
|
+
syntaxFiles.add(contract.getSourceFile().fileName);
|
|
145
|
+
}
|
|
146
|
+
routes.forEach(route => syntaxFiles.add(resolve(root, 'src/server/api', route.file)));
|
|
147
|
+
for (const file of syntaxFiles) {
|
|
148
|
+
const source = program.getSourceFile(file);
|
|
149
|
+
if (source)
|
|
150
|
+
program.getSyntacticDiagnostics(source).forEach(configError);
|
|
151
|
+
}
|
|
152
|
+
result.diagnostics.sort((a, b) => (a.file ?? '').localeCompare(b.file ?? '') || (a.line ?? 0) - (b.line ?? 0) || a.code.localeCompare(b.code));
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
export function formatCheck(result) {
|
|
156
|
+
const lines = result.diagnostics.map(d => `${d.file ?? result.project}${d.line ? `:${d.line}:${d.column}` : ''} ${d.severity} ${d.code}: ${d.message}${d.contract ? `\n Contract: ${d.contract.file}:${d.contract.line}:${d.contract.column}` : ''}`);
|
|
157
|
+
lines.push(`FastAng routes: ${result.verified} contracts verified, ${result.routes} API routes, ${result.diagnostics.filter(d => d.severity === 'error').length} errors, ${result.diagnostics.filter(d => d.severity === 'warning').length} warnings.`);
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,8 @@ 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 === 'check')
|
|
7
|
+
await import("./commands/check.js");
|
|
6
8
|
else if (command === 'routes')
|
|
7
9
|
await import("./commands/inspect-routes.js");
|
|
8
10
|
else if (command === 'dev')
|
|
@@ -10,7 +12,7 @@ try {
|
|
|
10
12
|
else if (command === 'build')
|
|
11
13
|
await import("./commands/build.js");
|
|
12
14
|
else if (!command || command === '--help' || command === '-h')
|
|
13
|
-
console.log('Usage: fastang <dev|generate|build|routes>\n build [--vercel]\n routes [--json]');
|
|
15
|
+
console.log('Usage: fastang <dev|generate|build|routes|check>\n build [--vercel]\n routes [--json]\n check [--project tsconfig.node.json] [--json] [--strict] [--quiet]');
|
|
14
16
|
else
|
|
15
17
|
throw new Error(`Unknown command: ${command}`);
|
|
16
18
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -14,6 +14,12 @@ import { prepareVercelPlan } from "../adapters/vercel-plan.js";
|
|
|
14
14
|
import { buildVercelOutput } from "../adapters/vercel.js";
|
|
15
15
|
import { staticFile } from "../static-path.js";
|
|
16
16
|
import { loadFastangConfig } from "./load-config.js";
|
|
17
|
+
const { checkRoutes, formatCheck } = await import("../check.js");
|
|
18
|
+
const check = await checkRoutes();
|
|
19
|
+
if (check.diagnostics.length)
|
|
20
|
+
console.log(formatCheck(check));
|
|
21
|
+
if (check.diagnostics.some(diagnostic => diagnostic.severity === 'error'))
|
|
22
|
+
throw new Error('FastAng route check failed');
|
|
17
23
|
const config = await loadFastangConfig('build');
|
|
18
24
|
const vercel = process.argv.includes('--vercel');
|
|
19
25
|
const paths = [...new Set(config.prerender)];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { checkRoutes, formatCheck } from "../check.js";
|
|
2
|
+
const args = process.argv.slice(3);
|
|
3
|
+
let project;
|
|
4
|
+
for (let index = 0; index < args.length; index++) {
|
|
5
|
+
if (args[index] === '--project' && args[index + 1] && !args[index + 1].startsWith('-'))
|
|
6
|
+
project = args[++index];
|
|
7
|
+
else if (!['--json', '--strict', '--quiet'].includes(args[index]))
|
|
8
|
+
throw new Error('Usage: fastang check [--project tsconfig.node.json] [--json] [--strict] [--quiet]');
|
|
9
|
+
}
|
|
10
|
+
const result = await checkRoutes({ project, strict: args.includes('--strict') });
|
|
11
|
+
if (args.includes('--json'))
|
|
12
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13
|
+
else if (!args.includes('--quiet') || result.diagnostics.length)
|
|
14
|
+
console.log(formatCheck(result));
|
|
15
|
+
if (result.diagnostics.some(diagnostic => diagnostic.severity === 'error'))
|
|
16
|
+
process.exitCode = 1;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LogLevel } from 'vite';
|
|
2
|
+
declare const levels: readonly ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
3
|
+
type Level = typeof levels[number];
|
|
4
|
+
/** Development only: production servers keep their own logger configuration. */
|
|
5
|
+
export declare function developmentLogging(value?: string | undefined): {
|
|
6
|
+
level: Level;
|
|
7
|
+
vite: LogLevel;
|
|
8
|
+
banner: boolean;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'];
|
|
2
|
+
/** Development only: production servers keep their own logger configuration. */
|
|
3
|
+
export function developmentLogging(value = process.env['FASTANG_LOG_LEVEL']) {
|
|
4
|
+
const level = value ?? 'warn';
|
|
5
|
+
if (!levels.includes(level))
|
|
6
|
+
throw new Error(`FASTANG_LOG_LEVEL must be one of: ${levels.join(', ')}`);
|
|
7
|
+
return {
|
|
8
|
+
level: level,
|
|
9
|
+
vite: ['trace', 'debug', 'info'].includes(level) ? 'info' : level === 'fatal' ? 'error' : level,
|
|
10
|
+
// Fastify already announces the address at info; preserve it at quieter levels.
|
|
11
|
+
banner: !['trace', 'debug', 'info'].includes(level),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -6,16 +6,18 @@ 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 { developmentLogging } from "./dev-logging.js";
|
|
9
10
|
import { loadFastangConfig } from "./load-config.js";
|
|
11
|
+
const logging = developmentLogging();
|
|
10
12
|
await loadFastangConfig('serve');
|
|
11
13
|
const { configureServer } = await tsImport(pathToFileURL(resolve('src/server/plugins.ts')).href, import.meta.url);
|
|
12
14
|
const { registerApi } = await tsImport(pathToFileURL(resolve('.fastang/api.ts')).href, import.meta.url);
|
|
13
15
|
let vite;
|
|
14
16
|
try {
|
|
15
17
|
const app = await createServer({
|
|
16
|
-
logger:
|
|
18
|
+
logger: { level: logging.level }, registerApi,
|
|
17
19
|
async configure(server) {
|
|
18
|
-
vite = await createViteServer({ server: { middlewareMode: true, hmr: { server: server.server } }, appType: 'custom' });
|
|
20
|
+
vite = await createViteServer({ logLevel: logging.vite, server: { middlewareMode: true, hmr: { server: server.server } }, appType: 'custom' });
|
|
19
21
|
server.addHook('onClose', async () => { await vite?.close(); });
|
|
20
22
|
await server.register(middie);
|
|
21
23
|
// Middie gestisce next(), risposte completate ed errori del middleware Connect.
|
|
@@ -36,6 +38,13 @@ try {
|
|
|
36
38
|
},
|
|
37
39
|
});
|
|
38
40
|
await listen(app);
|
|
41
|
+
if (logging.banner) {
|
|
42
|
+
const address = app.server.address();
|
|
43
|
+
if (address && typeof address !== 'string') {
|
|
44
|
+
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
|
|
45
|
+
console.log(`Server listening at http://${host}:${address.port}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
catch (error) {
|
|
41
50
|
await vite?.close();
|
package/dist/commands/dev.js
CHANGED
|
@@ -3,26 +3,27 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import { watch } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { once } from 'node:events';
|
|
6
|
+
import { stopChild } from "./stop-child.js";
|
|
6
7
|
let child;
|
|
7
8
|
let stopping = false;
|
|
8
9
|
let timer;
|
|
9
10
|
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
11
|
async function restart() {
|
|
18
|
-
|
|
12
|
+
if (stopping)
|
|
13
|
+
return;
|
|
14
|
+
// Keep the previous server available for Vite diagnostics while an edit is invalid.
|
|
15
|
+
const checker = spawn(process.execPath, [fileURLToPath(new URL('../cli.js', import.meta.url)), 'check', '--quiet'], { stdio: 'inherit', env: process.env });
|
|
16
|
+
const [checked] = await once(checker, 'exit');
|
|
17
|
+
if (checked !== 0 || stopping)
|
|
18
|
+
return;
|
|
19
|
+
await stopChild(child);
|
|
19
20
|
if (stopping)
|
|
20
21
|
return;
|
|
21
22
|
// Processo nuovo: anche le modifiche al generatore sono applicate al riavvio.
|
|
22
23
|
const generator = spawn(process.execPath, [fileURLToPath(new URL('../cli.js', import.meta.url)), 'generate'], { stdio: 'inherit', env: process.env });
|
|
23
24
|
const [code] = await once(generator, 'exit');
|
|
24
25
|
if (code !== 0)
|
|
25
|
-
throw new Error('
|
|
26
|
+
throw new Error('Route generation failed');
|
|
26
27
|
child = spawn(process.execPath, [fileURLToPath(new URL('./dev-server.js', import.meta.url))], { stdio: 'inherit', env: process.env });
|
|
27
28
|
child.on('error', (error) => { console.error(error); process.exitCode = 1; });
|
|
28
29
|
}
|
|
@@ -33,14 +34,15 @@ function changed() {
|
|
|
33
34
|
}, 150);
|
|
34
35
|
}
|
|
35
36
|
// 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
|
+
const watchers = ['src/server', 'src/shared', 'vite.config.ts', 'fastang.config.ts', 'tsconfig.json', 'tsconfig.node.json'].filter(existsSync).map(path => watch(path, { recursive: true }, changed));
|
|
37
38
|
async function shutdown() {
|
|
38
39
|
stopping = true;
|
|
39
40
|
clearTimeout(timer);
|
|
40
41
|
watchers.forEach((watcher) => watcher.close());
|
|
41
42
|
await queue;
|
|
42
|
-
await stopChild();
|
|
43
|
+
await stopChild(child);
|
|
43
44
|
}
|
|
44
45
|
process.once('SIGINT', shutdown);
|
|
45
46
|
process.once('SIGTERM', shutdown);
|
|
46
|
-
|
|
47
|
+
queue = restart();
|
|
48
|
+
await queue;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
/** Give application shutdown hooks time to finish, then bound the supervisor wait. */
|
|
3
|
+
export declare function stopChild(child: ChildProcess | undefined, timeoutMs?: number, warn?: {
|
|
4
|
+
(...data: any[]): void;
|
|
5
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
6
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Give application shutdown hooks time to finish, then bound the supervisor wait. */
|
|
2
|
+
export async function stopChild(child, timeoutMs = 3000, warn = console.warn) {
|
|
3
|
+
if (!child || child.exitCode !== null || child.signalCode !== null)
|
|
4
|
+
return;
|
|
5
|
+
await new Promise((resolveStop, reject) => {
|
|
6
|
+
let timer;
|
|
7
|
+
const cleanup = () => {
|
|
8
|
+
clearTimeout(timer);
|
|
9
|
+
child.removeListener('exit', exited);
|
|
10
|
+
child.removeListener('error', failed);
|
|
11
|
+
};
|
|
12
|
+
const exited = () => { cleanup(); resolveStop(); };
|
|
13
|
+
const failed = (error) => { cleanup(); reject(error); };
|
|
14
|
+
child.once('exit', exited);
|
|
15
|
+
child.once('error', failed);
|
|
16
|
+
timer = setTimeout(() => {
|
|
17
|
+
warn(`Development server did not stop within ${timeoutMs}ms; forcing termination.`);
|
|
18
|
+
child.kill('SIGKILL');
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
child.kill('SIGTERM');
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/listen.js
CHANGED
|
@@ -4,7 +4,7 @@ export async function listen(app) {
|
|
|
4
4
|
const port = Number(process.env['PORT'] ?? DEFAULT_PORT);
|
|
5
5
|
const host = process.env['HOST'] ?? '127.0.0.1';
|
|
6
6
|
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
7
|
-
throw new Error('PORT
|
|
7
|
+
throw new Error('PORT must be a valid port');
|
|
8
8
|
const shutdown = async () => {
|
|
9
9
|
try {
|
|
10
10
|
await app.close();
|
package/dist/redirect.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** Solo destinazioni locali: gli header Host del client non decidono l'origine. */
|
|
2
2
|
export function normalizeRedirectLocation(location) {
|
|
3
3
|
if (!location.startsWith('/') || location.startsWith('//') || /[\\\u0000-\u0020\u007f]/.test(location)) {
|
|
4
|
-
throw new Error('
|
|
4
|
+
throw new Error('Invalid redirect destination');
|
|
5
5
|
}
|
|
6
6
|
const target = new URL(location, 'http://fastang.local');
|
|
7
7
|
if (target.origin !== 'http://fastang.local')
|
|
8
|
-
throw new Error('
|
|
8
|
+
throw new Error('External redirects are not allowed');
|
|
9
9
|
// La normalizzazione può trasformare /a/..//host in //host: ricontrolla l'output.
|
|
10
10
|
if (target.pathname.startsWith('//'))
|
|
11
|
-
throw new Error('
|
|
11
|
+
throw new Error('Ambiguous redirect destination');
|
|
12
12
|
return target.pathname + target.search + target.hash;
|
|
13
13
|
}
|
package/dist/routes.js
CHANGED
|
@@ -4,14 +4,14 @@ const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'optio
|
|
|
4
4
|
export function fileRoute(file, kind) {
|
|
5
5
|
const suffix = kind === 'page' ? '.page.ts' : '.ts';
|
|
6
6
|
if (!file.endsWith(suffix))
|
|
7
|
-
throw new Error(`
|
|
7
|
+
throw new Error(`Invalid extension: ${file}`);
|
|
8
8
|
let stem = file.slice(0, -suffix.length);
|
|
9
9
|
let method;
|
|
10
10
|
if (kind === 'api') {
|
|
11
11
|
const parts = stem.split('.');
|
|
12
12
|
const candidate = parts.pop();
|
|
13
13
|
if (!METHODS.has(candidate))
|
|
14
|
-
throw new Error(`
|
|
14
|
+
throw new Error(`API ${file} must specify a method, e.g. health.get.ts`);
|
|
15
15
|
method = candidate.toUpperCase();
|
|
16
16
|
stem = parts.join('.');
|
|
17
17
|
}
|
|
@@ -27,7 +27,7 @@ export function fileRoute(file, kind) {
|
|
|
27
27
|
const wildcard = /^\[([A-Za-z_][\w]*)\*\]$/.exec(segment);
|
|
28
28
|
if (wildcard) {
|
|
29
29
|
if (index !== normalized.length - 1)
|
|
30
|
-
throw new Error(`Catch-all
|
|
30
|
+
throw new Error(`Catch-all must be the final segment: ${file}`);
|
|
31
31
|
paths.push(kind === 'page' ? '**' : '*');
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
@@ -37,13 +37,13 @@ export function fileRoute(file, kind) {
|
|
|
37
37
|
else if (/^[\w-]+$/.test(segment))
|
|
38
38
|
paths.push(segment);
|
|
39
39
|
else
|
|
40
|
-
throw new Error(`
|
|
40
|
+
throw new Error(`Unsupported segment in ${file}: ${segment}`);
|
|
41
41
|
}
|
|
42
42
|
if (!segments.length)
|
|
43
|
-
throw new Error(`
|
|
43
|
+
throw new Error(`Empty route: ${file}`);
|
|
44
44
|
const params = paths.filter((part) => part.startsWith(':'));
|
|
45
45
|
if (new Set(params).size !== params.length)
|
|
46
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`Duplicate parameter: ${file}`);
|
|
47
47
|
return { file, path: paths.join('/'), ...(method ? { method } : {}) };
|
|
48
48
|
}
|
|
49
49
|
export function sortAndValidate(routes) {
|
|
@@ -51,7 +51,7 @@ export function sortAndValidate(routes) {
|
|
|
51
51
|
for (const route of routes) {
|
|
52
52
|
const key = `${route.method ?? 'PAGE'} ${route.path.replace(/:[^/]+/g, ':')}`;
|
|
53
53
|
if (seen.has(key))
|
|
54
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`Route collision between ${seen.get(key)} and ${route.file}`);
|
|
55
55
|
seen.set(key, route.file);
|
|
56
56
|
}
|
|
57
57
|
const weight = (segment) => segment.startsWith('*') ? 0 : segment.startsWith(':') ? 1 : 2;
|
|
@@ -110,7 +110,7 @@ function leaves(nodes) {
|
|
|
110
110
|
export function buildPageTree(files) {
|
|
111
111
|
const pages = sortAndValidate(files.filter((file) => file.endsWith('.page.ts')).map((file) => fileRoute(file, 'page')));
|
|
112
112
|
if (pages.some((page) => /(^|\/)layout\.page\.ts$/.test(page.file))) {
|
|
113
|
-
throw new Error('layout.page.ts
|
|
113
|
+
throw new Error('layout.page.ts is reserved: use layout.ts for a layout or another page name');
|
|
114
114
|
}
|
|
115
115
|
const layouts = files.filter((file) => /(^|\/)layout\.ts$/.test(file))
|
|
116
116
|
.sort((a, b) => segments(a).length - segments(b).length || a.localeCompare(b));
|
|
@@ -135,10 +135,10 @@ export function buildPageTree(files) {
|
|
|
135
135
|
// Non includere se stesso nella ricerca del layout genitore.
|
|
136
136
|
const parent = parentFor(file);
|
|
137
137
|
if (/\[\.\.\./.test(directory))
|
|
138
|
-
throw new Error(`
|
|
138
|
+
throw new Error(`Layouts below catch-all routes are not supported: ${file}`);
|
|
139
139
|
const fullPath = fileRoute((directory ? directory + '/' : '') + 'index.page.ts', 'page').path;
|
|
140
140
|
if (fullPath.includes('**'))
|
|
141
|
-
throw new Error(`
|
|
141
|
+
throw new Error(`Layouts below catch-all routes are not supported: ${file}`);
|
|
142
142
|
const node = makeNode(file, fullPath, 'layout', parent?.node);
|
|
143
143
|
(parent?.node.children ?? roots).push(node);
|
|
144
144
|
parents.push({ directory, node });
|
|
@@ -152,7 +152,7 @@ export function buildPageTree(files) {
|
|
|
152
152
|
}
|
|
153
153
|
for (const file of files.filter((file) => file.endsWith('.route.ts'))) {
|
|
154
154
|
if (!usedMetadata.has(file))
|
|
155
|
-
throw new Error(`
|
|
155
|
+
throw new Error(`Metadata has no matching page or layout: ${file}`);
|
|
156
156
|
}
|
|
157
157
|
const ranks = new Map(pages.map((page, index) => [page.file, index]));
|
|
158
158
|
const rank = (node) => Math.min(...leaves([node]).map((page) => ranks.get(page.file)));
|
|
@@ -168,7 +168,7 @@ export function buildPageTree(files) {
|
|
|
168
168
|
for (let i = 0; i < ordered.length; i++) {
|
|
169
169
|
for (let j = i + 1; j < ordered.length; j++) {
|
|
170
170
|
if (ranks.get(ordered[i].file) > ranks.get(ordered[j].file) && overlaps(ordered[i].fullPath, ordered[j].fullPath)) {
|
|
171
|
-
throw new Error(`
|
|
171
|
+
throw new Error(`Ambiguous order between ${ordered[i].file} and ${ordered[j].file}: move the layout to a shared ancestor or separate the paths`);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
package/dist/server.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function createServer(options) {
|
|
|
44
44
|
async dispatchApi(path, method, body, headers, dispatchOptions = {}) {
|
|
45
45
|
path = normalizeRedirectLocation(path);
|
|
46
46
|
if (!path.startsWith('/api/'))
|
|
47
|
-
throw new Error('
|
|
47
|
+
throw new Error('Only local API paths are allowed');
|
|
48
48
|
const forwarded = {};
|
|
49
49
|
for (const name of ['cookie', 'authorization', 'x-request-label']) {
|
|
50
50
|
const value = request.headers[name];
|
|
@@ -75,7 +75,7 @@ export async function createServer(options) {
|
|
|
75
75
|
if (context.redirect) {
|
|
76
76
|
const status = context.redirect.status ?? 302;
|
|
77
77
|
if (![301, 302, 303, 307, 308].includes(status))
|
|
78
|
-
throw new Error('
|
|
78
|
+
throw new Error('Invalid redirect status');
|
|
79
79
|
const location = normalizeRedirectLocation(context.redirect.location);
|
|
80
80
|
if (responseCookies.length)
|
|
81
81
|
reply.header('set-cookie', responseCookies);
|
package/dist/static-path.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function staticFile(path) {
|
|
2
2
|
if (!path.startsWith('/') || path.includes('//') || !/^\/[a-zA-Z0-9_/-]*$/.test(path) || path.split('/').includes('..')) {
|
|
3
|
-
throw new Error(`
|
|
3
|
+
throw new Error(`Unsupported prerender path: ${path}`);
|
|
4
4
|
}
|
|
5
5
|
return `${path.replace(/^\/|\/$/g, '')}${path === '/' ? '' : '/'}index.html`;
|
|
6
6
|
}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -1,30 +1,74 @@
|
|
|
1
1
|
import { generate } from "./routes.js";
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { checkRoutes, formatCheck } from "./check.js";
|
|
2
4
|
export function isServerModule(id) {
|
|
3
5
|
const path = id.replaceAll('\\', '/').split('?')[0];
|
|
4
6
|
return /\/(src\/server|framework|\.fastang)\//.test(path)
|
|
5
7
|
|| /\.server\.[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);
|
|
8
|
+
|| /\/(?:@fastang\/core|packages\/core)\/(?:src|dist)\/(?:index|server|http-server|node-handler|listen|angular|api|contract-route|check|routes|vite|vite-plugin|cli|commands\/.*|adapters\/.*)\.[cm]?[jt]s$/.test(path);
|
|
7
9
|
}
|
|
8
10
|
export function fastang() {
|
|
11
|
+
let root = process.cwd();
|
|
12
|
+
const dependencies = new Set();
|
|
9
13
|
return {
|
|
14
|
+
configResolved(config) { root = config.root; },
|
|
10
15
|
name: 'fastang',
|
|
11
16
|
enforce: 'pre',
|
|
12
17
|
transform(_code, id, options) {
|
|
13
18
|
if (!options?.ssr && isServerModule(id))
|
|
14
|
-
this.error(`
|
|
19
|
+
this.error(`Server module imported into the client: ${id}`);
|
|
20
|
+
},
|
|
21
|
+
async buildStart() {
|
|
22
|
+
const result = await checkRoutes({ root, onFiles: files => files.forEach(file => dependencies.add(file.replaceAll('\\', '/'))) });
|
|
23
|
+
if (result.diagnostics.some(d => d.severity === 'error'))
|
|
24
|
+
this.error(formatCheck(result));
|
|
25
|
+
if (result.diagnostics.length)
|
|
26
|
+
this.warn(formatCheck(result));
|
|
27
|
+
await generate(root);
|
|
15
28
|
},
|
|
16
|
-
async buildStart() { await generate(); },
|
|
17
29
|
configureServer(server) {
|
|
18
|
-
// Full reload per cambiamenti della struttura; HMR Angular per i componenti.
|
|
19
30
|
let pending = Promise.resolve();
|
|
31
|
+
let timer;
|
|
32
|
+
let closed = false;
|
|
20
33
|
const update = (file) => {
|
|
21
|
-
|
|
34
|
+
const path = file.replaceAll('\\', '/');
|
|
35
|
+
if (!(/src\/(?:server|shared)\/.*\.ts$/.test(path) || /tsconfig[^/]*\.json$/.test(path) || /src\/app\/pages\/.*\.ts$/.test(path) || dependencies.has(path)) || /\/(?:node_modules|dist|\.fastang)\//.test(path) || path.endsWith('/fastang.routes.ts'))
|
|
22
36
|
return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
pending = pending.then(async () => {
|
|
40
|
+
if (closed)
|
|
41
|
+
return;
|
|
42
|
+
const result = await checkRoutes({ root, onFiles: files => { files.forEach(file => dependencies.add(file.replaceAll('\\', '/'))); server.watcher.add(files); } });
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
const message = formatCheck(result);
|
|
46
|
+
if (result.diagnostics.some(d => d.severity === 'error')) {
|
|
47
|
+
server.config.logger.error(message);
|
|
48
|
+
server.ws.send({ type: 'error', err: { message, stack: '', plugin: 'fastang' } });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (result.diagnostics.length)
|
|
52
|
+
server.config.logger.warn(message);
|
|
53
|
+
await generate(root);
|
|
54
|
+
server.ws.send({ type: 'full-reload' });
|
|
55
|
+
}
|
|
56
|
+
}).catch((error) => {
|
|
57
|
+
if (!closed) {
|
|
58
|
+
const message = String(error);
|
|
59
|
+
server.config.logger.error(message);
|
|
60
|
+
server.ws.send({ type: 'error', err: { message, stack: '', plugin: 'fastang' } });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}, 150);
|
|
26
64
|
};
|
|
27
|
-
server.watcher.
|
|
65
|
+
server.watcher.add([resolve(root, 'src/server'), resolve(root, 'src/shared'), ...dependencies]);
|
|
66
|
+
server.watcher.on('add', update).on('unlink', update).on('change', (file) => {
|
|
67
|
+
// Leave Angular page/component HMR to its native plugin.
|
|
68
|
+
if (!/src\/app\//.test(file.replaceAll('\\', '/')))
|
|
69
|
+
update(file);
|
|
70
|
+
});
|
|
71
|
+
server.watcher.once('close', () => { closed = true; clearTimeout(timer); });
|
|
28
72
|
},
|
|
29
73
|
};
|
|
30
74
|
}
|