@topik/core 0.1.0-alpha.2 → 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/index.d.mts CHANGED
@@ -1,141 +1,123 @@
1
1
  import { z } from "zod";
2
- import { Guide, Wiki, WikiPage } from "@topik/schema";
2
+ import { Asset, Guide, Wiki, WikiPage } from "@topik/schema";
3
+ import { TopikContentDiagnostic } from "@topik/content-schema";
3
4
 
4
5
  //#region src/config/collection.d.ts
5
6
  declare const collectionConfigSchema: z.ZodObject<{
6
7
  id: z.ZodString;
7
8
  title: z.ZodString;
8
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
- }, "strip", z.ZodTypeAny, {
10
- id: string;
11
- title: string;
12
- tags?: string[] | undefined;
13
- }, {
14
- id: string;
15
- title: string;
16
- tags?: string[] | undefined;
17
- }>;
9
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
+ }, z.core.$strip>;
18
11
  type CollectionConfig = z.infer<typeof collectionConfigSchema>;
19
12
  declare function parseCollectionConfig(raw: unknown): CollectionConfig;
20
13
  //#endregion
21
14
  //#region src/config/wiki.d.ts
22
- type WikiNavNode = string | {
23
- group: string;
15
+ type WikiPageNavNode = {
16
+ type: "page";
17
+ slug: string;
18
+ icon?: string;
19
+ hidden?: boolean;
20
+ };
21
+ type WikiGroupNavNode = {
22
+ type: "group";
23
+ title: string;
24
24
  slug?: string;
25
25
  icon?: string;
26
+ expanded?: boolean;
27
+ hidden?: boolean;
26
28
  children: WikiNavNode[];
27
- } | {
28
- tab: string;
29
+ };
30
+ type WikiInternalSwitcherNavNode = {
31
+ type: "tab" | "dropdown";
32
+ title: string;
29
33
  slug?: string;
30
34
  icon?: string;
35
+ hidden?: boolean;
31
36
  children: WikiNavNode[];
32
- } | {
37
+ };
38
+ type WikiExternalSwitcherNavNode = {
39
+ type: "tab" | "dropdown";
40
+ title: string;
33
41
  href: string;
42
+ icon?: string;
43
+ hidden?: boolean;
44
+ };
45
+ type WikiLinkNavNode = {
46
+ type: "link";
34
47
  title: string;
48
+ href: string;
35
49
  icon?: string;
50
+ hidden?: boolean;
36
51
  };
52
+ type WikiNavNode = string | WikiPageNavNode | WikiGroupNavNode | WikiInternalSwitcherNavNode | WikiExternalSwitcherNavNode | WikiLinkNavNode;
37
53
  declare const wikiConfigSchema: z.ZodObject<{
38
54
  id: z.ZodString;
39
55
  title: z.ZodString;
40
- navigation: z.ZodOptional<z.ZodArray<z.ZodType<WikiNavNode, z.ZodTypeDef, unknown>, "many">>;
56
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
57
+ navigation: z.ZodOptional<z.ZodArray<z.ZodType<WikiNavNode, unknown, z.core.$ZodTypeInternals<WikiNavNode, unknown>>>>;
41
58
  theme: z.ZodOptional<z.ZodObject<{
42
59
  colors: z.ZodOptional<z.ZodObject<{
43
60
  primary: z.ZodString;
44
61
  light: z.ZodOptional<z.ZodString>;
45
62
  dark: z.ZodOptional<z.ZodString>;
46
- }, "strip", z.ZodTypeAny, {
47
- primary: string;
48
- light?: string | undefined;
49
- dark?: string | undefined;
50
- }, {
51
- primary: string;
52
- light?: string | undefined;
53
- dark?: string | undefined;
54
- }>>;
63
+ }, z.core.$strip>>;
55
64
  appearance: z.ZodOptional<z.ZodObject<{
56
- default: z.ZodOptional<z.ZodEnum<["light", "dark", "system"]>>;
57
- }, "strip", z.ZodTypeAny, {
58
- default?: "dark" | "light" | "system" | undefined;
59
- }, {
60
- default?: "dark" | "light" | "system" | undefined;
61
- }>>;
62
- }, "strip", z.ZodTypeAny, {
63
- colors?: {
64
- primary: string;
65
- light?: string | undefined;
66
- dark?: string | undefined;
67
- } | undefined;
68
- appearance?: {
69
- default?: "dark" | "light" | "system" | undefined;
70
- } | undefined;
71
- }, {
72
- colors?: {
73
- primary: string;
74
- light?: string | undefined;
75
- dark?: string | undefined;
76
- } | undefined;
77
- appearance?: {
78
- default?: "dark" | "light" | "system" | undefined;
79
- } | undefined;
80
- }>>;
81
- }, "strip", z.ZodTypeAny, {
82
- id: string;
83
- title: string;
84
- navigation?: WikiNavNode[] | undefined;
85
- theme?: {
86
- colors?: {
87
- primary: string;
88
- light?: string | undefined;
89
- dark?: string | undefined;
90
- } | undefined;
91
- appearance?: {
92
- default?: "dark" | "light" | "system" | undefined;
93
- } | undefined;
94
- } | undefined;
95
- }, {
96
- id: string;
97
- title: string;
98
- navigation?: unknown[] | undefined;
99
- theme?: {
100
- colors?: {
101
- primary: string;
102
- light?: string | undefined;
103
- dark?: string | undefined;
104
- } | undefined;
105
- appearance?: {
106
- default?: "dark" | "light" | "system" | undefined;
107
- } | undefined;
108
- } | undefined;
109
- }>;
65
+ default: z.ZodOptional<z.ZodEnum<{
66
+ dark: "dark";
67
+ light: "light";
68
+ system: "system";
69
+ }>>;
70
+ }, z.core.$strip>>;
71
+ }, z.core.$strip>>;
72
+ }, z.core.$strip>;
110
73
  type WikiConfig = z.infer<typeof wikiConfigSchema>;
111
74
  declare function parseWikiConfig(raw: unknown): WikiConfig;
112
75
  //#endregion
113
76
  //#region src/resource.d.ts
