@xanots/xanoscript 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js ADDED
@@ -0,0 +1,483 @@
1
+ import {
2
+ emitWorkspaceMultidoc,
3
+ layoutRoots,
4
+ parseDocument,
5
+ placeMultidoc
6
+ } from "./chunk-677U72GF.js";
7
+
8
+ // src/plugin.ts
9
+ import { relative as relative3 } from "path";
10
+
11
+ // src/diff.ts
12
+ var SEPARATOR = "\n---\n";
13
+ var IDENTITY_SEPARATOR = "\0";
14
+ function docHeader(doc) {
15
+ for (const line of doc.split("\n")) {
16
+ const t = line.trim();
17
+ if (t !== "" && !t.startsWith("//")) return t;
18
+ }
19
+ return doc.trim().split("\n")[0] ?? "";
20
+ }
21
+ function docIdentity(doc) {
22
+ const parsed = parseDocument(doc);
23
+ if (parsed === null) return docHeader(doc);
24
+ return [parsed.type, parsed.name, parsed.verb ?? "", parsed.apiGroup ?? ""].join(IDENTITY_SEPARATOR);
25
+ }
26
+ function apiGroupIdentity(name) {
27
+ return ["api_group", name, "", ""].join(IDENTITY_SEPARATOR);
28
+ }
29
+ function splitDocs(text) {
30
+ const map = /* @__PURE__ */ new Map();
31
+ const body = text.replace(/\n$/, "");
32
+ if (body === "") return map;
33
+ for (const doc of body.split(SEPARATOR)) {
34
+ const id = docIdentity(doc);
35
+ const list = map.get(id) ?? [];
36
+ list.push(doc);
37
+ map.set(id, list);
38
+ }
39
+ return map;
40
+ }
41
+ function diffMultidocs(engine, local) {
42
+ const engineDocs = splitDocs(engine);
43
+ const localDocs = splitDocs(local);
44
+ const diffs = [];
45
+ let docs = 0;
46
+ let identical = 0;
47
+ for (const [id, list] of engineDocs) {
48
+ const other = localDocs.get(id) ?? [];
49
+ for (const [i, doc] of list.entries()) {
50
+ docs++;
51
+ const mine = other[i];
52
+ if (mine === void 0) {
53
+ diffs.push({ header: docHeader(doc), status: "missing-local", lines: [] });
54
+ continue;
55
+ }
56
+ if (mine === doc) {
57
+ identical++;
58
+ continue;
59
+ }
60
+ const a = doc.split("\n");
61
+ const b = mine.split("\n");
62
+ const lines = [];
63
+ for (let n = 0; n < Math.max(a.length, b.length); n++) {
64
+ if (a[n] !== b[n]) lines.push([n + 1, a[n], b[n]]);
65
+ }
66
+ const first = lines[0];
67
+ diffs.push({ header: docHeader(doc), status: "changed", line: first[0], engine: first[1], local: first[2], lines });
68
+ }
69
+ }
70
+ for (const [id, list] of localDocs) {
71
+ const have = engineDocs.get(id)?.length ?? 0;
72
+ for (let i = have; i < list.length; i++) {
73
+ docs++;
74
+ diffs.push({ header: docHeader(list[i]), status: "missing-engine", lines: [] });
75
+ }
76
+ }
77
+ return { docs, identical, diffs };
78
+ }
79
+ function diffsFor(diff, identities, engine) {
80
+ const byHeader = /* @__PURE__ */ new Map();
81
+ for (const doc of engine.replace(/\n$/, "").split(SEPARATOR)) byHeader.set(docHeader(doc), docIdentity(doc));
82
+ return new Set(diff.diffs.filter((d) => identities.has(byHeader.get(d.header) ?? "")));
83
+ }
84
+
85
+ // src/options.ts
86
+ import { isAbsolute, relative as relative2, resolve, sep as sep2 } from "path";
87
+
88
+ // src/writer.ts
89
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
90
+ import { dirname, join, relative, sep } from "path";
91
+ function emitBundleXanoScript(bundle, options = {}) {
92
+ const parsed = typeof bundle === "string" ? JSON.parse(bundle) : bundle;
93
+ return emitWorkspaceMultidoc({ payload: parsed.payload ?? {} }, { features: { guid: options.guid ?? true } });
94
+ }
95
+ function treeReadme(p) {
96
+ const src = `\`${p.source}\``;
97
+ const from = p.sourceKind === "entry" ? `rendered from the TypeScript workspace ${src} by \`xanots export --xanoscript\`` : `rendered from ${p.source} by \`xanots ephemeral export --format xs-tree\``;
98
+ const edit = p.sourceKind === "entry" ? `Edit the TypeScript under \`${dirname(p.source) === "." ? "." : dirname(p.source)}/\` and regenerate with \`${p.regenerate}\`; anything changed here is overwritten by the next run.` : `Change the environment and regenerate with \`${p.regenerate}\`; anything changed here is overwritten by the next run.`;
99
+ const honest = p.check !== void 0 ? `\`${p.check}\` fails when the committed copy is stale, so a change cannot ship with a tree that disagrees with its source.` : "Regenerate it in the same change as the source, so the two never disagree.";
100
+ const lines = [
101
+ "# XanoScript rendering (generated)",
102
+ "",
103
+ `**Do not edit these files.** They are ${from}, and the whole directory is replaced on every run.`,
104
+ edit,
105
+ "",
106
+ "## Why it is committed",
107
+ "",
108
+ "A change to the backend shows up in review as the XanoScript it produces, one file per",
109
+ "object, beside the code that produced it. Read the diff here to see what the engine will",
110
+ "run; ask for changes in the source.",
111
+ "",
112
+ honest,
113
+ "",
114
+ "## After a merge, regenerate",
115
+ "",
116
+ "A clean merge is not evidence this tree is correct. Two branches that change different",
117
+ "objects merge without a conflict, yet one side's files were rendered before the other's",
118
+ "source change existed, so the merged tree can name an object that was renamed away.",
119
+ "Nothing in git can see that: at the text level the two sides never touched the same file.",
120
+ "",
121
+ `Regenerate after every merge${p.check !== void 0 ? `, and let \`${p.check}\` confirm it` : ""}.`,
122
+ "Never hand-edit a file here and never hand-resolve a conflict in one: the correct content",
123
+ "is whatever the source renders to, so the only valid resolution is to re-derive it.",
124
+ "",
125
+ "## Reading the tree",
126
+ "",
127
+ "| Path | What it is |",
128
+ "|---|---|",
129
+ `| \`workspace/<name>.xs\` | the workspace settings; \`workspace/trigger/\` its triggers |`,
130
+ `| \`table/<name>.xs\` | a table; \`table/trigger/\` its triggers |`,
131
+ `| \`api/<group>/<group>.xs\` | an API group; \`api/<group>/<path>/<name>_<VERB>.xs\` each of its endpoints |`,
132
+ `| \`function/\`, \`task/\`, \`addon/\`, \`middleware/\` | one file per object |`,
133
+ `| \`ai/agent/\`, \`ai/mcp_server/\`, \`ai/tool/\` | agents, MCP servers and tools; a \`trigger/\` folder beneath each |`,
134
+ `| \`realtime/server/<server>/channel/<channel>/message/\` | realtime servers, their channels, their messages, with triggers beneath each |`,
135
+ `| \`workflow_test/\`, \`microservice/\` | one file per object |`,
136
+ "",
137
+ `Names are snake_cased (\`get-user\` becomes \`get_user.xs\`); a path parameter such as \`{id}\` becomes a`,
138
+ `directory segment. This is the layout the Xano CLI writes on \`pull\`, so the tree can be`,
139
+ `pushed to an environment as it is (\`xano ephemeral push <env> --directory ${DEFAULT_TREE_DIR}\`) and`,
140
+ "pulls back byte for byte.",
141
+ "",
142
+ `A \`guid = "..."\` line is the object's identity, pinned by the workspace's \`xano.lock\`; it is what`,
143
+ `keeps a rename a rename on the engine. A \`placeholder "<name>"\` line marks a statement the`,
144
+ "engine's XanoScript cannot express; the export warns about each one.",
145
+ ""
146
+ ];
147
+ if (p.version !== void 0) lines.push(`Generated with @xanots/sdk ${p.version}.`, "");
148
+ return lines.join("\n");
149
+ }
150
+ var DEFAULT_TREE_DIR = "xanoscript";
151
+ function projectTreeScripts(cwd) {
152
+ try {
153
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
154
+ const scripts = Object.entries(pkg.scripts ?? {}).filter(([, cmd]) => /xanots export\b/.test(cmd) && /--xanoscript\b|--format[= ]xs-tree/.test(cmd));
155
+ const check = scripts.find(([, cmd]) => /--frozen-lock/.test(cmd))?.[0];
156
+ const regenerate = scripts.find(([name]) => name !== check)?.[0] ?? scripts[0]?.[0];
157
+ return {
158
+ regenerate: regenerate !== void 0 ? `npm run ${regenerate}` : "xanots export <entry> --xanoscript",
159
+ ...check !== void 0 ? { check: `npm run ${check}` } : {}
160
+ };
161
+ } catch {
162
+ return { regenerate: "xanots export <entry> --xanoscript" };
163
+ }
164
+ }
165
+ function planTree(dir, multidoc, provenance) {
166
+ if (existsSync(dir) && !statSync(dir).isDirectory()) {
167
+ throw new Error(`${dir} is a file; the XanoScript tree is a directory of .xs files. Name a directory, or use \`--format xs\` for one file.`);
168
+ }
169
+ const placed = placeMultidoc(multidoc);
170
+ const keep = new Set(placed.map((doc) => doc.relPath));
171
+ const stale = [];
172
+ for (const root of layoutRoots()) {
173
+ const abs = join(dir, root);
174
+ if (existsSync(abs)) collectStale(abs, dir, keep, stale);
175
+ }
176
+ const changed = placed.filter((doc) => !fileHolds(join(dir, ...doc.relPath.split("/")), doc.content)).map((doc) => doc.relPath);
177
+ const readme = treeReadme(provenance);
178
+ return { placed, changed, stale, readme, readmeChanged: !fileHolds(join(dir, "README.md"), readme) };
179
+ }
180
+ function fileHolds(abs, content) {
181
+ return existsSync(abs) && readFileSync(abs, "utf8") === content;
182
+ }
183
+ function checkXanoScriptTree(dir, multidoc, provenance) {
184
+ const plan = planTree(dir, multidoc, provenance);
185
+ return { changed: [...plan.changed, ...plan.readmeChanged ? ["README.md"] : []], removed: plan.stale };
186
+ }
187
+ function writeXanoScriptTree(dir, multidoc, provenance) {
188
+ const plan = planTree(dir, multidoc, provenance);
189
+ for (const rel of plan.stale) rmSync(join(dir, ...rel.split("/")));
190
+ for (const root of layoutRoots()) {
191
+ const abs = join(dir, root);
192
+ if (existsSync(abs)) removeEmptyDirs(abs);
193
+ }
194
+ mkdirSync(dir, { recursive: true });
195
+ const changed = new Set(plan.changed);
196
+ for (const doc of plan.placed) {
197
+ if (!changed.has(doc.relPath)) continue;
198
+ const abs = join(dir, ...doc.relPath.split("/"));
199
+ mkdirSync(dirname(abs), { recursive: true });
200
+ writeFileSync(abs, doc.content, "utf8");
201
+ }
202
+ if (plan.readmeChanged) writeFileSync(join(dir, "README.md"), plan.readme, "utf8");
203
+ return { written: plan.placed.map((doc) => doc.relPath), removed: plan.stale };
204
+ }
205
+ function collectStale(abs, root, keep, out) {
206
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
207
+ const child = join(abs, entry.name);
208
+ if (entry.isDirectory()) {
209
+ collectStale(child, root, keep, out);
210
+ } else if (entry.name.endsWith(".xs")) {
211
+ const rel = relative(root, child).split(sep).join("/");
212
+ if (!keep.has(rel)) out.push(rel);
213
+ }
214
+ }
215
+ }
216
+ function removeEmptyDirs(abs) {
217
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
218
+ if (entry.isDirectory()) removeEmptyDirs(join(abs, entry.name));
219
+ }
220
+ if (readdirSync(abs).length === 0) rmSync(abs, { recursive: true });
221
+ }
222
+ var AUTHORED_AS = {
223
+ // Emitted by `s.db.get_by_id` before 0.0.19; since then that builder emits the
224
+ // field match (`db.get` on `id`), so a bundle carrying this was built by an
225
+ // older SDK, or pulled from a workspace edited elsewhere.
226
+ "mvp:dbo_get": "s.db.get_by_id before @xanots/sdk 0.0.19 \u2014 re-export with the current SDK",
227
+ // Emitted by `s.security.create_guid`, removed in 0.0.20; `s.security.create_uuid` renders.
228
+ "mvp:guid": "s.security.create_guid, removed in @xanots/sdk 0.0.20 \u2014 use s.security.create_uuid"
229
+ };
230
+ function placeholderStatements(xanoscript) {
231
+ const counts = /* @__PURE__ */ new Map();
232
+ for (const m of xanoscript.matchAll(/^\s*placeholder "([^"]*)"\s*$/gm)) counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
233
+ return [...counts].map(([name, count]) => ({ name, count, ...AUTHORED_AS[name] ? { authoredAs: AUTHORED_AS[name] } : {} }));
234
+ }
235
+ function placeholderWarning(xanoscript) {
236
+ const found = placeholderStatements(xanoscript);
237
+ if (found.length === 0) return null;
238
+ const list = found.map((f) => `${f.name} \xD7${f.count}${f.authoredAs ? ` (${f.authoredAs})` : ""}`).join(", ");
239
+ return `${found.reduce((n, f) => n + f.count, 0)} statement(s) have no XanoScript form and rendered as \`placeholder\`: ${list}. The engine's own pull renders them the same way, and pushing the placeholder does not recreate the statement.`;
240
+ }
241
+
242
+ // src/options.ts
243
+ var PACKAGE_NAME = "@xanots/xanoscript";
244
+ var FORBIDDEN_ROOTS = {
245
+ // A build directory is wiped by the build, so a committed review surface
246
+ // written into it is deleted by the next `npm run build` and silently
247
+ // regenerated — a tree that exists only between two commands.
248
+ dist: "a build directory the toolchain deletes and regenerates",
249
+ // The SDK's own scratch directory. A generated tree there is neither
250
+ // committed nor reviewable, which is the entire point of writing one.
251
+ ".xano": "the SDK's own scratch directory, which is not committed"
252
+ };
253
+ var OptionsError = class extends Error {
254
+ name = "OptionsError";
255
+ };
256
+ function fail(message) {
257
+ throw new OptionsError(`${PACKAGE_NAME}: ${message}`);
258
+ }
259
+ function resolveOptions(config, cwd) {
260
+ const raw = config ?? {};
261
+ const enabledRaw = raw.enabled;
262
+ if (enabledRaw !== void 0 && typeof enabledRaw !== "boolean") {
263
+ fail(`the "enabled" option must be true or false (got ${describe(enabledRaw)}).`);
264
+ }
265
+ const enabled = enabledRaw ?? true;
266
+ const dirRaw = raw.dir;
267
+ if (dirRaw !== void 0 && typeof dirRaw !== "string") {
268
+ fail(`the "dir" option must be a string naming a directory (got ${describe(dirRaw)}).`);
269
+ }
270
+ const dir = dirRaw === void 0 || dirRaw === "" ? DEFAULT_TREE_DIR : dirRaw;
271
+ if (isAbsolute(dir)) {
272
+ fail(`the "dir" option must be relative to the project, not an absolute path (got ${JSON.stringify(dir)}).`);
273
+ }
274
+ const absDir = resolve(cwd, dir);
275
+ const rel = relative2(cwd, absDir);
276
+ if (rel === "") {
277
+ fail(`the "dir" option must name a directory inside the project, not the project root itself (got ${JSON.stringify(dir)}).`);
278
+ }
279
+ if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel)) {
280
+ fail(
281
+ `the "dir" option resolves outside the project root (${JSON.stringify(dir)} \u2192 ${absDir}). The tree directory is pruned on every write, so it must stay inside the project.`
282
+ );
283
+ }
284
+ const first = rel.split(sep2)[0] ?? "";
285
+ const why = FORBIDDEN_ROOTS[first];
286
+ if (why !== void 0) {
287
+ fail(`the "dir" option must not be inside ${JSON.stringify(first)} \u2014 ${why} (got ${JSON.stringify(dir)}).`);
288
+ }
289
+ return { enabled, dir, absDir };
290
+ }
291
+ function describe(v) {
292
+ if (v === null) return "null";
293
+ if (Array.isArray(v)) return "an array";
294
+ return typeof v;
295
+ }
296
+
297
+ // src/plugin.ts
298
+ var MIN_SDK = "0.0.29";
299
+ var MAX_SDK_EXCLUSIVE = "0.1.0";
300
+ var PEER_RANGE = `>=${MIN_SDK} <${MAX_SDK_EXCLUSIVE}`;
301
+ function parseVersion(v) {
302
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
303
+ return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];
304
+ }
305
+ function compare(a, b) {
306
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
307
+ }
308
+ function assertSdkVersion(sdkVersion) {
309
+ if (sdkVersion.includes("-")) return;
310
+ const found = parseVersion(sdkVersion);
311
+ if (found === null) return;
312
+ const min = parseVersion(MIN_SDK);
313
+ const max = parseVersion(MAX_SDK_EXCLUSIVE);
314
+ if (compare(found, min) < 0 || compare(found, max) >= 0) {
315
+ throw new OptionsError(
316
+ `${PACKAGE_NAME} requires @xanots/sdk ${PEER_RANGE}, but ${sdkVersion} is running. Upgrade @xanots/sdk, or remove ${PACKAGE_NAME} from this project \u2014 the XanoScript tree is not written by an SDK outside that range.`
317
+ );
318
+ }
319
+ }
320
+ var Q_ENABLED = "xanoscript";
321
+ var Q_DIR = "dir";
322
+ var questions = [
323
+ {
324
+ id: Q_ENABLED,
325
+ label: "Commit a XanoScript rendering of the backend, so changes review as what the engine runs",
326
+ type: "boolean",
327
+ default: true
328
+ },
329
+ {
330
+ id: Q_DIR,
331
+ label: "Directory for the XanoScript tree",
332
+ type: "string",
333
+ default: DEFAULT_TREE_DIR,
334
+ // Derived from `id` this would be `--dir`, which reads as the SDK's own
335
+ // and would collide with the next module that wants a directory. The flag
336
+ // is namespaced to the thing it configures.
337
+ flag: "xanoscript-dir",
338
+ // A project that declined the tree is never asked where to put it, and —
339
+ // per the contract — the unasked question's default is NOT written to the
340
+ // config block either. Recording `dir` for a disabled module would put a
341
+ // directory nobody chose in front of the next reader.
342
+ when: (answers) => answers[Q_ENABLED] !== false
343
+ }
344
+ ];
345
+ function files(answers) {
346
+ if (answers[Q_ENABLED] === false) return {};
347
+ const dir = typeof answers[Q_DIR] === "string" && answers[Q_DIR] !== "" ? answers[Q_DIR] : DEFAULT_TREE_DIR;
348
+ return {
349
+ gitattributes: [
350
+ "",
351
+ `# GitHub's built-in ".xs" language is Perl XS (tm_scope: source.c), so without`,
352
+ "# this every XanoScript file renders with C syntax highlighting and counts as",
353
+ "# Perl XS in the repository language bar. HCL is the closest built-in match to",
354
+ "# XanoScript's `block name { key = value }` grammar with // comments.",
355
+ "*.xs linguist-language=HCL",
356
+ "",
357
+ `# ${dir}/ is generated but committed on purpose: a backend change is`,
358
+ "# reviewed as the XanoScript it produces. Deliberately NOT linguist-generated:",
359
+ "# that is the one attribute that COLLAPSES a file in pull request diffs, which",
360
+ "# would hide the thing this directory exists to show. linguist-vendored drops it",
361
+ "# from the language bar and leaves the diff expanded.",
362
+ `${dir}/** linguist-vendored=true`
363
+ ],
364
+ config: { enabled: true, dir }
365
+ };
366
+ }
367
+ var NO_ENTRY_MESSAGE = "xanoscript \u2014 skipped: this bundle was supplied with `--bundle`, so there is no entry it was compiled from. The committed tree describes the source beside it; regenerate it by exporting the workspace entry.";
368
+ async function onBundle(ctx) {
369
+ const opts = resolveOptions(ctx.config, ctx.cwd);
370
+ assertSdkVersion(ctx.sdkVersion);
371
+ if (!opts.enabled) return {};
372
+ if (ctx.entry === void 0) return { message: NO_ENTRY_MESSAGE };
373
+ const multidoc = emitBundleXanoScript(ctx.bundle, { guid: true });
374
+ const warnings = [];
375
+ const placeholders = placeholderWarning(multidoc);
376
+ if (placeholders !== null) warnings.push(placeholders);
377
+ const provenance = treeProvenance(ctx);
378
+ if (ctx.frozen) {
379
+ const { changed, removed: removed2 } = checkXanoScriptTree(opts.absDir, multidoc, provenance);
380
+ if (changed.length === 0 && removed2.length === 0) {
381
+ return { message: `xanoscript \u2713 ${opts.dir}/ matches the source`, ...warnings.length > 0 ? { warnings } : {} };
382
+ }
383
+ return {
384
+ failed: true,
385
+ message: `xanoscript \u2717 ${opts.dir}/ is stale \u2014 ${changed.length} file(s) differ, ${removed2.length} left over. Regenerate it and commit the result.`,
386
+ warnings: [
387
+ ...changed.slice(0, 20).map((f) => `differs: ${f}`),
388
+ ...removed2.slice(0, 20).map((f) => `no longer produced: ${f}`),
389
+ ...changed.length + removed2.length > 40 ? [`\u2026 ${changed.length + removed2.length - 40} more`] : [],
390
+ ...warnings
391
+ ]
392
+ };
393
+ }
394
+ const { written, removed } = writeXanoScriptTree(opts.absDir, multidoc, provenance);
395
+ return {
396
+ message: `xanoscript \u2192 ${opts.dir}/ (${written.length} document(s)${removed.length > 0 ? `, ${removed.length} removed` : ""})`,
397
+ ...warnings.length > 0 ? { warnings } : {}
398
+ };
399
+ }
400
+ function treeProvenance(ctx) {
401
+ const entry = ctx.entry;
402
+ const rel = relative3(ctx.cwd, entry);
403
+ return {
404
+ // A path outside the project (an absolute entry elsewhere) stays absolute
405
+ // rather than becoming a `../../` the README's reader cannot follow.
406
+ source: rel === "" || rel.startsWith("..") ? entry : rel,
407
+ sourceKind: "entry",
408
+ ...projectTreeScripts(ctx.cwd),
409
+ version: ctx.sdkVersion
410
+ };
411
+ }
412
+ async function onPreflight(ctx) {
413
+ const opts = resolveOptions(ctx.config, ctx.cwd);
414
+ assertSdkVersion(ctx.sdkVersion);
415
+ if (!opts.enabled) return {};
416
+ if (ctx.engineRendering.kind === "error") {
417
+ return {
418
+ message: "xanoscript ? the engine's rendering could not be fetched, so the emitter was not compared",
419
+ warnings: [ctx.engineRendering.why]
420
+ };
421
+ }
422
+ const engine = ctx.engineRendering.text;
423
+ const features = { guid: true };
424
+ const fromExported = emitWorkspaceMultidoc({ payload: ctx.exportedPayload }, { features });
425
+ const fromBundle = emitWorkspaceMultidoc({ payload: ctx.remappedPayload }, { features });
426
+ const port = diffMultidocs(engine, fromExported);
427
+ const exportedGroups = new Set(records(ctx.exportedPayload.app).map((a) => String(a.name ?? "")));
428
+ const queryLessApiGroups = records(ctx.remappedPayload.app).map((a) => String(a.name ?? "")).filter((name) => name !== "" && !exportedGroups.has(name));
429
+ const skipped = new Set(
430
+ [...diffsFor(port, new Set(queryLessApiGroups.map(apiGroupIdentity)), engine)].filter((d) => d.status === "missing-local")
431
+ );
432
+ port.diffs = port.diffs.filter((d) => !skipped.has(d));
433
+ const compared = port.docs - skipped.size;
434
+ const warnings = [];
435
+ if (skipped.size > 0) {
436
+ warnings.push(`${skipped.size} api group(s) without queries are absent from the engine's JSON export and were not compared`);
437
+ }
438
+ const failed = port.diffs.length > 0;
439
+ const message = failed ? `xanoscript \u2717 emitter differs from the engine in ${port.diffs.length} of ${compared} document(s)` : `xanoscript \u2713 emitter matches the engine (${compared} document(s), byte-identical)`;
440
+ if (failed) {
441
+ const shown = ctx.verbose ? port.diffs : port.diffs.slice(0, 10);
442
+ for (const d of shown) warnings.push(describeDiff(d, ctx.verbose));
443
+ if (!ctx.verbose && port.diffs.length > shown.length) warnings.push(`\u2026 ${port.diffs.length - shown.length} more (run with --verbose)`);
444
+ }
445
+ const authored = diffMultidocs(engine, fromBundle);
446
+ const authoredDiffs = authored.diffs.filter((d) => d.status !== "changed" || d.lines.some(([, e, l]) => !isGuidLine(e) || !isGuidLine(l)));
447
+ if (authoredDiffs.length > 0) {
448
+ warnings.push(
449
+ `this bundle's own rendering differs from the engine in ${authoredDiffs.length} document(s) \u2014 the JSON the engine fills in on import, not the emitter`
450
+ );
451
+ if (ctx.verbose) for (const d of authoredDiffs) warnings.push(` ${describeDiff(d, false)}`);
452
+ }
453
+ return { message, ...failed ? { failed: true } : {}, ...warnings.length > 0 ? { warnings } : {} };
454
+ }
455
+ function describeDiff(d, verbose) {
456
+ if (d.status !== "changed") return `${d.header} ${d.status === "missing-local" ? "not rendered locally" : "not rendered by the engine"}`;
457
+ const head = `${d.header} line ${d.line}: engine ${fmt(d.engine)}, local ${fmt(d.local)}`;
458
+ if (!verbose) return head;
459
+ return [head, ...d.lines.slice(1).map(([n, e, l]) => ` line ${n}: engine ${fmt(e)}, local ${fmt(l)}`)].join("\n");
460
+ }
461
+ function isGuidLine(line) {
462
+ return line !== void 0 && /^\s*guid = "/.test(line);
463
+ }
464
+ function fmt(v) {
465
+ return v === void 0 ? "(absent)" : JSON.stringify(v);
466
+ }
467
+ function records(v) {
468
+ return Array.isArray(v) ? v.filter((r) => typeof r === "object" && r !== null) : [];
469
+ }
470
+ var plugin = {
471
+ kind: "toolchain",
472
+ questions,
473
+ files,
474
+ onBundle,
475
+ onPreflight
476
+ };
477
+ var plugin_default = plugin;
478
+ export {
479
+ PEER_RANGE,
480
+ assertSdkVersion,
481
+ plugin_default as default
482
+ };
483
+ //# sourceMappingURL=plugin.js.map
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@xanots/xanoscript",
3
+ "version": "0.0.1",
4
+ "description": "Renders a XanoTS workspace to XanoScript — the language the Xano engine runs. A file-for-file port of the engine's encoder, proven byte-for-byte against the engine's own golden corpus.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Xano",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "keywords": [
12
+ "xano",
13
+ "xanots",
14
+ "xanoscript",
15
+ "toolchain",
16
+ "codegen"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/xanots/xanoscript.git"
21
+ },
22
+ "homepage": "https://github.com/xanots/xanoscript#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/xanots/xanoscript/issues"
25
+ },
26
+ "sideEffects": false,
27
+ "xanots": {
28
+ "kind": "toolchain",
29
+ "plugin": "./dist/plugin.js"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./plugin": {
39
+ "types": "./dist/plugin.d.ts",
40
+ "import": "./dist/plugin.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "!dist/**/*.map",
47
+ "README.md",
48
+ "llms.txt"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "lint": "eslint .",
56
+ "xs:vendor": "tsx scripts/xs-engine/vendor-engine.ts",
57
+ "xs:corpus": "tsx scripts/xs-engine/sync-corpus.ts",
58
+ "xs:drift": "tsx scripts/xs-engine/drift.ts",
59
+ "xs:report": "tsx scripts/xs-engine/corpus-report.ts",
60
+ "audit:credentials": "bash scripts/scan-corpus-secrets.sh",
61
+ "prepublishOnly": "npm run build",
62
+ "release": "npm publish --access public",
63
+ "release:beta": "npm version prerelease --preid=beta -m \"chore(release): %s\" && npm publish --tag beta --access public"
64
+ },
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
68
+ "peerDependencies": {
69
+ "@xanots/sdk": ">=0.0.29 <0.1.0"
70
+ },
71
+ "dependencies": {
72
+ "lodash.snakecase": "4.1.1"
73
+ },
74
+ "devDependencies": {
75
+ "@eslint/js": "^9.0.0",
76
+ "@types/lodash.snakecase": "^4.1.9",
77
+ "@types/node": "^20.0.0",
78
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
79
+ "@typescript-eslint/parser": "^8.0.0",
80
+ "@xanots/sdk": "^0.0.29",
81
+ "eslint": "^9.0.0",
82
+ "tsup": "^8.0.0",
83
+ "tsx": "^4.23.1",
84
+ "typescript": "^5.5.0",
85
+ "vitest": "^3.2.6",
86
+ "yaml": "^2.9.1"
87
+ }
88
+ }