@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.
Files changed (2) hide show
  1. package/dist/cli.mjs +104 -25
  2. 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 { compile, validateResources, watch } from "@topik/core";
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 { resources } = await compile({ dir });
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",
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
- "@markdoc/markdoc": "^0.5.7",
28
- "yaml": "^2.8.3",
29
- "@topik/core": "0.1.0-alpha.3"
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.13",
33
- "@types/node": "^24",
34
- "@typescript/native-preview": "7.0.0-dev.20260427.1",
35
- "typescript": "^6",
36
- "vite-plus": "^0.1.18"
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",