114
- type Resource = Guide | Wiki | WikiPage;
77
+ type Resource = Asset | Guide | Wiki | WikiPage;
115
78
  type ResourceType = Resource["type"];
116
79
  //#endregion
117
80
  //#region src/compile/shared.d.ts
118
81
  interface CompileResult {
82
+ diagnostics: TopikContentDiagnostic[];
119
83
  resources: Resource[];
120
84
  }
85
+ type LinkValidationPolicy = "error" | "warning" | "off";
86
+ interface CompileValidationOptions {
87
+ /** How unresolved wiki page links and same-page guide fragments are handled. */
88
+ links?: LinkValidationPolicy;
89
+ }
90
+ declare class CompileError extends Error {
91
+ readonly diagnostics: TopikContentDiagnostic[];
92
+ constructor(diagnostics: TopikContentDiagnostic[]);
93
+ }
94
+ declare function isErrorDiagnostic(diagnostic: TopikContentDiagnostic): boolean;
121
95
  //#endregion
122
96
  //#region src/compile/wiki.d.ts
123
97
  interface CompileWikiOptions {
124
98
  dir: string;
99
+ validation?: CompileValidationOptions;
125
100
  }
126
101
  declare function compileWiki(options: CompileWikiOptions): Promise<CompileResult>;
102
+ declare function pagePathToName(wikiId: string, pagePath: string): string;
127
103
  //#endregion
128
104
  //#region src/compile/guide.d.ts
129
105
  interface CompileGuidesOptions {
130
106
  dir: string;
107
+ validation?: CompileValidationOptions;
131
108
  }
132
109
  declare function compileGuides(options: CompileGuidesOptions): Promise<CompileResult>;
133
110
  //#endregion
134
111
  //#region src/compile/index.d.ts
135
112
  interface CompileOptions {
136
113
  dir: string;
114
+ validation?: CompileValidationOptions;
137
115
  }
138
116
  declare function compile(options: CompileOptions): Promise<CompileResult>;
117
+ interface LintResult {
118
+ diagnostics: CompileResult["diagnostics"];
119
+ }
120
+ declare function lint(options: CompileOptions): Promise<LintResult>;
139
121
  //#endregion
140
122
  //#region src/validate.d.ts
