@vertesia/build-tools 1.5.0-dev.20260722.120446Z → 1.5.0-dev.20260804.124748Z
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/README.md +297 -266
- package/lib/bin/build.js +26 -1
- package/lib/bin/build.js.map +1 -1
- package/lib/bin/config.d.ts +12 -0
- package/lib/bin/config.d.ts.map +1 -1
- package/lib/bin/config.js +25 -0
- package/lib/bin/config.js.map +1 -1
- package/lib/core/skill-markdown/load-catalog.d.ts +30 -0
- package/lib/core/skill-markdown/load-catalog.d.ts.map +1 -0
- package/lib/core/skill-markdown/load-catalog.js +69 -0
- package/lib/core/skill-markdown/load-catalog.js.map +1 -0
- package/lib/core/skill-markdown/preprocess.d.ts +127 -0
- package/lib/core/skill-markdown/preprocess.d.ts.map +1 -0
- package/lib/core/skill-markdown/preprocess.js +455 -0
- package/lib/core/skill-markdown/preprocess.js.map +1 -0
- package/lib/core/skill-markdown/schema-validator.d.ts +109 -0
- package/lib/core/skill-markdown/schema-validator.d.ts.map +1 -0
- package/lib/core/skill-markdown/schema-validator.js +420 -0
- package/lib/core/skill-markdown/schema-validator.js.map +1 -0
- package/lib/core/transformers/index.d.ts +1 -1
- package/lib/core/transformers/index.d.ts.map +1 -1
- package/lib/core/transformers/index.js +1 -1
- package/lib/core/transformers/index.js.map +1 -1
- package/lib/core/transformers/skill.d.ts +15 -1
- package/lib/core/transformers/skill.d.ts.map +1 -1
- package/lib/core/transformers/skill.js +82 -60
- package/lib/core/transformers/skill.js.map +1 -1
- package/lib/import-transform/detector.d.ts +19 -6
- package/lib/import-transform/detector.d.ts.map +1 -1
- package/lib/import-transform/detector.js +73 -24
- package/lib/import-transform/detector.js.map +1 -1
- package/lib/import-transform/index.d.ts.map +1 -1
- package/lib/import-transform/index.js +10 -1
- package/lib/import-transform/index.js.map +1 -1
- package/lib/import-transform/patterns.d.ts +7 -8
- package/lib/import-transform/patterns.d.ts.map +1 -1
- package/lib/import-transform/patterns.js +7 -8
- package/lib/import-transform/patterns.js.map +1 -1
- package/lib/index.d.ts +7 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -3
- package/lib/index.js.map +1 -1
- package/lib/vite/dev-server.d.ts +10 -0
- package/lib/vite/dev-server.d.ts.map +1 -1
- package/lib/vite/dev-server.js +36 -4
- package/lib/vite/dev-server.js.map +1 -1
- package/package.json +9 -12
- package/src/bin/build.ts +31 -2
- package/src/bin/config.ts +27 -0
- package/src/core/skill-markdown/load-catalog.ts +100 -0
- package/src/core/skill-markdown/preprocess.ts +632 -0
- package/src/core/skill-markdown/schema-validator.ts +517 -0
- package/src/core/transformers/index.ts +3 -0
- package/src/core/transformers/skill.ts +116 -53
- package/src/import-transform/detector.ts +78 -26
- package/src/import-transform/index.ts +12 -2
- package/src/import-transform/patterns.ts +7 -12
- package/src/index.ts +35 -2
- package/src/vite/dev-server.ts +52 -4
|
@@ -6,7 +6,12 @@ import { existsSync } from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { parseFrontmatter } from '../parsers/frontmatter.js';
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
assertSkillMarkdown,
|
|
11
|
+
type PreprocessSkillMarkdownOptions,
|
|
12
|
+
preprocessSkillMarkdown,
|
|
13
|
+
} from '../skill-markdown/preprocess.js';
|
|
14
|
+
import type { TransformerPreset, TransformerRule, TransformResult } from '../types.js';
|
|
10
15
|
import { discoverSkillAssets } from '../utils/asset-discovery.js';
|
|
11
16
|
|
|
12
17
|
/**
|
|
@@ -78,6 +83,7 @@ const SkillFrontmatterSchema = z
|
|
|
78
83
|
// Flat structure fields (legacy)
|
|
79
84
|
keywords: z.array(z.string()).optional(),
|
|
80
85
|
tools: z.array(z.string()).optional(),
|
|
86
|
+
supporting_tools: z.array(z.string()).optional(),
|
|
81
87
|
data_patterns: z.array(z.string()).optional(),
|
|
82
88
|
language: z.string().optional(),
|
|
83
89
|
packages: z.array(z.string()).optional(),
|
|
@@ -234,9 +240,12 @@ function buildSkillDefinition(
|
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
// Tools unlocked by this skill
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
// Tools unlocked by this skill. `supporting_tools` unlock identically — the split exists only
|
|
244
|
+
// so validation knows which grants the author owes the reader an explanation for. Merged here so
|
|
245
|
+
// no runtime path has to know about the distinction.
|
|
246
|
+
const unlocked = [...(frontmatter.tools ?? []), ...(frontmatter.supporting_tools ?? [])];
|
|
247
|
+
if (unlocked.length > 0) {
|
|
248
|
+
skill.tools = unlocked;
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
// Input schema from frontmatter
|
|
@@ -267,51 +276,106 @@ function buildSkillDefinition(
|
|
|
267
276
|
* // Both are SkillDefinition objects
|
|
268
277
|
* ```
|
|
269
278
|
*/
|
|
270
|
-
export
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const errors = frontmatterValidation.error.issues
|
|
280
|
-
.map((err) => {
|
|
281
|
-
const pathStr = err.path.length > 0 ? err.path.join('.') : 'frontmatter';
|
|
282
|
-
return ` - ${pathStr}: ${err.message}`;
|
|
283
|
-
})
|
|
284
|
-
.join('\n');
|
|
285
|
-
throw new Error(`Invalid frontmatter in ${filePath}:\n${errors}`);
|
|
286
|
-
}
|
|
279
|
+
export interface SkillTransformerConfig {
|
|
280
|
+
/**
|
|
281
|
+
* Catalog used to resolve `{@tool …}` / `{@skill …}` and validate `tool=`-tagged examples.
|
|
282
|
+
*
|
|
283
|
+
* Supplied by the consuming build, never discovered here: a transformer that went looking
|
|
284
|
+
* for a registry would couple build-tools to whichever package happens to own one.
|
|
285
|
+
*/
|
|
286
|
+
markdown?: PreprocessSkillMarkdownOptions;
|
|
287
|
+
}
|
|
287
288
|
|
|
288
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Render the skill body, failing closed when the semantic syntax is used without a catalog.
|
|
291
|
+
*
|
|
292
|
+
* Without this guard a package that writes `{@tool x}` but forgets to configure `skillCatalog`
|
|
293
|
+
* would ship the raw construct straight to the model — silently, and looking fine in review.
|
|
294
|
+
*/
|
|
295
|
+
function renderSkillMarkdown(markdown: string, filePath: string, options?: PreprocessSkillMarkdownOptions): string {
|
|
296
|
+
if (options) {
|
|
297
|
+
return assertSkillMarkdown(markdown, options, filePath).markdown;
|
|
298
|
+
}
|
|
299
|
+
const probe = preprocessSkillMarkdown(markdown, { tools: new Set(), skills: new Set() });
|
|
300
|
+
if (probe.references.length > 0 || probe.examples.length > 0 || probe.errors.length > 0) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`${filePath} uses {@tool …}, {@skill …} or a tool= example tag, but this build supplies no ` +
|
|
303
|
+
'skill catalog. Set "vertesia-build.skillCatalog" in package.json to a module exporting ' +
|
|
304
|
+
'{ tools, skills } (and optionally validateExample).',
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return markdown;
|
|
308
|
+
}
|
|
289
309
|
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Marks a rule as *this* preset rather than any rule that happens to share its pattern.
|
|
312
|
+
*
|
|
313
|
+
* The CLI swaps in a catalog-bound skill transformer; matching on `pattern.source` would also
|
|
314
|
+
* replace an unrelated custom transformer registered for the same files.
|
|
315
|
+
*/
|
|
316
|
+
const SKILL_TRANSFORMER = Symbol.for('@vertesia/build-tools:skillTransformer');
|
|
292
317
|
|
|
293
|
-
|
|
294
|
-
|
|
318
|
+
/** True when `rule` is a skill transformer produced by `createSkillTransformer`. */
|
|
319
|
+
export function isSkillTransformer(rule: TransformerRule): boolean {
|
|
320
|
+
return (rule as unknown as Record<PropertyKey, unknown>)[SKILL_TRANSFORMER] === true;
|
|
321
|
+
}
|
|
295
322
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
323
|
+
/** Build a skill transformer bound to a specific catalog. `skillTransformer` is the unbound one. */
|
|
324
|
+
export function createSkillTransformer(config: SkillTransformerConfig = {}): TransformerPreset {
|
|
325
|
+
return {
|
|
326
|
+
[SKILL_TRANSFORMER]: true,
|
|
327
|
+
pattern: /(\.md\?skill$|\/SKILL\.md$)/,
|
|
328
|
+
schema: SkillDefinitionSchema,
|
|
329
|
+
transform: (content: string, filePath: string) => transformSkill(content, filePath, config),
|
|
330
|
+
} as TransformerPreset;
|
|
331
|
+
}
|
|
304
332
|
|
|
305
|
-
|
|
306
|
-
const skillDir = path.dirname(filePath);
|
|
307
|
-
const propertiesPath = path.join(skillDir, 'properties.ts');
|
|
308
|
-
const hasProperties = existsSync(propertiesPath);
|
|
333
|
+
export const skillTransformer: TransformerPreset = createSkillTransformer();
|
|
309
334
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
function transformSkill(content: string, filePath: string, config: SkillTransformerConfig): TransformResult {
|
|
336
|
+
const { frontmatter, content: rawMarkdown } = parseFrontmatter(content);
|
|
337
|
+
const markdown = renderSkillMarkdown(rawMarkdown, filePath, config.markdown);
|
|
338
|
+
|
|
339
|
+
// Validate frontmatter first to catch unknown properties
|
|
340
|
+
const frontmatterValidation = SkillFrontmatterSchema.safeParse(frontmatter);
|
|
341
|
+
if (!frontmatterValidation.success) {
|
|
342
|
+
const errors = frontmatterValidation.error.issues
|
|
343
|
+
.map((err) => {
|
|
344
|
+
const pathStr = err.path.length > 0 ? err.path.join('.') : 'frontmatter';
|
|
345
|
+
return ` - ${pathStr}: ${err.message}`;
|
|
346
|
+
})
|
|
347
|
+
.join('\n');
|
|
348
|
+
throw new Error(`Invalid frontmatter in ${filePath}:\n${errors}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const validatedFrontmatter = frontmatterValidation.data;
|
|
352
|
+
|
|
353
|
+
// Determine content type from frontmatter or file extension
|
|
354
|
+
const content_type: SkillContentType = validatedFrontmatter.content_type || 'md';
|
|
355
|
+
|
|
356
|
+
// Discover assets (scripts and widgets) in the skill directory
|
|
357
|
+
const assets = discoverSkillAssets(filePath);
|
|
358
|
+
|
|
359
|
+
// Build skill definition using the same logic as parseSkillFile in tools-sdk
|
|
360
|
+
const skillData = buildSkillDefinition(
|
|
361
|
+
validatedFrontmatter,
|
|
362
|
+
markdown,
|
|
363
|
+
content_type,
|
|
364
|
+
assets.widgets,
|
|
365
|
+
assets.scripts,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Check if properties.ts exists in the skill directory
|
|
369
|
+
const skillDir = path.dirname(filePath);
|
|
370
|
+
const propertiesPath = path.join(skillDir, 'properties.ts');
|
|
371
|
+
const hasProperties = existsSync(propertiesPath);
|
|
372
|
+
|
|
373
|
+
// If properties.ts exists, generate custom code with import and merge.
|
|
374
|
+
// `tsc` has already emitted properties.js beside this chunk — the pipeline runs after it and
|
|
375
|
+
// never compiles TypeScript itself — so the generated module can import './properties.js'.
|
|
376
|
+
if (hasProperties) {
|
|
377
|
+
const skillDataJson = JSON.stringify(skillData, null, 2);
|
|
378
|
+
const code = `import properties from './properties.js';
|
|
315
379
|
|
|
316
380
|
// Runtime validation for function properties
|
|
317
381
|
if ('isEnabled' in properties && typeof properties.isEnabled !== 'function') {
|
|
@@ -322,18 +386,17 @@ const skill = ${skillDataJson};
|
|
|
322
386
|
|
|
323
387
|
export default { ...skill, ...properties };
|
|
324
388
|
`;
|
|
325
|
-
return {
|
|
326
|
-
data: skillData,
|
|
327
|
-
assets: assets.assetFiles,
|
|
328
|
-
widgets: assets.widgetMetadata,
|
|
329
|
-
code,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
389
|
return {
|
|
334
390
|
data: skillData,
|
|
335
391
|
assets: assets.assetFiles,
|
|
336
392
|
widgets: assets.widgetMetadata,
|
|
393
|
+
code,
|
|
337
394
|
};
|
|
338
|
-
}
|
|
339
|
-
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
data: skillData,
|
|
399
|
+
assets: assets.assetFiles,
|
|
400
|
+
widgets: assets.widgetMetadata,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Finds Vertesia query-style import specifiers
|
|
3
|
-
*
|
|
2
|
+
* Finds Vertesia query-style import specifiers in a JavaScript source file and maps each one to
|
|
3
|
+
* the transformer that should handle it.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Detection uses a real ESM lexer (`es-module-lexer`) rather than a regex over string literals,
|
|
6
|
+
* because a regex models the wrong thing: it matches anything that *looks* like a specifier. A doc
|
|
7
|
+
* comment mentioning `` `?skill` ``, or an ordinary constant such as
|
|
8
|
+
* `const example = './example/SKILL.md'`, were both detected as imports — the former resolved to a
|
|
9
|
+
* directory and killed the build with `EISDIR`.
|
|
10
|
+
*
|
|
11
|
+
* Lexing the module handles comments, template literals, regex literals, static imports,
|
|
12
|
+
* re-exports (`export … from`) and dynamic `import()` in one pass, and still yields the source
|
|
13
|
+
* offsets the rewriter needs.
|
|
9
14
|
*/
|
|
10
15
|
|
|
16
|
+
import { initSync, parse } from 'es-module-lexer';
|
|
11
17
|
import type { TransformerRule } from '../core/types.js';
|
|
12
|
-
import { QUERY_STRING_LITERAL } from './patterns.js';
|
|
13
18
|
|
|
14
19
|
export interface ImportOccurrence {
|
|
15
20
|
/** The transformer whose pattern matched the specifier. */
|
|
@@ -28,28 +33,75 @@ export interface ImportOccurrence {
|
|
|
28
33
|
quote: "'" | '"' | '`';
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
/**
|
|
37
|
+
* `es-module-lexer` is WASM-backed and must be initialised before the first `parse`.
|
|
38
|
+
*
|
|
39
|
+
* Initialised synchronously and once, so `detectQueryImports` stays a plain function — making it
|
|
40
|
+
* async would push a promise through the scanner and every caller for no benefit.
|
|
41
|
+
*/
|
|
42
|
+
let initialised = false;
|
|
43
|
+
function ensureInitialised(): void {
|
|
44
|
+
if (!initialised) {
|
|
45
|
+
initSync();
|
|
46
|
+
initialised = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const QUOTES = new Set(["'", '"', '`']);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Locate the quotes around a specifier.
|
|
54
|
+
*
|
|
55
|
+
* The lexer reports the specifier *without* surrounding quotes for static imports but *with* them
|
|
56
|
+
* for dynamic ones, and the rewriter splices over the quoted range — so normalise here rather than
|
|
57
|
+
* making every caller know the difference.
|
|
58
|
+
*/
|
|
59
|
+
function quoteBounds(source: string, start: number, end: number): { quoteStart: number; quoteEnd: number } | undefined {
|
|
60
|
+
if (QUOTES.has(source[start - 1]) && QUOTES.has(source[end])) {
|
|
61
|
+
return { quoteStart: start - 1, quoteEnd: end + 1 };
|
|
62
|
+
}
|
|
63
|
+
if (QUOTES.has(source[start]) && QUOTES.has(source[end - 1])) {
|
|
64
|
+
return { quoteStart: start, quoteEnd: end };
|
|
65
|
+
}
|
|
66
|
+
// A dynamic import whose specifier is a computed expression: nothing to rewrite.
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Throws if `content` cannot be lexed.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately not caught: the scanner only reaches this for files that already contain a
|
|
74
|
+
* transformation marker, so a swallowed lexer failure would mean a real `?skill` import is left
|
|
75
|
+
* untransformed and the raw specifier ships to the runtime. A build that cannot read a file must
|
|
76
|
+
* stop, not quietly emit less. `transformImports` adds the file path to the message.
|
|
77
|
+
*/
|
|
31
78
|
export function detectQueryImports(content: string, transformers: TransformerRule[]): ImportOccurrence[] {
|
|
79
|
+
ensureInitialised();
|
|
80
|
+
|
|
81
|
+
const [imports] = parse(content);
|
|
82
|
+
|
|
32
83
|
const occurrences: ImportOccurrence[] = [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
quoteStart: match.index,
|
|
47
|
-
quoteEnd: match.index + match[0].length,
|
|
48
|
-
quote,
|
|
49
|
-
});
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
84
|
+
for (const entry of imports) {
|
|
85
|
+
const specifier = entry.n;
|
|
86
|
+
if (specifier === undefined) {
|
|
87
|
+
// `import.meta`, or a dynamic import with a non-literal specifier.
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const bounds = quoteBounds(content, entry.s, entry.e);
|
|
91
|
+
if (!bounds) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const transformer = transformers.find((rule) => rule.pattern.test(specifier));
|
|
95
|
+
if (!transformer) {
|
|
96
|
+
continue;
|
|
52
97
|
}
|
|
98
|
+
occurrences.push({
|
|
99
|
+
transformer,
|
|
100
|
+
specifier,
|
|
101
|
+
quoteStart: bounds.quoteStart,
|
|
102
|
+
quoteEnd: bounds.quoteEnd,
|
|
103
|
+
quote: content[bounds.quoteStart] as "'" | '"' | '`',
|
|
104
|
+
});
|
|
53
105
|
}
|
|
54
106
|
|
|
55
107
|
return occurrences;
|
|
@@ -23,7 +23,7 @@ import type { AssetFile, TransformerRule } from '../core/types.js';
|
|
|
23
23
|
import { copyAssets } from '../core/utils/asset-copy.js';
|
|
24
24
|
import type { WidgetMetadata } from '../core/utils/asset-discovery.js';
|
|
25
25
|
import { emitChunk } from './chunk-emitter.js';
|
|
26
|
-
import { detectQueryImports } from './detector.js';
|
|
26
|
+
import { detectQueryImports, type ImportOccurrence } from './detector.js';
|
|
27
27
|
import { SNIFF_PATTERN } from './patterns.js';
|
|
28
28
|
import { resolveImport } from './resolver.js';
|
|
29
29
|
import { type ImportReplacement, writeRewrittenFile } from './rewriter.js';
|
|
@@ -94,7 +94,17 @@ export async function transformImports(opts: TransformImportsOptions): Promise<T
|
|
|
94
94
|
|
|
95
95
|
while (workQueue.length > 0) {
|
|
96
96
|
const file = workQueue.shift() as WorkItem;
|
|
97
|
-
|
|
97
|
+
|
|
98
|
+
let occurrences: ImportOccurrence[];
|
|
99
|
+
try {
|
|
100
|
+
occurrences = detectQueryImports(file.content, transformers);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Fail closed. This file reached the queue because it contains a transformation
|
|
103
|
+
// marker, so a lexer failure means a real import may go untransformed — the raw
|
|
104
|
+
// specifier would then ship to the runtime.
|
|
105
|
+
throw new Error(`Failed to parse imports in ${file.path}: ${(error as Error).message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
if (occurrences.length === 0) {
|
|
99
109
|
continue;
|
|
100
110
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared regex fragments used by the scanner
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Shared regex fragments used by the scanner and orchestrator to cheaply pre-filter files that
|
|
3
|
+
* could contain Vertesia query-style imports. Keep these in sync with the `pattern` field of each
|
|
4
|
+
* transformer in `core/transformers/`.
|
|
5
|
+
*
|
|
6
|
+
* These are a *sniff* only. Deciding what is actually an import is the detector's job, and it
|
|
7
|
+
* lexes the module rather than matching string literals — a literal-matching regex used to live
|
|
8
|
+
* here too, and it could not tell an import from a comment or a constant.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
/**
|
|
@@ -23,12 +27,3 @@ const BARE_GROUP = BARE_FILENAMES.map((name) => `\\/${name.replace('.', '\\.')}`
|
|
|
23
27
|
* Cheap regex check before invoking the more expensive detector.
|
|
24
28
|
*/
|
|
25
29
|
export const SNIFF_PATTERN = new RegExp(`\\?(?:${QUERY_GROUP})\\b|(?:${BARE_GROUP})\\b`);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Matches a quoted string literal whose contents end with a query marker.
|
|
29
|
-
* Capture group 1 is the quote character; capture group 2 is the specifier.
|
|
30
|
-
*/
|
|
31
|
-
export const QUERY_STRING_LITERAL = new RegExp(
|
|
32
|
-
`(['"\`])([^'"\`]*?(?:\\?(?:${QUERY_GROUP})|${BARE_GROUP})[^'"\`]*)\\1`,
|
|
33
|
-
'g',
|
|
34
|
-
);
|
package/src/index.ts
CHANGED
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
22
22
|
* ```typescript
|
|
23
|
-
* import { transformImports } from '@vertesia/build-tools';
|
|
23
|
+
* import { resolveTransformerNames, transformImports } from '@vertesia/build-tools';
|
|
24
24
|
*
|
|
25
25
|
* await transformImports({
|
|
26
26
|
* libDir: './lib',
|
|
27
27
|
* srcDir: './src',
|
|
28
|
-
*
|
|
28
|
+
* // Resolved rules, not names — use resolveTransformerNames to go from one to the other.
|
|
29
|
+
* transformers: resolveTransformerNames(['skill', 'raw']),
|
|
29
30
|
* assetsDir: './dist',
|
|
30
31
|
* });
|
|
31
32
|
* ```
|
|
@@ -40,8 +41,39 @@ export {
|
|
|
40
41
|
} from './core/compilers/widget.js';
|
|
41
42
|
// Parsers
|
|
42
43
|
export { type FrontmatterResult, parseFrontmatter } from './core/parsers/frontmatter.js';
|
|
44
|
+
export {
|
|
45
|
+
loadSkillCatalog,
|
|
46
|
+
readPackageJson,
|
|
47
|
+
} from './core/skill-markdown/load-catalog.js';
|
|
48
|
+
// Skill markdown preprocessor: `{@tool …}` / `{@skill …}` resolution and tagged-example
|
|
49
|
+
// validation. Pure — the consuming build supplies the catalog and the schema validator.
|
|
50
|
+
export {
|
|
51
|
+
assertSkillMarkdown,
|
|
52
|
+
DEFAULT_SKILL_TOOL_PREFIX,
|
|
53
|
+
type PreprocessSkillMarkdownOptions,
|
|
54
|
+
type PreprocessSkillMarkdownResult,
|
|
55
|
+
preprocessSkillMarkdown,
|
|
56
|
+
type SkillExample,
|
|
57
|
+
type SkillReference,
|
|
58
|
+
type SkillReferenceKind,
|
|
59
|
+
} from './core/skill-markdown/preprocess.js';
|
|
60
|
+
export {
|
|
61
|
+
checkDispatchDescriptor,
|
|
62
|
+
createSchemaExampleValidator,
|
|
63
|
+
createSchemaFieldValidator,
|
|
64
|
+
type DispatchResolution,
|
|
65
|
+
nodesAtPath,
|
|
66
|
+
pairDispatchedInputs,
|
|
67
|
+
resolveDispatchedNames,
|
|
68
|
+
schemaNodeAtPath,
|
|
69
|
+
type ToolDispatchDescriptor,
|
|
70
|
+
type ToolSchemaEntry,
|
|
71
|
+
toolNamesAtPath,
|
|
72
|
+
} from './core/skill-markdown/schema-validator.js';
|
|
43
73
|
// Transformers (the pure transformation functions)
|
|
44
74
|
export {
|
|
75
|
+
createSkillTransformer,
|
|
76
|
+
isSkillTransformer,
|
|
45
77
|
type PromptContentType,
|
|
46
78
|
type PromptDefinition,
|
|
47
79
|
PromptDefinitionSchema,
|
|
@@ -54,6 +86,7 @@ export {
|
|
|
54
86
|
type SkillDefinition,
|
|
55
87
|
SkillDefinitionSchema,
|
|
56
88
|
SkillPropertiesSchema,
|
|
89
|
+
type SkillTransformerConfig,
|
|
57
90
|
skillCollectionTransformer,
|
|
58
91
|
skillTransformer,
|
|
59
92
|
TemplateType,
|
package/src/vite/dev-server.ts
CHANGED
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
import { readFileSync } from 'node:fs';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
import type { Plugin } from 'vite';
|
|
36
|
+
import { loadSkillCatalog, readPackageJson, resolveSkillCatalogPath } from '../core/skill-markdown/load-catalog.js';
|
|
37
|
+
import type { PreprocessSkillMarkdownOptions } from '../core/skill-markdown/preprocess.js';
|
|
38
|
+
import { createSkillTransformer, isSkillTransformer } from '../core/transformers/skill.js';
|
|
36
39
|
import type { TransformerRule } from '../core/types.js';
|
|
37
40
|
import { BUILTIN_TRANSFORMER_NAMES, resolveTransformerNames } from '../import-transform/builtins.js';
|
|
38
41
|
|
|
@@ -43,16 +46,57 @@ export interface VertesiaDevServerPluginOptions {
|
|
|
43
46
|
* Pass a subset to disable transformers you don't use.
|
|
44
47
|
*/
|
|
45
48
|
transformers?: readonly string[];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Catalog used to resolve `{@tool …}` / `{@skill …}` and validate `tool=`-tagged examples.
|
|
52
|
+
*
|
|
53
|
+
* Defaults to the module named by `vertesia-build.skillCatalog` in the package.json of the
|
|
54
|
+
* Vite root, which is the same key the CLI reads — so dev, test and build agree without the
|
|
55
|
+
* setting being written twice. Pass a path to override it, or an object to supply a catalog
|
|
56
|
+
* directly (useful in tests).
|
|
57
|
+
*/
|
|
58
|
+
skillCatalog?: string | PreprocessSkillMarkdownOptions | false;
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
export function vertesiaDevServerPlugin(options: VertesiaDevServerPluginOptions = {}): Plugin {
|
|
49
62
|
const names = options.transformers ?? BUILTIN_TRANSFORMER_NAMES;
|
|
50
63
|
const transformers = resolveTransformerNames([...names]);
|
|
51
64
|
|
|
65
|
+
// Resolved once, lazily: the catalog module is compiled output, so it may not exist when the
|
|
66
|
+
// config is first evaluated.
|
|
67
|
+
let root = process.cwd();
|
|
68
|
+
|
|
69
|
+
let boundPromise: Promise<TransformerRule | null> | undefined;
|
|
70
|
+
|
|
71
|
+
/** The catalog-bound skill transformer, or null when this package configures no catalog. */
|
|
72
|
+
const boundSkillTransformer = (): Promise<TransformerRule | null> => {
|
|
73
|
+
if (!boundPromise) {
|
|
74
|
+
boundPromise = (async () => {
|
|
75
|
+
if (options.skillCatalog === false) return null;
|
|
76
|
+
const markdown =
|
|
77
|
+
typeof options.skillCatalog === 'object'
|
|
78
|
+
? options.skillCatalog
|
|
79
|
+
: await (async () => {
|
|
80
|
+
const modulePath =
|
|
81
|
+
typeof options.skillCatalog === 'string'
|
|
82
|
+
? path.resolve(root, options.skillCatalog)
|
|
83
|
+
: resolveSkillCatalogPath(readPackageJson(root) ?? {}, root);
|
|
84
|
+
return modulePath ? await loadSkillCatalog(modulePath) : null;
|
|
85
|
+
})();
|
|
86
|
+
return markdown ? createSkillTransformer({ markdown }) : null;
|
|
87
|
+
})();
|
|
88
|
+
}
|
|
89
|
+
return boundPromise;
|
|
90
|
+
};
|
|
91
|
+
|
|
52
92
|
return {
|
|
53
93
|
name: 'vertesia-dev-server',
|
|
54
94
|
enforce: 'pre',
|
|
55
95
|
|
|
96
|
+
configResolved(config: { root?: string }) {
|
|
97
|
+
if (config.root) root = config.root;
|
|
98
|
+
},
|
|
99
|
+
|
|
56
100
|
/**
|
|
57
101
|
* Map relative query-style imports to absolute paths so Vite's loader
|
|
58
102
|
* sees a stable id keyed by the original file location. The query
|
|
@@ -87,13 +131,17 @@ export function vertesiaDevServerPlugin(options: VertesiaDevServerPluginOptions
|
|
|
87
131
|
const matched = findMatchingTransformer(id, transformers);
|
|
88
132
|
if (!matched) return null;
|
|
89
133
|
|
|
134
|
+
// Bind the skill transformer to the catalog on first use. Matched by identity rather
|
|
135
|
+
// than by pattern so a consumer's own transformer for the same files is left alone.
|
|
136
|
+
const active = isSkillTransformer(matched) ? ((await boundSkillTransformer()) ?? matched) : matched;
|
|
137
|
+
|
|
90
138
|
const cleanId = stripQuery(id);
|
|
91
139
|
try {
|
|
92
|
-
const content =
|
|
93
|
-
const result = await
|
|
140
|
+
const content = active.virtual ? '' : readFileSync(cleanId, 'utf-8');
|
|
141
|
+
const result = await active.transform(content, cleanId);
|
|
94
142
|
|
|
95
|
-
if (
|
|
96
|
-
const validation =
|
|
143
|
+
if (active.schema) {
|
|
144
|
+
const validation = active.schema.safeParse(result.data);
|
|
97
145
|
if (!validation.success) {
|
|
98
146
|
const errors = validation.error.issues
|
|
99
147
|
.map((err) => ` - ${err.path.join('.')}: ${err.message}`)
|