@topik/cli 0.1.0-alpha.3 → 0.1.0-alpha.4
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/cli.mjs +104 -25
- package/package.json +9 -9
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { boolean, command, positional, run, string } from "@drizzle-team/brocli";
|
|
3
|
+
import { CompileError, compile, isErrorDiagnostic, lint, validateResources, watch } from "@topik/core";
|
|
3
4
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { extname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
7
7
|
import { createServer } from "node:http";
|
|
8
8
|
import { parseAllDocuments } from "yaml";
|
|
9
|
+
//#region src/diagnostics.ts
|
|
10
|
+
function formatDiagnostic(diagnostic) {
|
|
11
|
+
return `${diagnostic.file ?? "content"}${diagnostic.lines.length > 0 ? `:${diagnostic.lines.join(",")}` : ""} ${diagnostic.level} ${diagnostic.id}: ${diagnostic.message}`;
|
|
12
|
+
}
|
|
13
|
+
function printDiagnostics(diagnostics) {
|
|
14
|
+
for (const diagnostic of diagnostics) {
|
|
15
|
+
const output = formatDiagnostic(diagnostic);
|
|
16
|
+
if (diagnostic.level === "error" || diagnostic.level === "critical") console.error(output);
|
|
17
|
+
else console.warn(output);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
9
21
|
//#region src/errors.ts
|
|
10
22
|
var CliError = class extends Error {
|
|
11
23
|
constructor(message, options) {
|
|
@@ -49,12 +61,18 @@ const compile$1 = command({
|
|
|
49
61
|
format: string("format").alias("f").desc("Output format").enum("json", "jsonl", "yaml").default("json"),
|
|
50
62
|
dryRun: boolean("dry-run").desc("Show what would be compiled without writing files").default(false),
|
|
51
63
|
clean: boolean("clean").desc("Remove existing output before compiling").default(false),
|
|
52
|
-
validate: boolean("validate").desc("Validate compiled resources against schemas").default(false)
|
|
64
|
+
validate: boolean("validate").desc("Validate compiled resources against schemas").default(false),
|
|
65
|
+
links: string("links").desc("How unresolved wiki links and local guide fragments are handled").enum("error", "warning", "off").default("error")
|
|
53
66
|
},
|
|
54
67
|
handler: async (options) => {
|
|
55
68
|
const dir = resolve(options.dir);
|
|
56
69
|
const format = options.format;
|
|
57
|
-
const
|
|
70
|
+
const links = options.links;
|
|
71
|
+
const { diagnostics, resources } = await compile({
|
|
72
|
+
dir,
|
|
73
|
+
validation: { links }
|
|
74
|
+
});
|
|
75
|
+
printDiagnostics(diagnostics);
|
|
58
76
|
if (options.validate) {
|
|
59
77
|
const { valid, errors } = validateResources(resources);
|
|
60
78
|
if (!valid) throw new CliError(formatValidationFailure(errors, resources.length, "validating compiled output"));
|
|
@@ -119,6 +137,62 @@ function handleEvents(watcher, _req, res) {
|
|
|
119
137
|
watcher.off("update", onUpdate);
|
|
120
138
|
});
|
|
121
139
|
}
|
|
140
|
+
function getAsset(watcher, name) {
|
|
141
|
+
const resource = watcher.resources.get(`Asset/${name}`);
|
|
142
|
+
return resource?.type === "Asset" ? resource : null;
|
|
143
|
+
}
|
|
144
|
+
function handleAsset(watcher, dir, url, res) {
|
|
145
|
+
if (!url.pathname.startsWith("/assets/")) return false;
|
|
146
|
+
let name;
|
|
147
|
+
try {
|
|
148
|
+
name = decodeURIComponent(url.pathname.slice(8));
|
|
149
|
+
} catch {
|
|
150
|
+
res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
|
|
151
|
+
res.end();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
155
|
+
res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
|
|
156
|
+
res.end();
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const asset = getAsset(watcher, name);
|
|
160
|
+
if (!asset) {
|
|
161
|
+
res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
|
|
162
|
+
res.end();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
let filePath;
|
|
166
|
+
let realDir;
|
|
167
|
+
let realFilePath;
|
|
168
|
+
try {
|
|
169
|
+
filePath = resolve(join(dir, asset.spec.uri));
|
|
170
|
+
realDir = realpathSync(dir);
|
|
171
|
+
realFilePath = realpathSync(filePath);
|
|
172
|
+
} catch {
|
|
173
|
+
res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
|
|
174
|
+
res.end();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
const relativeAssetPath = relative(realDir, realFilePath);
|
|
178
|
+
if (relativeAssetPath !== "" && (relativeAssetPath.startsWith("..") || isAbsolute(relativeAssetPath)) || !existsSync(realFilePath)) {
|
|
179
|
+
res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
|
|
180
|
+
res.end();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
readFile(filePath).then((data) => {
|
|
184
|
+
res.writeHead(200, {
|
|
185
|
+
"Content-Type": asset.spec.mediaType ?? ASSET_EXTENSIONS[extname(filePath)] ?? "application/octet-stream",
|
|
186
|
+
"Access-Control-Allow-Origin": "*",
|
|
187
|
+
"Cache-Control": "no-cache"
|
|
188
|
+
});
|
|
189
|
+
res.end(data);
|
|
190
|
+
}).catch(() => {
|
|
191
|
+
res.writeHead(500, { "Access-Control-Allow-Origin": "*" });
|
|
192
|
+
res.end();
|
|
193
|
+
});
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
122
196
|
const dev = command({
|
|
123
197
|
name: "dev",
|
|
124
198
|
desc: "Start a development server that watches for content changes",
|
|
@@ -165,25 +239,7 @@ const dev = command({
|
|
|
165
239
|
handleEvents(watcher, req, res);
|
|
166
240
|
return;
|
|
167
241
|
}
|
|
168
|
-
if (req.method === "GET")
|
|
169
|
-
const mime = ASSET_EXTENSIONS[extname(url.pathname)];
|
|
170
|
-
if (mime) {
|
|
171
|
-
const filePath = resolve(join(dir, url.pathname));
|
|
172
|
-
if (filePath.startsWith(dir + "/") && existsSync(filePath)) {
|
|
173
|
-
readFile(filePath).then((data) => {
|
|
174
|
-
res.writeHead(200, {
|
|
175
|
-
"Content-Type": mime,
|
|
176
|
-
"Access-Control-Allow-Origin": "*"
|
|
177
|
-
});
|
|
178
|
-
res.end(data);
|
|
179
|
-
}).catch(() => {
|
|
180
|
-
res.writeHead(500, { "Access-Control-Allow-Origin": "*" });
|
|
181
|
-
res.end();
|
|
182
|
-
});
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
242
|
+
if (req.method === "GET" && handleAsset(watcher, dir, url, res)) return;
|
|
187
243
|
res.writeHead(404, {
|
|
188
244
|
"Content-Type": "text/plain",
|
|
189
245
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -201,6 +257,28 @@ const dev = command({
|
|
|
201
257
|
}
|
|
202
258
|
});
|
|
203
259
|
//#endregion
|
|
260
|
+
//#region src/lint/index.ts
|
|
261
|
+
const lint$1 = command({
|
|
262
|
+
name: "lint",
|
|
263
|
+
desc: "Lint Topik source content without writing compiled resources",
|
|
264
|
+
options: {
|
|
265
|
+
dir: positional("dir").desc("Path to the content directory").default("."),
|
|
266
|
+
links: string("links").desc("How unresolved wiki links and local guide fragments are handled").enum("error", "warning", "off").default("error")
|
|
267
|
+
},
|
|
268
|
+
handler: async (options) => {
|
|
269
|
+
const links = options.links;
|
|
270
|
+
const { diagnostics } = await lint({
|
|
271
|
+
dir: resolve(options.dir),
|
|
272
|
+
validation: { links }
|
|
273
|
+
});
|
|
274
|
+
printDiagnostics(diagnostics);
|
|
275
|
+
const errors = diagnostics.filter(isErrorDiagnostic);
|
|
276
|
+
if (errors.length > 0) throw new CliError(`${errors.length} lint error(s)`);
|
|
277
|
+
const warnings = diagnostics.filter((diagnostic) => diagnostic.level === "warning").length;
|
|
278
|
+
console.log(`Lint passed${warnings > 0 ? ` with ${warnings} warning(s)` : ""}`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
//#endregion
|
|
204
282
|
//#region src/validate/index.ts
|
|
205
283
|
function parseJson(path, content) {
|
|
206
284
|
try {
|
|
@@ -276,13 +354,14 @@ try {
|
|
|
276
354
|
await run([
|
|
277
355
|
compile$1,
|
|
278
356
|
dev,
|
|
357
|
+
lint$1,
|
|
279
358
|
validate
|
|
280
359
|
], {
|
|
281
360
|
name: "topik",
|
|
282
361
|
description: "Topik CLI"
|
|
283
362
|
});
|
|
284
363
|
} catch (error) {
|
|
285
|
-
if (error instanceof CliError) {
|
|
364
|
+
if (error instanceof CliError || error instanceof CompileError) {
|
|
286
365
|
console.error(error.message);
|
|
287
366
|
process.exit(1);
|
|
288
367
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topik/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "Topik CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@drizzle-team/brocli": "^0.12.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"@topik/core": "0.1.0-alpha.
|
|
27
|
+
"yaml": "^2.9.0",
|
|
28
|
+
"@topik/content-schema": "0.1.0-alpha.4",
|
|
29
|
+
"@topik/core": "0.1.0-alpha.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/bun": "^1.3.
|
|
33
|
-
"@types/node": "
|
|
34
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
35
|
-
"typescript": "^6",
|
|
36
|
-
"vite-plus": "
|
|
32
|
+
"@types/bun": "^1.3.14",
|
|
33
|
+
"@types/node": "22.20.1",
|
|
34
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vite-plus": "0.2.4"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "vp pack",
|