141
123
  interface ValidationError {
@@ -168,4 +150,4 @@ interface Watcher {
168
150
  }
169
151
  declare function watch(options: WatchOptions): Promise<Watcher>;
170
152
  //#endregion
171
- export { type CollectionConfig, type CompileGuidesOptions, type CompileOptions, type CompileResult, type CompileWikiOptions, type Resource, type ResourceType, type ValidationError, type ValidationResult, type WatchOptions, type Watcher, type WikiConfig, compile, compileGuides, compileWiki, parseCollectionConfig, parseWikiConfig, validateResources, watch };
153
+ export { type CollectionConfig, CompileError, type CompileGuidesOptions, type CompileOptions, type CompileResult, type CompileValidationOptions, type CompileWikiOptions, type LinkValidationPolicy, type LintResult, type Resource, type ResourceType, type ValidationError, type ValidationResult, type WatchOptions, type Watcher, type WikiConfig, compile, compileGuides, compileWiki, isErrorDiagnostic, lint, pagePathToName, parseCollectionConfig, parseWikiConfig, validateResources, watch };
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
- import { join, resolve } from "node:path";
2
+ import { WIKI_EXTERNAL_HREF_PATTERN, WIKI_NAV_ICON_PATTERN, assetSchema, guideSchema, joinWikiPath, wikiPageSchema, wikiSchema } from "@topik/schema";
3
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
4
  import { access, readFile, readdir } from "node:fs/promises";
4
5
  import { parse } from "yaml";
6
+ import { createHash } from "node:crypto";
7
+ import { analyzeTopikContent, formatTopikContent, parseTopikContent, validateTopikContent } from "@topik/content-schema";
5
8
  import Ajv2020 from "ajv/dist/2020.js";
6
9
  import addFormats from "ajv-formats";
7
- import { guideSchema, wikiPageSchema, wikiSchema } from "@topik/schema";
8
10
  import { EventEmitter } from "node:events";
9
11
  import { watch as watch$1 } from "chokidar";
10
12
  //#region src/config/collection.ts
@@ -18,27 +20,125 @@ function parseCollectionConfig(raw) {
18
20
  }
19
21
  //#endregion
20
22
  //#region src/config/wiki.ts
23
+ const wikiNavIconPattern = new RegExp(WIKI_NAV_ICON_PATTERN);
24
+ const wikiExternalHrefPattern = new RegExp(WIKI_EXTERNAL_HREF_PATTERN);
25
+ const pathSegment = z.string().min(1).max(256).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slugs must be lowercase DNS-style segments");
21
26
  const wikiPagePath = z.string().min(1).max(512).regex(/^(?:[a-z0-9]+(?:-[a-z0-9]+)*)(?:\/(?:[a-z0-9]+(?:-[a-z0-9]+)*))*$/, "Wiki page paths must use lowercase DNS-style segments separated by '/'");
22
- const navNodeSchema = z.union([
27
+ const title = z.string().min(1).max(256);
28
+ const icon = z.string().min(1).max(256).regex(wikiNavIconPattern, "Icons must be lowercase DNS-style names").optional();
29
+ const href = z.string().max(2048).refine((value) => {
30
+ if (!wikiExternalHrefPattern.test(value)) return false;
31
+ try {
32
+ const url = new URL(value);
33
+ return url.protocol === "https:" || url.protocol === "http:";
34
+ } catch {
35
+ return false;
36
+ }
37
+ }, { message: "href must be a valid http or https URL" });
38
+ const navNodeSchema = z.lazy(() => z.union([
23
39
  wikiPagePath,
24
40
  z.object({
25
- group: z.string().min(1).max(256),
26
- slug: z.string().min(1).max(256).optional(),
27
- icon: z.string().min(1).max(256).optional(),
28
- children: z.lazy(() => z.array(navNodeSchema)).default([])
29
- }),
41
+ type: z.literal("page"),
42
+ slug: wikiPagePath,
43
+ icon,
44
+ hidden: z.boolean().optional()
45
+ }).strict(),
46
+ z.object({
47
+ type: z.literal("group"),
48
+ title,
49
+ slug: pathSegment.optional(),
50
+ icon,
51
+ expanded: z.boolean().optional(),
52
+ hidden: z.boolean().optional(),
53
+ children: z.array(navNodeSchema).default([])
54
+ }).strict(),
55
+ z.object({
56
+ type: z.union([z.literal("tab"), z.literal("dropdown")]),
57
+ title,
58
+ slug: pathSegment.optional(),
59
+ icon,
60
+ hidden: z.boolean().optional(),
61
+ children: z.array(navNodeSchema).default([])
62
+ }).strict(),
30
63
  z.object({
31
- tab: z.string().min(1).max(256),
32
- slug: z.string().min(1).max(256).optional(),
33
- icon: z.string().min(1).max(256).optional(),
34
- children: z.lazy(() => z.array(navNodeSchema)).default([])
35
- }),
64
+ type: z.union([z.literal("tab"), z.literal("dropdown")]),
65
+ title,
66
+ href,
67
+ icon,
68
+ hidden: z.boolean().optional()
69
+ }).strict(),
36
70
  z.object({
37
- href: z.string().max(2048).refine((v) => v.startsWith("https://") || v.startsWith("http://"), { message: "href must be an http or https URL" }),
38
- title: z.string().min(1).max(256),
39
- icon: z.string().min(1).max(256).optional()
40
- })
41
- ]);
71
+ type: z.literal("link"),
72
+ title,
73
+ href,
74
+ icon,
75
+ hidden: z.boolean().optional()
76
+ }).strict()
77
+ ]));
78
+ function nodeKind(node) {
79
+ if (typeof node !== "string" && node.type === "tab") return "tab";
80
+ if (typeof node !== "string" && node.type === "dropdown") return "dropdown";
81
+ return "sidebar";
82
+ }
83
+ function validateHomogeneousLevel(nodes, allowed, path, context) {
84
+ const kinds = nodes.map(nodeKind);
85
+ nodes.forEach((_node, index) => {
86
+ const kind = kinds[index];
87
+ if (!allowed.has(kind)) {
88
+ const nodePath = [...path, index];
89
+ context.addIssue({
90
+ code: "custom",
91
+ path: nodePath,
92
+ message: `Navigation ${kind} nodes are not allowed at ${formatConfigPath(nodePath)}`
93
+ });
94
+ }
95
+ });
96
+ const expectedKind = kinds[0];
97
+ kinds.forEach((kind, index) => {
98
+ if (kind === expectedKind) return;
99
+ const nodePath = [...path, index];
100
+ context.addIssue({
101
+ code: "custom",
102
+ path: nodePath,
103
+ message: `Navigation surfaces cannot be mixed at the same level: expected ${expectedKind}, found ${kind} at ${formatConfigPath(nodePath)}`
104
+ });
105
+ });
106
+ nodes.forEach((node, index) => {
107
+ if (typeof node === "string" || !("children" in node)) return;
108
+ const childPath = [
109
+ ...path,
110
+ index,
111
+ "children"
112
+ ];
113
+ if (node.type === "tab") validateHomogeneousLevel(node.children, /* @__PURE__ */ new Set(["dropdown", "sidebar"]), childPath, context);
114
+ else validateHomogeneousLevel(node.children, /* @__PURE__ */ new Set(["sidebar"]), childPath, context);
115
+ });
116
+ }
117
+ function validateUniquePageRoutes(nodes, path, context, prefix = "", routes = /* @__PURE__ */ new Map()) {
118
+ nodes.forEach((node, index) => {
119
+ const nodePath = [...path, index];
120
+ if (typeof node === "string" || node.type === "page") {
121
+ const sourcePath = joinNavigationPath(prefix, typeof node === "string" ? node : node.slug);
122
+ const route = sourcePath === "index" ? "" : sourcePath.replace(/\/index$/, "");
123
+ const previousPath = routes.get(route);
124
+ if (previousPath) context.addIssue({
125
+ code: "custom",
126
+ path: nodePath,
127
+ message: `Navigation contains duplicate page route /${route}: first defined at ${formatConfigPath(previousPath)}, duplicated at ${formatConfigPath(nodePath)}`
128
+ });
129
+ else routes.set(route, nodePath);
130
+ return;
131
+ }
132
+ if ("children" in node) validateUniquePageRoutes(node.children, [...nodePath, "children"], context, joinNavigationPath(prefix, node.slug), routes);
133
+ });
134
+ }
135
+ function formatConfigPath(path) {
136
+ return path.reduce((formatted, segment) => typeof segment === "number" ? `${formatted}[${segment}]` : formatted ? `${formatted}.${segment}` : segment, "");
137
+ }
138
+ function joinNavigationPath(prefix, path) {
139
+ if (!path) return prefix;
140
+ return prefix ? `${prefix}/${path}` : path;
141
+ }
42
142
  const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, "Must be a hex color (e.g. #ff73a8)");
43
143
  const themeSchema = z.object({
44
144
  colors: z.object({
@@ -52,11 +152,23 @@ const themeSchema = z.object({
52
152
  "system"
53
153
  ]).optional() }).optional()
54
154
  });
155
+ const nameRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
156
+ const MAX_WIKI_ID_FOR_PAGE_NAMES = 46;
55
157
  const wikiConfigSchema = z.object({
56
- id: z.string().min(1).max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
158
+ id: z.string().min(1).max(MAX_WIKI_ID_FOR_PAGE_NAMES, `Wiki id must be ${MAX_WIKI_ID_FOR_PAGE_NAMES} characters or fewer`).regex(nameRegex),
57
159
  title: z.string().min(1).max(256),
160
+ description: z.union([z.string().max(1024), z.null()]).optional(),
58
161
  navigation: z.array(navNodeSchema).optional(),
59
162
  theme: themeSchema.optional()
163
+ }).superRefine((config, context) => {
164
+ if (config.navigation) {
165
+ validateHomogeneousLevel(config.navigation, /* @__PURE__ */ new Set([
166
+ "tab",
167
+ "dropdown",
168
+ "sidebar"
169
+ ]), ["navigation"], context);
170
+ validateUniquePageRoutes(config.navigation, ["navigation"], context);
171
+ }
60
172
  });
