@databrainhq/mcp-server 0.3.0-rc.1 → 0.3.0-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +158 -7
  2. package/dist/apps/server.d.ts +82 -44
  3. package/dist/apps/server.js +5 -2
  4. package/dist/apps/stdio.js +6 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/bundle-TDQXWIPM.js +24 -0
  7. package/dist/bundle-TDQXWIPM.js.map +1 -0
  8. package/dist/{chunk-RF3HTNO7.js → chunk-DM7S4R57.js} +2 -2
  9. package/dist/{chunk-7DS7OLWP.js → chunk-ES2LUP4U.js} +2030 -1214
  10. package/dist/chunk-ES2LUP4U.js.map +1 -0
  11. package/dist/chunk-IBYBK5ZE.js +34 -0
  12. package/dist/chunk-IBYBK5ZE.js.map +1 -0
  13. package/dist/chunk-NBFUKVWP.js +603 -0
  14. package/dist/chunk-NBFUKVWP.js.map +1 -0
  15. package/dist/chunk-Q5EYDKSA.js +1224 -0
  16. package/dist/chunk-Q5EYDKSA.js.map +1 -0
  17. package/dist/chunk-RHK62ZJ6.js +884 -0
  18. package/dist/chunk-RHK62ZJ6.js.map +1 -0
  19. package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
  20. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  21. package/dist/cli.d.ts +15 -0
  22. package/dist/cli.js +497 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/custom-chart/custom-runtime.js +63 -0
  25. package/dist/custom-chart/runtime-shim.js +146 -0
  26. package/dist/custom-chart/template/AGENTS.md +374 -0
  27. package/dist/custom-chart/template/README.md +374 -0
  28. package/dist/custom-chart/template/chart.css +28 -0
  29. package/dist/custom-chart/template/chart.ts +192 -0
  30. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  31. package/dist/custom-chart/template/icon.svg +1 -0
  32. package/dist/custom-chart/template/manifest.json +23 -0
  33. package/dist/custom-chart/template/package.json +14 -0
  34. package/dist/custom-chart/template/sample-data.json +18 -0
  35. package/dist/custom-chart/template/tsconfig.json +12 -0
  36. package/dist/index.js +15 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/main-JEYYFGJM.js +872 -0
  39. package/dist/main-JEYYFGJM.js.map +1 -0
  40. package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
  41. package/dist/test-only-K7QTSVO4.js.map +1 -0
  42. package/package.json +9 -4
  43. package/dist/chunk-7DS7OLWP.js.map +0 -1
  44. package/dist/chunk-JCXUZ3QZ.js.map +0 -1
  45. package/dist/chunk-NDVW3ZSJ.js +0 -310
  46. package/dist/chunk-NDVW3ZSJ.js.map +0 -1
  47. package/dist/main-7QNRH6X4.js +0 -438
  48. package/dist/main-7QNRH6X4.js.map +0 -1
  49. package/dist/test-only-EJ5O4WN5.js.map +0 -1
  50. /package/dist/{chunk-RF3HTNO7.js.map → chunk-DM7S4R57.js.map} +0 -0
