@askrjs/cli 0.0.23 → 0.0.25
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/dist/add.d.ts +1 -1
- package/dist/add.js +110 -5
- package/dist/analyze.d.ts +1 -1
- package/dist/analyze.js +1 -1
- package/dist/cli.js +6 -1
- package/dist/database.js +1 -1
- package/dist/docs-BSPiXiAW.js +224 -0
- package/dist/file-changes-CsrYAEYu.js +154 -0
- package/dist/{file-changes-cmFN-rF8.d.ts → file-changes-Va4jkhMm.d.ts} +2 -0
- package/dist/generate.js +7 -5
- package/dist/{guardrails-BVxzrcBX.js → guardrails-uLDOExsl.js} +1 -1
- package/dist/{runner-D2iNzz9g.js → runner-CtcqO3cF.js} +1 -1
- package/dist/{runner-N2qHCBar.js → runner-D_GIsgIj.js} +1 -1
- package/dist/ssg-config.d.ts +39 -2
- package/package.json +5 -2
- package/dist/file-changes-BAFLhEHZ.js +0 -66
package/dist/add.d.ts
CHANGED
package/dist/add.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
|
|
3
|
-
import { t as writeFileChanges } from "./file-changes-
|
|
3
|
+
import { t as writeFileChanges } from "./file-changes-CsrYAEYu.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/bin/add.ts
|
|
@@ -41,10 +41,12 @@ function helpText() {
|
|
|
41
41
|
"Usage:",
|
|
42
42
|
" askr add page <name> [--branch app|public] [--cwd <dir>] [--title <title>] [--route <path>] [--force]",
|
|
43
43
|
" askr add action <name> --route <path> [--cwd <dir>] [--force]",
|
|
44
|
+
" askr add database postgres|sqlite [--cwd <dir>]",
|
|
44
45
|
"",
|
|
45
46
|
"Commands:",
|
|
46
47
|
" page Scaffold a route page and register it in a route-first SPA branch",
|
|
47
48
|
" action Scaffold a browser descriptor, server handler, registration, authorization, and test",
|
|
49
|
+
" database Scaffold a dialect-specific Askr ORM database definition",
|
|
48
50
|
"",
|
|
49
51
|
"Options:",
|
|
50
52
|
" --branch <name> Route branch to target (default: app)",
|
|
@@ -59,6 +61,7 @@ function helpText() {
|
|
|
59
61
|
" askr add page ops/audit-log --branch public",
|
|
60
62
|
" askr add page approvals --title \"Human approvals\" --route /app/approvals",
|
|
61
63
|
" askr add action approve-request --route /requests/{id}",
|
|
64
|
+
" askr add database sqlite",
|
|
62
65
|
"",
|
|
63
66
|
"Notes:",
|
|
64
67
|
" Page generation supports route-first SPA projects created from `askr create spa`.",
|
|
@@ -362,9 +365,11 @@ async function addPage(parsed, io, writeChanges) {
|
|
|
362
365
|
return 1;
|
|
363
366
|
}
|
|
364
367
|
const importSpecifier = toImportSpecifier(routesFile, pageFile);
|
|
368
|
+
let routeFileContent = "";
|
|
365
369
|
let updatedRoutes = "";
|
|
366
370
|
try {
|
|
367
|
-
|
|
371
|
+
routeFileContent = await fs.readFile(routesFile, "utf8");
|
|
372
|
+
updatedRoutes = createUpdatedRouteFile(routeFileContent, {
|
|
368
373
|
componentName,
|
|
369
374
|
importSpecifier,
|
|
370
375
|
routePath
|
|
@@ -384,7 +389,8 @@ async function addPage(parsed, io, writeChanges) {
|
|
|
384
389
|
})
|
|
385
390
|
}, {
|
|
386
391
|
filePath: routesFile,
|
|
387
|
-
content: updatedRoutes
|
|
392
|
+
content: updatedRoutes,
|
|
393
|
+
expectedContent: routeFileContent
|
|
388
394
|
}]);
|
|
389
395
|
} catch (error) {
|
|
390
396
|
io.error("Failed to write generated page artifacts.");
|
|
@@ -434,6 +440,7 @@ async function addAction(parsed, io, writeChanges) {
|
|
|
434
440
|
routePath: parsed.routePath,
|
|
435
441
|
slug
|
|
436
442
|
});
|
|
443
|
+
const [registryContent, authorizationContent] = await Promise.all([fs.readFile(registryFile, "utf8"), fs.readFile(authorizationFile, "utf8")]);
|
|
437
444
|
const actions = await discoverDeclaredActions(projectRoot, {
|
|
438
445
|
filePath: descriptorFile,
|
|
439
446
|
content: descriptor
|
|
@@ -460,11 +467,13 @@ async function addAction(parsed, io, writeChanges) {
|
|
|
460
467
|
},
|
|
461
468
|
{
|
|
462
469
|
filePath: registryFile,
|
|
463
|
-
content: renderServerActionRegistry(actions)
|
|
470
|
+
content: renderServerActionRegistry(actions),
|
|
471
|
+
expectedContent: registryContent
|
|
464
472
|
},
|
|
465
473
|
{
|
|
466
474
|
filePath: authorizationFile,
|
|
467
|
-
content: renderAuthorizationRegistry(actions)
|
|
475
|
+
content: renderAuthorizationRegistry(actions),
|
|
476
|
+
expectedContent: authorizationContent
|
|
468
477
|
}
|
|
469
478
|
]);
|
|
470
479
|
} catch (error) {
|
|
@@ -478,6 +487,101 @@ async function addAction(parsed, io, writeChanges) {
|
|
|
478
487
|
io.log(` Test: ${path.relative(projectRoot, testFile).replace(/\\/g, "/")}`);
|
|
479
488
|
return 0;
|
|
480
489
|
}
|
|
490
|
+
async function addDatabase(parsed, io, writeChanges) {
|
|
491
|
+
if (parsed.name !== "postgres" && parsed.name !== "sqlite") {
|
|
492
|
+
io.error("Database dialect must be `postgres` or `sqlite`.");
|
|
493
|
+
return 1;
|
|
494
|
+
}
|
|
495
|
+
const projectRoot = path.resolve(parsed.cwd);
|
|
496
|
+
const manifestFile = path.join(projectRoot, "package.json");
|
|
497
|
+
const definitionFile = path.join(projectRoot, "src", "database", "index.ts");
|
|
498
|
+
const generatedFile = path.join(projectRoot, "src", "database", "generated.ts");
|
|
499
|
+
const migrationKeep = path.join(projectRoot, "src", "database", "migrations", ".gitkeep");
|
|
500
|
+
const environmentFile = path.join(projectRoot, ".env.example");
|
|
501
|
+
if (!await pathExists(manifestFile)) {
|
|
502
|
+
io.error("Database generation requires a project package.json.");
|
|
503
|
+
return 1;
|
|
504
|
+
}
|
|
505
|
+
if (!parsed.force && (await pathExists(definitionFile) || await pathExists(generatedFile))) {
|
|
506
|
+
io.error("Database files already exist. Pass --force to replace generated scaffolding.");
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const manifestContent = await fs.readFile(manifestFile, "utf8");
|
|
511
|
+
const manifest = JSON.parse(manifestContent);
|
|
512
|
+
manifest.dependencies = {
|
|
513
|
+
...manifest.dependencies,
|
|
514
|
+
"@askrjs/orm": "0.0.0",
|
|
515
|
+
...parsed.name === "postgres" ? {
|
|
516
|
+
pg: "^8.16.0",
|
|
517
|
+
"pg-query-stream": "^4.10.0"
|
|
518
|
+
} : {}
|
|
519
|
+
};
|
|
520
|
+
manifest.dependencies = Object.fromEntries(Object.entries(manifest.dependencies).sort(([left], [right]) => left.localeCompare(right)));
|
|
521
|
+
const currentEnvironment = await fs.readFile(environmentFile, "utf8").catch((error) => {
|
|
522
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
523
|
+
throw error;
|
|
524
|
+
});
|
|
525
|
+
const additions = (parsed.name === "postgres" ? ["DATABASE_URL=postgres://postgres:postgres@localhost:5432/app", "DATABASE_SHADOW_URL=postgres://postgres:postgres@localhost:5432/app_shadow"] : ["DATABASE_PATH=./data/app.sqlite"]).filter((line) => !currentEnvironment?.includes(`${line.split("=")[0]}=`));
|
|
526
|
+
const environment = `${currentEnvironment ?? ""}${currentEnvironment && !currentEnvironment.endsWith("\n") ? "\n" : ""}${additions.join("\n")}${additions.length ? "\n" : ""}`;
|
|
527
|
+
const driverImport = parsed.name;
|
|
528
|
+
const definition = [
|
|
529
|
+
"import { defineDatabase } from '@askrjs/orm';",
|
|
530
|
+
`import { ${driverImport} } from '@askrjs/orm/${driverImport}';`,
|
|
531
|
+
"import { generated } from './generated.js';",
|
|
532
|
+
"",
|
|
533
|
+
"export const database = defineDatabase({",
|
|
534
|
+
` driver: ${driverImport}(),`,
|
|
535
|
+
" tables: {},",
|
|
536
|
+
" queries: {},",
|
|
537
|
+
" generated,",
|
|
538
|
+
"});",
|
|
539
|
+
""
|
|
540
|
+
].join("\n");
|
|
541
|
+
const generated = [
|
|
542
|
+
"import type { GeneratedDatabaseArtifact } from '@askrjs/orm';",
|
|
543
|
+
"",
|
|
544
|
+
"// Generated by `askr database generate`; commit this file.",
|
|
545
|
+
"export const generated = {",
|
|
546
|
+
" manifest: { migrations: [] },",
|
|
547
|
+
" queries: {},",
|
|
548
|
+
"} as const satisfies GeneratedDatabaseArtifact;",
|
|
549
|
+
""
|
|
550
|
+
].join("\n");
|
|
551
|
+
await writeChanges([
|
|
552
|
+
{
|
|
553
|
+
filePath: definitionFile,
|
|
554
|
+
content: definition
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
filePath: generatedFile,
|
|
558
|
+
content: generated
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
filePath: migrationKeep,
|
|
562
|
+
content: ""
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
filePath: environmentFile,
|
|
566
|
+
content: environment,
|
|
567
|
+
expectedContent: currentEnvironment
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
filePath: manifestFile,
|
|
571
|
+
content: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
572
|
+
expectedContent: manifestContent
|
|
573
|
+
}
|
|
574
|
+
]);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
io.error("Failed to write generated database artifacts.");
|
|
577
|
+
io.error(error instanceof Error ? error.message : String(error));
|
|
578
|
+
return 1;
|
|
579
|
+
}
|
|
580
|
+
io.log(`Added ${parsed.name} database support.`);
|
|
581
|
+
io.log(" Definition: src/database/index.ts");
|
|
582
|
+
io.log(" Migrations: src/database/migrations");
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
481
585
|
async function runAddCli(args = process.argv.slice(2), io = console, writeChanges = writeFileChanges) {
|
|
482
586
|
const parsed = parseArgs(args);
|
|
483
587
|
if (parsed.errors.length > 0) {
|
|
@@ -494,6 +598,7 @@ async function runAddCli(args = process.argv.slice(2), io = console, writeChange
|
|
|
494
598
|
}
|
|
495
599
|
if (parsed.command === "page") return addPage(parsed, io, writeChanges);
|
|
496
600
|
if (parsed.command === "action") return addAction(parsed, io, writeChanges);
|
|
601
|
+
if (parsed.command === "database") return addDatabase(parsed, io, writeChanges);
|
|
497
602
|
io.error(`Unknown add command: ${parsed.command}`);
|
|
498
603
|
io.error("Run `askr add --help` to see available commands.");
|
|
499
604
|
return 1;
|
package/dist/analyze.d.ts
CHANGED
package/dist/analyze.js
CHANGED
|
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
|
|
|
71
71
|
io.log(helpText.trimEnd());
|
|
72
72
|
return 0;
|
|
73
73
|
}
|
|
74
|
-
const report = await (runtime.analyze ?? (await import("./runner-
|
|
74
|
+
const report = await (runtime.analyze ?? (await import("./runner-CtcqO3cF.js")).runAnalysis)({
|
|
75
75
|
cwd: parsed.cwd,
|
|
76
76
|
workspacePatterns: parsed.workspacePatterns,
|
|
77
77
|
check: parsed.check
|
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ function printHelp(io = console) {
|
|
|
37
37
|
io.log(" check Run the complete project validation path");
|
|
38
38
|
io.log(" create Create a new Askr app from a template or product prompt");
|
|
39
39
|
io.log(" database Generate, validate, and migrate project databases");
|
|
40
|
+
io.log(" docs Check or snapshot consumer-visible API documentation");
|
|
40
41
|
io.log(" doctor Diagnose environment and Askr project health");
|
|
41
42
|
io.log(" generate Generate an @askrjs/fetch client from OpenAPI");
|
|
42
43
|
io.log(" openapi Generate or check an OpenAPI YAML artifact");
|
|
@@ -91,12 +92,16 @@ async function runCli(args = process.argv.slice(2), io = console) {
|
|
|
91
92
|
const { runDatabaseCommand } = await import("./database.js");
|
|
92
93
|
return runDatabaseCommand(args.slice(1), io);
|
|
93
94
|
}
|
|
95
|
+
if (command === "docs") {
|
|
96
|
+
const { runDocsCli } = await import("./docs-BSPiXiAW.js");
|
|
97
|
+
return runDocsCli(args.slice(1), io);
|
|
98
|
+
}
|
|
94
99
|
if (command === "analyze") {
|
|
95
100
|
const { runAnalyzeCli } = await import("./analyze.js");
|
|
96
101
|
return runAnalyzeCli(args.slice(1), io);
|
|
97
102
|
}
|
|
98
103
|
if (command === "check" || command === "doctor" || command === "repair") {
|
|
99
|
-
const { runGuardrailCli } = await import("./guardrails-
|
|
104
|
+
const { runGuardrailCli } = await import("./guardrails-uLDOExsl.js");
|
|
100
105
|
return runGuardrailCli(command, args.slice(1), io);
|
|
101
106
|
}
|
|
102
107
|
if (command === "generate") {
|
package/dist/database.js
CHANGED
|
@@ -14,7 +14,7 @@ async function loadOrmTooling(cwd) {
|
|
|
14
14
|
if (!relative) throw new Error("Missing import export for @askrjs/orm/tooling.");
|
|
15
15
|
entry = path.resolve(path.dirname(manifestPath), relative);
|
|
16
16
|
} catch (error) {
|
|
17
|
-
throw new Error("This project does not have @askrjs/orm installed.
|
|
17
|
+
throw new Error("This project does not have @askrjs/orm installed. Run `npm install @askrjs/orm` before `askr database`.", { cause: error });
|
|
18
18
|
}
|
|
19
19
|
const tooling = await import(pathToFileURL(entry).href);
|
|
20
20
|
if (typeof tooling.runDatabaseCli !== "function") throw new Error("The installed @askrjs/orm package does not expose compatible database tooling.");
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
//#region src/docs.ts
|
|
6
|
+
const text = (parts) => ts.displayPartsToString([...parts]).trim();
|
|
7
|
+
const commentText = (comment) => typeof comment === "string" ? comment.trim() : comment ? String(comment).trim() : "";
|
|
8
|
+
function tagsFor(symbol, checker) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const tag of symbol.getJsDocTags(checker)) {
|
|
11
|
+
const value = typeof tag.text === "string" ? tag.text : ts.displayPartsToString([...tag.text ?? []]);
|
|
12
|
+
(result[tag.name] ??= []).push(value.trim());
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function declarationFor(symbol) {
|
|
17
|
+
return symbol.declarations?.find((declaration) => /\.d\.(?:ts|mts|cts)$/.test(declaration.getSourceFile().fileName));
|
|
18
|
+
}
|
|
19
|
+
function signatureFor(symbol, checker) {
|
|
20
|
+
const declaration = declarationFor(symbol);
|
|
21
|
+
if (!declaration) return symbol.name;
|
|
22
|
+
if (ts.isTypeAliasDeclaration(declaration)) return `${symbol.name}: ${declaration.type.getText(declaration.getSourceFile())}`;
|
|
23
|
+
if (ts.isInterfaceDeclaration(declaration) || ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) return declaration.getText(declaration.getSourceFile());
|
|
24
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
25
|
+
return `${symbol.name}: ${checker.typeToString(type, declaration, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)}`;
|
|
26
|
+
}
|
|
27
|
+
function memberDocs(symbol, checker) {
|
|
28
|
+
const declaration = declarationFor(symbol);
|
|
29
|
+
if (!declaration) return [];
|
|
30
|
+
if (ts.isVariableDeclaration(declaration)) return checker.getTypeOfSymbolAtLocation(symbol, declaration).getProperties().flatMap((property) => {
|
|
31
|
+
const propertyDeclaration = declarationFor(property);
|
|
32
|
+
if (!propertyDeclaration) return [];
|
|
33
|
+
const summary = text(property.getDocumentationComment(checker));
|
|
34
|
+
return [{
|
|
35
|
+
name: property.name,
|
|
36
|
+
summary,
|
|
37
|
+
tags: tagsFor(property, checker),
|
|
38
|
+
signature: checker.typeToString(checker.getTypeOfSymbolAtLocation(property, propertyDeclaration), propertyDeclaration)
|
|
39
|
+
}];
|
|
40
|
+
});
|
|
41
|
+
if (!(ts.isInterfaceDeclaration(declaration) || ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration) || ts.isTypeLiteralNode(declaration))) return [];
|
|
42
|
+
return (ts.isEnumDeclaration(declaration) ? declaration.members : ts.isTypeLiteralNode(declaration) ? declaration.members : declaration.members).flatMap((member) => {
|
|
43
|
+
const name = member.name && ts.isIdentifier(member.name) ? member.name.text : void 0;
|
|
44
|
+
if (!name) return [];
|
|
45
|
+
const docs = ts.getJSDocCommentsAndTags(member);
|
|
46
|
+
const summary = docs.filter(ts.isJSDoc).map((doc) => commentText(doc.comment)).filter(Boolean).join("\n");
|
|
47
|
+
const tags = {};
|
|
48
|
+
for (const tag of docs) {
|
|
49
|
+
if (!("tagName" in tag)) continue;
|
|
50
|
+
const name = tag.tagName.text;
|
|
51
|
+
(tags[name] ??= []).push(tag.comment ? String(tag.comment) : "");
|
|
52
|
+
}
|
|
53
|
+
return [{
|
|
54
|
+
name,
|
|
55
|
+
summary,
|
|
56
|
+
tags,
|
|
57
|
+
signature: member.getText(member.getSourceFile())
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function resolveTypes(value, out) {
|
|
62
|
+
if (typeof value === "string") out.push(value);
|
|
63
|
+
else if (value && typeof value === "object") {
|
|
64
|
+
for (const [key, child] of Object.entries(value)) if (key === "types") resolveTypes(child, out);
|
|
65
|
+
else if (key !== "default" && key !== "import" && key !== "require" && key !== "node" && key !== "browser") resolveTypes(child, out);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function walk(directory) {
|
|
69
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
70
|
+
const files = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const current = path.join(directory, entry.name);
|
|
73
|
+
if (entry.isDirectory()) files.push(...await walk(current));
|
|
74
|
+
else files.push(current);
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
async function declarationEntrypoints(root, pkg) {
|
|
79
|
+
const targets = [];
|
|
80
|
+
resolveTypes(pkg.exports, targets);
|
|
81
|
+
if (targets.length === 0) targets.push("./dist/index.d.ts", "./dist/index.d.mts", "./dist/index.d.cts");
|
|
82
|
+
const files = await walk(root);
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const target of new Set(targets)) {
|
|
85
|
+
if (!target.endsWith(".d.ts") && !target.endsWith(".d.mts") && !target.endsWith(".d.cts")) continue;
|
|
86
|
+
if (target.includes("*")) {
|
|
87
|
+
const prefix = target.slice(0, target.indexOf("*"));
|
|
88
|
+
const suffix = target.slice(target.indexOf("*") + 1);
|
|
89
|
+
for (const file of files) if (file.startsWith(path.resolve(root, prefix)) && file.endsWith(suffix)) result.push({
|
|
90
|
+
name: target,
|
|
91
|
+
file
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
const file = path.resolve(root, target);
|
|
95
|
+
if (files.includes(file)) result.push({
|
|
96
|
+
name: target,
|
|
97
|
+
file
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
async function inspectDocs(root = process.cwd()) {
|
|
104
|
+
const pkg = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
105
|
+
const entries = await declarationEntrypoints(root, pkg);
|
|
106
|
+
const program = ts.createProgram(entries.map((entry) => entry.file), {
|
|
107
|
+
allowJs: false,
|
|
108
|
+
skipLibCheck: true,
|
|
109
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler
|
|
110
|
+
});
|
|
111
|
+
const checker = program.getTypeChecker();
|
|
112
|
+
const symbols = [];
|
|
113
|
+
const diagnostics = [];
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const source = program.getSourceFile(entry.file);
|
|
116
|
+
const module = source && checker.getSymbolAtLocation(source);
|
|
117
|
+
if (!module) continue;
|
|
118
|
+
for (const exported of checker.getExportsOfModule(module)) {
|
|
119
|
+
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
120
|
+
const declaration = declarationFor(symbol) ?? declarationFor(exported);
|
|
121
|
+
if (!declaration) continue;
|
|
122
|
+
const summary = text(symbol.getDocumentationComment(checker));
|
|
123
|
+
const tags = tagsFor(symbol, checker);
|
|
124
|
+
const signature = signatureFor(symbol, checker);
|
|
125
|
+
const members = memberDocs(symbol, checker);
|
|
126
|
+
const declarationPath = path.relative(root, declaration.getSourceFile().fileName);
|
|
127
|
+
const base = {
|
|
128
|
+
package: pkg.name ?? path.basename(root),
|
|
129
|
+
entrypoint: entry.name,
|
|
130
|
+
symbol: exported.name,
|
|
131
|
+
declaration: declarationPath
|
|
132
|
+
};
|
|
133
|
+
if (!summary || summary === exported.name) diagnostics.push({
|
|
134
|
+
...base,
|
|
135
|
+
missing: "summary"
|
|
136
|
+
});
|
|
137
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
138
|
+
for (const signatureInfo of type.getCallSignatures()) {
|
|
139
|
+
for (const parameter of signatureInfo.parameters) if (!tags.param?.some((value) => {
|
|
140
|
+
return (value.trim().split(/\s+/, 1)[0] ?? "").replace(/^\{[^}]+\}\s*/, "") === parameter.name;
|
|
141
|
+
})) diagnostics.push({
|
|
142
|
+
...base,
|
|
143
|
+
missing: `@param ${parameter.name}`
|
|
144
|
+
});
|
|
145
|
+
if (signatureInfo.getReturnType().flags !== ts.TypeFlags.Void && !tags.returns) diagnostics.push({
|
|
146
|
+
...base,
|
|
147
|
+
missing: "@returns"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
for (const member of members) if (!member.summary || member.summary === member.name) diagnostics.push({
|
|
151
|
+
...base,
|
|
152
|
+
symbol: `${exported.name}.${member.name}`,
|
|
153
|
+
missing: "member summary"
|
|
154
|
+
});
|
|
155
|
+
symbols.push({
|
|
156
|
+
name: exported.name,
|
|
157
|
+
entrypoint: entry.name,
|
|
158
|
+
declaration: declarationPath,
|
|
159
|
+
summary,
|
|
160
|
+
...tags.remarks ? { remarks: tags.remarks.join("\n") } : {},
|
|
161
|
+
tags,
|
|
162
|
+
signature,
|
|
163
|
+
members
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
symbols.sort((a, b) => `${a.entrypoint}:${a.name}`.localeCompare(`${b.entrypoint}:${b.name}`));
|
|
168
|
+
diagnostics.sort((a, b) => `${a.entrypoint}:${a.symbol}:${a.missing}`.localeCompare(`${b.entrypoint}:${b.symbol}:${b.missing}`));
|
|
169
|
+
return {
|
|
170
|
+
snapshot: {
|
|
171
|
+
package: pkg.name ?? path.basename(root),
|
|
172
|
+
version: pkg.version,
|
|
173
|
+
generatedBy: "askr docs snapshot",
|
|
174
|
+
symbols
|
|
175
|
+
},
|
|
176
|
+
diagnostics
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/bin/docs.ts
|
|
181
|
+
async function runDocsCli(args = process.argv.slice(2), io = console) {
|
|
182
|
+
const command = args[0] ?? "check";
|
|
183
|
+
const json = args.includes("--json");
|
|
184
|
+
const rootIndex = args.findIndex((arg) => arg === "--root" || arg.startsWith("--root="));
|
|
185
|
+
const rootArg = rootIndex >= 0 ? args[rootIndex].includes("=") ? args[rootIndex].slice(7) : args[rootIndex + 1] : void 0;
|
|
186
|
+
const root = path.resolve(rootArg ?? process.cwd());
|
|
187
|
+
if (command === "--help" || command === "-h") {
|
|
188
|
+
io.log("askr docs check|snapshot [--json] [--root <path>] [--output <path>]");
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (command !== "check" && command !== "snapshot") {
|
|
192
|
+
io.error(`Unknown docs command: ${command}`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const result = await inspectDocs(root);
|
|
197
|
+
if (command === "snapshot") {
|
|
198
|
+
const outputIndex = args.findIndex((arg) => arg === "--output" || arg.startsWith("--output="));
|
|
199
|
+
const outputArg = outputIndex >= 0 ? args[outputIndex].includes("=") ? args[outputIndex].slice(9) : args[outputIndex + 1] : void 0;
|
|
200
|
+
const output = outputArg ? path.resolve(root, outputArg) : path.join(root, "docs", "api-snapshot.json");
|
|
201
|
+
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
202
|
+
await fs.writeFile(output, `${JSON.stringify(result.snapshot, null, 2)}\n`, "utf8");
|
|
203
|
+
if (json) io.log(JSON.stringify({
|
|
204
|
+
output,
|
|
205
|
+
symbols: result.snapshot.symbols.length,
|
|
206
|
+
diagnostics: result.diagnostics.length
|
|
207
|
+
}));
|
|
208
|
+
else io.log(`Wrote ${path.relative(root, output)} (${result.snapshot.symbols.length} symbols)`);
|
|
209
|
+
return result.diagnostics.length === 0 ? 0 : 1;
|
|
210
|
+
}
|
|
211
|
+
if (json) io.log(JSON.stringify(result.diagnostics));
|
|
212
|
+
else if (result.diagnostics.length) for (const diagnostic of result.diagnostics) io.error(`${diagnostic.package} ${diagnostic.entrypoint} ${diagnostic.symbol} (${diagnostic.declaration}): missing ${diagnostic.missing}`);
|
|
213
|
+
else io.log(`Documentation check passed (${result.snapshot.symbols.length} symbols)`);
|
|
214
|
+
return result.diagnostics.length === 0 ? 0 : 1;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
if (json) io.log(JSON.stringify({ error: message }));
|
|
218
|
+
else io.error(`Documentation check failed: ${message}`);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (isDirectExecution(import.meta.url)) runDocsCli();
|
|
223
|
+
//#endregion
|
|
224
|
+
export { runDocsCli };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/file-changes.ts
|
|
5
|
+
const LOCK_RETRY_MS = 10;
|
|
6
|
+
const LOCK_TIMEOUT_MS = 1e4;
|
|
7
|
+
const ORPHANED_LOCK_AGE_MS = 3e4;
|
|
8
|
+
function isNodeError(error, code) {
|
|
9
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
10
|
+
}
|
|
11
|
+
function delay(milliseconds) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
13
|
+
}
|
|
14
|
+
async function ownerIsAlive(lockPath) {
|
|
15
|
+
try {
|
|
16
|
+
const owner = JSON.parse(await fs.readFile(path.join(lockPath, "owner.json"), "utf8"));
|
|
17
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0) return void 0;
|
|
18
|
+
try {
|
|
19
|
+
process.kill(owner.pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (isNodeError(error, "ESRCH")) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function removeOrphanedLock(lockPath) {
|
|
30
|
+
const ownerAlive = await ownerIsAlive(lockPath);
|
|
31
|
+
if (ownerAlive === true) return false;
|
|
32
|
+
if (ownerAlive === void 0) {
|
|
33
|
+
const stat = await fs.stat(lockPath).catch(() => null);
|
|
34
|
+
if (!stat || Date.now() - stat.mtimeMs < ORPHANED_LOCK_AGE_MS) return false;
|
|
35
|
+
}
|
|
36
|
+
await fs.rm(lockPath, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
force: true
|
|
39
|
+
});
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
async function acquireFileLock(filePath) {
|
|
43
|
+
const lockPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.askr-lock`);
|
|
44
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
45
|
+
while (true) try {
|
|
46
|
+
await fs.mkdir(lockPath);
|
|
47
|
+
await fs.writeFile(path.join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid })}\n`, { flag: "wx" });
|
|
48
|
+
return { lockPath };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!isNodeError(error, "EEXIST")) {
|
|
51
|
+
await fs.rm(lockPath, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
force: true
|
|
54
|
+
}).catch(() => void 0);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (await removeOrphanedLock(lockPath)) continue;
|
|
58
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for file transaction lock: ${filePath}`);
|
|
59
|
+
await delay(LOCK_RETRY_MS);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function releaseFileLocks(locks) {
|
|
63
|
+
await Promise.all([...locks].reverse().map((lock) => fs.rm(lock.lockPath, {
|
|
64
|
+
recursive: true,
|
|
65
|
+
force: true
|
|
66
|
+
})));
|
|
67
|
+
}
|
|
68
|
+
async function readCurrentContent(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return await fs.readFile(filePath, "utf8");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (isNodeError(error, "ENOENT")) return null;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function hasExpectedContent(change) {
|
|
77
|
+
return Object.prototype.hasOwnProperty.call(change, "expectedContent");
|
|
78
|
+
}
|
|
79
|
+
async function remove(paths) {
|
|
80
|
+
await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
|
|
81
|
+
}
|
|
82
|
+
async function restore(changes) {
|
|
83
|
+
let complete = true;
|
|
84
|
+
for (const change of [...changes].reverse()) try {
|
|
85
|
+
if (change.original === null) {
|
|
86
|
+
await fs.rm(change.filePath, { force: true });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
|
|
90
|
+
await fs.writeFile(rollbackPath, change.original, {
|
|
91
|
+
flag: "wx",
|
|
92
|
+
mode: change.mode
|
|
93
|
+
});
|
|
94
|
+
await fs.rename(rollbackPath, change.filePath);
|
|
95
|
+
} catch {
|
|
96
|
+
complete = false;
|
|
97
|
+
}
|
|
98
|
+
return complete;
|
|
99
|
+
}
|
|
100
|
+
async function writeFileChanges(changes, options = {}) {
|
|
101
|
+
const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
102
|
+
if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
|
|
103
|
+
const replace = options.replace ?? fs.rename;
|
|
104
|
+
const guarded = ordered.filter(hasExpectedContent);
|
|
105
|
+
const locks = [];
|
|
106
|
+
try {
|
|
107
|
+
for (const change of guarded) {
|
|
108
|
+
await fs.mkdir(path.dirname(change.filePath), { recursive: true });
|
|
109
|
+
locks.push(await acquireFileLock(change.filePath));
|
|
110
|
+
}
|
|
111
|
+
for (const change of guarded) if (await readCurrentContent(change.filePath) !== change.expectedContent) throw new Error(`File changed before writing: ${change.filePath}`);
|
|
112
|
+
await writeStagedChanges(ordered, replace);
|
|
113
|
+
} finally {
|
|
114
|
+
await releaseFileLocks(locks);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function writeStagedChanges(ordered, replace) {
|
|
118
|
+
const staged = [];
|
|
119
|
+
try {
|
|
120
|
+
for (const change of ordered) {
|
|
121
|
+
await fs.mkdir(path.dirname(change.filePath), { recursive: true });
|
|
122
|
+
const stat = await fs.stat(change.filePath).catch(() => null);
|
|
123
|
+
const original = stat ? await fs.readFile(change.filePath) : null;
|
|
124
|
+
const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
|
|
125
|
+
const mode = stat?.mode ?? 420;
|
|
126
|
+
await fs.writeFile(temporaryPath, change.content, {
|
|
127
|
+
flag: "wx",
|
|
128
|
+
mode
|
|
129
|
+
});
|
|
130
|
+
staged.push({
|
|
131
|
+
...change,
|
|
132
|
+
original,
|
|
133
|
+
mode,
|
|
134
|
+
temporaryPath
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
await remove(staged.map((change) => change.temporaryPath));
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const replaced = [];
|
|
142
|
+
try {
|
|
143
|
+
for (const change of staged) {
|
|
144
|
+
await replace(change.temporaryPath, change.filePath);
|
|
145
|
+
replaced.push(change);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
const complete = await restore(replaced);
|
|
149
|
+
await remove(staged.map((change) => change.temporaryPath));
|
|
150
|
+
throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { writeFileChanges as t };
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
interface FileChange {
|
|
3
3
|
readonly filePath: string;
|
|
4
4
|
readonly content: string;
|
|
5
|
+
/** Content observed while planning a shared-file edit; `null` means the file was absent. */
|
|
6
|
+
readonly expectedContent?: string | null;
|
|
5
7
|
}
|
|
6
8
|
interface FileChangeWriterOptions {
|
|
7
9
|
readonly replace?: (temporaryPath: string, filePath: string) => Promise<void>;
|
package/dist/generate.js
CHANGED
|
@@ -588,11 +588,13 @@ async function writeGenerated(directory, files, check) {
|
|
|
588
588
|
force: true
|
|
589
589
|
});
|
|
590
590
|
} catch (error) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
591
|
+
if (moved) {
|
|
592
|
+
await rm(output, {
|
|
593
|
+
recursive: true,
|
|
594
|
+
force: true
|
|
595
|
+
});
|
|
596
|
+
await rename(backup, output);
|
|
597
|
+
}
|
|
596
598
|
throw error;
|
|
597
599
|
} finally {
|
|
598
600
|
await rm(stage, {
|
|
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
|
|
|
98
98
|
cwd: parsed.cwd,
|
|
99
99
|
workspacePatterns: parsed.workspacePatterns
|
|
100
100
|
};
|
|
101
|
-
const { runCheck, runDoctor, runRepair } = await import("./runner-
|
|
101
|
+
const { runCheck, runDoctor, runRepair } = await import("./runner-D_GIsgIj.js");
|
|
102
102
|
const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
|
|
103
103
|
if (parsed.json) io.log(JSON.stringify(report));
|
|
104
104
|
else if (report.command === "doctor") printDoctor(report, io);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as writeFileChanges } from "./file-changes-
|
|
1
|
+
import { t as writeFileChanges } from "./file-changes-CsrYAEYu.js";
|
|
2
2
|
import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as inspectBundledSkills } from "./skills-C72Z3-Cr.js";
|
|
2
2
|
import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
|
|
3
|
-
import { analysisHasBlockingFindings, runAnalysis } from "./runner-
|
|
3
|
+
import { analysisHasBlockingFindings, runAnalysis } from "./runner-CtcqO3cF.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
package/dist/ssg-config.d.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
//#region src/ssg/sitemap.d.ts
|
|
2
|
+
/** Allowed XML sitemap change-frequency values. */
|
|
2
3
|
type SitemapChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
4
|
+
/** Metadata emitted for one generated sitemap URL. */
|
|
3
5
|
interface SitemapRouteConfig {
|
|
4
6
|
/** Canonical URL. Relative values resolve against siteUrl. */
|
|
5
7
|
url?: string;
|
|
6
8
|
/** W3C date/datetime or Date describing the last meaningful page update. */
|
|
7
9
|
lastModified?: string | Date;
|
|
10
|
+
/** How often the resource is expected to change. */
|
|
8
11
|
changeFrequency?: SitemapChangeFrequency;
|
|
9
12
|
/** Relative crawl priority from 0 through 1. */
|
|
10
13
|
priority?: number;
|
|
11
14
|
/** hreflang to canonical URL mapping, including optional x-default. */
|
|
12
15
|
alternates?: Readonly<Record<string, string>>;
|
|
13
16
|
}
|
|
17
|
+
/** Route information passed to a sitemap metadata resolver. */
|
|
14
18
|
interface SitemapRouteContext {
|
|
19
|
+
/** Published route pathname. */
|
|
15
20
|
path: string;
|
|
21
|
+
/** Generated file path for the route. */
|
|
16
22
|
filePath: string;
|
|
23
|
+
/** Render status reported by the SSG pipeline. */
|
|
17
24
|
status: string;
|
|
18
25
|
/** Resolved rendered canonical URL, when the document declares one. */
|
|
19
26
|
canonical?: string;
|
|
20
27
|
}
|
|
28
|
+
/** Configuration for sitemap generation during static-site generation. */
|
|
21
29
|
interface SitemapConfig {
|
|
22
30
|
/** Site-wide values inherited by included routes. */
|
|
23
31
|
defaults?: Omit<SitemapRouteConfig, "url" | "alternates">;
|
|
@@ -28,79 +36,108 @@ interface SitemapConfig {
|
|
|
28
36
|
/** Output path relative to the SSG output directory. */
|
|
29
37
|
output?: string;
|
|
30
38
|
/** Maintain a Sitemap directive in robots.txt. Defaults to true. */
|
|
39
|
+
/** Maintain a Sitemap directive in robots.txt; an object selects its output path. */
|
|
31
40
|
robots?: boolean | {
|
|
32
41
|
output?: string;
|
|
33
42
|
};
|
|
34
43
|
/** Optional lower limits for deterministic partitioning and tests. */
|
|
35
44
|
limits?: {
|
|
36
|
-
urlsPerFile?: number;
|
|
37
|
-
bytesPerFile?: number;
|
|
45
|
+
/** Maximum URLs per generated file. */ urlsPerFile?: number;
|
|
46
|
+
/** Maximum bytes per generated file. */ bytesPerFile?: number;
|
|
38
47
|
};
|
|
39
48
|
/** Maximum concurrent async route metadata resolvers. Defaults to 16. */
|
|
40
49
|
resolverConcurrency?: number;
|
|
41
50
|
}
|
|
42
51
|
//#endregion
|
|
43
52
|
//#region src/ssg/output-report.d.ts
|
|
53
|
+
/** Raw and gzip byte limits for one emitted artifact. */
|
|
44
54
|
interface SsgByteBudget {
|
|
55
|
+
/** Maximum uncompressed byte count. */
|
|
45
56
|
raw?: number;
|
|
57
|
+
/** Maximum gzip-compressed byte count. */
|
|
46
58
|
gzip?: number;
|
|
47
59
|
}
|
|
60
|
+
/** Optional limits applied while validating an SSG output report. */
|
|
48
61
|
interface SsgOutputBudgets {
|
|
49
62
|
/** Default raw/gzip HTML limits for every route. */
|
|
50
63
|
routes?: SsgByteBudget;
|
|
51
64
|
/** Exact route overrides merged with defaults; false exempts a route. */
|
|
52
65
|
routeOverrides?: Readonly<Record<string, SsgByteBudget | false>>;
|
|
66
|
+
/** Hydration-size limits, expressed as a share of raw HTML. */
|
|
53
67
|
hydration?: {
|
|
54
68
|
/** Maximum hydration bytes as a 0..1 share of raw HTML. */
|
|
69
|
+
/** Default maximum hydration share from 0 through 1. */
|
|
55
70
|
share?: number;
|
|
56
71
|
/** Exact share overrides; false exempts a route. */
|
|
72
|
+
/** Exact route shares; false exempts a route. */
|
|
57
73
|
routes?: Readonly<Record<string, number | false>>;
|
|
58
74
|
};
|
|
59
75
|
/** Exact emitted asset raw/gzip limits; false exempts an asset. */
|
|
60
76
|
assets?: Readonly<Record<string, SsgByteBudget | false>>;
|
|
77
|
+
/** Aggregate JavaScript and CSS limits. */
|
|
61
78
|
aggregate?: {
|
|
62
79
|
javascript?: SsgByteBudget;
|
|
63
80
|
css?: SsgByteBudget;
|
|
64
81
|
};
|
|
65
82
|
}
|
|
83
|
+
/** Configuration for generating and validating an SSG output report. */
|
|
66
84
|
interface SsgOutputReportConfig {
|
|
67
85
|
/** Deployment pathname prefix stripped from root-absolute asset references. */
|
|
68
86
|
basePath?: string;
|
|
87
|
+
/** Byte and hydration limits to enforce. */
|
|
69
88
|
budgets?: SsgOutputBudgets;
|
|
70
89
|
/** Number of largest pages retained in the summary. Defaults to 20. */
|
|
71
90
|
largestPages?: number;
|
|
72
91
|
/** Number of largest assets retained in the summary. Defaults to 20. */
|
|
73
92
|
largestAssets?: number;
|
|
74
93
|
}
|
|
94
|
+
/** Raw and gzip sizes measured for an emitted resource. */
|
|
75
95
|
interface SsgOutputSize {
|
|
96
|
+
/** Uncompressed byte count. */
|
|
76
97
|
raw: number;
|
|
98
|
+
/** Gzip-compressed byte count. */
|
|
77
99
|
gzip: number;
|
|
78
100
|
}
|
|
101
|
+
/** An emitted JavaScript, CSS, or other static asset. */
|
|
79
102
|
interface SsgOutputAsset extends SsgOutputSize {
|
|
103
|
+
/** Output-relative asset path. */
|
|
80
104
|
path: string;
|
|
105
|
+
/** Classified asset type. */
|
|
81
106
|
type: "javascript" | "css" | "other";
|
|
82
107
|
}
|
|
108
|
+
/** Size and initial-asset information for one generated route. */
|
|
83
109
|
interface SsgOutputRoute {
|
|
110
|
+
/** Published route pathname. */
|
|
84
111
|
route: string;
|
|
112
|
+
/** Output-relative HTML file path. */
|
|
85
113
|
filePath: string;
|
|
114
|
+
/** Rendered HTML sizes. */
|
|
86
115
|
html: SsgOutputSize;
|
|
116
|
+
/** Hydration payload size and share of HTML. */
|
|
87
117
|
hydration: {
|
|
88
118
|
raw: number;
|
|
89
119
|
share: number;
|
|
90
120
|
};
|
|
121
|
+
/** Initial JavaScript and CSS assets referenced by the route. */
|
|
91
122
|
initial: {
|
|
92
123
|
javascript: SsgOutputAsset[];
|
|
93
124
|
css: SsgOutputAsset[];
|
|
94
125
|
};
|
|
95
126
|
}
|
|
127
|
+
/** Complete size report produced for one SSG output directory. */
|
|
96
128
|
interface SsgOutputReport {
|
|
129
|
+
/** Schema version of this report. */
|
|
97
130
|
version: 1;
|
|
131
|
+
/** All generated route measurements. */
|
|
98
132
|
routes: SsgOutputRoute[];
|
|
133
|
+
/** All emitted non-HTML assets. */
|
|
99
134
|
assets: SsgOutputAsset[];
|
|
135
|
+
/** Aggregate JavaScript and CSS sizes. */
|
|
100
136
|
aggregate: {
|
|
101
137
|
javascript: SsgOutputSize;
|
|
102
138
|
css: SsgOutputSize;
|
|
103
139
|
};
|
|
140
|
+
/** Largest pages and assets retained for diagnostics. */
|
|
104
141
|
largest: {
|
|
105
142
|
pages: SsgOutputRoute[];
|
|
106
143
|
assets: SsgOutputAsset[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "Unified CLI for the Askr platform",
|
|
5
5
|
"homepage": "https://github.com/askrjs/askr-cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,13 +43,16 @@
|
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
44
|
"test:changelog": "vp test run -c vitest.config.ts tests/changelog.test.ts",
|
|
45
45
|
"test:peer-floor": "vp test run -c vitest.integration.config.ts tests/integration/peer-floor.test.ts",
|
|
46
|
+
"test:registry-database": "vp test run -c vitest.integration.config.ts tests/integration/registry-database.test.ts",
|
|
46
47
|
"test:publint": "publint",
|
|
47
48
|
"pack:check": "npm pack --ignore-scripts --dry-run --json",
|
|
48
49
|
"test:templates": "vp test run -c vitest.integration.config.ts tests/integration/packed-templates.test.ts",
|
|
49
50
|
"bench": "npm run build --silent && npm run bench:analyze && node --import tsx benchmarks/cli.mjs --gate",
|
|
50
51
|
"bench:analyze": "vp test bench --run -c vitest.bench.config.ts",
|
|
51
52
|
"bench:json": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate --json",
|
|
52
|
-
"check": "npm run
|
|
53
|
+
"docs:check": "npm run build && node ./dist/cli.js docs check",
|
|
54
|
+
"docs:snapshot": "npm run build && node ./dist/cli.js docs snapshot",
|
|
55
|
+
"check": "npm run lint && npm run typecheck && npm run test:coverage && npm run test:changelog && npm run build && npm run docs:check && npm run test:publint && npm run pack:check",
|
|
53
56
|
"prepack": "npm run build",
|
|
54
57
|
"prepublishOnly": "npm run check && npm run test:templates && npm run test:peer-floor"
|
|
55
58
|
},
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
//#region src/file-changes.ts
|
|
5
|
-
async function remove(paths) {
|
|
6
|
-
await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
|
|
7
|
-
}
|
|
8
|
-
async function restore(changes) {
|
|
9
|
-
let complete = true;
|
|
10
|
-
for (const change of [...changes].reverse()) try {
|
|
11
|
-
if (change.original === null) {
|
|
12
|
-
await fs.rm(change.filePath, { force: true });
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
|
|
16
|
-
await fs.writeFile(rollbackPath, change.original, {
|
|
17
|
-
flag: "wx",
|
|
18
|
-
mode: change.mode
|
|
19
|
-
});
|
|
20
|
-
await fs.rename(rollbackPath, change.filePath);
|
|
21
|
-
} catch {
|
|
22
|
-
complete = false;
|
|
23
|
-
}
|
|
24
|
-
return complete;
|
|
25
|
-
}
|
|
26
|
-
async function writeFileChanges(changes, options = {}) {
|
|
27
|
-
const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
28
|
-
if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
|
|
29
|
-
const replace = options.replace ?? fs.rename;
|
|
30
|
-
const staged = [];
|
|
31
|
-
try {
|
|
32
|
-
for (const change of ordered) {
|
|
33
|
-
await fs.mkdir(path.dirname(change.filePath), { recursive: true });
|
|
34
|
-
const stat = await fs.stat(change.filePath).catch(() => null);
|
|
35
|
-
const original = stat ? await fs.readFile(change.filePath) : null;
|
|
36
|
-
const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
|
|
37
|
-
const mode = stat?.mode ?? 420;
|
|
38
|
-
await fs.writeFile(temporaryPath, change.content, {
|
|
39
|
-
flag: "wx",
|
|
40
|
-
mode
|
|
41
|
-
});
|
|
42
|
-
staged.push({
|
|
43
|
-
...change,
|
|
44
|
-
original,
|
|
45
|
-
mode,
|
|
46
|
-
temporaryPath
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
await remove(staged.map((change) => change.temporaryPath));
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
const replaced = [];
|
|
54
|
-
try {
|
|
55
|
-
for (const change of staged) {
|
|
56
|
-
await replace(change.temporaryPath, change.filePath);
|
|
57
|
-
replaced.push(change);
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
const complete = await restore(replaced);
|
|
61
|
-
await remove(staged.map((change) => change.temporaryPath));
|
|
62
|
-
throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
66
|
-
export { writeFileChanges as t };
|