61
173
  function parseWikiConfig(raw) {
62
174
  return wikiConfigSchema.parse(raw);
@@ -91,7 +203,222 @@ async function findConfigFile(dir, candidates) {
91
203
  return null;
92
204
  }
93
205
  //#endregion
206
+ //#region src/compile/assets.ts
207
+ const MIME_BY_EXT = {
208
+ ".png": "image/png",
209
+ ".jpg": "image/jpeg",
210
+ ".jpeg": "image/jpeg",
211
+ ".gif": "image/gif",
212
+ ".svg": "image/svg+xml",
213
+ ".webp": "image/webp",
214
+ ".avif": "image/avif",
215
+ ".ico": "image/x-icon",
216
+ ".bmp": "image/bmp",
217
+ ".mp4": "video/mp4",
218
+ ".webm": "video/webm",
219
+ ".pdf": "application/pdf",
220
+ ".zip": "application/zip",
221
+ ".tar": "application/x-tar",
222
+ ".gz": "application/gzip",
223
+ ".mp3": "audio/mpeg",
224
+ ".wav": "audio/wav",
225
+ ".ogg": "audio/ogg",
226
+ ".m4a": "audio/mp4",
227
+ ".flac": "audio/flac",
228
+ ".csv": "text/csv",
229
+ ".txt": "text/plain",
230
+ ".doc": "application/msword",
231
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
232
+ ".xls": "application/vnd.ms-excel",
233
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
234
+ ".ppt": "application/vnd.ms-powerpoint",
235
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
236
+ };
237
+ const ASSET_TAG_ATTRS = [
238
+ "src",
239
+ "darkSrc",
240
+ "href"
241
+ ];
242
+ async function extractAssets(source, options) {
243
+ const { baseDir, filePath } = options;
244
+ const ast = parseTopikContent(source);
245
+ const refs = /* @__PURE__ */ new Map();
246
+ const slots = /* @__PURE__ */ new Map();
247
+ const collect = (url, slot, requireKnownExtension) => {
248
+ if (!isLocalRef(url)) return;
249
+ if (requireKnownExtension && !hasKnownAssetExtension(url)) return;
250
+ if (!refs.has(url)) {
251
+ const absPath = resolveAssetPath(url, {
252
+ baseDir,
253
+ filePath
254
+ });
255
+ const relFromBase = relative(baseDir, absPath).split(sep).join(posix.sep);
256
+ refs.set(url, {
257
+ original: url,
258
+ absPath,
259
+ relFromBase
260
+ });
261
+ }
262
+ const list = slots.get(url) ?? [];
263
+ list.push(slot);
264
+ slots.set(url, list);
265
+ };
266
+ walk(ast, (node) => {
267
+ if (node.type === "image") {
268
+ const url = stringAttr(node, "src");
269
+ if (url) collect(url, {
270
+ node,
271
+ attr: "src"
272
+ }, false);
273
+ } else if (node.type === "link") {
274
+ const url = stringAttr(node, "href");
275
+ if (url) collect(url, {
276
+ node,
277
+ attr: "href"
278
+ }, true);
279
+ } else if (node.type === "tag") for (const attr of ASSET_TAG_ATTRS) {
280
+ const url = stringAttr(node, attr);
281
+ if (url) collect(url, {
282
+ node,
283
+ attr
284
+ }, true);
285
+ }
286
+ });
287
+ if (refs.size === 0) return {
288
+ content: source,
289
+ assets: [],
290
+ manifest: []
291
+ };
292
+ const urlToId = /* @__PURE__ */ new Map();
293
+ const byName = /* @__PURE__ */ new Map();
294
+ const refList = Array.from(refs.values());
295
+ const settled = await Promise.allSettled(refList.map((ref) => toAsset(ref)));
296
+ const failures = [];
297
+ for (let i = 0; i < settled.length; i++) {
298
+ const result = settled[i];
299
+ if (result.status === "fulfilled") {
300
+ urlToId.set(refList[i].original, result.value.name);
301
+ byName.set(result.value.name, result.value);
302
+ } else failures.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
303
+ }
304
+ if (failures.length > 0) throw new Error(`Failed to extract ${failures.length} asset(s):\n - ${failures.join("\n - ")}`);
305
+ const manifest = [];
306
+ const seen = /* @__PURE__ */ new Set();
307
+ for (const ref of refs.values()) {
308
+ const name = urlToId.get(ref.original);
309
+ if (name && !seen.has(name)) {
310
+ manifest.push(name);
311
+ seen.add(name);
312
+ }
313
+ }
314
+ for (const [url, slotList] of slots.entries()) {
315
+ const id = urlToId.get(url);
316
+ if (!id) continue;
317
+ for (const slot of slotList) slot.node.attributes[slot.attr] = `asset:${id}`;
318
+ }
319
+ return {
320
+ content: formatTopikContent(ast),
321
+ assets: Array.from(byName.values()),
322
+ manifest
323
+ };
324
+ }
325
+ function walk(node, fn) {
326
+ fn(node);
327
+ for (const child of node.children) walk(child, fn);
328
+ }
329
+ function stringAttr(node, attr) {
330
+ const value = node.attributes?.[attr];
331
+ return typeof value === "string" ? value : void 0;
332
+ }
333
+ function hasKnownAssetExtension(url) {
334
+ const [path] = splitUrl(url);
335
+ const decoded = decodeSafe(path);
336
+ const m = /\.([a-z0-9]+)$/i.exec(decoded);
337
+ if (!m) return false;
338
+ return `.${m[1].toLowerCase()}` in MIME_BY_EXT;
339
+ }
340
+ function isLocalRef(url) {
341
+ if (url.length === 0) return false;
342
+ if (url.startsWith("#")) return false;
343
+ if (url.startsWith("mailto:")) return false;
344
+ if (url.startsWith("tel:")) return false;
345
+ if (url.startsWith("data:")) return false;
346
+ if (url.startsWith("//")) return false;
347
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return false;
348
+ return true;
349
+ }
350
+ function resolveAssetPath(url, { baseDir, filePath }) {
351
+ const [pathPart] = splitUrl(url);
352
+ const decoded = decodeSafe(pathPart);
353
+ const absolute = resolve(isAbsolute(decoded) || decoded.startsWith("/") ? baseDir : dirname(filePath), decoded.replace(/^\/+/, ""));
354
+ const rel = relative(baseDir, absolute);
355
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Asset reference "${url}" in ${filePath} resolves outside the compilation directory`);
356
+ return absolute;
357
+ }
358
+ function splitUrl(url) {
359
+ const hashIdx = url.indexOf("#");
360
+ const queryIdx = url.indexOf("?");
361
+ let cut = -1;
362
+ if (hashIdx >= 0) cut = hashIdx;
363
+ if (queryIdx >= 0 && (cut < 0 || queryIdx < cut)) cut = queryIdx;
364
+ if (cut < 0) return [url, ""];
365
+ return [url.slice(0, cut), url.slice(cut)];
366
+ }
367
+ function decodeSafe(s) {
368
+ try {
369
+ return decodeURIComponent(s);
370
+ } catch {
371
+ return s;
372
+ }
373
+ }
374
+ async function toAsset(ref) {
375
+ let bytes;
376
+ try {
377
+ bytes = await readFile(ref.absPath);
378
+ } catch (error) {
379
+ const message = error instanceof Error ? error.message : String(error);
380
+ throw new Error(`Asset "${ref.original}" not found at ${ref.absPath}: ${message}`, { cause: error });
381
+ }
382
+ const digest = createHash("sha256").update(bytes).digest();
383
+ const integrity = `sha256-${digest.toString("base64")}`;
384
+ const name = digest.toString("hex").slice(0, 16);
385
+ const extMatch = /\.([a-z0-9]+)(?:[?#].*)?$/i.exec(ref.relFromBase);
386
+ const ext = extMatch ? `.${extMatch[1].toLowerCase()}` : "";
387
+ const mediaType = MIME_BY_EXT[ext];
388
+ return {
389
+ apiVersion: "v1",
390
+ type: "Asset",
391
+ name,
392
+ spec: {
393
+ uri: ref.relFromBase,
394
+ integrity,
395
+ ...mediaType ? { mediaType } : {}
396
+ }
397
+ };
398
+ }
399
+ //#endregion
94
400
  //#region src/compile/shared.ts
401
+ var CompileError = class extends Error {
402
+ diagnostics;
403
+ constructor(diagnostics) {
404
+ super(formatContentDiagnostics(diagnostics));
405
+ this.diagnostics = diagnostics;
406
+ this.name = "CompileError";
407
+ }
408
+ };
409
+ function linkValidationPolicy(options) {
410
+ return options?.links ?? "error";
411
+ }
412
+ function isErrorDiagnostic(diagnostic) {
413
+ return diagnostic.level === "error" || diagnostic.level === "critical";
414
+ }
415
+ function hasCompileErrors(diagnostics) {
416
+ return diagnostics.some(isErrorDiagnostic);
417
+ }
418
+ function throwOnCompileErrors(diagnostics) {
419
+ const errors = diagnostics.filter(isErrorDiagnostic);
420
+ if (errors.length > 0) throw new CompileError(errors);
421
+ }
95
422
  const DNS_LABEL_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
96
423
  function parseMarkdownFrontmatter(raw, filePath) {
97
424
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -121,6 +448,11 @@ function extractMarkdownTitle(content, fallback) {
121
448
  if (match) return match[1].trim();
122
449
  return fallback.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
123
450
  }
451
+ function formatContentDiagnostics(diagnostics) {
452
+ return diagnostics.filter(isErrorDiagnostic).map((diagnostic) => {
453
+ return `${diagnostic.file ?? "content"}${diagnostic.lines.length > 0 ? `:${diagnostic.lines.join(",")}` : ""} ${diagnostic.level} ${diagnostic.id}: ${diagnostic.message}`;
454
+ }).join("\n");
455
+ }
124
456
  function parseReferenceList(value, fieldName, filePath) {
125
457
  if (value == null) return;
126
458
  if (!Array.isArray(value)) throw new Error(`${fieldName} in ${filePath} must be an array of resource names`);
@@ -132,58 +464,177 @@ function parseReferenceList(value, fieldName, filePath) {
132
464
  return references.length > 0 ? references : void 0;
133
465
  }
134
466
  //#endregion
467
+ //#region src/compile/links.ts
468
+ const NON_PAGE_SCHEME = /^(?:asset|https?|mailto|tel):/i;
469
+ const LINK_BASE = "https://topik.local";
470
+ function validateWikiLinks(pages, policy) {
471
+ if (policy === "off") return [];
472
+ const level = policy === "error" ? "error" : "warning";
473
+ const pagesBySlug = new Map(pages.map((page) => [page.slug, page]));
474
+ const diagnostics = [];
475
+ for (const page of pages) for (const link of page.analysis.links) {
476
+ if (NON_PAGE_SCHEME.test(link.href)) continue;
477
+ const target = resolveInternalTarget(link.href, page.sourcePath);
478
+ if (!target) continue;
479
+ const targetPage = pagesBySlug.get(target.slug);
480
+ if (!targetPage) {
481
+ diagnostics.push(linkDiagnostic("link-page-not-found", level, `Internal link '${link.href}' resolves to missing page '/${target.slug}'.`, link));
482
+ continue;
483
+ }
484
+ if (target.fragment && !targetPage.analysis.headings.some((heading) => heading.id === target.fragment)) diagnostics.push(linkDiagnostic("link-fragment-not-found", level, `Internal link '${link.href}' references missing heading '#${target.fragment}' on '/${target.slug}'.`, link));
485
+ }
486
+ return diagnostics;
487
+ }
488
+ function validateLocalFragments(analysis, policy) {
489
+ if (policy === "off") return [];
490
+ const level = policy === "error" ? "error" : "warning";
491
+ const headingIds = new Set(analysis.headings.map((heading) => heading.id));
492
+ const diagnostics = [];
493
+ for (const link of analysis.links) {
494
+ if (!link.href.startsWith("#") || link.href === "#") continue;
495
+ const fragment = decodeFragment(link.href.slice(1));
496
+ if (fragment && !headingIds.has(fragment)) diagnostics.push(linkDiagnostic("link-fragment-not-found", level, `Link '${link.href}' references a missing heading in this document.`, link));
497
+ }
498
+ return diagnostics;
499
+ }
500
+ function resolveInternalTarget(href, sourcePath) {
501
+ let url;
502
+ try {
503
+ url = new URL(href, `${LINK_BASE}/${sourcePath}`);
504
+ } catch {
505
+ return;
506
+ }
507
+ if (url.origin !== LINK_BASE) return void 0;
508
+ return {
509
+ fragment: decodeFragment(url.hash.slice(1)),
510
+ slug: decodePath(url.pathname)
511
+ };
512
+ }
513
+ function decodeFragment(value) {
514
+ if (!value) return "";
515
+ try {
516
+ return decodeURIComponent(value);
517
+ } catch {
518
+ return value;
519
+ }
520
+ }
521
+ function decodePath(pathname) {
522
+ const normalized = pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map((segment) => {
523
+ try {
524
+ return decodeURIComponent(segment);
525
+ } catch {
526
+ return segment;
527
+ }
528
+ }).join("/").replace(/\.(?:mdx?|markdown)$/i, "");
529
+ if (normalized === "index") return "";
530
+ return normalized.replace(/\/index$/, "");
531
+ }
532
+ function linkDiagnostic(id, level, message, link) {
533
+ return {
534
+ id,
535
+ type: link.kind,
536
+ level,
537
+ message,
538
+ lines: link.lines,
539
+ ...link.file ? { file: link.file } : {}
540
+ };
541
+ }
542
+ //#endregion
135
543
  //#region src/compile/wiki.ts
136
544
  async function compileWiki(options) {
545
+ const result = await inspectWiki(options);
546
+ throwOnCompileErrors(result.diagnostics);
547
+ return result;
548
+ }
549
+ async function inspectWiki(options) {
137
550
  const dir = resolve(options.dir);
138
551
  const raw = await readOptionalConfigFile(dir, [
139
552
  "wiki.yaml",
140
553
  "wiki.yml",
141
554
  "wiki.json"
142
555
  ]);
143
- if (raw == null) return { resources: [] };
556
+ if (raw == null) return {
557
+ diagnostics: [],
558
+ resources: []
559
+ };
144
560
  const config = parseWikiConfig(raw);
145
561
  const pagePaths = config.navigation ? [...new Set(collectPagePaths(config.navigation))] : [];
146
- const fileContents = await Promise.all(pagePaths.map(async (pagePath) => {
147
- return readFile(await resolveFilePath(dir, pagePath), "utf-8");
562
+ const resolvedFiles = await Promise.all(pagePaths.map(async (pagePath) => {
563
+ const filePath = await resolveFilePath(dir, pagePath);
564
+ return {
565
+ filePath,
566
+ raw: await readFile(filePath, "utf-8")
567
+ };
148
568
  }));
149
569
  const resources = [];
570
+ const diagnostics = [];
571
+ const pageAnalyses = [];
572
+ const assetsById = /* @__PURE__ */ new Map();
150
573
  for (let i = 0; i < pagePaths.length; i++) {
151
574
  const pagePath = pagePaths[i];
152
- const { frontmatter, content } = parseMarkdownFrontmatter(fileContents[i], pagePath);
153
- const name = pagePathToName(pagePath);
154
- const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(content, name);
575
+ const { filePath, raw } = resolvedFiles[i];
576
+ const { frontmatter, content } = parseMarkdownFrontmatter(raw, pagePath);
577
+ const validation = validateTopikContent(content, { file: filePath });
578
+ diagnostics.push(...validation.errors);
579
+ if (!validation.valid) continue;
580
+ const { content: rewritten, assets, manifest } = await extractAssets(content, {
581
+ baseDir: dir,
582
+ filePath
583
+ });
584
+ for (const asset of assets) if (!assetsById.has(asset.name)) assetsById.set(asset.name, asset);
585
+ const name = pagePathToName(config.id, pagePath);
586
+ const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(rewritten, pagePathToTitleFallback(pagePath));
587
+ const description = normalizeWikiPageDescription(frontmatter.description);
588
+ const analysis = analyzeTopikContent(rewritten, { file: filePath });
589
+ diagnostics.push(...analysis.diagnostics);
590
+ pageAnalyses.push({
591
+ analysis,
592
+ slug: pagePathToSlug(pagePath),
593
+ sourcePath: pagePath
594
+ });
155
595
  const pageResource = {
156
596
  apiVersion: "v1",
157
597
  type: "WikiPage",
158
- name: `${config.id}-${name}`,
598
+ name,
159
599
  spec: {
160
600
  wiki: config.id,
161
601
  title,
602
+ ...description != null ? { description } : {},
162
603
  content: {
163
604
  format: "topik",
164
- value: content
165
- }
605
+ value: rewritten
606
+ },
607
+ ...manifest.length > 0 ? { assets: manifest } : {}
166
608
  }
167
609
  };
168
610
  resources.push(pageResource);
169
611
  }
612
+ for (const asset of assetsById.values()) resources.push(asset);
170
613
  const wikiResource = {
171
614
  apiVersion: "v1",
172
615
  type: "Wiki",
173
616
  name: config.id,
174
617
  spec: {
175
618
  title: config.title,
619
+ ...config.description != null ? { description: config.description } : {},
176
620
  ...config.navigation ? { navigation: resolveNavigation(config.navigation, config.id) } : {},
177
621
  ...config.theme ? { theme: config.theme } : {}
178
622
  }
179
623
  };
180
624
  resources.push(wikiResource);
181
- return { resources };
625
+ if (!hasCompileErrors(diagnostics)) diagnostics.push(...validateWikiLinks(pageAnalyses, linkValidationPolicy(options.validation)));
626
+ return {
627
+ diagnostics,
628
+ resources
629
+ };
630
+ }
631
+ function normalizeWikiPageDescription(description) {
632
+ return typeof description === "string" ? description.slice(0, 1024) : void 0;
182
633
  }
183
- function collectPagePaths(nodes) {
634
+ function collectPagePaths(nodes, prefix = "") {
184
635
  const paths = [];
185
- for (const node of nodes) if (typeof node === "string") paths.push(node);
186
- else if ("group" in node || "tab" in node) paths.push(...collectPagePaths(node.children));
636
+ for (const node of nodes) if (typeof node === "string" || node.type === "page") paths.push(joinWikiPath(prefix, typeof node === "string" ? node : node.slug));
637
+ else if ("children" in node) paths.push(...collectPagePaths(node.children, joinWikiPath(prefix, node.slug)));
187
638
  return paths;
188
639
  }
189
640
  async function resolveFilePath(dir, pagePath) {
@@ -200,56 +651,124 @@ function pagePathToSlug(pagePath) {
200
651
  if (pagePath === "index") return "";
201
652
  return pagePath.replace(/\/index$/, "");
202
653
  }
203
- function pagePathToName(pagePath) {
204
- return pagePath.replace(/^\//, "").replaceAll("/", "-");
654
+ function pagePathToName(wikiId, pagePath) {
655
+ const normalizedPath = normalizePagePath(pagePath);
656
+ return `${wikiId}-${createHash("sha256").update(normalizedPath).digest("hex").slice(0, 16)}`;
205
657
  }
206
- function resolveNavigation(nodes, wikiId) {
658
+ function resolveNavigation(nodes, wikiId, prefix = "") {
207
659
  return nodes.map((node) => {
208
- if (typeof node === "string") return {
209
- type: "page",
210
- page: `${wikiId}-${pagePathToName(node)}`,
211
- slug: pagePathToSlug(node)
212
- };
213
- if ("group" in node || "tab" in node) {
214
- const isTab = "tab" in node;
215
- const title = isTab ? node.tab : node.group;
660
+ if (typeof node === "string" || node.type === "page") {
661
+ const localPath = typeof node === "string" ? node : node.slug;
662
+ const pagePath = joinWikiPath(prefix, localPath);
216
663
  return {
217
- type: isTab ? "tab" : "group",
218
- title,
664
+ type: "page",
665
+ page: pagePathToName(wikiId, pagePath),
666
+ slug: pagePathToSlug(localPath),
667
+ sourcePath: pagePath,
668
+ ...typeof node !== "string" && node.icon ? { icon: node.icon } : {},
669
+ ...typeof node !== "string" && node.hidden ? { hidden: true } : {}
670
+ };
671
+ }
672
+ if ("children" in node) {
673
+ const children = resolveNavigation(node.children, wikiId, joinWikiPath(prefix, node.slug));
674
+ if (node.type === "group") return {
675
+ type: "group",
676
+ title: node.title,
677
+ ...node.slug ? { slug: node.slug } : {},
678
+ ...node.icon ? { icon: node.icon } : {},
679
+ ...node.hidden ? { hidden: true } : {},
680
+ ...node.expanded ? { expanded: true } : {},
681
+ children
682
+ };
683
+ if (node.type === "tab") return {
684
+ type: "tab",
685
+ title: node.title,
686
+ ...node.slug ? { slug: node.slug } : {},
687
+ ...node.icon ? { icon: node.icon } : {},
688
+ ...node.hidden ? { hidden: true } : {},
689
+ children
690
+ };
691
+ return {
692
+ type: "dropdown",
693
+ title: node.title,
219
694
  ...node.slug ? { slug: node.slug } : {},
220
695
  ...node.icon ? { icon: node.icon } : {},
221
- children: resolveNavigation(node.children, wikiId)
696
+ ...node.hidden ? { hidden: true } : {},
697
+ children
222
698
  };
223
699
  }
700
+ if (node.type === "tab") return {
701
+ type: "tab",
702
+ title: node.title,
703
+ href: node.href,
704
+ ...node.icon ? { icon: node.icon } : {},
705
+ ...node.hidden ? { hidden: true } : {}
706
+ };
707
+ if (node.type === "dropdown") return {
708
+ type: "dropdown",
709
+ title: node.title,
710
+ href: node.href,
711
+ ...node.icon ? { icon: node.icon } : {},
712
+ ...node.hidden ? { hidden: true } : {}
713
+ };
224
714
  return {
225
715
  type: "link",
226
716
  title: node.title,
227
717
  href: node.href,
228
- ...node.icon ? { icon: node.icon } : {}
718
+ ...node.icon ? { icon: node.icon } : {},
719
+ ...node.hidden ? { hidden: true } : {}
229
720
  };
230
721
  });
231
722
  }
723
+ function normalizePagePath(pagePath) {
724
+ return pagePath.replace(/^\//, "").replace(/\.(?:mdx?|markdown)$/i, "");
725
+ }
726
+ function pagePathToTitleFallback(pagePath) {
727
+ return normalizePagePath(pagePath).replaceAll("/", "-");
728
+ }
232
729
  //#endregion
233
730
  //#region src/compile/guide.ts
234
731
  async function compileGuides(options) {
732
+ const result = await inspectGuides(options);
733
+ throwOnCompileErrors(result.diagnostics);
734
+ return result;
735
+ }
736
+ async function inspectGuides(options) {
235
737
  const dir = resolve(options.dir);
236
738
  const raw = await readOptionalConfigFile(dir, [
237
739
  "collection.yaml",
238
740
  "collection.yml",
239
741
  "collection.json"
240
742
  ]);
241
- if (raw == null) return { resources: [] };
743
+ if (raw == null) return {
744
+ diagnostics: [],
745
+ resources: []
746
+ };
242
747
  const config = parseCollectionConfig(raw);
243
748
  const markdownFiles = (await readdir(dir)).filter((f) => f.endsWith(".md") || f.endsWith(".mdx")).sort();
244
749
  const resources = [];
750
+ const diagnostics = [];
751
+ const assetsById = /* @__PURE__ */ new Map();
245
752
  for (const file of markdownFiles) {
246
- const { frontmatter, content } = parseMarkdownFrontmatter(await readFile(join(dir, file), "utf-8"), file);
753
+ const filePath = join(dir, file);
754
+ const { frontmatter, content } = parseMarkdownFrontmatter(await readFile(filePath, "utf-8"), file);
755
+ const validation = validateTopikContent(content, { file: filePath });
756
+ diagnostics.push(...validation.errors);
757
+ if (!validation.valid) continue;
758
+ const { content: rewritten, assets, manifest } = await extractAssets(content, {
759
+ baseDir: dir,
760
+ filePath
761
+ });
762
+ for (const asset of assets) if (!assetsById.has(asset.name)) assetsById.set(asset.name, asset);
247
763
  const slug = fileToSlug(file);
248
764
  const name = `${config.id}-${slug}`;
249
- const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(content, slug);
765
+ const title = typeof frontmatter.title === "string" ? frontmatter.title : extractMarkdownTitle(rewritten, slug);
250
766
  const tags = mergeTags(config.tags, frontmatter.tags);
251
767
  const authors = parseReferenceList(frontmatter.authors, "authors", file);
252
768
  const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
769
+ const analysis = analyzeTopikContent(rewritten, { file: filePath });
770
+ diagnostics.push(...analysis.diagnostics);
771
+ diagnostics.push(...validateLocalFragments(analysis, linkValidationPolicy(options.validation)));
253
772
  const guide = {
254
773
  apiVersion: "v1",
255
774
  type: "Guide",
@@ -262,13 +781,18 @@ async function compileGuides(options) {
262
781
  ...tags.length > 0 ? { tags } : {},
263
782
  content: {
264
783
  format: "topik",
265
- value: content
266
- }
784
+ value: rewritten
785
+ },
786
+ ...manifest.length > 0 ? { assets: manifest } : {}
267
787
  }
268
788
  };
269
789
  resources.push(guide);
270
790
  }
271
- return { resources };
791
+ for (const asset of assetsById.values()) resources.push(asset);
792
+ return {
793
+ diagnostics,
794
+ resources
795
+ };
272
796
  }
273
797
  function fileToSlug(filename) {
274
798
  return filename.replace(/\.(mdx?|md)$/, "");
@@ -296,15 +820,40 @@ const COLLECTION_CONFIG_FILES = [
296
820
  async function compile(options) {
297
821
  const dir = resolve(options.dir);
298
822
  const resources = [];
823
+ const diagnostics = [];
299
824
  if (await findConfigFile(dir, WIKI_CONFIG_FILES)) {
300
- const result = await compileWiki({ dir });
825
+ const result = await compileWiki({
826
+ dir,
827
+ validation: options.validation
828
+ });
301
829
  resources.push(...result.resources);
830
+ diagnostics.push(...result.diagnostics);
302
831
  }
303
832
  if (await findConfigFile(dir, COLLECTION_CONFIG_FILES)) {
304
- const result = await compileGuides({ dir });
833
+ const result = await compileGuides({
834
+ dir,
835
+ validation: options.validation
836
+ });
305
837
  resources.push(...result.resources);
838
+ diagnostics.push(...result.diagnostics);
306
839
  }
307
- return { resources };
840
+ return {
841
+ diagnostics,
842
+ resources
843
+ };
844
+ }
845
+ async function lint(options) {
846
+ const dir = resolve(options.dir);
847
+ const diagnostics = [];
848
+ if (await findConfigFile(dir, WIKI_CONFIG_FILES)) diagnostics.push(...(await inspectWiki({
849
+ dir,
850
+ validation: options.validation
851
+ })).diagnostics);
852
+ if (await findConfigFile(dir, COLLECTION_CONFIG_FILES)) diagnostics.push(...(await inspectGuides({
853
+ dir,
854
+ validation: options.validation
855
+ })).diagnostics);
856
+ return { diagnostics };
308
857
  }
309
858
  //#endregion
310
859
  //#region src/validate.ts
@@ -314,6 +863,7 @@ const ajv = new Ajv2020({
314
863
  });
315
864
  addFormats(ajv);
316
865
  const validators = {
866
+ Asset: ajv.compile(assetSchema),
317
867
  Guide: ajv.compile(guideSchema),
318
868
  Wiki: ajv.compile(wikiSchema),
319
869
  WikiPage: ajv.compile(wikiPageSchema)
@@ -427,4 +977,4 @@ async function watch(options) {
427
977
  return watcher;
428
978
  }
429
979
  //#endregion
430
- export { compile, compileGuides, compileWiki, parseCollectionConfig, parseWikiConfig, validateResources, watch };
980
+ export { CompileError, compile, compileGuides, compileWiki, isErrorDiagnostic, lint, pagePathToName, parseCollectionConfig, parseWikiConfig, validateResources, watch };
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@topik/core",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Topik core library",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/subtopik/topik.git",
9
+ "directory": "packages/core"
10
+ },
6
11
  "files": [
7
12
  "dist"
8
13
  ],
@@ -15,19 +20,19 @@
15
20
  "access": "public"
16
21
  },
17
22
  "dependencies": {
18
- "ajv": "^8.18.0",
23
+ "ajv": "^8.20.0",
19
24
  "ajv-formats": "^3.0.1",
20
- "chokidar": "^4.0.0",
21
- "yaml": "^2.7.0",
22
- "zod": "^3.25.0",
23
- "@topik/schema": "0.1.0-alpha.2"
25
+ "chokidar": "^5.0.0",
26
+ "yaml": "^2.9.0",
27
+ "zod": "^4.4.3",
28
+ "@topik/content-schema": "0.1.0-alpha.4",
29
+ "@topik/schema": "0.1.0-alpha.4"
24
30
  },
25
31
  "devDependencies": {
26
- "@types/node": "^24",
27
- "@typescript/native-preview": "7.0.0-dev.20260316.1",
28
- "typescript": "^5",
29
- "vite-plus": "^0.1.12",
30
- "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
32
+ "@types/node": "22.20.1",
33
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
34
+ "typescript": "^6.0.3",
35
+ "vite-plus": "0.2.4"
31
36
  },
32
37
  "scripts": {
33
38
  "build": "vp pack",