tosijs-ui 1.6.1 → 1.6.3
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/bin/make-icon-data.js +327 -0
- package/dist/doc-system/site/make-llms-txt.d.ts +11 -1
- package/dist/doc-system/site/make-llms-txt.js +16 -10
- package/dist/doc-system/site/orchestrator.js +41 -3
- package/dist/doc-system/site/site-config.d.ts +19 -5
- package/dist/iife.js +35 -35
- package/dist/iife.js.map +5 -5
- package/dist/live-example/code-transform.d.ts +15 -2
- package/dist/live-example/code-transform.js +33 -7
- package/dist/live-example/execution.js +3 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -6
- package/package.json +9 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/*#
|
|
3
|
+
# make-icon-data
|
|
4
|
+
|
|
5
|
+
<!--{ "pin": "bottom" }-->
|
|
6
|
+
|
|
7
|
+
Ingests SVG files from icon directories and generates an icon-data module
|
|
8
|
+
(TypeScript or JavaScript). This is the build tool behind the `icons` system.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
bun run bin/make-icon-data.js [options]
|
|
13
|
+
|
|
14
|
+
### Options
|
|
15
|
+
|
|
16
|
+
- `--input <dir1,dir2,...>` — directories to scan (default: `./icons`)
|
|
17
|
+
- `--output <path>` — output file (default: `./src/icon-data.ts`)
|
|
18
|
+
- `--optimize <true|false>` — round coordinates based on viewBox size (default: true)
|
|
19
|
+
|
|
20
|
+
### Examples
|
|
21
|
+
|
|
22
|
+
bun run bin/make-icon-data.js
|
|
23
|
+
bun run bin/make-icon-data.js --input ./my-icons --output ./dist/icons.js
|
|
24
|
+
bun run bin/make-icon-data.js --optimize false
|
|
25
|
+
|
|
26
|
+
## Directory conventions
|
|
27
|
+
|
|
28
|
+
### Style classes
|
|
29
|
+
|
|
30
|
+
Folder names control CSS classes on the generated SVGs:
|
|
31
|
+
|
|
32
|
+
- `color/` — icons with baked-in colors (class `color`, fill/stroke preserved)
|
|
33
|
+
- `stroked/` — stroke-only icons (class `stroked`, fill styles removed)
|
|
34
|
+
- `filled/` — fill-only icons (class `filled`, stroke styles removed)
|
|
35
|
+
|
|
36
|
+
### Directional folders
|
|
37
|
+
|
|
38
|
+
Place icons in specially named folders to auto-generate directional
|
|
39
|
+
redirects, eliminating redundant SVG files:
|
|
40
|
+
|
|
41
|
+
- `right-left/` — base icon points right; generates a flipped left variant
|
|
42
|
+
- `up-down/` — base icon points up; generates a flipped down variant
|
|
43
|
+
- `up-down-right-left/` — base icon points right; generates down (90°),
|
|
44
|
+
left (180°), and up (270°) rotation variants
|
|
45
|
+
|
|
46
|
+
For example, `chevron-right.svg` in an `up-down-right-left/` folder produces
|
|
47
|
+
`chevronRight` (SVG) plus `chevronDown`, `chevronLeft`, `chevronUp` (redirects).
|
|
48
|
+
|
|
49
|
+
### Comma-separated names
|
|
50
|
+
|
|
51
|
+
For icons where the directional variant has a different name (not just a
|
|
52
|
+
direction swap), use commas in the filename:
|
|
53
|
+
|
|
54
|
+
skip-forward,skip-back.svg → skipForward (SVG) + skipBack (redirect)
|
|
55
|
+
refresh-cw,refresh-ccw.svg → refreshCw (SVG) + refreshCcw (redirect)
|
|
56
|
+
|
|
57
|
+
The first name is the base, additional names are mapped to the folder's
|
|
58
|
+
variant suffixes in order.
|
|
59
|
+
|
|
60
|
+
## SVG processing
|
|
61
|
+
|
|
62
|
+
The tool automatically:
|
|
63
|
+
- Strips XML declarations, namespaces, IDs, and comments
|
|
64
|
+
- Removes redundant attributes (default fills, strokes)
|
|
65
|
+
- Collapses whitespace
|
|
66
|
+
- Rounds coordinates based on viewBox size (larger viewBox = fewer decimals)
|
|
67
|
+
- Converts filenames to camelCase (`arrow-right.svg` → `arrowRight`)
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/*{ "parent": "Appendices" }*/
|
|
71
|
+
|
|
72
|
+
import fs from 'fs'
|
|
73
|
+
import path from 'path'
|
|
74
|
+
|
|
75
|
+
// Default values for options
|
|
76
|
+
const defaultOptions = {
|
|
77
|
+
input: './icons',
|
|
78
|
+
output: './src/icon-data.ts',
|
|
79
|
+
optimize: true, // Optimize coordinate precision based on viewBox size
|
|
80
|
+
}
|
|
81
|
+
const flags = Object.keys(defaultOptions)
|
|
82
|
+
|
|
83
|
+
// Parse command-line arguments
|
|
84
|
+
const args = process.argv.slice(2)
|
|
85
|
+
const parsedArgs = {}
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const arg = args[i]
|
|
88
|
+
if (arg.startsWith('--')) {
|
|
89
|
+
const key = arg.substring(2)
|
|
90
|
+
const value = args[i + 1]
|
|
91
|
+
if (value && !value.startsWith('--')) {
|
|
92
|
+
parsedArgs[key] = value
|
|
93
|
+
i++ // Skip the value in the next iteration
|
|
94
|
+
} else {
|
|
95
|
+
parsedArgs[key] = true // Handle boolean flags (though not used for these options currently)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Combine default options with parsed arguments
|
|
101
|
+
const options = { ...defaultOptions }
|
|
102
|
+
for (const flag of flags) {
|
|
103
|
+
if (parsedArgs[flag] !== undefined) {
|
|
104
|
+
options[flag] = parsedArgs[flag]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Destructure options for easier use
|
|
109
|
+
const { input: iconDirectories, output: outputFilePath, optimize } = options
|
|
110
|
+
const isTypescript = outputFilePath.endsWith('.ts')
|
|
111
|
+
const shouldOptimize = optimize === true || optimize === 'true'
|
|
112
|
+
|
|
113
|
+
// Calculate appropriate decimal precision based on viewBox size
|
|
114
|
+
// Larger viewBox = fewer decimal places needed
|
|
115
|
+
function getPrecisionForViewBox(viewBoxSize) {
|
|
116
|
+
if (viewBoxSize >= 512) return 0
|
|
117
|
+
if (viewBoxSize >= 64) return 1
|
|
118
|
+
return 2
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Round numbers in SVG content based on viewBox-appropriate precision
|
|
122
|
+
function optimizeCoordinates(svgSource, precision) {
|
|
123
|
+
return svgSource.replace(/\d+\.\d+/g, (number) => {
|
|
124
|
+
const rounded = Number(number).toFixed(precision)
|
|
125
|
+
// Remove trailing zeros and unnecessary decimal point
|
|
126
|
+
return parseFloat(rounded).toString()
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract viewBox size from SVG
|
|
131
|
+
function getViewBoxSize(svgSource) {
|
|
132
|
+
const match = svgSource.match(/viewBox="([^"]+)"/)
|
|
133
|
+
if (match) {
|
|
134
|
+
const parts = match[1].split(/\s+/)
|
|
135
|
+
if (parts.length >= 4) {
|
|
136
|
+
const width = parseFloat(parts[2])
|
|
137
|
+
const height = parseFloat(parts[3])
|
|
138
|
+
return Math.max(width, height)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return 24 // Default assumption
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const typeDeclaration = isTypescript
|
|
145
|
+
? 'export interface IconData { [key: string]: string }'
|
|
146
|
+
: '' // No type declaration for JS output
|
|
147
|
+
|
|
148
|
+
const iconData = {}
|
|
149
|
+
const iconRedirects = {}
|
|
150
|
+
|
|
151
|
+
// Directional folder conventions:
|
|
152
|
+
// right-left: base has "Right", generate "Left" as flip
|
|
153
|
+
// up-down: base has "Up", generate "Down" as flip
|
|
154
|
+
// up-down-right-left: base has "Right", generate Down/Left/Up via rotation
|
|
155
|
+
const DIRECTION_FOLDERS = {
|
|
156
|
+
'right-left': {
|
|
157
|
+
base: 'Right',
|
|
158
|
+
variants: { Left: '0f' },
|
|
159
|
+
},
|
|
160
|
+
'up-down': {
|
|
161
|
+
base: 'Up',
|
|
162
|
+
variants: { Down: '1f' },
|
|
163
|
+
},
|
|
164
|
+
'up-down-right-left': {
|
|
165
|
+
base: 'Right',
|
|
166
|
+
variants: {
|
|
167
|
+
Down: '90r',
|
|
168
|
+
Left: '180r',
|
|
169
|
+
Up: '270r',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getDirectionConfig(dir) {
|
|
175
|
+
const dirName = path.basename(dir)
|
|
176
|
+
return DIRECTION_FOLDERS[dirName] || null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function generateDirectionalRedirects(name, dirConfig) {
|
|
180
|
+
// Find the base direction word in the camelCase name
|
|
181
|
+
const baseDir = dirConfig.base
|
|
182
|
+
if (!name.includes(baseDir)) return
|
|
183
|
+
|
|
184
|
+
for (const [targetDir, suffix] of Object.entries(dirConfig.variants)) {
|
|
185
|
+
const redirectName = name.replace(baseDir, targetDir)
|
|
186
|
+
if (redirectName !== name) {
|
|
187
|
+
iconRedirects[redirectName] = name + suffix
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function findIcons(dirs, ignore = []) {
|
|
193
|
+
function traverseDirectory(dir) {
|
|
194
|
+
if (!fs.existsSync(dir)) {
|
|
195
|
+
console.warn(`Warning: Directory not found: ${dir}. Skipping.`)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// Sort so output is reproducible across machines — readdirSync returns
|
|
199
|
+
// entries in filesystem order, which differs by OS and otherwise produces
|
|
200
|
+
// spurious reordering diffs in the generated icon-data on every build.
|
|
201
|
+
const files = fs.readdirSync(dir).sort()
|
|
202
|
+
if (ignore.includes(dir)) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
files.forEach((file) => {
|
|
207
|
+
const filePath = path.join(dir, file)
|
|
208
|
+
const stats = fs.statSync(filePath)
|
|
209
|
+
|
|
210
|
+
if (stats.isDirectory()) {
|
|
211
|
+
traverseDirectory(filePath)
|
|
212
|
+
} else if (path.extname(file) === '.svg') {
|
|
213
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
214
|
+
const rawName = file.split('.')[0]
|
|
215
|
+
const names = rawName.split(',').map((n) =>
|
|
216
|
+
n.trim().replace(/-([a-z0-9])/g, (_, char) => char.toLocaleUpperCase())
|
|
217
|
+
)
|
|
218
|
+
const name = names[0]
|
|
219
|
+
let svgSource = content
|
|
220
|
+
.replace(/(<\?xml.*?>|<!DOCTYPE.*?>)\s?/g, '')
|
|
221
|
+
.replace(/<svg.*?>/, (a) =>
|
|
222
|
+
a.replace(
|
|
223
|
+
/\s(x|y|width|height|class|xmlns|xmlns:xlink)="[^"]+"/g,
|
|
224
|
+
''
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
.replace(
|
|
228
|
+
/\s?\b(opacity:1;|fill="#000000"|fill="#000"|fill="none"|class="[^"]+"|stroke="currentColor"|stroke="#000000"|stroke-linejoin="[^"]+"|stroke-width="[^"]+"|(stroke-)?stroke-linecap="[^"]+")/g,
|
|
229
|
+
''
|
|
230
|
+
)
|
|
231
|
+
.replace(/stroke-stroke/g, 'stroke')
|
|
232
|
+
.replace(/fill-fill/g, 'fill')
|
|
233
|
+
.replace(/\s+/g, ' ')
|
|
234
|
+
.replace(/>\s+</g, '><')
|
|
235
|
+
.replace(/\s(id)="[^"]+"/g, '')
|
|
236
|
+
.replace(/<!--.*?-->/g, '')
|
|
237
|
+
|
|
238
|
+
// Optimize coordinate precision based on viewBox size
|
|
239
|
+
if (shouldOptimize) {
|
|
240
|
+
const viewBoxSize = getViewBoxSize(svgSource)
|
|
241
|
+
const precision = getPrecisionForViewBox(viewBoxSize)
|
|
242
|
+
svgSource = optimizeCoordinates(svgSource, precision)
|
|
243
|
+
}
|
|
244
|
+
const classes = []
|
|
245
|
+
if (dir.includes('color')) {
|
|
246
|
+
classes.push('color')
|
|
247
|
+
} else {
|
|
248
|
+
// If not a color icon, remove fill/stroke styles
|
|
249
|
+
svgSource = svgSource.replace(/(fill|stroke)(-\w+)?:[^;]+;?/g, '')
|
|
250
|
+
}
|
|
251
|
+
if (dir.includes('stroked')) {
|
|
252
|
+
classes.push('stroked')
|
|
253
|
+
svgSource = svgSource.replace(/(fill)(-\w+)?:[^;]+;?/g, '')
|
|
254
|
+
}
|
|
255
|
+
if (dir.includes('filled')) {
|
|
256
|
+
classes.push('filled')
|
|
257
|
+
svgSource = svgSource.replace(/(stroke)(-\w+)?:[^;]+;?/g, '')
|
|
258
|
+
}
|
|
259
|
+
if (classes.length) {
|
|
260
|
+
svgSource = svgSource.replace(
|
|
261
|
+
/^<svg/,
|
|
262
|
+
`<svg class="${classes.join(' ')}"`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
iconData[name] = svgSource
|
|
266
|
+
|
|
267
|
+
// Comma-separated names: skip-forward,skip-back.svg
|
|
268
|
+
// First name is the base, subsequent names are redirects using
|
|
269
|
+
// the folder's convention (0f for right-left, etc.)
|
|
270
|
+
const dirConfig = getDirectionConfig(dir)
|
|
271
|
+
if (names.length > 1 && dirConfig) {
|
|
272
|
+
const suffixes = Object.values(dirConfig.variants)
|
|
273
|
+
for (let n = 1; n < names.length && n - 1 < suffixes.length; n++) {
|
|
274
|
+
iconRedirects[names[n]] = name + suffixes[n - 1]
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Generate directional redirects based on folder convention
|
|
279
|
+
if (dirConfig) {
|
|
280
|
+
generateDirectionalRedirects(name, dirConfig)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
dirs.forEach((dir) => {
|
|
287
|
+
traverseDirectory(dir)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
findIcons(iconDirectories.split(','))
|
|
292
|
+
|
|
293
|
+
// Manual redirects for icons that don't fit folder conventions
|
|
294
|
+
const manualRedirects = {
|
|
295
|
+
arrowDownRight: 'arrowUpRight90r',
|
|
296
|
+
arrowDownLeft: 'arrowUpRight180r',
|
|
297
|
+
arrowUpLeft: 'arrowUpRight270r',
|
|
298
|
+
}
|
|
299
|
+
Object.assign(iconRedirects, manualRedirects)
|
|
300
|
+
|
|
301
|
+
// Merge redirects — only add if the target name doesn't already have SVG data
|
|
302
|
+
let redirectCount = 0
|
|
303
|
+
for (const [name, redirect] of Object.entries(iconRedirects)) {
|
|
304
|
+
if (!iconData[name]) {
|
|
305
|
+
iconData[name] = redirect
|
|
306
|
+
redirectCount++
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (redirectCount > 0) {
|
|
310
|
+
console.log(`Generated ${redirectCount} directional redirects`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Ensure the output directory exists
|
|
314
|
+
const outputDir = path.dirname(outputFilePath)
|
|
315
|
+
if (!fs.existsSync(outputDir)) {
|
|
316
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const source =
|
|
320
|
+
(typeDeclaration ? typeDeclaration + '\n\n' : '') +
|
|
321
|
+
'export default ' +
|
|
322
|
+
JSON.stringify(iconData, null, 2).replace(/"(\w+)":/g, '$1:') +
|
|
323
|
+
(isTypescript ? ' as IconData\n' : '\n')
|
|
324
|
+
|
|
325
|
+
fs.writeFileSync(outputFilePath, source, 'utf8')
|
|
326
|
+
|
|
327
|
+
console.log(`Successfully generated icon data to: ${outputFilePath}`)
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface LlmsTxtMeta {
|
|
2
|
+
name?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
/** site origin, used for the Docs link */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** project links — `github` / `npm` (or any) become Source/npm links */
|
|
7
|
+
projectLinks?: Record<string, string | undefined>;
|
|
8
|
+
/** optional framing line(s) under the description */
|
|
9
|
+
tagline?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateLlmsTxt(outputPath: string, meta?: LlmsTxtMeta): void;
|
|
@@ -33,8 +33,8 @@ function extractDescription(text) {
|
|
|
33
33
|
}
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
|
-
export function generateLlmsTxt(outputPath) {
|
|
37
|
-
const
|
|
36
|
+
export function generateLlmsTxt(outputPath, meta = {}) {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
38
38
|
const docs = [];
|
|
39
39
|
const files = fs.readdirSync(SRC).filter((f) => f.endsWith('.ts'));
|
|
40
40
|
for (const file of files) {
|
|
@@ -59,17 +59,23 @@ export function generateLlmsTxt(outputPath) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
docs.sort((a, b) => a.title.localeCompare(b.title));
|
|
62
|
+
const name = meta.name ?? pkg.name;
|
|
63
|
+
const description = meta.description ?? pkg.description;
|
|
64
|
+
const links = [];
|
|
65
|
+
if (meta.baseUrl)
|
|
66
|
+
links.push(`- Docs: ${meta.baseUrl}`);
|
|
67
|
+
if (meta.projectLinks?.github)
|
|
68
|
+
links.push(`- Source: ${meta.projectLinks.github}`);
|
|
69
|
+
const npm = meta.projectLinks?.npm ?? `https://www.npmjs.com/package/${pkg.name}`;
|
|
70
|
+
if (npm)
|
|
71
|
+
links.push(`- npm: ${npm}`);
|
|
62
72
|
const lines = [
|
|
63
|
-
`# ${
|
|
73
|
+
`# ${name} v${pkg.version}`,
|
|
64
74
|
'',
|
|
65
|
-
|
|
75
|
+
description,
|
|
76
|
+
...(meta.tagline ? ['', meta.tagline] : []),
|
|
66
77
|
'',
|
|
67
|
-
|
|
68
|
-
'rather than replacing native elements.',
|
|
69
|
-
'',
|
|
70
|
-
'- Docs: https://ui.tosijs.net',
|
|
71
|
-
'- Source: https://github.com/tonioloewald/xinjs-ui',
|
|
72
|
-
'- npm: https://www.npmjs.com/package/tosijs-ui',
|
|
78
|
+
...links,
|
|
73
79
|
'',
|
|
74
80
|
'## Documentation',
|
|
75
81
|
'',
|
|
@@ -57,7 +57,17 @@ export async function buildSite(config) {
|
|
|
57
57
|
console.time('build');
|
|
58
58
|
// Optionally also build the library (ESM + type declarations) — for repos
|
|
59
59
|
// whose single build publishes both an npm package and its doc site.
|
|
60
|
-
if (config.
|
|
60
|
+
if (config.libraryTsconfig) {
|
|
61
|
+
// Consumer-controlled library build (handles root noEmit, removeComments,
|
|
62
|
+
// outDir, etc.).
|
|
63
|
+
try {
|
|
64
|
+
await $ `bun tsc -p ${config.libraryTsconfig}`;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
console.log('library (tsc -p) build finished');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (config.emitLibrary) {
|
|
61
71
|
try {
|
|
62
72
|
await $ `bun tsc --declaration --incremental --outDir dist`;
|
|
63
73
|
}
|
|
@@ -70,6 +80,13 @@ export async function buildSite(config) {
|
|
|
70
80
|
// consumer supplies (e.g. via staticDirs or an absolute URL).
|
|
71
81
|
const scriptName = (config.scriptUrl ?? '/iife.js').replace(/^\//, '');
|
|
72
82
|
if (config.bundleEntry) {
|
|
83
|
+
// IIFE externals become a synchronous require() shim that throws at
|
|
84
|
+
// module-eval ("Dynamic require of … is not supported") — the build still
|
|
85
|
+
// succeeds, so warn rather than fail silently.
|
|
86
|
+
if (config.bundleExternals && config.bundleExternals.length > 0) {
|
|
87
|
+
console.warn(`⚠️ bundleExternals in an IIFE bundle (${config.bundleExternals.join(', ')}): Bun emits a dynamic require() shim that throws at runtime. Load these\n` +
|
|
88
|
+
` via import() (dynamically, so the bundler keeps them async) or an importmap, not a static import.`);
|
|
89
|
+
}
|
|
73
90
|
const result = await Bun.build({
|
|
74
91
|
entrypoints: [config.bundleEntry],
|
|
75
92
|
outdir: DIST,
|
|
@@ -87,11 +104,32 @@ export async function buildSite(config) {
|
|
|
87
104
|
}
|
|
88
105
|
await $ `cp ${DIST}/${scriptName} ${PUBLIC}`.text();
|
|
89
106
|
const bundleFile = await Bun.file(`${DIST}/${scriptName}`).arrayBuffer();
|
|
107
|
+
// import.meta is illegal in a classic <script> — if it survived bundling
|
|
108
|
+
// (a branch the bundler couldn't eliminate) the IIFE will SyntaxError.
|
|
109
|
+
if (Buffer.from(bundleFile).toString('utf8').includes('import.meta')) {
|
|
110
|
+
console.warn(`⚠️ ${scriptName} contains \`import.meta\`, which is a SyntaxError in a classic <script>.\n` +
|
|
111
|
+
` A dependency referenced it in a branch the bundler couldn't drop — mark that dep external\n` +
|
|
112
|
+
` (+ importmap) or choose a browser-only entry point.`);
|
|
113
|
+
}
|
|
90
114
|
const bundleGzip = gzipSync(Buffer.from(bundleFile));
|
|
91
115
|
console.log(`${scriptName}: ${(bundleFile.byteLength / 1024).toFixed(1)}kb (${(bundleGzip.length / 1024).toFixed(1)}kb gzip)`);
|
|
92
116
|
}
|
|
93
|
-
if (config.llmsTxt
|
|
94
|
-
|
|
117
|
+
if (config.llmsTxt !== false) {
|
|
118
|
+
if (typeof config.llmsTxt === 'function') {
|
|
119
|
+
const corpus = JSON.parse(await Bun.file('demo/docs.json').text());
|
|
120
|
+
await Bun.write('llms.txt', config.llmsTxt(corpus));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
generateLlmsTxt('llms.txt', {
|
|
124
|
+
name: config.name,
|
|
125
|
+
description: config.description,
|
|
126
|
+
baseUrl: config.baseUrl,
|
|
127
|
+
projectLinks: config.projectLinks,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// Also place it at the served web root so {baseUrl}/llms.txt resolves (the
|
|
131
|
+
// root copy stays for the npm package's `files`).
|
|
132
|
+
await $ `cp llms.txt ${PUBLIC}/llms.txt`.text();
|
|
95
133
|
}
|
|
96
134
|
// Generate the static, pre-rendered doc site (one /slug/index.html per doc).
|
|
97
135
|
// Runs after the static-asset copy so the generated index.html (README) wins,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProjectLinks, LinkItem } from '../../doc-browser';
|
|
2
2
|
import type { DocSystemTheme } from '../doc-system-styles';
|
|
3
|
+
import type { Doc } from './docs';
|
|
3
4
|
export type SiteHost = 'github-pages' | 'firebase' | 'static';
|
|
4
5
|
export interface SiteConfig {
|
|
5
6
|
/** project / brand name — header, <title> suffix, og:site_name */
|
|
@@ -82,13 +83,26 @@ export interface SiteConfig {
|
|
|
82
83
|
*/
|
|
83
84
|
prebuild?: () => void | Promise<void>;
|
|
84
85
|
/**
|
|
85
|
-
* Also build the library: `tsc --declaration --outDir dist`
|
|
86
|
-
* Default false. Repos whose single build publishes BOTH an
|
|
87
|
-
* its doc site (the tosijs-* libs) set this true; a pure docs
|
|
86
|
+
* Also build the library: `tsc --declaration --incremental --outDir dist`
|
|
87
|
+
* (ESM + types). Default false. Repos whose single build publishes BOTH an
|
|
88
|
+
* npm package and its doc site (the tosijs-* libs) set this true; a pure docs
|
|
89
|
+
* site omits it. Ignored when `libraryTsconfig` is set.
|
|
88
90
|
*/
|
|
89
91
|
emitLibrary?: boolean;
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Path to a tsconfig for the library build, run as `tsc -p <path>` instead of
|
|
94
|
+
* the fixed `emitLibrary` command. Use this when the root tsconfig has
|
|
95
|
+
* `noEmit: true`, or to control `removeComments`/`outDir`/`declaration`
|
|
96
|
+
* yourself (e.g. keep doc comments in the published JS for AI readers).
|
|
97
|
+
*/
|
|
98
|
+
libraryTsconfig?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Emit llms.txt agent-discoverability index. Default true (uses `name` /
|
|
101
|
+
* `description` / `baseUrl` / `projectLinks`). Set false to skip, or pass a
|
|
102
|
+
* function for a fully custom index — it receives the doc corpus and returns
|
|
103
|
+
* the file contents.
|
|
104
|
+
*/
|
|
105
|
+
llmsTxt?: boolean | ((docs: Doc[]) => string);
|
|
92
106
|
/** served web-root output dir, default 'docs' */
|
|
93
107
|
outputDir?: string;
|
|
94
108
|
/** dev-server port, default 8787 */
|