@@ -0,0 +1,1224 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/custom-chart/bundle.ts
4
+ import { createHash } from "crypto";
5
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
6
+ import { join as join2, resolve as resolve2, sep } from "path";
7
+ import { build as esbuild } from "esbuild";
8
+
9
+ // src/custom-chart/assets.ts
10
+ import { buildSync } from "esbuild";
11
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
12
+ import { dirname, join, resolve } from "path";
13
+ import { fileURLToPath } from "url";
14
+ var cachedDir;
15
+ function customChartAssetDir() {
16
+ if (cachedDir) return cachedDir;
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const candidates = [
19
+ here,
20
+ join(here, "custom-chart"),
21
+ join(here, "..", "custom-chart"),
22
+ join(here, "..", "..", "custom-chart"),
23
+ join(here, "..", "src", "custom-chart")
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (existsSync(join(candidate, "runtime-shim.js")) && existsSync(join(candidate, "template", "manifest.json"))) {
27
+ cachedDir = resolve(candidate);
28
+ return cachedDir;
29
+ }
30
+ }
31
+ throw new Error(
32
+ `Custom chart assets not found near ${here}. Expected runtime-shim.js and template/ (run npm run build).`
33
+ );
34
+ }
35
+ function readRuntimeShim() {
36
+ return readFileSync(join(customChartAssetDir(), "runtime-shim.js"), "utf8");
37
+ }
38
+ function templateDir() {
39
+ return join(customChartAssetDir(), "template");
40
+ }
41
+ function readTemplateFiles() {
42
+ const dir = templateDir();
43
+ const files = {};
44
+ for (const name of readdirSync(dir).sort()) {
45
+ const path = join(dir, name);
46
+ if (statSync(path).isFile()) files[name] = readFileSync(path, "utf8");
47
+ }
48
+ return files;
49
+ }
50
+ var cachedBrowserRuntime;
51
+ function readCustomChartBrowserRuntime() {
52
+ if (cachedBrowserRuntime) return cachedBrowserRuntime;
53
+ const dir = customChartAssetDir();
54
+ const packed = join(dir, "custom-runtime.js");
55
+ cachedBrowserRuntime = existsSync(packed) ? readFileSync(packed, "utf8") : buildSync({
56
+ entryPoints: [join(dir, "..", "apps", "custom-runtime", "browser.ts")],
57
+ bundle: true,
58
+ write: false,
59
+ platform: "browser",
60
+ target: "es2020",
61
+ minify: true
62
+ }).outputFiles[0].text;
63
+ return cachedBrowserRuntime;
64
+ }
65
+
66
+ // src/custom-chart/sanitize-svg.ts
67
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
68
+ "svg",
69
+ "g",
70
+ "defs",
71
+ "symbol",
72
+ "use",
73
+ "title",
74
+ "desc",
75
+ "metadata",
76
+ "path",
77
+ "rect",
78
+ "circle",
79
+ "ellipse",
80
+ "line",
81
+ "polyline",
82
+ "polygon",
83
+ "text",
84
+ "tspan",
85
+ "textpath",
86
+ "image",
87
+ "lineargradient",
88
+ "radialgradient",
89
+ "stop",
90
+ "clippath",
91
+ "mask",
92
+ "pattern",
93
+ "marker",
94
+ "filter",
95
+ "fegaussianblur",
96
+ "feoffset",
97
+ "feblend",
98
+ "fecolormatrix",
99
+ "fecomposite",
100
+ "feflood",
101
+ "femerge",
102
+ "femergenode",
103
+ "fedropshadow",
104
+ "style",
105
+ "switch"
106
+ ]);
107
+ var ALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
108
+ "id",
109
+ "class",
110
+ "style",
111
+ "href",
112
+ "xlink:href",
113
+ "xml:space",
114
+ "lang",
115
+ "xml:lang",
116
+ "version",
117
+ "baseprofile",
118
+ "viewbox",
119
+ "preserveaspectratio",
120
+ "width",
121
+ "height",
122
+ "x",
123
+ "y",
124
+ "x1",
125
+ "y1",
126
+ "x2",
127
+ "y2",
128
+ "cx",
129
+ "cy",
130
+ "r",
131
+ "rx",
132
+ "ry",
133
+ "fx",
134
+ "fy",
135
+ "fr",
136
+ "d",
137
+ "points",
138
+ "transform",
139
+ "transform-origin",
140
+ "fill",
141
+ "fill-opacity",
142
+ "fill-rule",
143
+ "stroke",
144
+ "stroke-width",
145
+ "stroke-linecap",
146
+ "stroke-linejoin",
147
+ "stroke-miterlimit",
148
+ "stroke-dasharray",
149
+ "stroke-dashoffset",
150
+ "stroke-opacity",
151
+ "opacity",
152
+ "color",
153
+ "display",
154
+ "visibility",
155
+ "overflow",
156
+ "clip-path",
157
+ "clip-rule",
158
+ "clippathunits",
159
+ "mask",
160
+ "maskunits",
161
+ "maskcontentunits",
162
+ "filter",
163
+ "filterunits",
164
+ "primitiveunits",
165
+ "in",
166
+ "in2",
167
+ "result",
168
+ "mode",
169
+ "operator",
170
+ "values",
171
+ "type",
172
+ "stddeviation",
173
+ "dx",
174
+ "dy",
175
+ "flood-color",
176
+ "flood-opacity",
177
+ "offset",
178
+ "stop-color",
179
+ "stop-opacity",
180
+ "gradientunits",
181
+ "gradienttransform",
182
+ "spreadmethod",
183
+ "patternunits",
184
+ "patterncontentunits",
185
+ "patterntransform",
186
+ "markerunits",
187
+ "markerwidth",
188
+ "markerheight",
189
+ "refx",
190
+ "refy",
191
+ "orient",
192
+ "font-family",
193
+ "font-size",
194
+ "font-weight",
195
+ "font-style",
196
+ "letter-spacing",
197
+ "text-anchor",
198
+ "dominant-baseline",
199
+ "alignment-baseline",
200
+ "text-decoration",
201
+ "word-spacing",
202
+ "paint-order",
203
+ "vector-effect",
204
+ "shape-rendering",
205
+ "text-rendering",
206
+ "image-rendering",
207
+ "color-interpolation",
208
+ "color-interpolation-filters",
209
+ "enable-background",
210
+ "startoffset",
211
+ "method",
212
+ "spacing",
213
+ "requiredfeatures",
214
+ "systemlanguage",
215
+ "role",
216
+ "aria-label",
217
+ "aria-labelledby",
218
+ "aria-hidden",
219
+ "focusable",
220
+ "tabindex"
221
+ ]);
222
+ var TAG_RE = /^<\s*(\/?)\s*([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?)\s*>$/;
223
+ var ATTRIBUTE_RE = /([^\s=/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
224
+ var decodeEntities = (value) => value.replace(
225
+ /&#x([0-9a-fA-F]+);?/g,
226
+ (_m, hex) => String.fromCodePoint(parseInt(hex, 16))
227
+ ).replace(
228
+ /&#(\d+);?/g,
229
+ (_m, dec) => String.fromCodePoint(parseInt(dec, 10))
230
+ ).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
231
+ var normaliseForSchemeCheck = (value) => decodeEntities(value).replace(/[\u0000-\u0020\u007f-\u00a0\u200b-\u200f\u2028\u2029\ufeff]/g, "").toLowerCase();
232
+ var DANGEROUS_SCHEME_RE = /^(javascript|vbscript|livescript|mocha|data:text\/html|data:application)/;
233
+ var isDangerousValue = (value) => {
234
+ const normalised = normaliseForSchemeCheck(value);
235
+ return DANGEROUS_SCHEME_RE.test(normalised) || normalised.includes("javascript:") || normalised.includes("vbscript:");
236
+ };
237
+ var SAFE_DATA_IMAGE_RE = /^data:image\/(png|jpeg|jpg|gif|webp);base64,[a-z0-9+/=]+$/;
238
+ var isAcceptableHref = (value, element) => {
239
+ const normalised = normaliseForSchemeCheck(value);
240
+ if (normalised.startsWith("#")) return true;
241
+ return element === "image" && SAFE_DATA_IMAGE_RE.test(normalised);
242
+ };
243
+ var LOCAL_URL_RE = /url\s*\(\s*(['"]?)#[^)'"]*\1\s*\)/gi;
244
+ var STYLE_ESCAPE_RE = /url\s*\(|@import|expression\s*\(|-moz-binding|behavior\s*:/i;
245
+ var isSafeStyle = (value) => !STYLE_ESCAPE_RE.test(decodeEntities(value).replace(LOCAL_URL_RE, ""));
246
+ var tokenize = (input) => {
247
+ const tokens = [];
248
+ let index = 0;
249
+ const length = input.length;
250
+ const pushSpecial = (end, terminator) => {
251
+ const close = input.indexOf(terminator, end);
252
+ const stop = close === -1 ? length : close + terminator.length;
253
+ tokens.push({ kind: "special", raw: input.slice(index, stop) });
254
+ index = stop;
255
+ };
256
+ while (index < length) {
257
+ const open = input.indexOf("<", index);
258
+ if (open === -1) {
259
+ tokens.push({ kind: "text", raw: input.slice(index) });
260
+ break;
261
+ }
262
+ if (open > index) {
263
+ tokens.push({ kind: "text", raw: input.slice(index, open) });
264
+ index = open;
265
+ }
266
+ if (input.startsWith("<!--", index)) {
267
+ pushSpecial(index + 4, "-->");
268
+ continue;
269
+ }
270
+ if (input.startsWith("<![CDATA[", index)) {
271
+ pushSpecial(index + 9, "]]>");
272
+ continue;
273
+ }
274
+ if (input.startsWith("<?", index)) {
275
+ pushSpecial(index + 2, "?>");
276
+ continue;
277
+ }
278
+ if (input.startsWith("<!", index)) {
279
+ const bracket = input.indexOf("[", index);
280
+ const plainEnd = input.indexOf(">", index);
281
+ if (bracket !== -1 && (plainEnd === -1 || bracket < plainEnd)) {
282
+ const subsetEnd = input.indexOf("]", bracket);
283
+ pushSpecial(subsetEnd === -1 ? index + 2 : subsetEnd, ">");
284
+ } else {
285
+ pushSpecial(index + 2, ">");
286
+ }
287
+ continue;
288
+ }
289
+ let cursor = index + 1;
290
+ let quote = null;
291
+ while (cursor < length) {
292
+ const ch = input[cursor];
293
+ if (quote) {
294
+ if (ch === quote) quote = null;
295
+ } else if (ch === '"' || ch === "'") {
296
+ quote = ch;
297
+ } else if (ch === ">") {
298
+ break;
299
+ }
300
+ cursor += 1;
301
+ }
302
+ const stop = cursor >= length ? length : cursor + 1;
303
+ tokens.push({ kind: "tag", raw: input.slice(index, stop) });
304
+ index = stop;
305
+ }
306
+ return tokens;
307
+ };
308
+ var isXmlDeclaration = (raw) => /^<\?xml\b/i.test(raw);
309
+ var cleanAttributes = (element, rawAttributes) => {
310
+ let removed = 0;
311
+ let dropElement = false;
312
+ const kept = [];
313
+ ATTRIBUTE_RE.lastIndex = 0;
314
+ let match;
315
+ while ((match = ATTRIBUTE_RE.exec(rawAttributes)) !== null) {
316
+ const [whole, name, dq, sq, bare] = match;
317
+ if (!name || name === "/") continue;
318
+ const lower = name.toLowerCase();
319
+ const value = dq ?? sq ?? bare ?? "";
320
+ if (lower.startsWith("on")) {
321
+ removed += 1;
322
+ continue;
323
+ }
324
+ const isNamespaceDeclaration = lower === "xmlns" || lower.startsWith("xmlns:");
325
+ const isData = lower.startsWith("data-");
326
+ if (!isNamespaceDeclaration && !isData && !ALLOWED_ATTRIBUTES.has(lower)) {
327
+ removed += 1;
328
+ continue;
329
+ }
330
+ if (isDangerousValue(value)) {
331
+ removed += 1;
332
+ continue;
333
+ }
334
+ if (lower === "href" || lower === "xlink:href") {
335
+ if (!isAcceptableHref(value, element)) {
336
+ removed += 1;
337
+ if (element === "image" || element === "use") dropElement = true;
338
+ continue;
339
+ }
340
+ }
341
+ if (lower === "style" && !isSafeStyle(value)) {
342
+ removed += 1;
343
+ continue;
344
+ }
345
+ kept.push(whole.trim());
346
+ }
347
+ return {
348
+ attributes: kept.length ? ` ${kept.join(" ")}` : "",
349
+ removed,
350
+ dropElement
351
+ };
352
+ };
353
+ var STYLE_OPEN_MARKER = "\0style-open\0";
354
+ var sanitizeSvg = (input) => {
355
+ const tokens = tokenize(input);
356
+ const out = [];
357
+ let removed = 0;
358
+ let isSvg = false;
359
+ let dropDepth = 0;
360
+ const dropStack = [];
361
+ let styleBuffer = null;
362
+ for (const token of tokens) {
363
+ if (token.kind === "special") {
364
+ if (isXmlDeclaration(token.raw) && dropDepth === 0) {
365
+ out.push(token.raw);
366
+ } else {
367
+ removed += 1;
368
+ }
369
+ continue;
370
+ }
371
+ if (token.kind === "text") {
372
+ if (dropDepth > 0) continue;
373
+ if (styleBuffer) {
374
+ styleBuffer.push(token.raw);
375
+ } else {
376
+ out.push(token.raw);
377
+ }
378
+ continue;
379
+ }
380
+ const parsed = TAG_RE.exec(token.raw);
381
+ if (!parsed) {
382
+ removed += 1;
383
+ continue;
384
+ }
385
+ const [, closing, rawName, rawAttributes, selfClosingSlash] = parsed;
386
+ const name = rawName.toLowerCase();
387
+ const isClosing = closing === "/";
388
+ const isSelfClosing = selfClosingSlash === "/";
389
+ if (name === "svg") isSvg = true;
390
+ if (dropDepth > 0) {
391
+ if (isClosing) {
392
+ if (dropStack[dropStack.length - 1] === name) {
393
+ dropStack.pop();
394
+ dropDepth -= 1;
395
+ }
396
+ } else if (!isSelfClosing) {
397
+ dropStack.push(name);
398
+ dropDepth += 1;
399
+ }
400
+ continue;
401
+ }
402
+ if (isClosing) {
403
+ if (name === "style" && styleBuffer) {
404
+ const css = styleBuffer.join("");
405
+ styleBuffer = null;
406
+ if (isSafeStyle(css)) {
407
+ out.push(css, token.raw);
408
+ } else {
409
+ const openIndex = out.lastIndexOf(STYLE_OPEN_MARKER);
410
+ if (openIndex !== -1) out.splice(openIndex);
411
+ removed += 1;
412
+ }
413
+ continue;
414
+ }
415
+ if (ALLOWED_ELEMENTS.has(name)) out.push(token.raw);
416
+ continue;
417
+ }
418
+ if (!ALLOWED_ELEMENTS.has(name)) {
419
+ removed += 1;
420
+ if (!isSelfClosing) {
421
+ dropStack.push(name);
422
+ dropDepth += 1;
423
+ }
424
+ continue;
425
+ }
426
+ const cleaned = cleanAttributes(name, rawAttributes);
427
+ removed += cleaned.removed;
428
+ if (cleaned.dropElement) {
429
+ removed += 1;
430
+ if (!isSelfClosing) {
431
+ dropStack.push(name);
432
+ dropDepth += 1;
433
+ }
434
+ continue;
435
+ }
436
+ const rebuilt = `<${rawName}${cleaned.attributes}${isSelfClosing ? "/" : ""}>`;
437
+ if (name === "style" && !isSelfClosing) {
438
+ out.push(STYLE_OPEN_MARKER, rebuilt);
439
+ styleBuffer = [];
440
+ continue;
441
+ }
442
+ out.push(rebuilt);
443
+ }
444
+ if (styleBuffer) {
445
+ const openIndex = out.lastIndexOf(STYLE_OPEN_MARKER);
446
+ if (openIndex !== -1) out.splice(openIndex);
447
+ removed += 1;
448
+ }
449
+ return {
450
+ svg: out.filter((chunk) => chunk !== STYLE_OPEN_MARKER).join(""),
451
+ removed,
452
+ isSvg
453
+ };
454
+ };
455
+
456
+ // src/custom-chart/types.ts
457
+ var MAX_MANIFEST_BYTES = 32 * 1024;
458
+ var MAX_BUNDLE_JS_BYTES = 2 * 1024 * 1024;
459
+ var MAX_BUNDLE_CSS_BYTES = 256 * 1024;
460
+ var MAX_ICON_BYTES = 16 * 1024;
461
+ var MAX_SLOTS = 16;
462
+ var MAX_OPTIONS = 32;
463
+
464
+ // src/custom-chart/icon.ts
465
+ var UNSAFE = "bundle.icon.unsafe";
466
+ var RULES = [
467
+ { pattern: /<\s*script\b/i, message: "Remove every <script> element from icon.svg." },
468
+ { pattern: /<\s*foreignObject\b/i, message: "Remove every <foreignObject> element from icon.svg." },
469
+ { pattern: /<\s*image\b/i, message: "Remove every <image> element from icon.svg. Draw with paths and shapes." },
470
+ { pattern: /[\s/]on[a-z]+\s*=/i, message: "Remove every on*= event attribute from icon.svg." },
471
+ { pattern: /<!--/, message: "Remove every <!-- comment --> from icon.svg." },
472
+ { pattern: /<!(?:ENTITY|DOCTYPE)\b/i, message: "Remove the DOCTYPE and entity declarations from icon.svg." },
473
+ { pattern: /<!\[CDATA\[/, message: "Remove every CDATA section from icon.svg." },
474
+ { pattern: /(?:xlink:)?href\s*=\s*["']\s*(?!#)/i, message: "Every href in icon.svg must start with # (no external or data references)." },
475
+ { pattern: /javascript\s*:/i, message: "Remove every javascript: reference from icon.svg." },
476
+ { pattern: /@import|expression\s*\(|-moz-binding/i, message: "Remove @import, expression() and -moz-binding from icon.svg styles." },
477
+ { pattern: /url\s*\(\s*['"]?\s*(?!#)/i, message: "Every url() in icon.svg must point at a same-document #id." }
478
+ ];
479
+ var GENERIC_UNSAFE = "icon.svg must use only plain SVG drawing elements and attributes: no script, event handlers, foreignObject, image, animation, external hrefs, url() styles, comments, DOCTYPE or unknown attributes.";
480
+ var XML_DECLARATION = /^\s*<\?xml[^>]*\?>\s*/i;
481
+ function isSingleSvgRoot(svg) {
482
+ const body = svg.replace(/^/, "").replace(XML_DECLARATION, "").trim();
483
+ return /^<svg[\s>][\s\S]*<\/svg>$/i.test(body) || /^<svg\b[^>]*\/>$/i.test(body);
484
+ }
485
+ function validateIcon(svg, path = "/icon.svg") {
486
+ if (typeof svg !== "string" || svg.trim().length === 0) {
487
+ return [{ code: "bundle.icon.required", path, message: "icon.svg is required and must not be empty." }];
488
+ }
489
+ if (Buffer.byteLength(svg, "utf8") > MAX_ICON_BYTES) {
490
+ return [{ code: "bundle.icon.size", path, message: `icon.svg must be at most ${MAX_ICON_BYTES} bytes.` }];
491
+ }
492
+ const cleaned = sanitizeSvg(svg);
493
+ if (!cleaned.isSvg || !isSingleSvgRoot(cleaned.svg)) {
494
+ return [{ code: "bundle.icon.notSvg", path, message: "icon.svg must be one <svg> element with nothing after it." }];
495
+ }
496
+ if (cleaned.removed > 0 || /<\s*image\b/i.test(svg)) {
497
+ const rule = RULES.find((candidate) => candidate.pattern.test(svg));
498
+ return [{ code: UNSAFE, path, message: rule ? rule.message : GENERIC_UNSAFE }];
499
+ }
500
+ return [];
501
+ }
502
+ function sanitizeIcon(svg) {
503
+ return svg.replace(/^/, "").replace(XML_DECLARATION, "").replace(/<!--[\s\S]*?-->/g, "").trim();
504
+ }
505
+
506
+ // src/custom-chart/manifest.ts
507
+ import { z } from "zod";
508
+ var ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
509
+ var FORBIDDEN_IDS = ["constructor", "prototype", "__proto__"];
510
+ var LANGUAGE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
511
+ var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
512
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
513
+ var FIELD_TYPES = ["string", "number", "boolean"];
514
+ var MAX_TEXT = 256;
515
+ var MAX_DESCRIPTION = 500;
516
+ var MAX_GROUP = 64;
517
+ var MAX_CHOICES = 32;
518
+ var MAX_STRING_OPTION_LENGTH = 1024;
519
+ var V1_TOP_KEYS = ["schemaVersion", "id", "name", "slots", "options"];
520
+ var V2_TOP_EXTRA_KEYS = ["version", "description", "translations"];
521
+ var V1_SLOT_KEYS = ["id", "label", "role", "types", "required"];
522
+ var V2_SLOT_EXTRA_KEYS = ["description"];
523
+ var OPTION_BASE_KEYS = ["id", "label", "type", "default"];
524
+ var OPTION_TYPE_KEYS = {
525
+ boolean: [],
526
+ number: ["min", "max"],
527
+ string: ["maxLength"],
528
+ select: ["choices"],
529
+ color: []
530
+ };
531
+ var V2_OPTION_EXTRA_KEYS = {
532
+ boolean: ["group"],
533
+ number: ["group", "step"],
534
+ string: ["group"],
535
+ select: ["group"],
536
+ color: ["group"]
537
+ };
538
+ var TRANSLATION_KEYS = ["name", "description", "slots", "options"];
539
+ var SLOT_TRANSLATION_KEYS = ["label", "description"];
540
+ var OPTION_TRANSLATION_KEYS = ["label", "group", "choices"];
541
+ var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
542
+ var isId = (value) => typeof value === "string" && ID_PATTERN.test(value) && !FORBIDDEN_IDS.includes(value);
543
+ var isText = (value, max = MAX_TEXT) => typeof value === "string" && value.length > 0 && value.length <= max;
544
+ var isTranslationText = (value) => typeof value === "string" && value.length <= MAX_TEXT;
545
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
546
+ var jsonBytes = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
547
+ var Collector = class {
548
+ problems = [];
549
+ add(code, path, message) {
550
+ this.problems.push({ code, path, message });
551
+ }
552
+ /**
553
+ * Unknown keys report `<scope>.unknownKey`, one per key; a missing required
554
+ * key reports `missingCode(key)`. Returns false when anything was reported,
555
+ * so the caller can stop looking at a shape it cannot trust.
556
+ */
557
+ checkKeys(value, path, scope, required, optional, missingCode = (key) => `${scope}.${key}`) {
558
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
559
+ let ok = true;
560
+ for (const key of Object.keys(value)) {
561
+ if (!allowed.has(key)) {
562
+ this.add(`${scope}.unknownKey`, `${path}/${key}`, `Remove "${key}". It is not a known key here.`);
563
+ ok = false;
564
+ }
565
+ }
566
+ for (const key of required) {
567
+ if (!Object.hasOwn(value, key)) {
568
+ this.add(missingCode(key), `${path}/${key}`, `Add the required key "${key}".`);
569
+ ok = false;
570
+ }
571
+ }
572
+ return ok;
573
+ }
574
+ };
575
+ function validateSlot(c, value, path, v2) {
576
+ if (!isRecord(value)) {
577
+ c.add("manifest.slot.invalid", path, "A slot must be an object.");
578
+ return;
579
+ }
580
+ if (!c.checkKeys(value, path, "manifest.slot", V1_SLOT_KEYS, v2 ? V2_SLOT_EXTRA_KEYS : [])) return;
581
+ if (!isId(value.id)) {
582
+ c.add("manifest.slot.id", `${path}/id`, "A slot id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters.");
583
+ }
584
+ if (!isText(value.label)) {
585
+ c.add("manifest.slot.label", `${path}/label`, `A slot label is 1 to ${MAX_TEXT} characters.`);
586
+ }
587
+ const role = value.role;
588
+ if (role !== "dimension" && role !== "measure") {
589
+ c.add("manifest.slot.role", `${path}/role`, 'A slot role is "dimension" or "measure".');
590
+ }
591
+ if (typeof value.required !== "boolean") {
592
+ c.add("manifest.slot.required", `${path}/required`, "required is true or false.");
593
+ }
594
+ const types = value.types;
595
+ const typesOk = Array.isArray(types) && types.length > 0 && types.length <= 3 && new Set(types).size === types.length && types.every((type) => FIELD_TYPES.includes(type));
596
+ if (!typesOk) {
597
+ c.add("manifest.slot.types", `${path}/types`, 'types is a list of 1 to 3 distinct values from "string", "number" and "boolean".');
598
+ } else if (role === "measure" && (types.length !== 1 || types[0] !== "number")) {
599
+ c.add("manifest.slot.types", `${path}/types`, 'A measure slot accepts only ["number"].');
600
+ }
601
+ if (Object.hasOwn(value, "description") && !isText(value.description)) {
602
+ c.add("manifest.slot.description", `${path}/description`, `A slot description is 1 to ${MAX_TEXT} characters.`);
603
+ }
604
+ }
605
+ function validateOption(c, value, path, v2) {
606
+ if (!isRecord(value)) {
607
+ c.add("manifest.option.invalid", path, "An option must be an object.");
608
+ return;
609
+ }
610
+ const id = value.id;
611
+ if (typeof id === "string" && (id === "theme" || id.startsWith("theme."))) {
612
+ c.add("manifest.option.reserved", `${path}/id`, 'The option id "theme" (and any id starting with "theme.") is reserved for the host theme.');
613
+ return;
614
+ }
615
+ const type = typeof value.type === "string" ? value.type : "";
616
+ const typeKeys = Object.hasOwn(OPTION_TYPE_KEYS, type) ? OPTION_TYPE_KEYS[type] : void 0;
617
+ if (!typeKeys || type === "color" && !v2) {
618
+ c.add(
619
+ "manifest.option.type",
620
+ `${path}/type`,
621
+ v2 ? 'An option type is one of "boolean", "number", "string", "select", "color".' : 'An option type is one of "boolean", "number", "string", "select" (color needs schemaVersion 2).'
622
+ );
623
+ return;
624
+ }
625
+ if (!c.checkKeys(value, path, "manifest.option", [...OPTION_BASE_KEYS, ...typeKeys], v2 ? V2_OPTION_EXTRA_KEYS[type] : [])) return;
626
+ if (!isId(id)) {
627
+ c.add("manifest.option.id", `${path}/id`, "An option id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters.");
628
+ }
629
+ if (!isText(value.label)) {
630
+ c.add("manifest.option.label", `${path}/label`, `An option label is 1 to ${MAX_TEXT} characters.`);
631
+ }
632
+ if (Object.hasOwn(value, "group") && !isText(value.group, MAX_GROUP)) {
633
+ c.add("manifest.option.group", `${path}/group`, `An option group is 1 to ${MAX_GROUP} characters.`);
634
+ }
635
+ switch (type) {
636
+ case "boolean":
637
+ if (typeof value.default !== "boolean") {
638
+ c.add("manifest.option.default", `${path}/default`, "A boolean default is true or false.");
639
+ }
640
+ break;
641
+ case "number": {
642
+ const rangeOk = isFiniteNumber(value.min) && isFiniteNumber(value.max) && value.min <= value.max;
643
+ if (!rangeOk) {
644
+ c.add("manifest.option.range", `${path}/min`, "Set finite min and max with min <= max.");
645
+ break;
646
+ }
647
+ const min = value.min;
648
+ const max = value.max;
649
+ if (!isFiniteNumber(value.default) || value.default < min || value.default > max) {
650
+ c.add("manifest.option.default", `${path}/default`, "A number default is a finite number between min and max.");
651
+ }
652
+ if (Object.hasOwn(value, "step") && (!isFiniteNumber(value.step) || value.step <= 0 || value.step > max - min)) {
653
+ c.add("manifest.option.step", `${path}/step`, "step is a positive number no larger than max - min.");
654
+ }
655
+ break;
656
+ }
657
+ case "string": {
658
+ const maxLength = value.maxLength;
659
+ if (!isFiniteNumber(maxLength) || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_STRING_OPTION_LENGTH) {
660
+ c.add("manifest.option.maxLength", `${path}/maxLength`, `A string option needs an integer "maxLength" from 1 to ${MAX_STRING_OPTION_LENGTH}.`);
661
+ break;
662
+ }
663
+ if (typeof value.default !== "string" || value.default.length > maxLength) {
664
+ c.add("manifest.option.default", `${path}/default`, "A string default is a string no longer than maxLength.");
665
+ }
666
+ break;
667
+ }
668
+ case "select": {
669
+ const choices = value.choices;
670
+ const choicesOk = Array.isArray(choices) && choices.length > 0 && choices.length <= MAX_CHOICES && choices.every(
671
+ (choice) => isRecord(choice) && Object.keys(choice).length === 2 && isId(choice.value) && isText(choice.label)
672
+ ) && new Set(choices.map((choice) => choice.value)).size === choices.length;
673
+ if (!choicesOk) {
674
+ c.add("manifest.option.choices", `${path}/choices`, `choices is a list of 1 to ${MAX_CHOICES} distinct { value, label } pairs; a value follows the id rule.`);
675
+ break;
676
+ }
677
+ if (!choices.some((choice) => choice.value === value.default)) {
678
+ c.add("manifest.option.default", `${path}/default`, "A select default is the value of one of its choices.");
679
+ }
680
+ break;
681
+ }
682
+ case "color":
683
+ if (typeof value.default !== "string" || !COLOR_PATTERN.test(value.default)) {
684
+ c.add("manifest.option.color", `${path}/default`, 'A color default is a 6 digit hex colour like "#3366cc".');
685
+ }
686
+ break;
687
+ default:
688
+ break;
689
+ }
690
+ }
691
+ function validateTranslations(c, value, slots, options) {
692
+ const path = "/translations";
693
+ if (!isRecord(value)) {
694
+ c.add("manifest.translations.invalid", path, "translations is an object keyed by language code.");
695
+ return;
696
+ }
697
+ for (const [language, entry] of Object.entries(value)) {
698
+ const langPath = `${path}/${language}`;
699
+ if (!LANGUAGE_PATTERN.test(language)) {
700
+ c.add("manifest.translations.language", langPath, `"${language}" is not a language code like "fr" or "pt-BR".`);
701
+ continue;
702
+ }
703
+ if (!isRecord(entry)) {
704
+ c.add("manifest.translations.invalid", langPath, "A translation is an object with optional name, description, slots and options.");
705
+ continue;
706
+ }
707
+ if (!c.checkKeys(entry, langPath, "manifest.translations", [], TRANSLATION_KEYS)) continue;
708
+ for (const key of ["name", "description"]) {
709
+ if (Object.hasOwn(entry, key) && !isTranslationText(entry[key])) {
710
+ c.add("manifest.translations.text", `${langPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
711
+ }
712
+ }
713
+ if (Object.hasOwn(entry, "slots")) {
714
+ const entrySlots = entry.slots;
715
+ if (!isRecord(entrySlots)) {
716
+ c.add("manifest.translations.invalid", `${langPath}/slots`, "slots is an object keyed by slot id.");
717
+ } else {
718
+ for (const [slotId, slotEntry] of Object.entries(entrySlots)) {
719
+ const slotPath = `${langPath}/slots/${slotId}`;
720
+ if (!slots.some((slot) => slot.id === slotId)) {
721
+ c.add("manifest.translations.slot", slotPath, `"${slotId}" is not a slot id in this manifest.`);
722
+ continue;
723
+ }
724
+ if (!isRecord(slotEntry)) {
725
+ c.add("manifest.translations.invalid", slotPath, "A slot translation is an object with optional label and description.");
726
+ continue;
727
+ }
728
+ if (!c.checkKeys(slotEntry, slotPath, "manifest.translations", [], SLOT_TRANSLATION_KEYS)) continue;
729
+ for (const key of ["label", "description"]) {
730
+ if (Object.hasOwn(slotEntry, key) && !isTranslationText(slotEntry[key])) {
731
+ c.add("manifest.translations.text", `${slotPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
732
+ }
733
+ }
734
+ }
735
+ }
736
+ }
737
+ if (Object.hasOwn(entry, "options")) {
738
+ const entryOptions = entry.options;
739
+ if (!isRecord(entryOptions)) {
740
+ c.add("manifest.translations.invalid", `${langPath}/options`, "options is an object keyed by option id.");
741
+ } else {
742
+ for (const [optionId, optionEntry] of Object.entries(entryOptions)) {
743
+ const optionPath = `${langPath}/options/${optionId}`;
744
+ const option = options.find((item) => item.id === optionId);
745
+ if (!option) {
746
+ c.add("manifest.translations.option", optionPath, `"${optionId}" is not an option id in this manifest.`);
747
+ continue;
748
+ }
749
+ if (!isRecord(optionEntry)) {
750
+ c.add("manifest.translations.invalid", optionPath, "An option translation is an object with optional label, group and choices.");
751
+ continue;
752
+ }
753
+ if (!c.checkKeys(optionEntry, optionPath, "manifest.translations", [], OPTION_TRANSLATION_KEYS)) continue;
754
+ for (const key of ["label", "group"]) {
755
+ if (Object.hasOwn(optionEntry, key) && !isTranslationText(optionEntry[key])) {
756
+ c.add("manifest.translations.text", `${optionPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
757
+ }
758
+ }
759
+ if (!Object.hasOwn(optionEntry, "choices")) continue;
760
+ const choices = optionEntry.choices;
761
+ if (!isRecord(choices)) {
762
+ c.add("manifest.translations.invalid", `${optionPath}/choices`, "choices is an object keyed by choice value.");
763
+ continue;
764
+ }
765
+ for (const [choiceValue, label] of Object.entries(choices)) {
766
+ const choicePath = `${optionPath}/choices/${choiceValue}`;
767
+ if (option.type !== "select" || !(option.choices ?? []).some((choice) => choice.value === choiceValue)) {
768
+ c.add("manifest.translations.choice", choicePath, `"${choiceValue}" is not a choice of option "${optionId}".`);
769
+ } else if (!isTranslationText(label)) {
770
+ c.add("manifest.translations.text", choicePath, `A translated string is at most ${MAX_TEXT} characters.`);
771
+ }
772
+ }
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+ function validateManifest(value) {
779
+ const c = new Collector();
780
+ if (!isRecord(value)) {
781
+ c.add("manifest.invalid", "", "manifest.json must be a JSON object.");
782
+ return c.problems;
783
+ }
784
+ const schemaVersion = value.schemaVersion;
785
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
786
+ c.add("manifest.schemaVersion.unsupported", "/schemaVersion", "schemaVersion must be 1 or 2.");
787
+ return c.problems;
788
+ }
789
+ const v2 = schemaVersion === 2;
790
+ if (!c.checkKeys(value, "", "manifest", V1_TOP_KEYS, v2 ? V2_TOP_EXTRA_KEYS : [], (key) => `manifest.${key}.required`)) {
791
+ return c.problems;
792
+ }
793
+ if (!isId(value.id)) {
794
+ c.add("manifest.id.invalid", "/id", 'id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters. It becomes the chart type "custom:<id>".');
795
+ }
796
+ if (!isText(value.name)) {
797
+ c.add("manifest.name.invalid", "/name", `name is 1 to ${MAX_TEXT} characters.`);
798
+ }
799
+ if (Object.hasOwn(value, "version") && (typeof value.version !== "string" || !SEMVER_PATTERN.test(value.version))) {
800
+ c.add("manifest.version.invalid", "/version", 'version is a semver string like "1.2.0".');
801
+ }
802
+ if (Object.hasOwn(value, "description") && !isText(value.description, MAX_DESCRIPTION)) {
803
+ c.add("manifest.description.invalid", "/description", `description is 1 to ${MAX_DESCRIPTION} characters.`);
804
+ }
805
+ const slots = value.slots;
806
+ if (!Array.isArray(slots)) {
807
+ c.add("manifest.slots.invalid", "/slots", "slots must be an array.");
808
+ } else if (slots.length === 0) {
809
+ c.add("manifest.slots.empty", "/slots", "Declare at least one slot.");
810
+ } else if (slots.length > MAX_SLOTS) {
811
+ c.add("manifest.slots.tooMany", "/slots", `Declare at most ${MAX_SLOTS} slots.`);
812
+ } else {
813
+ slots.forEach((slot, index) => validateSlot(c, slot, `/slots/${index}`, v2));
814
+ const seen = /* @__PURE__ */ new Set();
815
+ slots.forEach((slot, index) => {
816
+ if (!isRecord(slot) || typeof slot.id !== "string") return;
817
+ if (seen.has(slot.id)) {
818
+ c.add("manifest.slot.duplicate", `/slots/${index}/id`, `Slot id "${slot.id}" is used more than once.`);
819
+ }
820
+ seen.add(slot.id);
821
+ });
822
+ }
823
+ const options = value.options;
824
+ if (!Array.isArray(options)) {
825
+ c.add("manifest.options.invalid", "/options", "options must be an array (it may be empty).");
826
+ } else if (options.length > MAX_OPTIONS) {
827
+ c.add("manifest.options.tooMany", "/options", `Declare at most ${MAX_OPTIONS} options.`);
828
+ } else {
829
+ options.forEach((option, index) => validateOption(c, option, `/options/${index}`, v2));
830
+ const seen = /* @__PURE__ */ new Set();
831
+ options.forEach((option, index) => {
832
+ if (!isRecord(option) || typeof option.id !== "string") return;
833
+ if (seen.has(option.id)) {
834
+ c.add("manifest.option.duplicate", `/options/${index}/id`, `Option id "${option.id}" is used more than once.`);
835
+ }
836
+ seen.add(option.id);
837
+ });
838
+ }
839
+ if (c.problems.length === 0 && Object.hasOwn(value, "translations")) {
840
+ validateTranslations(
841
+ c,
842
+ value.translations,
843
+ slots,
844
+ options
845
+ );
846
+ }
847
+ if (c.problems.length === 0 && jsonBytes(value) > MAX_MANIFEST_BYTES) {
848
+ c.add("manifest.size", "", `manifest.json must be at most ${MAX_MANIFEST_BYTES} bytes as JSON.`);
849
+ }
850
+ return c.problems;
851
+ }
852
+ var slotSchema = z.object({
853
+ id: z.string(),
854
+ label: z.string(),
855
+ role: z.enum(["dimension", "measure"]),
856
+ types: z.array(z.enum(["string", "number", "boolean"])),
857
+ required: z.boolean(),
858
+ description: z.string().optional()
859
+ });
860
+ var optionSchema = z.object({
861
+ id: z.string(),
862
+ label: z.string(),
863
+ type: z.enum(["boolean", "number", "string", "select", "color"]),
864
+ default: z.union([z.string(), z.number(), z.boolean()]),
865
+ min: z.number().optional(),
866
+ max: z.number().optional(),
867
+ step: z.number().optional(),
868
+ maxLength: z.number().optional(),
869
+ choices: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
870
+ group: z.string().optional()
871
+ });
872
+ var manifestSchema = z.object({
873
+ schemaVersion: z.union([z.literal(1), z.literal(2)]),
874
+ id: z.string(),
875
+ name: z.string(),
876
+ version: z.string().optional(),
877
+ description: z.string().optional(),
878
+ slots: z.array(slotSchema),
879
+ options: z.array(optionSchema),
880
+ translations: z.record(z.unknown()).optional()
881
+ });
882
+ function sortKeys(value) {
883
+ if (Array.isArray(value)) return value.map((item) => sortKeys(item));
884
+ if (isRecord(value)) {
885
+ const out = {};
886
+ for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key]);
887
+ return out;
888
+ }
889
+ return value;
890
+ }
891
+
892
+ // src/custom-chart/zip.ts
893
+ import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
894
+ var BUNDLE_ZIP_FILES = ["index.js", "manifest.json", "icon.svg", "index.css"];
895
+ function writeBundleZip(files) {
896
+ const entries = {};
897
+ for (const name of BUNDLE_ZIP_FILES) {
898
+ const content = files[name];
899
+ if (typeof content === "string") entries[name] = strToU8(content);
900
+ }
901
+ return zipSync(entries, { level: 6, mtime: /* @__PURE__ */ new Date("2000-01-01T00:00:00Z") });
902
+ }
903
+
904
+ // src/custom-chart/bundle.ts
905
+ var utf8Bytes = (text) => Buffer.byteLength(text, "utf8");
906
+ function contentHash(bundle) {
907
+ const payload = JSON.stringify({
908
+ manifest: sortKeys(bundle.manifest),
909
+ bundleJs: bundle.bundleJs,
910
+ bundleCss: bundle.bundleCss ?? "",
911
+ iconSvg: bundle.iconSvg
912
+ });
913
+ return createHash("sha256").update(payload, "utf8").digest("hex");
914
+ }
915
+ function bundleSizeBytes(bundle) {
916
+ return utf8Bytes(JSON.stringify(bundle.manifest)) + utf8Bytes(bundle.bundleJs) + (bundle.bundleCss ? utf8Bytes(bundle.bundleCss) : 0) + utf8Bytes(bundle.iconSvg);
917
+ }
918
+ function validateBundle(input) {
919
+ const problems = validateManifest(input.manifest);
920
+ if (input.bundleJs !== void 0) {
921
+ if (typeof input.bundleJs !== "string") {
922
+ problems.push({ code: "bundle.js.type", path: "/bundleJs", message: "bundleJs must be a string." });
923
+ } else if (input.bundleJs.trim().length === 0) {
924
+ problems.push({ code: "bundle.js.empty", path: "/bundleJs", message: "bundleJs must contain JavaScript." });
925
+ } else if (utf8Bytes(input.bundleJs) > MAX_BUNDLE_JS_BYTES) {
926
+ problems.push({
927
+ code: "bundle.js.size",
928
+ path: "/bundleJs",
929
+ message: `bundleJs must be at most ${MAX_BUNDLE_JS_BYTES} bytes (2 MiB) as UTF-8. Drop or shrink a dependency.`
930
+ });
931
+ }
932
+ }
933
+ if (input.bundleCss !== void 0) {
934
+ if (typeof input.bundleCss !== "string") {
935
+ problems.push({ code: "bundle.css.type", path: "/bundleCss", message: "bundleCss must be a string when present." });
936
+ } else if (utf8Bytes(input.bundleCss) > MAX_BUNDLE_CSS_BYTES) {
937
+ problems.push({
938
+ code: "bundle.css.size",
939
+ path: "/bundleCss",
940
+ message: `bundleCss must be at most ${MAX_BUNDLE_CSS_BYTES} bytes (256 KiB).`
941
+ });
942
+ }
943
+ }
944
+ if (input.iconSvg !== void 0) problems.push(...validateIcon(input.iconSvg, "/iconSvg"));
945
+ return problems;
946
+ }
947
+ function readProject(dirInput) {
948
+ const dir = resolve2(dirInput);
949
+ const problems = [];
950
+ const project = { dir, manifest: void 0, problems };
951
+ const manifestPath = join2(dir, "manifest.json");
952
+ if (!existsSync2(manifestPath)) {
953
+ problems.push({ code: "project.manifest.missing", path: "/manifest.json", message: `No manifest.json in ${dir}.` });
954
+ } else {
955
+ project.manifestText = readFileSync2(manifestPath, "utf8");
956
+ try {
957
+ project.manifest = JSON.parse(project.manifestText);
958
+ } catch (err) {
959
+ problems.push({
960
+ code: "project.manifest.parse",
961
+ path: "/manifest.json",
962
+ message: `manifest.json is not valid JSON: ${err.message}`
963
+ });
964
+ }
965
+ }
966
+ const entry = ["chart.ts", "chart.js"].map((name) => join2(dir, name)).find((path) => existsSync2(path));
967
+ if (!entry) {
968
+ problems.push({ code: "project.chart.missing", path: "/chart.ts", message: `No chart.ts in ${dir}. It must export render(ctx).` });
969
+ } else {
970
+ project.chartEntry = entry;
971
+ }
972
+ const cssPath = join2(dir, "chart.css");
973
+ if (existsSync2(cssPath)) project.css = readFileSync2(cssPath, "utf8");
974
+ const iconPath = join2(dir, "icon.svg");
975
+ if (!existsSync2(iconPath)) {
976
+ problems.push({ code: "bundle.icon.required", path: "/icon.svg", message: `No icon.svg in ${dir}.` });
977
+ } else {
978
+ project.icon = readFileSync2(iconPath, "utf8");
979
+ }
980
+ const samplePath = join2(dir, "sample-data.json");
981
+ if (existsSync2(samplePath)) {
982
+ try {
983
+ project.sampleData = JSON.parse(readFileSync2(samplePath, "utf8"));
984
+ } catch (err) {
985
+ problems.push({
986
+ code: "project.sampleData.parse",
987
+ path: "/sample-data.json",
988
+ message: `sample-data.json is not valid JSON: ${err.message}`
989
+ });
990
+ }
991
+ }
992
+ return project;
993
+ }
994
+ function validateProject(dir) {
995
+ const project = readProject(dir);
996
+ const problems = [...project.problems];
997
+ if (project.manifest !== void 0) problems.push(...validateManifest(project.manifest));
998
+ if (project.icon !== void 0) problems.push(...validateIcon(sanitizeIcon(project.icon)));
999
+ if (project.css !== void 0 && utf8Bytes(project.css) > MAX_BUNDLE_CSS_BYTES) {
1000
+ problems.push({ code: "bundle.css.size", path: "/chart.css", message: `chart.css must be at most ${MAX_BUNDLE_CSS_BYTES} bytes.` });
1001
+ }
1002
+ return { ok: problems.length === 0, problems, project };
1003
+ }
1004
+ var DIST_FILES = ["index.js", "manifest.json", "icon.svg", "index.css", "bundle.zip"];
1005
+ function insideProjectOnly(dir) {
1006
+ const root = dir.endsWith(sep) ? dir : `${dir}${sep}`;
1007
+ return {
1008
+ name: "databrain-inside-project-only",
1009
+ setup(build) {
1010
+ build.onResolve({ filter: /.*/ }, async (args) => {
1011
+ if (args.kind === "entry-point" || args.pluginData?.databrainChecked) return void 0;
1012
+ const resolved = await build.resolve(args.path, {
1013
+ kind: args.kind,
1014
+ resolveDir: args.resolveDir,
1015
+ importer: args.importer,
1016
+ pluginData: { databrainChecked: true }
1017
+ });
1018
+ if (resolved.errors.length > 0) return { errors: resolved.errors };
1019
+ if (resolved.external || resolved.path.startsWith(root)) return resolved;
1020
+ return {
1021
+ errors: [
1022
+ {
1023
+ text: `"${args.path}" is outside the chart project. Only files inside ${dir} can be imported.`
1024
+ }
1025
+ ]
1026
+ };
1027
+ });
1028
+ }
1029
+ };
1030
+ }
1031
+ async function buildProject(dir, options = {}) {
1032
+ const validated = validateProject(dir);
1033
+ const { project } = validated;
1034
+ const problems = [...validated.problems];
1035
+ if (!project.chartEntry || project.manifest === void 0 || problems.length > 0) {
1036
+ return { ok: false, problems, written: [] };
1037
+ }
1038
+ const manifest = project.manifest;
1039
+ const common = {
1040
+ plugins: [insideProjectOnly(project.dir)],
1041
+ entryPoints: [project.chartEntry],
1042
+ absWorkingDir: project.dir,
1043
+ bundle: true,
1044
+ target: "es2020",
1045
+ platform: "browser",
1046
+ write: false,
1047
+ legalComments: "none",
1048
+ logLevel: "silent",
1049
+ charset: "utf8"
1050
+ };
1051
+ let moduleJs;
1052
+ try {
1053
+ const shape = await esbuild({ ...common, format: "esm", metafile: true });
1054
+ const exported = Object.values(shape.metafile.outputs)[0]?.exports ?? [];
1055
+ if (!exported.includes("render")) {
1056
+ problems.push({
1057
+ code: "bundle.build.noRender",
1058
+ path: "/chart.ts",
1059
+ message: "chart.ts must be an ES module that exports a function named render: `export function render(ctx) { ... }`. module.exports and default exports are not read."
1060
+ });
1061
+ return { ok: false, problems, written: [] };
1062
+ }
1063
+ const result = await esbuild({
1064
+ ...common,
1065
+ format: "iife",
1066
+ globalName: "__dbnCustomChart",
1067
+ minify: true
1068
+ });
1069
+ moduleJs = result.outputFiles.map((file) => file.text).join("\n");
1070
+ } catch (err) {
1071
+ const errors = err.errors;
1072
+ if (Array.isArray(errors) && errors.length > 0) {
1073
+ for (const error of errors) {
1074
+ const where = error.location ? `${error.location.file ?? "chart.ts"}:${error.location.line ?? 0}` : "chart.ts";
1075
+ problems.push({ code: "bundle.build.failed", path: `/${where}`, message: error.text });
1076
+ }
1077
+ } else {
1078
+ problems.push({ code: "bundle.build.failed", path: "/chart.ts", message: err.message });
1079
+ }
1080
+ return { ok: false, problems, written: [] };
1081
+ }
1082
+ const shim = readRuntimeShim();
1083
+ const slotIds = manifest.slots.map((slot) => slot.id);
1084
+ const globals = `globalThis.__dbnCustomChartSlots=${JSON.stringify(slotIds)};
1085
+ `;
1086
+ const bundleJs = `${shim.trimEnd()}
1087
+ ${globals}${moduleJs.trimEnd()}
1088
+ globalThis.__dbnCustomChart=__dbnCustomChart;
1089
+ `;
1090
+ const iconSvg = sanitizeIcon(project.icon);
1091
+ const bundleCss = project.css;
1092
+ problems.push(...validateBundle({ manifest, bundleJs, bundleCss, iconSvg }));
1093
+ if (problems.length > 0) return { ok: false, problems, written: [] };
1094
+ const bundle = { manifest, bundleJs, bundleCss, iconSvg };
1095
+ const outputs = {
1096
+ ...bundle,
1097
+ contentHash: contentHash(bundle),
1098
+ sizeBytes: bundleSizeBytes(bundle)
1099
+ };
1100
+ const written = [];
1101
+ if (options.write !== false) {
1102
+ const dist = join2(project.dir, "dist");
1103
+ mkdirSync(dist, { recursive: true });
1104
+ const manifestJson = JSON.stringify(manifest, null, 2) + "\n";
1105
+ const write = (name, content) => {
1106
+ const path = join2(dist, name);
1107
+ writeFileSync(path, content);
1108
+ written.push(path);
1109
+ };
1110
+ write("index.js", bundleJs);
1111
+ write("manifest.json", manifestJson);
1112
+ write("icon.svg", iconSvg);
1113
+ if (bundleCss !== void 0) write("index.css", bundleCss);
1114
+ write(
1115
+ "bundle.zip",
1116
+ writeBundleZip({
1117
+ "index.js": bundleJs,
1118
+ "manifest.json": manifestJson,
1119
+ "icon.svg": iconSvg,
1120
+ ...bundleCss !== void 0 ? { "index.css": bundleCss } : {}
1121
+ })
1122
+ );
1123
+ }
1124
+ return { ok: true, problems: [], outputs, written };
1125
+ }
1126
+ function readDist(dir) {
1127
+ const dist = join2(resolve2(dir), "dist");
1128
+ const problems = [];
1129
+ const need = ["index.js", "manifest.json", "icon.svg"];
1130
+ for (const name of need) {
1131
+ if (!existsSync2(join2(dist, name))) {
1132
+ problems.push({ code: "project.dist.missing", path: `/dist/${name}`, message: `dist/${name} is missing. Run databrain custom-chart build first.` });
1133
+ }
1134
+ }
1135
+ if (problems.length > 0) return { problems };
1136
+ let manifest;
1137
+ try {
1138
+ manifest = JSON.parse(readFileSync2(join2(dist, "manifest.json"), "utf8"));
1139
+ } catch (err) {
1140
+ return { problems: [{ code: "project.dist.parse", path: "/dist/manifest.json", message: err.message }] };
1141
+ }
1142
+ const manifestProblems = validateManifest(manifest);
1143
+ if (manifestProblems.length > 0) return { problems: manifestProblems };
1144
+ const cssPath = join2(dist, "index.css");
1145
+ return {
1146
+ problems,
1147
+ bundle: {
1148
+ manifest,
1149
+ bundleJs: readFileSync2(join2(dist, "index.js"), "utf8"),
1150
+ bundleCss: existsSync2(cssPath) ? readFileSync2(cssPath, "utf8") : void 0,
1151
+ iconSvg: readFileSync2(join2(dist, "icon.svg"), "utf8")
1152
+ }
1153
+ };
1154
+ }
1155
+ async function buildSource(input) {
1156
+ const problems = validateManifest(input.manifest);
1157
+ if (problems.length) return { ok: false, problems, written: [] };
1158
+ const manifest = input.manifest;
1159
+ try {
1160
+ const common = {
1161
+ stdin: { contents: input.chartTs, loader: "ts", sourcefile: "chart.ts" },
1162
+ bundle: true,
1163
+ platform: "browser",
1164
+ target: "es2020",
1165
+ write: false,
1166
+ legalComments: "none",
1167
+ logLevel: "silent",
1168
+ plugins: [{ name: "no-host-imports", setup(build) {
1169
+ build.onResolve({ filter: /.*/ }, (args) => ({ errors: [{ text: `Import "${args.path}" is unavailable in hosted chart source. Use browser APIs or pass a prebuilt bundle.` }] }));
1170
+ } }]
1171
+ };
1172
+ let shape = await esbuild({ ...common, format: "esm", metafile: true });
1173
+ if (!Object.values(shape.metafile.outputs)[0]?.exports.includes("render")) {
1174
+ common.stdin.contents += "\nexport { render };";
1175
+ shape = await esbuild({ ...common, format: "esm", metafile: true });
1176
+ }
1177
+ if (!Object.values(shape.metafile.outputs)[0]?.exports.includes("render")) {
1178
+ return { ok: false, problems: [{ code: "bundle.build.noRender", path: "/chart.ts", message: "Export function render(ctx)." }], written: [] };
1179
+ }
1180
+ const built = await esbuild({ ...common, format: "iife", globalName: "__dbnCustomChart", minify: true });
1181
+ const bundle = {
1182
+ manifest,
1183
+ bundleCss: input.bundleCss,
1184
+ iconSvg: sanitizeIcon(input.iconSvg),
1185
+ bundleJs: `${readRuntimeShim().trimEnd()}
1186
+ globalThis.__dbnCustomChartSlots=${JSON.stringify(manifest.slots.map((s) => s.id))};
1187
+ ${built.outputFiles.map((f) => f.text).join("\n")}
1188
+ globalThis.__dbnCustomChart=__dbnCustomChart;
1189
+ `
1190
+ };
1191
+ const errors = validateBundle(bundle);
1192
+ return errors.length ? { ok: false, problems: errors, written: [] } : {
1193
+ ok: true,
1194
+ problems: [],
1195
+ written: [],
1196
+ outputs: { ...bundle, contentHash: contentHash(bundle), sizeBytes: bundleSizeBytes(bundle) }
1197
+ };
1198
+ } catch (error) {
1199
+ return { ok: false, problems: [{ code: "bundle.build.failed", path: "/chart.ts", message: error.message }], written: [] };
1200
+ }
1201
+ }
1202
+
1203
+ export {
1204
+ readTemplateFiles,
1205
+ readCustomChartBrowserRuntime,
1206
+ MAX_MANIFEST_BYTES,
1207
+ MAX_BUNDLE_JS_BYTES,
1208
+ MAX_BUNDLE_CSS_BYTES,
1209
+ MAX_ICON_BYTES,
1210
+ isId,
1211
+ validateManifest,
1212
+ slotSchema,
1213
+ optionSchema,
1214
+ contentHash,
1215
+ bundleSizeBytes,
1216
+ validateBundle,
1217
+ readProject,
1218
+ validateProject,
1219
+ DIST_FILES,
1220
+ buildProject,
1221
+ readDist,
1222
+ buildSource
1223
+ };
1224
+ //# sourceMappingURL=chunk-Q5EYDKSA.js.map