@thelol3882/sapar 0.0.0-stage → 0.3.0

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/LICENSE +21 -0
  2. package/README.md +380 -2
  3. package/dist/adapter-Y9yB3-q3.d.ts +14 -0
  4. package/dist/adapter-fFZPsINL.d.ts +14 -0
  5. package/dist/chunk-7B66QEMY.js +536 -0
  6. package/dist/chunk-7B66QEMY.js.map +1 -0
  7. package/dist/chunk-FM64UDKJ.js +893 -0
  8. package/dist/chunk-FM64UDKJ.js.map +1 -0
  9. package/dist/chunk-VC6XLWM3.js +176 -0
  10. package/dist/chunk-VC6XLWM3.js.map +1 -0
  11. package/dist/chunk-ZBVQGMR4.js +583 -0
  12. package/dist/chunk-ZBVQGMR4.js.map +1 -0
  13. package/dist/cli/index.js +1335 -0
  14. package/dist/core/index.d.ts +273 -0
  15. package/dist/core/index.js +71 -0
  16. package/dist/core/index.js.map +1 -0
  17. package/dist/expo/index.d.ts +189 -0
  18. package/dist/expo/index.js +39 -0
  19. package/dist/expo/index.js.map +1 -0
  20. package/dist/expo-router/index.d.ts +18 -0
  21. package/dist/expo-router/index.js +46 -0
  22. package/dist/expo-router/index.js.map +1 -0
  23. package/dist/next/index.d.ts +18 -0
  24. package/dist/next/index.js +49 -0
  25. package/dist/next/index.js.map +1 -0
  26. package/dist/presentation-CClsPRT6.d.ts +12 -0
  27. package/dist/react/index.d.ts +35 -0
  28. package/dist/react/index.js +29 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/rtk/index.d.ts +751 -0
  31. package/dist/rtk/index.js +114 -0
  32. package/dist/rtk/index.js.map +1 -0
  33. package/dist/stepCount-CTYYFWiV.d.ts +57 -0
  34. package/dist/types-CHD2pB5X.d.ts +192 -0
  35. package/dist/web/index.d.ts +112 -0
  36. package/dist/web/index.js +34 -0
  37. package/dist/web/index.js.map +1 -0
  38. package/package.json +147 -3
  39. package/templates/expo/AnnouncementModal.tsx +102 -0
  40. package/templates/expo/TourCard.tsx +112 -0
  41. package/templates/expo/WhatsNewButton.tsx +87 -0
  42. package/templates/expo/WhatsNewPanel.tsx +167 -0
  43. package/templates/mantine/AnnouncementModal.tsx +66 -0
  44. package/templates/mantine/TourCard.tsx +84 -0
  45. package/templates/mantine/WhatsNewButton.tsx +62 -0
  46. package/templates/mantine/WhatsNewPanel.tsx +129 -0
  47. package/templates/tailwind/AnnouncementModal.tsx +100 -0
  48. package/templates/tailwind/TourCard.tsx +69 -0
  49. package/templates/tailwind/WhatsNewButton.tsx +56 -0
  50. package/templates/tailwind/WhatsNewPanel.tsx +174 -0
@@ -0,0 +1,1335 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/prompt.ts
4
+ import { createInterface } from "readline/promises";
5
+ function createPrompter() {
6
+ let readline;
7
+ return {
8
+ async ask(question, defaultAnswer) {
9
+ readline ??= createInterface({ input: process.stdin, output: process.stdout });
10
+ const answer = await readline.question(`${question} [${defaultAnswer}] `);
11
+ return answer.trim() || defaultAnswer;
12
+ },
13
+ close: () => readline?.close()
14
+ };
15
+ }
16
+
17
+ // src/cli/help.ts
18
+ var GENERAL_HELP = `Usage: sapar <command> [options]
19
+
20
+ Commands:
21
+ init Create sapar.json, a sample definitions file, and a provider
22
+ add Copy UI components into your project
23
+ check Verify that every tour anchor exists in your source
24
+
25
+ Run \`sapar <command> --help\` for details.
26
+
27
+ Options:
28
+ -h, --help Show help
29
+ -v, --version Show the version`;
30
+ var INIT_HELP = `Usage: sapar init [options]
31
+
32
+ Detects your setup, writes sapar.json, <toursDir>/definitions.ts, anchors.ts and
33
+ SaparProvider.tsx, then copies the TourCard component. Existing files are kept.
34
+ Written files are formatted with your project's Prettier when it has a config.
35
+
36
+ Options:
37
+ --cwd <dir> Project root (default: current directory)
38
+ --yes Accept detected values without asking
39
+ --framework <name> next | expo | react
40
+ --style <name> tailwind | mantine | native
41
+ --source <name> memory | http | rtk
42
+ --import-base <specifier> Package name or alias of a copied src folder
43
+ --components-dir <dir> Where components are copied
44
+ --tours-dir <dir> Where definitions, anchors and the provider go
45
+ --no-format Do not format the written files with Prettier
46
+ --force Overwrite existing files`;
47
+ var ADD_HELP = `Usage: sapar add [names...] [options]
48
+
49
+ Copies components from the template set of your style into componentsDir.
50
+ Names are case-insensitive; kebab-case works (whats-new-panel).
51
+ Copied files are formatted with your project's Prettier when it has a config.
52
+
53
+ Options:
54
+ --cwd <dir> Project root (default: current directory)
55
+ --all Copy every component
56
+ --list List the available components
57
+ --style <name> Use another template set than sapar.json
58
+ --no-format Do not format the copied files with Prettier
59
+ --force Overwrite existing files`;
60
+ var CHECK_HELP = `Usage: sapar check [options]
61
+
62
+ Reads tour definitions and anchor declarations from your source and fails when
63
+ a step points at an anchor that does not exist on a platform the tour targets.
64
+ Needs the \`typescript\` package.
65
+
66
+ Options:
67
+ --cwd <dir> Project root (default: current directory)
68
+ --format <format> text | json (default: text)
69
+ --strict Treat warnings as errors
70
+ --verbose Also list declared anchors that no tour uses`;
71
+
72
+ // src/cli/commands/add.ts
73
+ import { join as join5 } from "path";
74
+
75
+ // src/cli/args.ts
76
+ import { resolve } from "path";
77
+ import { parseArgs } from "util";
78
+
79
+ // src/cli/errors.ts
80
+ var CliError = class extends Error {
81
+ };
82
+
83
+ // src/cli/args.ts
84
+ function parseCommandArgs(args, options) {
85
+ try {
86
+ return parseArgs({ args: [...args], options, allowPositionals: true });
87
+ } catch (error) {
88
+ throw new CliError(error instanceof Error ? error.message : String(error));
89
+ }
90
+ }
91
+ function resolveCwd(context, cwdFlag) {
92
+ return cwdFlag === void 0 ? context.cwd : resolve(context.cwd, cwdFlag);
93
+ }
94
+
95
+ // src/cli/config.ts
96
+ import { join } from "path";
97
+
98
+ // src/cli/fs.ts
99
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
100
+ import { dirname } from "path";
101
+
102
+ // src/cli/paths.ts
103
+ import * as nodePath from "path";
104
+ function toForwardSlashes(path, { sep } = nodePath) {
105
+ return path.split(sep).join("/");
106
+ }
107
+ function displayPath(from, to, pathModule = nodePath) {
108
+ return toForwardSlashes(pathModule.relative(from, to), pathModule);
109
+ }
110
+ function displayJoin(segments, pathModule = nodePath) {
111
+ return toForwardSlashes(pathModule.join(...segments), pathModule);
112
+ }
113
+
114
+ // src/cli/fs.ts
115
+ function isNotFound(error) {
116
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
117
+ }
118
+ async function statOrUndefined(path) {
119
+ try {
120
+ return await stat(path);
121
+ } catch (error) {
122
+ if (isNotFound(error)) return void 0;
123
+ throw error;
124
+ }
125
+ }
126
+ async function pathExists(path) {
127
+ return await statOrUndefined(path) !== void 0;
128
+ }
129
+ async function isDirectory(path) {
130
+ return (await statOrUndefined(path))?.isDirectory() ?? false;
131
+ }
132
+ async function writeFileSafely(path, content, force) {
133
+ const existed = await pathExists(path);
134
+ if (existed && !force) return { path, outcome: "skipped" };
135
+ await mkdir(dirname(path), { recursive: true });
136
+ await writeFile(path, content);
137
+ return { path, outcome: existed ? "overwritten" : "created" };
138
+ }
139
+ async function readJsonObject(path) {
140
+ let text;
141
+ try {
142
+ text = await readFile(path, "utf8");
143
+ } catch (error) {
144
+ if (isNotFound(error)) return void 0;
145
+ throw error;
146
+ }
147
+ let parsed;
148
+ try {
149
+ parsed = JSON.parse(text);
150
+ } catch {
151
+ throw new CliError(`${toForwardSlashes(path)} is not valid JSON.`);
152
+ }
153
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
154
+ throw new CliError(`${toForwardSlashes(path)} must contain a JSON object.`);
155
+ }
156
+ return Object.fromEntries(Object.entries(parsed));
157
+ }
158
+
159
+ // src/cli/config.ts
160
+ var FRAMEWORKS = ["next", "expo", "react"];
161
+ var STYLES = ["tailwind", "mantine", "native"];
162
+ var SOURCES = ["memory", "http", "rtk"];
163
+ var PLATFORMS = ["web", "native"];
164
+ var CONFIG_FILE = "sapar.json";
165
+ var platformOf = (framework) => framework === "expo" ? "native" : "web";
166
+ var isPlatform = (value) => PLATFORMS.includes(value);
167
+ function parseChoice(label, value, choices) {
168
+ const match = choices.find((choice) => choice === value);
169
+ if (match === void 0) throw new CliError(`Invalid ${label}: expected one of ${choices.join(", ")}, got "${value}".`);
170
+ return match;
171
+ }
172
+ function readChoice(raw, field, choices) {
173
+ return parseChoice(`"${field}" in ${CONFIG_FILE}`, String(raw[field]), choices);
174
+ }
175
+ function readString(raw, field) {
176
+ const value = raw[field];
177
+ if (typeof value !== "string" || value.trim() === "") {
178
+ throw new CliError(`"${field}" in ${CONFIG_FILE} must be a non-empty string.`);
179
+ }
180
+ return value;
181
+ }
182
+ function readRoots(platforms, platform) {
183
+ const roots = platforms[platform] ?? [];
184
+ if (!Array.isArray(roots) || roots.some((root) => typeof root !== "string")) {
185
+ throw new CliError(`"platforms.${platform}" in ${CONFIG_FILE} must be an array of directories.`);
186
+ }
187
+ return roots.map(String);
188
+ }
189
+ function readPlatforms(raw) {
190
+ const { platforms } = raw;
191
+ if (typeof platforms !== "object" || platforms === null || Array.isArray(platforms)) {
192
+ throw new CliError(`"platforms" in ${CONFIG_FILE} must be an object such as { "web": ["src"], "native": [] }.`);
193
+ }
194
+ const roots = Object.fromEntries(Object.entries(platforms));
195
+ return { web: readRoots(roots, "web"), native: readRoots(roots, "native") };
196
+ }
197
+ function assertStyleMatchesFramework(framework, style) {
198
+ if (framework === "expo" !== (style === "native")) {
199
+ throw new CliError('The "native" style goes with the "expo" framework, and only with it.');
200
+ }
201
+ }
202
+ function validateConfig(raw) {
203
+ const framework = readChoice(raw, "framework", FRAMEWORKS);
204
+ const style = readChoice(raw, "style", STYLES);
205
+ assertStyleMatchesFramework(framework, style);
206
+ return {
207
+ framework,
208
+ style,
209
+ source: readChoice(raw, "source", SOURCES),
210
+ componentsDir: readString(raw, "componentsDir"),
211
+ toursDir: readString(raw, "toursDir"),
212
+ importBase: readString(raw, "importBase"),
213
+ platforms: readPlatforms(raw)
214
+ };
215
+ }
216
+ async function readConfig(cwd) {
217
+ const raw = await readJsonObject(join(cwd, CONFIG_FILE));
218
+ if (raw === void 0) throw new CliError(`No ${CONFIG_FILE} in ${toForwardSlashes(cwd)}. Run \`sapar init\` first.`);
219
+ return validateConfig(raw);
220
+ }
221
+ function writeConfig(cwd, config, force) {
222
+ return writeFileSafely(join(cwd, CONFIG_FILE), `${JSON.stringify(config, null, 4)}
223
+ `, force);
224
+ }
225
+
226
+ // src/cli/format.ts
227
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
228
+ import { createRequire } from "module";
229
+ import { join as join2 } from "path";
230
+
231
+ // src/cli/report.ts
232
+ var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
233
+ function formatFinding({ code, message, file, line }) {
234
+ const location = file === void 0 ? "" : `${file}${line === void 0 ? "" : `:${line}`} `;
235
+ return ` ${location}${message} [${code}]`;
236
+ }
237
+ function formatSection(title, findings) {
238
+ return findings.length === 0 ? [] : [`${title} (${findings.length})`, ...findings.map(formatFinding), ""];
239
+ }
240
+ function formatSummary({ errors, warnings, stats }) {
241
+ const problems = errors.length + warnings.length === 0 ? "No problems found." : `Found ${plural(errors.length, "error")} and ${plural(warnings.length, "warning")}.`;
242
+ return `${problems} Scanned ${plural(stats.files, "file")} and ${plural(stats.tours, "tour")}.`;
243
+ }
244
+ function formatCheckText(report) {
245
+ return [
246
+ ...formatSection("Errors", report.errors),
247
+ ...formatSection("Warnings", report.warnings),
248
+ ...formatSection("Info", report.info),
249
+ formatSummary(report)
250
+ ].join("\n");
251
+ }
252
+ var formatCheckJson = (report) => JSON.stringify(report, null, 2);
253
+ function formatWrites(cwd, records) {
254
+ return records.map(({ path, outcome }) => {
255
+ const note = outcome === "skipped" ? " (already exists; use --force to overwrite)" : "";
256
+ return `${outcome.padEnd(11)} ${displayPath(cwd, path)}${note}`;
257
+ }).join("\n");
258
+ }
259
+
260
+ // src/cli/format.ts
261
+ var FORMATTED_HINT = "formatted with Prettier";
262
+ var UNFORMATTED_HINT = "no Prettier config found; files use 4-space indentation and single quotes";
263
+ function isPrettier(value) {
264
+ return typeof value === "object" && value !== null && "resolveConfig" in value && typeof value.resolveConfig === "function" && "format" in value && typeof value.format === "function";
265
+ }
266
+ function loadProjectPrettier(cwd) {
267
+ try {
268
+ const module = createRequire(join2(cwd, "package.json"))("prettier");
269
+ return isPrettier(module) ? module : void 0;
270
+ } catch {
271
+ return void 0;
272
+ }
273
+ }
274
+ async function formatFile(prettier, path) {
275
+ const config = await prettier.resolveConfig(path);
276
+ if (config === null) return "unconfigured";
277
+ const source = await readFile2(path, "utf8");
278
+ await writeFile2(path, await prettier.format(source, { ...config, filepath: path }));
279
+ return "formatted";
280
+ }
281
+ async function tryFormatFile(prettier, cwd, path, context) {
282
+ try {
283
+ return await formatFile(prettier, path);
284
+ } catch (error) {
285
+ const reason = error instanceof Error ? error.message : String(error);
286
+ context.stderr(`warning: could not format ${displayPath(cwd, path)}: ${reason}`);
287
+ return "failed";
288
+ }
289
+ }
290
+ function hintFor(results) {
291
+ if (results.includes("formatted")) return FORMATTED_HINT;
292
+ return results.includes("failed") ? void 0 : UNFORMATTED_HINT;
293
+ }
294
+ async function formatWrittenFiles(cwd, records, context) {
295
+ const paths = records.filter(({ outcome }) => outcome !== "skipped").map(({ path }) => path);
296
+ if (paths.length === 0) return void 0;
297
+ const prettier = loadProjectPrettier(cwd);
298
+ const results = [];
299
+ for (const path of paths) {
300
+ results.push(prettier === void 0 ? "unconfigured" : await tryFormatFile(prettier, cwd, path, context));
301
+ }
302
+ return hintFor(results);
303
+ }
304
+ async function reportWrites(cwd, records, context, format) {
305
+ const hint = format ? await formatWrittenFiles(cwd, records, context) : void 0;
306
+ return [formatWrites(cwd, records), hint].filter(Boolean).join("\n");
307
+ }
308
+
309
+ // src/cli/packageInfo.ts
310
+ import { dirname as dirname2, join as join3 } from "path";
311
+ import { fileURLToPath } from "url";
312
+ async function findPackageRoot(directory) {
313
+ if (await isDirectory(join3(directory, "templates"))) return directory;
314
+ const parent = dirname2(directory);
315
+ if (parent === directory) throw new CliError("Cannot locate the package templates directory.");
316
+ return findPackageRoot(parent);
317
+ }
318
+ async function readPackageInfo() {
319
+ const root = await findPackageRoot(dirname2(fileURLToPath(import.meta.url)));
320
+ const manifest = await readJsonObject(join3(root, "package.json"));
321
+ const { name, version } = manifest ?? {};
322
+ if (typeof name !== "string" || typeof version !== "string") {
323
+ throw new CliError(`${toForwardSlashes(join3(root, "package.json"))} has no name and version.`);
324
+ }
325
+ return { root, name, version };
326
+ }
327
+
328
+ // src/cli/templates.ts
329
+ import { readdir, readFile as readFile3 } from "fs/promises";
330
+ import { join as join4 } from "path";
331
+
332
+ // src/cli/imports.ts
333
+ var TEMPLATE_IMPORT = /(from\s+)(['"])\.\.\/\.\.\/src\/(\w+)\2/g;
334
+ var AFTER_COPYING_NOTE = /^\/\/ After copying, change these imports.*\r?\n/m;
335
+ function importSpecifier(entry, importBase, packageName) {
336
+ if (importBase !== packageName) return `${importBase}/${entry}`;
337
+ return entry === "core" ? packageName : `${packageName}/${entry}`;
338
+ }
339
+ function rewriteTemplateImports(source, importBase, packageName) {
340
+ return source.replace(AFTER_COPYING_NOTE, "").replace(
341
+ TEMPLATE_IMPORT,
342
+ (_match, from, quote, entry) => `${from}${quote}${importSpecifier(entry, importBase, packageName)}${quote}`
343
+ );
344
+ }
345
+
346
+ // src/cli/suggest.ts
347
+ var MAX_SUGGESTIONS = 3;
348
+ function editDistance(a, b) {
349
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
350
+ for (let row = 1; row <= a.length; row++) {
351
+ const current = [row];
352
+ for (let column = 1; column <= b.length; column++) {
353
+ const substitution = previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1);
354
+ current.push(Math.min(substitution, previous[column] + 1, current[column - 1] + 1));
355
+ }
356
+ previous = current;
357
+ }
358
+ return previous[b.length];
359
+ }
360
+ function distanceBetween(target, candidate) {
361
+ const [shorter, longer] = target.length <= candidate.length ? [target, candidate] : [candidate, target];
362
+ return longer.includes(shorter) ? 0 : editDistance(target, candidate);
363
+ }
364
+ function closestMatches(target, candidates) {
365
+ const maxDistance = Math.max(2, Math.floor(target.length / 3));
366
+ return [...candidates].map((candidate) => ({ candidate, distance: distanceBetween(target.toLowerCase(), candidate.toLowerCase()) })).filter(({ distance }) => distance <= maxDistance).sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate)).slice(0, MAX_SUGGESTIONS).map(({ candidate }) => candidate);
367
+ }
368
+ function didYouMean(suggestions) {
369
+ return suggestions.length > 0 ? `Did you mean ${suggestions.map((key) => `"${key}"`).join(" or ")}?` : "";
370
+ }
371
+
372
+ // src/cli/templates.ts
373
+ var TEMPLATE_DIRECTORIES = { tailwind: "tailwind", mantine: "mantine", native: "expo" };
374
+ var TEMPLATE_FILE = /^(?!.*\.test\.)(.+)\.tsx$/;
375
+ var templateDirectory = ({ root }, style) => join4(root, "templates", TEMPLATE_DIRECTORIES[style]);
376
+ async function listTemplates(packageInfo, style) {
377
+ let files = [];
378
+ try {
379
+ files = await readdir(templateDirectory(packageInfo, style));
380
+ } catch (error) {
381
+ if (!isNotFound(error)) throw error;
382
+ }
383
+ const names = files.flatMap((file) => TEMPLATE_FILE.exec(file)?.[1] ?? []).sort();
384
+ if (names.length === 0) throw new CliError(`No templates are available for the "${style}" style.`);
385
+ return names;
386
+ }
387
+ var comparable = (name) => name.replace(/[-_]/g, "").toLowerCase();
388
+ function matchTemplateName(requested, available) {
389
+ const match = available.find((name) => comparable(name) === comparable(requested));
390
+ if (match !== void 0) return match;
391
+ const message = [`Unknown component "${requested}".`, didYouMean(closestMatches(requested, available)), `Available: ${available.join(", ")}.`];
392
+ throw new CliError(message.filter(Boolean).join(" "));
393
+ }
394
+ function matchTemplateNames(requested, available) {
395
+ return [...new Set(requested.map((name) => matchTemplateName(name, available)))];
396
+ }
397
+ async function copyTemplates(options) {
398
+ const { packageInfo, style, names, destination, importBase, force } = options;
399
+ const records = [];
400
+ for (const name of names) {
401
+ const source = await readFile3(join4(templateDirectory(packageInfo, style), `${name}.tsx`), "utf8");
402
+ const content = rewriteTemplateImports(source, importBase, packageInfo.name);
403
+ records.push(await writeFileSafely(join4(destination, `${name}.tsx`), content, force));
404
+ }
405
+ return records;
406
+ }
407
+
408
+ // src/cli/commands/add.ts
409
+ async function addCommand(args, context) {
410
+ const { values, positionals } = parseCommandArgs(args, {
411
+ cwd: { type: "string" },
412
+ all: { type: "boolean" },
413
+ list: { type: "boolean" },
414
+ force: { type: "boolean" },
415
+ style: { type: "string" },
416
+ "no-format": { type: "boolean" }
417
+ });
418
+ const cwd = resolveCwd(context, values.cwd);
419
+ const config = await readConfig(cwd);
420
+ const style = values.style === void 0 ? config.style : parseChoice("style", values.style, STYLES);
421
+ const packageInfo = await readPackageInfo();
422
+ const available = await listTemplates(packageInfo, style);
423
+ if (values.list) {
424
+ context.stdout(available.join("\n"));
425
+ return 0;
426
+ }
427
+ const names = values.all ? available : matchTemplateNames(positionals, available);
428
+ if (names.length === 0) {
429
+ throw new CliError(`Name the components to add, or use --all. Available: ${available.join(", ")}.`);
430
+ }
431
+ const records = await copyTemplates({
432
+ packageInfo,
433
+ style,
434
+ names,
435
+ destination: join5(cwd, config.componentsDir),
436
+ importBase: config.importBase,
437
+ force: values.force ?? false
438
+ });
439
+ context.stdout(await reportWrites(cwd, records, context, !values["no-format"]));
440
+ return 0;
441
+ }
442
+
443
+ // src/core/routes.ts
444
+ function isValidRoutePattern(pattern) {
445
+ if (!pattern) return false;
446
+ if (pattern === "*") return true;
447
+ const body = pattern.endsWith("/*") ? pattern.slice(0, -2) : pattern;
448
+ return !body.includes("*");
449
+ }
450
+
451
+ // src/core/define.ts
452
+ var KEY_PATTERN = /^[a-z0-9_]+(\.[a-z0-9_]+)*$/;
453
+
454
+ // src/cli/analyze.ts
455
+ var at = ({ file, line }) => ({ file, line });
456
+ var describeTour = (tour) => tour.key ? `tour "${tour.key.value}"` : "a tour";
457
+ function keyedPlaces(file, located) {
458
+ return located.map(({ value, line }) => ({ key: value, file, line }));
459
+ }
460
+ function groupBy(items, keyOf) {
461
+ const groups = /* @__PURE__ */ new Map();
462
+ for (const item of items) {
463
+ const group = groups.get(keyOf(item));
464
+ if (group) group.push(item);
465
+ else groups.set(keyOf(item), [item]);
466
+ }
467
+ return [...groups.values()];
468
+ }
469
+ var perPlatform = (build) => ({
470
+ web: build("web"),
471
+ native: build("native")
472
+ });
473
+ function declarationsOn(scan, platform) {
474
+ const places = scan.files.filter((file) => file.platforms.includes(platform)).flatMap((file) => keyedPlaces(file.path, file.anchorUses));
475
+ return new Map(groupBy(places, ({ key }) => key).map((group) => [group[0].key, group]));
476
+ }
477
+ function buildAnalysis(scan, config) {
478
+ return {
479
+ scan,
480
+ config,
481
+ tours: scan.files.flatMap((file) => file.tours.map((tour) => ({ ...tour, file: file.path }))),
482
+ catalogue: scan.files.flatMap((file) => keyedPlaces(file.path, file.catalogue)),
483
+ uses: scan.files.flatMap((file) => keyedPlaces(file.path, file.anchorUses)),
484
+ declarations: perPlatform((platform) => declarationsOn(scan, platform)),
485
+ filesByPlatform: perPlatform((platform) => scan.files.filter((file) => file.platforms.includes(platform)).length)
486
+ };
487
+ }
488
+ function targetedPlatforms(tour, config) {
489
+ return tour.platforms?.filter(isPlatform) ?? PLATFORMS.filter((platform) => config.platforms[platform].length > 0);
490
+ }
491
+ function occurrence(file, description, located, context = "") {
492
+ return located ? [{ key: located.value, file, line: located.line, description, context }] : [];
493
+ }
494
+ function tourKeyOccurrences({ tours }) {
495
+ return tours.flatMap((tour) => {
496
+ const context = ` in ${describeTour(tour)}`;
497
+ return [
498
+ ...occurrence(tour.file, "tour key", tour.key),
499
+ ...tour.steps.flatMap((step) => [
500
+ ...occurrence(tour.file, "step key", step.key, context),
501
+ ...occurrence(tour.file, "step anchor", step.anchor, context)
502
+ ])
503
+ ];
504
+ });
505
+ }
506
+ function invalidKeyFindings(analysis) {
507
+ const occurrences = [
508
+ ...tourKeyOccurrences(analysis),
509
+ ...analysis.catalogue.map((entry) => ({ ...entry, description: "catalogue anchor", context: "" })),
510
+ ...analysis.uses.map((use) => ({ ...use, description: "declared anchor", context: "" }))
511
+ ];
512
+ return occurrences.filter(({ key }) => !KEY_PATTERN.test(key)).map((entry) => ({
513
+ code: "key_invalid",
514
+ message: `Invalid ${entry.description} "${entry.key}"${entry.context}: use lowercase letters, digits and underscores, dot-separated.`,
515
+ ...at(entry)
516
+ }));
517
+ }
518
+ function invalidRouteFindings({ tours }) {
519
+ return tours.flatMap((tour) => [
520
+ ...occurrence(tour.file, "trigger route", tour.triggerRoute, ` of ${describeTour(tour)}`),
521
+ ...tour.steps.flatMap((step) => occurrence(tour.file, "step route", step.route, ` in ${describeTour(tour)}`))
522
+ ]).filter(({ key }) => !isValidRoutePattern(key)).map((entry) => ({
523
+ code: "route_invalid",
524
+ message: `Invalid ${entry.description} "${entry.key}"${entry.context}: use a path, a prefix ending in /*, or *.`,
525
+ ...at(entry)
526
+ }));
527
+ }
528
+ function duplicateFindings(code, what, places) {
529
+ return groupBy(places, ({ key }) => key).filter((group) => group.length > 1).flatMap(
530
+ ([first, ...repeats]) => repeats.map((repeat) => ({
531
+ code,
532
+ message: `Duplicate ${what}: "${repeat.key}" (first at ${first.file}:${first.line}).`,
533
+ ...at(repeat)
534
+ }))
535
+ );
536
+ }
537
+ function duplicateTourFindings({ tours }) {
538
+ const places = tours.flatMap((tour) => occurrence(tour.file, "tour key", tour.key));
539
+ return duplicateFindings("tour_duplicate", "tour key", places);
540
+ }
541
+ function duplicateStepFindings({ tours }) {
542
+ return tours.flatMap(
543
+ (tour) => duplicateFindings(
544
+ "step_duplicate",
545
+ `step key in ${describeTour(tour)}`,
546
+ tour.steps.flatMap((step) => occurrence(tour.file, "step key", step.key))
547
+ )
548
+ );
549
+ }
550
+ function missingAnchorFinding(analysis, tour, anchor, platform) {
551
+ const declaredHere = analysis.declarations[platform];
552
+ const elsewhere = PLATFORMS.filter((other) => other !== platform && analysis.declarations[other].has(anchor.value));
553
+ const suggestions = closestMatches(anchor.value, declaredHere.keys());
554
+ const message = [
555
+ `In ${describeTour(tour)}: anchor "${anchor.value}" is not declared on ${platform}.`,
556
+ elsewhere.length > 0 ? `It is declared on ${elsewhere.join(", ")}.` : "",
557
+ didYouMean(suggestions)
558
+ ];
559
+ return { code: "anchor_missing", message: message.filter(Boolean).join(" "), file: tour.file, line: anchor.line, suggestions };
560
+ }
561
+ function missingAnchorFindings(analysis) {
562
+ return analysis.tours.flatMap(
563
+ (tour) => targetedPlatforms(tour, analysis.config).filter((platform) => analysis.filesByPlatform[platform] > 0).flatMap(
564
+ (platform) => tour.steps.flatMap(
565
+ ({ anchor }) => anchor && !analysis.declarations[platform].has(anchor.value) ? [missingAnchorFinding(analysis, tour, anchor, platform)] : []
566
+ )
567
+ )
568
+ );
569
+ }
570
+ function uncataloguedFindings({ catalogue, uses }) {
571
+ const known = new Set(catalogue.map(({ key }) => key));
572
+ if (known.size === 0) return [];
573
+ return uses.filter(({ key }) => !known.has(key)).map((use) => {
574
+ const suggestions = closestMatches(use.key, known);
575
+ const message = `Anchor "${use.key}" is declared here but missing from defineAnchors([...]). ${didYouMean(suggestions)}`;
576
+ return { code: "anchor_uncatalogued", message: message.trim(), ...at(use), suggestions };
577
+ });
578
+ }
579
+ function missingRootFindings({ scan }) {
580
+ return scan.missingRoots.map((root) => ({
581
+ code: "source_root_missing",
582
+ message: `Scan root "${root}" from sapar.json is not a directory.`
583
+ }));
584
+ }
585
+ function unreadableFindings({ scan }) {
586
+ return scan.files.flatMap(
587
+ (file) => file.unverifiable.map(({ value, line }) => ({ code: "value_unverifiable", message: value, file: file.path, line }))
588
+ );
589
+ }
590
+ function undeclaredCatalogueFindings({ catalogue, uses }) {
591
+ const used = new Set(uses.map(({ key }) => key));
592
+ return catalogue.filter(({ key }) => !used.has(key)).map((entry) => ({
593
+ code: "anchor_undeclared",
594
+ message: `Anchor "${entry.key}" is in defineAnchors([...]) but no element declares it.`,
595
+ ...at(entry)
596
+ }));
597
+ }
598
+ function duplicateDeclarationFindings({ declarations }) {
599
+ return PLATFORMS.flatMap(
600
+ (platform) => duplicateFindings("anchor_duplicate", `anchor on ${platform}`, [...declarations[platform].values()].flat())
601
+ );
602
+ }
603
+ function platformFindings({ scan, config, tours, filesByPlatform }) {
604
+ if (scan.files.length === 0) {
605
+ return [
606
+ {
607
+ code: "nothing_scanned",
608
+ message: 'Scanned 0 files, so sapar check did not verify anything. Check "platforms" and "toursDir" in sapar.json.'
609
+ }
610
+ ];
611
+ }
612
+ const explicitTargets = new Set(tours.flatMap((tour) => tour.platforms ?? []));
613
+ return PLATFORMS.filter(
614
+ (platform) => filesByPlatform[platform] === 0 && (config.platforms[platform].length > 0 || explicitTargets.has(platform))
615
+ ).map((platform) => ({
616
+ code: "platform_unverified",
617
+ message: config.platforms[platform].length === 0 ? `"platforms.${platform}" in sapar.json has no scan roots, so tours targeting ${platform} were not verified.` : `No source files found under ${config.platforms[platform].join(", ")}, so nothing was verified for ${platform}.`
618
+ }));
619
+ }
620
+ function unusedAnchorFindings({ tours, uses }) {
621
+ const inTours = new Set(tours.flatMap((tour) => tour.steps.flatMap((step) => step.anchor?.value ?? [])));
622
+ return groupBy(uses, ({ key }) => key).filter(([first]) => !inTours.has(first.key)).map(([first]) => ({
623
+ code: "anchor_unused",
624
+ message: `Anchor "${first.key}" is declared but no tour uses it.`,
625
+ ...at(first)
626
+ }));
627
+ }
628
+ function analyze(scan, config, { verbose }) {
629
+ const analysis = buildAnalysis(scan, config);
630
+ return {
631
+ errors: [
632
+ ...missingRootFindings(analysis),
633
+ ...invalidKeyFindings(analysis),
634
+ ...invalidRouteFindings(analysis),
635
+ ...duplicateTourFindings(analysis),
636
+ ...duplicateStepFindings(analysis),
637
+ ...missingAnchorFindings(analysis),
638
+ ...uncataloguedFindings(analysis)
639
+ ],
640
+ warnings: [
641
+ ...platformFindings(analysis),
642
+ ...undeclaredCatalogueFindings(analysis),
643
+ ...duplicateDeclarationFindings(analysis),
644
+ ...unreadableFindings(analysis)
645
+ ],
646
+ info: verbose ? unusedAnchorFindings(analysis) : [],
647
+ stats: {
648
+ files: scan.files.length,
649
+ filesByPlatform: analysis.filesByPlatform,
650
+ tours: analysis.tours.length,
651
+ anchorsDeclared: analysis.uses.length,
652
+ anchorsCatalogued: analysis.catalogue.length
653
+ }
654
+ };
655
+ }
656
+
657
+ // src/cli/scan/project.ts
658
+ import { readFile as readFile4 } from "fs/promises";
659
+ import { join as join7 } from "path";
660
+
661
+ // src/cli/scan/ast.ts
662
+ function lineOf({ sourceFile }, node) {
663
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
664
+ }
665
+ function visit(typescript, node, callback) {
666
+ callback(node);
667
+ typescript.forEachChild(node, (child) => visit(typescript, child, callback));
668
+ }
669
+ function calleeName({ typescript }, call) {
670
+ return typescript.isIdentifier(call.expression) ? call.expression.text : void 0;
671
+ }
672
+
673
+ // src/cli/scan/constants.ts
674
+ function withoutWrappers(typescript, node) {
675
+ let current = node;
676
+ while (typescript.isParenthesizedExpression(current) || typescript.isAsExpression(current) || typescript.isSatisfiesExpression(current)) {
677
+ current = current.expression;
678
+ }
679
+ return current;
680
+ }
681
+ function literalString(typescript, node) {
682
+ const unwrapped = withoutWrappers(typescript, node);
683
+ return typescript.isStringLiteralLike(unwrapped) ? unwrapped.text : void 0;
684
+ }
685
+ function collectStringConstants(typescript, sourceFile) {
686
+ const constants = /* @__PURE__ */ new Map();
687
+ for (const statement of sourceFile.statements) {
688
+ if (!typescript.isVariableStatement(statement)) continue;
689
+ if (!(statement.declarationList.flags & typescript.NodeFlags.Const)) continue;
690
+ for (const { name, initializer } of statement.declarationList.declarations) {
691
+ const value = initializer && literalString(typescript, initializer);
692
+ if (typescript.isIdentifier(name) && value !== void 0) constants.set(name.text, value);
693
+ }
694
+ }
695
+ return constants;
696
+ }
697
+ function createSourceContext(typescript, path, text) {
698
+ const sourceFile = typescript.createSourceFile(path, text, typescript.ScriptTarget.Latest);
699
+ return { typescript, sourceFile, constants: collectStringConstants(typescript, sourceFile) };
700
+ }
701
+ function stringValue({ typescript, constants }, node) {
702
+ if (node === void 0) return void 0;
703
+ const unwrapped = withoutWrappers(typescript, node);
704
+ if (typescript.isIdentifier(unwrapped)) return constants.get(unwrapped.text);
705
+ return literalString(typescript, unwrapped);
706
+ }
707
+
708
+ // src/cli/scan/anchors.ts
709
+ var ANCHOR_HOOK = /^use\w*Anchor$/;
710
+ var ANCHOR_ATTRIBUTE = "data-sapar";
711
+ function isLogicalOperator({ typescript }, kind) {
712
+ const { AmpersandAmpersandToken, BarBarToken, QuestionQuestionToken } = typescript.SyntaxKind;
713
+ return kind === AmpersandAmpersandToken || kind === BarBarToken || kind === QuestionQuestionToken;
714
+ }
715
+ function literalsIn(context, node) {
716
+ const { typescript } = context;
717
+ const literal = stringValue(context, node);
718
+ if (literal !== void 0) return [literal];
719
+ if (node === void 0) return [];
720
+ if (typescript.isParenthesizedExpression(node)) return literalsIn(context, node.expression);
721
+ if (typescript.isConditionalExpression(node)) {
722
+ return [...literalsIn(context, node.whenTrue), ...literalsIn(context, node.whenFalse)];
723
+ }
724
+ if (typescript.isBinaryExpression(node) && isLogicalOperator(context, node.operatorToken.kind)) {
725
+ return [...literalsIn(context, node.left), ...literalsIn(context, node.right)];
726
+ }
727
+ return [];
728
+ }
729
+ function keysDeclaredBy(context, node) {
730
+ const { typescript } = context;
731
+ if (typescript.isCallExpression(node) && ANCHOR_HOOK.test(calleeName(context, node) ?? "")) {
732
+ return literalsIn(context, node.arguments[0]);
733
+ }
734
+ if (typescript.isJsxAttribute(node) && typescript.isIdentifier(node.name) && node.name.text === ANCHOR_ATTRIBUTE) {
735
+ const { initializer } = node;
736
+ return literalsIn(context, initializer && typescript.isJsxExpression(initializer) ? initializer.expression : initializer);
737
+ }
738
+ return [];
739
+ }
740
+ function findAnchorUses(context) {
741
+ const uses = [];
742
+ visit(context.typescript, context.sourceFile, (node) => {
743
+ keysDeclaredBy(context, node).forEach((key) => uses.push({ value: key, line: lineOf(context, node) }));
744
+ });
745
+ return uses;
746
+ }
747
+
748
+ // src/cli/scan/definitions.ts
749
+ var STATIC_VALUE_HINT = " (use a string literal or a const declared in this file).";
750
+ function noteUnverifiable(collector, node, explanation) {
751
+ collector.unverifiable.push({ value: explanation, line: lineOf(collector, node) });
752
+ }
753
+ function propertyValue(collector, object, name) {
754
+ const { typescript } = collector;
755
+ for (const property of object.properties) {
756
+ if (!typescript.isPropertyAssignment(property) && !typescript.isShorthandPropertyAssignment(property)) continue;
757
+ const propertyName = property.name;
758
+ const isMatch = (typescript.isIdentifier(propertyName) || typescript.isStringLiteralLike(propertyName)) && propertyName.text === name;
759
+ if (!isMatch) continue;
760
+ return typescript.isPropertyAssignment(property) ? property.initializer : property.name;
761
+ }
762
+ return void 0;
763
+ }
764
+ function readString2(collector, node, description) {
765
+ if (node === void 0) return void 0;
766
+ const value = stringValue(collector, node);
767
+ if (value === void 0) {
768
+ noteUnverifiable(collector, node, `The ${description} is not a string literal, so it was not verified${STATIC_VALUE_HINT}`);
769
+ return void 0;
770
+ }
771
+ return { value, line: lineOf(collector, node) };
772
+ }
773
+ function readStringProperty(collector, object, name, description) {
774
+ return readString2(collector, propertyValue(collector, object, name), description);
775
+ }
776
+ function readArray(collector, node, description) {
777
+ if (node === void 0) return [];
778
+ if (!collector.typescript.isArrayLiteralExpression(node)) {
779
+ noteUnverifiable(collector, node, `The ${description} is not an array literal, so it was not verified.`);
780
+ return [];
781
+ }
782
+ return node.elements;
783
+ }
784
+ function readStrings(collector, node, description) {
785
+ return readArray(collector, node, description).flatMap(
786
+ (element) => readString2(collector, element, `${description} entry`)?.value ?? []
787
+ );
788
+ }
789
+ function readStep(collector, node) {
790
+ if (!collector.typescript.isObjectLiteralExpression(node)) {
791
+ noteUnverifiable(collector, node, "A step is not an object literal, so it was not verified.");
792
+ return void 0;
793
+ }
794
+ return {
795
+ key: readStringProperty(collector, node, "key", "step key"),
796
+ anchor: readStringProperty(collector, node, "anchor", "step anchor"),
797
+ route: readStringProperty(collector, node, "route", "step route")
798
+ };
799
+ }
800
+ function readTriggerRoute(collector, trigger) {
801
+ if (trigger === void 0 || !collector.typescript.isObjectLiteralExpression(trigger)) return void 0;
802
+ return readStringProperty(collector, trigger, "route", "trigger route");
803
+ }
804
+ function readTour(collector, call) {
805
+ const [input] = call.arguments;
806
+ if (input === void 0 || !collector.typescript.isObjectLiteralExpression(input)) {
807
+ noteUnverifiable(collector, call, "defineTour() is not called with an object literal, so it was not verified.");
808
+ return void 0;
809
+ }
810
+ const platformsValue = propertyValue(collector, input, "platforms");
811
+ return {
812
+ line: lineOf(collector, call),
813
+ key: readStringProperty(collector, input, "key", "tour key"),
814
+ triggerRoute: readTriggerRoute(collector, propertyValue(collector, input, "trigger")),
815
+ platforms: platformsValue && readStrings(collector, platformsValue, "platforms"),
816
+ steps: readArray(collector, propertyValue(collector, input, "steps"), "steps").flatMap(
817
+ (step) => readStep(collector, step) ?? []
818
+ )
819
+ };
820
+ }
821
+ function readCatalogue(collector, call) {
822
+ return readArray(collector, call.arguments[0], "defineAnchors() argument").flatMap(
823
+ (element) => readString2(collector, element, "anchor key") ?? []
824
+ );
825
+ }
826
+ function findDefinitions(context) {
827
+ const collector = { ...context, unverifiable: [] };
828
+ const findings = { catalogue: [], tours: [], unverifiable: collector.unverifiable };
829
+ visit(context.typescript, context.sourceFile, (node) => {
830
+ if (!context.typescript.isCallExpression(node)) return;
831
+ const callee = calleeName(context, node);
832
+ if (callee === "defineAnchors") findings.catalogue.push(...readCatalogue(collector, node));
833
+ const tour = callee === "defineTour" ? readTour(collector, node) : void 0;
834
+ if (tour !== void 0) findings.tours.push(tour);
835
+ });
836
+ return findings;
837
+ }
838
+
839
+ // src/cli/scan/files.ts
840
+ import { readdir as readdir2 } from "fs/promises";
841
+ import { join as join6, resolve as resolve2 } from "path";
842
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage"]);
843
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
844
+ var TEST_FILE = /\.test\./;
845
+ function isScannedDirectory(name) {
846
+ return !name.startsWith(".") && !SKIPPED_DIRECTORIES.has(name);
847
+ }
848
+ async function listSourceFiles(directory) {
849
+ const entries = (await readdir2(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
850
+ const nested = await Promise.all(
851
+ entries.map(async (entry) => {
852
+ const path = join6(directory, entry.name);
853
+ if (entry.isDirectory()) return isScannedDirectory(entry.name) ? listSourceFiles(path) : [];
854
+ const isSource = entry.isFile() && SOURCE_FILE.test(entry.name) && !TEST_FILE.test(entry.name);
855
+ return isSource ? [path] : [];
856
+ })
857
+ );
858
+ return nested.flat();
859
+ }
860
+ function scanRoots(config) {
861
+ return [
862
+ ...PLATFORMS.flatMap((platform) => config.platforms[platform].map((directory) => ({ directory, platform }))),
863
+ { directory: config.toursDir }
864
+ ];
865
+ }
866
+ async function discoverSourceFiles(cwd, config) {
867
+ const platformsByFile = /* @__PURE__ */ new Map();
868
+ const missingRoots = [];
869
+ for (const { directory, platform } of scanRoots(config)) {
870
+ const absolute = resolve2(cwd, directory);
871
+ if (!await isDirectory(absolute)) {
872
+ missingRoots.push(directory);
873
+ continue;
874
+ }
875
+ for (const file of await listSourceFiles(absolute)) {
876
+ const platforms = platformsByFile.get(file) ?? /* @__PURE__ */ new Set();
877
+ if (platform !== void 0) platforms.add(platform);
878
+ platformsByFile.set(file, platforms);
879
+ }
880
+ }
881
+ const files = [...platformsByFile].map(([file, platforms]) => ({
882
+ path: displayPath(cwd, file),
883
+ platforms: [...platforms]
884
+ }));
885
+ return { files, missingRoots };
886
+ }
887
+
888
+ // src/cli/scan/project.ts
889
+ async function scanFile(typescript, cwd, path) {
890
+ const text = await readFile4(join7(cwd, path), "utf8");
891
+ const context = createSourceContext(typescript, path, text);
892
+ return { anchorUses: findAnchorUses(context), ...findDefinitions(context) };
893
+ }
894
+ async function scanProject(typescript, cwd, config) {
895
+ const { files: discovered, missingRoots } = await discoverSourceFiles(cwd, config);
896
+ const files = [];
897
+ for (const file of discovered) {
898
+ files.push({ ...file, ...await scanFile(typescript, cwd, file.path) });
899
+ }
900
+ return { files, missingRoots };
901
+ }
902
+
903
+ // src/cli/commands/check.ts
904
+ var OUTPUT_FORMATS = ["text", "json"];
905
+ async function loadTypescript() {
906
+ try {
907
+ const module = await import("typescript");
908
+ return module.default ?? module;
909
+ } catch {
910
+ throw new CliError("`sapar check` needs the `typescript` package. Install it as a dev dependency: pnpm add -D typescript");
911
+ }
912
+ }
913
+ var failed = ({ errors, warnings }, strict) => errors.length > 0 || strict && warnings.length > 0;
914
+ async function checkCommand(args, context) {
915
+ const { values } = parseCommandArgs(args, {
916
+ cwd: { type: "string" },
917
+ format: { type: "string", default: "text" },
918
+ strict: { type: "boolean" },
919
+ verbose: { type: "boolean" }
920
+ });
921
+ const format = parseChoice("format", values.format, OUTPUT_FORMATS);
922
+ const cwd = resolveCwd(context, values.cwd);
923
+ const config = await readConfig(cwd);
924
+ const scan = await scanProject(await loadTypescript(), cwd, config);
925
+ const report = analyze(scan, config, { verbose: values.verbose ?? false });
926
+ context.stdout(format === "json" ? formatCheckJson(report) : formatCheckText(report));
927
+ return failed(report, values.strict ?? false) ? 1 : 0;
928
+ }
929
+
930
+ // src/cli/commands/init.ts
931
+ import { join as join10 } from "path";
932
+
933
+ // src/cli/detect.ts
934
+ import { join as join8 } from "path";
935
+ var DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
936
+ function dependencyNames(manifest) {
937
+ return DEPENDENCY_FIELDS.flatMap((field) => {
938
+ const entries = manifest[field];
939
+ return typeof entries === "object" && entries !== null ? Object.keys(entries) : [];
940
+ });
941
+ }
942
+ async function readProject(cwd) {
943
+ const manifest = await readJsonObject(join8(cwd, "package.json"));
944
+ if (manifest === void 0) throw new CliError(`No package.json in ${toForwardSlashes(cwd)}. Run this from your project root.`);
945
+ return {
946
+ dependencies: new Set(dependencyNames(manifest)),
947
+ hasSrcDir: await isDirectory(join8(cwd, "src"))
948
+ };
949
+ }
950
+ function detectFramework({ dependencies }) {
951
+ if (dependencies.has("next")) return "next";
952
+ if (dependencies.has("expo")) return "expo";
953
+ return "react";
954
+ }
955
+ function detectStyle({ dependencies }, framework) {
956
+ if (framework === "expo") return "native";
957
+ return dependencies.has("@mantine/core") ? "mantine" : "tailwind";
958
+ }
959
+ function detectSource({ dependencies }) {
960
+ return dependencies.has("@reduxjs/toolkit") ? "rtk" : "memory";
961
+ }
962
+ function defaultDirectories({ hasSrcDir }) {
963
+ const base = hasSrcDir ? "src/" : "";
964
+ return { componentsDir: `${base}components/tours`, toursDir: `${base}tours` };
965
+ }
966
+ function defaultPlatforms({ hasSrcDir }, framework) {
967
+ const roots = [hasSrcDir ? "src" : "."];
968
+ const platform = platformOf(framework);
969
+ return { web: platform === "web" ? roots : [], native: platform === "native" ? roots : [] };
970
+ }
971
+
972
+ // src/cli/scaffold.ts
973
+ import { join as join9 } from "path";
974
+ var HTTP_BASE_URLS = { web: "/api/tours", native: "https://api.example.com/tours" };
975
+ function generateDefinitions(config, specifier) {
976
+ return `import { defineAnchors, defineTour } from '${specifier("core")}';
977
+
978
+ export const anchors = defineAnchors(['welcome.header']);
979
+
980
+ export const welcomeTour = defineTour({
981
+ key: 'welcome',
982
+ title: 'Welcome',
983
+ platforms: ['${platformOf(config.framework)}'],
984
+ steps: [
985
+ { key: 'hello', title: 'Welcome aboard', body: 'A quick look around.' },
986
+ {
987
+ key: 'header',
988
+ anchor: 'welcome.header',
989
+ title: 'Your header',
990
+ body: 'Tours can point at any element you mark with an anchor.',
991
+ },
992
+ ],
993
+ });
994
+
995
+ export const tours = [welcomeTour];
996
+ `;
997
+ }
998
+ function generateAnchors(config, specifier) {
999
+ const anchorEntry = platformOf(config.framework) === "native" ? "expo" : "web";
1000
+ return `import type { AnchorKey } from '${specifier("core")}';
1001
+ import { useTourAnchor } from '${specifier(anchorEntry)}';
1002
+ import type { anchors } from './definitions';
1003
+
1004
+ /** Mark the element a step points at: \`<button {...useAnchor('welcome.header')} />\`. */
1005
+ export function useAnchor(key: AnchorKey<typeof anchors>) {
1006
+ return useTourAnchor(key);
1007
+ }
1008
+ `;
1009
+ }
1010
+ function memorySetup(config, specifier) {
1011
+ const isExpo = config.framework === "expo";
1012
+ const storage = isExpo ? { named: { module: specifier("expo"), names: ["asyncStorageStore"] }, expression: "asyncStorageStore(AsyncStorage)" } : { named: { module: specifier("web"), names: ["localStorageStore"] }, expression: "localStorageStore()" };
1013
+ return {
1014
+ named: [{ module: specifier("core"), names: ["memorySource"] }, storage.named],
1015
+ otherImports: [
1016
+ ...isExpo ? [`import AsyncStorage from '@react-native-async-storage/async-storage';`] : [],
1017
+ `import { tours } from './definitions';`
1018
+ ],
1019
+ moduleScope: [],
1020
+ reactHooks: ["useMemo"],
1021
+ insideComponent: [
1022
+ "// A string key, so a `new Date(...)` passed on every render does not recreate the source.",
1023
+ "const registeredAtIso = registeredAt === undefined ? undefined : new Date(registeredAt).toISOString();",
1024
+ "// A new user is a new source on purpose: each gets their own namespace and grace period.",
1025
+ "const source = useMemo(",
1026
+ " () =>",
1027
+ " memorySource({",
1028
+ " tours,",
1029
+ ` storage: ${storage.expression},`,
1030
+ " namespace: `app.sapar.${userId}`,",
1031
+ " registeredAt: registeredAtIso,",
1032
+ " }),",
1033
+ " [userId, registeredAtIso],",
1034
+ ");"
1035
+ ],
1036
+ componentProps: [
1037
+ { name: "userId", type: "string" },
1038
+ { name: "registeredAt?", type: "Date | string" }
1039
+ ]
1040
+ };
1041
+ }
1042
+ function httpSetup(config, specifier) {
1043
+ const baseUrl = HTTP_BASE_URLS[platformOf(config.framework)];
1044
+ return {
1045
+ named: [{ module: specifier("core"), names: ["httpSource"] }],
1046
+ otherImports: [],
1047
+ moduleScope: [`const source = httpSource({ baseUrl: '${baseUrl}' });`],
1048
+ insideComponent: [],
1049
+ reactHooks: [],
1050
+ componentProps: []
1051
+ };
1052
+ }
1053
+ function rtkSetup(specifier) {
1054
+ return {
1055
+ named: [{ module: specifier("rtk"), names: ["createSaparRtk"] }],
1056
+ otherImports: ["// TODO: point this at your RTK Query api slice", `import { baseApi } from '@/store/api';`],
1057
+ moduleScope: ["const toursRtk = createSaparRtk(baseApi);"],
1058
+ insideComponent: ["const source = toursRtk.useSaparSource();"],
1059
+ reactHooks: [],
1060
+ componentProps: []
1061
+ };
1062
+ }
1063
+ function sourceSetup(config, specifier) {
1064
+ if (config.source === "http") return httpSetup(config, specifier);
1065
+ if (config.source === "rtk") return rtkSetup(specifier);
1066
+ return memorySetup(config, specifier);
1067
+ }
1068
+ function providerSetup(config, specifier) {
1069
+ if (config.framework === "next") {
1070
+ return {
1071
+ named: [{ module: specifier("next"), names: ["NextSaparProvider"] }],
1072
+ moduleScope: [],
1073
+ tag: "NextSaparProvider",
1074
+ props: "source={source}"
1075
+ };
1076
+ }
1077
+ if (config.framework === "expo") {
1078
+ return {
1079
+ named: [{ module: specifier("expo-router"), names: ["ExpoSaparProvider"] }],
1080
+ moduleScope: [],
1081
+ tag: "ExpoSaparProvider",
1082
+ props: "source={source}"
1083
+ };
1084
+ }
1085
+ return {
1086
+ named: [
1087
+ { module: specifier("react"), names: ["SaparProvider"] },
1088
+ { module: specifier("web"), names: ["createWebAdapter"] }
1089
+ ],
1090
+ moduleScope: [
1091
+ [
1092
+ "// TODO: use your router here, and pass `route` to <SaparProvider> to enable route triggers",
1093
+ "const adapter = createWebAdapter({",
1094
+ ` navigate: (route) => window.history.pushState(null, '', route),`,
1095
+ " currentRoute: () => window.location.pathname,",
1096
+ "});"
1097
+ ].join("\n")
1098
+ ],
1099
+ tag: "SaparProvider",
1100
+ props: "source={source} adapter={adapter}"
1101
+ };
1102
+ }
1103
+ function renderNamedImports(imports) {
1104
+ const namesByModule = /* @__PURE__ */ new Map();
1105
+ for (const { module, names } of imports) namesByModule.set(module, [...namesByModule.get(module) ?? [], ...names]);
1106
+ return [...namesByModule].map(([module, names]) => `import { ${names.sort().join(", ")} } from '${module}';`);
1107
+ }
1108
+ var indent = (lines) => lines.map((line) => ` ${line}`);
1109
+ function renderComponentParameters(props) {
1110
+ const all = [...props, { name: "children", type: "ReactNode" }];
1111
+ const names = all.map(({ name }) => name.replace("?", "")).join(", ");
1112
+ const types = all.map(({ name, type }) => `${name}: ${type}`).join("; ");
1113
+ return `{ ${names} }: { ${types} }`;
1114
+ }
1115
+ function reactImport(hooks) {
1116
+ return hooks.length === 0 ? "import type { ReactNode } from 'react';" : `import { ${hooks.join(", ")}, type ReactNode } from 'react';`;
1117
+ }
1118
+ function generateProvider(config, specifier) {
1119
+ const provider = providerSetup(config, specifier);
1120
+ const source = sourceSetup(config, specifier);
1121
+ const directive = config.framework === "next" ? ["'use client';", ""] : [];
1122
+ const imports = [
1123
+ reactImport(source.reactHooks),
1124
+ ...renderNamedImports([...provider.named, ...source.named]),
1125
+ ...source.otherImports
1126
+ ];
1127
+ const moduleScope = [...provider.moduleScope, ...source.moduleScope];
1128
+ return `${[
1129
+ ...directive,
1130
+ ...imports,
1131
+ "",
1132
+ ...moduleScope.flatMap((block) => [block, ""]),
1133
+ `export function AppSaparProvider(${renderComponentParameters(source.componentProps)}) {`,
1134
+ ...indent(source.insideComponent),
1135
+ ...indent([
1136
+ "return (",
1137
+ ` <${provider.tag} ${provider.props}>{children}</${provider.tag}>`,
1138
+ ");"
1139
+ ]),
1140
+ "}"
1141
+ ].join("\n")}
1142
+ `;
1143
+ }
1144
+ function scaffoldFiles(config, packageName) {
1145
+ const specifier = (entry) => importSpecifier(entry, config.importBase, packageName);
1146
+ return [
1147
+ { path: join9(config.toursDir, "definitions.ts"), content: generateDefinitions(config, specifier) },
1148
+ { path: join9(config.toursDir, "anchors.ts"), content: generateAnchors(config, specifier) },
1149
+ { path: join9(config.toursDir, "SaparProvider.tsx"), content: generateProvider(config, specifier) }
1150
+ ];
1151
+ }
1152
+
1153
+ // src/cli/commands/init.ts
1154
+ var STARTER_COMPONENT = "TourCard";
1155
+ function parseText(raw) {
1156
+ if (raw.trim() === "") throw new CliError("The value cannot be empty.");
1157
+ return raw.trim();
1158
+ }
1159
+ var choiceSetting = (label, choices, flag, detected) => ({
1160
+ question: `${label} (${choices.join("/")})`,
1161
+ flag,
1162
+ detected,
1163
+ parse: (raw) => parseChoice(label, raw, choices)
1164
+ });
1165
+ async function resolveSetting({ question, flag, detected, parse }, context, prompt) {
1166
+ if (flag !== void 0) return parse(flag);
1167
+ if (prompt === void 0) return detected;
1168
+ for (; ; ) {
1169
+ try {
1170
+ return parse(await prompt(question, detected));
1171
+ } catch (error) {
1172
+ if (!(error instanceof CliError)) throw error;
1173
+ context.stderr(error.message);
1174
+ }
1175
+ }
1176
+ }
1177
+ async function resolveConfig(flags, cwd, packageName, context, prompt) {
1178
+ const project = await readProject(cwd);
1179
+ const framework = await resolveSetting(
1180
+ choiceSetting("framework", FRAMEWORKS, flags.framework, detectFramework(project)),
1181
+ context,
1182
+ prompt
1183
+ );
1184
+ const style = await resolveSetting(
1185
+ choiceSetting("style", STYLES, flags.style, detectStyle(project, framework)),
1186
+ context,
1187
+ prompt
1188
+ );
1189
+ const source = await resolveSetting(
1190
+ choiceSetting("source", SOURCES, flags.source, detectSource(project)),
1191
+ context,
1192
+ prompt
1193
+ );
1194
+ const importBase = await resolveSetting(
1195
+ { question: "import base", flag: flags.importBase, detected: packageName, parse: parseText },
1196
+ context,
1197
+ prompt
1198
+ );
1199
+ const directories = defaultDirectories(project);
1200
+ const componentsDir = await resolveSetting(
1201
+ { question: "components dir", flag: flags.componentsDir, detected: directories.componentsDir, parse: parseText },
1202
+ context,
1203
+ prompt
1204
+ );
1205
+ const toursDir = await resolveSetting(
1206
+ { question: "tours dir", flag: flags.toursDir, detected: directories.toursDir, parse: parseText },
1207
+ context,
1208
+ prompt
1209
+ );
1210
+ assertStyleMatchesFramework(framework, style);
1211
+ return {
1212
+ framework,
1213
+ style,
1214
+ source,
1215
+ componentsDir,
1216
+ toursDir,
1217
+ importBase,
1218
+ platforms: defaultPlatforms(project, framework)
1219
+ };
1220
+ }
1221
+ function nextSteps(config) {
1222
+ const providerPath = displayJoin([config.toursDir, "SaparProvider.tsx"]);
1223
+ const providerStep = {
1224
+ rtk: `Point the baseApi import in ${providerPath} at your RTK Query slice, and mount <AppSaparProvider /> inside the Redux <Provider>.`,
1225
+ http: `Mount <AppSaparProvider /> from ${providerPath} near the root of your app.`,
1226
+ memory: `Mount <AppSaparProvider userId={...} registeredAt={...} /> from ${providerPath} near the root of your app: pass the signed-in user's id (it namespaces their saved progress) and when they registered (it enables the new-user grace period).`
1227
+ }[config.source];
1228
+ return [
1229
+ "",
1230
+ "Next steps:",
1231
+ ` 1. ${providerStep}`,
1232
+ ` 2. Render <${STARTER_COMPONENT} /> from ${config.componentsDir} inside it.`,
1233
+ ` 3. Mark the element a step points at with useAnchor('welcome.header') from ${displayJoin([config.toursDir, "anchors"])}.`,
1234
+ " 4. Run `sapar check` in CI to catch anchors that go missing."
1235
+ ].join("\n");
1236
+ }
1237
+ async function initCommand(args, context) {
1238
+ const { values } = parseCommandArgs(args, {
1239
+ cwd: { type: "string" },
1240
+ yes: { type: "boolean" },
1241
+ force: { type: "boolean" },
1242
+ framework: { type: "string" },
1243
+ style: { type: "string" },
1244
+ source: { type: "string" },
1245
+ "import-base": { type: "string" },
1246
+ "components-dir": { type: "string" },
1247
+ "tours-dir": { type: "string" },
1248
+ "no-format": { type: "boolean" }
1249
+ });
1250
+ const cwd = resolveCwd(context, values.cwd);
1251
+ const force = values.force ?? false;
1252
+ const packageInfo = await readPackageInfo();
1253
+ const config = await resolveConfig(
1254
+ {
1255
+ framework: values.framework,
1256
+ style: values.style,
1257
+ source: values.source,
1258
+ importBase: values["import-base"],
1259
+ componentsDir: values["components-dir"],
1260
+ toursDir: values["tours-dir"]
1261
+ },
1262
+ cwd,
1263
+ packageInfo.name,
1264
+ context,
1265
+ values.yes ? void 0 : context.prompt
1266
+ );
1267
+ const records = [await writeConfig(cwd, config, force)];
1268
+ for (const file of scaffoldFiles(config, packageInfo.name)) {
1269
+ records.push(await writeFileSafely(join10(cwd, file.path), file.content, force));
1270
+ }
1271
+ records.push(
1272
+ ...await copyTemplates({
1273
+ packageInfo,
1274
+ style: config.style,
1275
+ names: [STARTER_COMPONENT],
1276
+ destination: join10(cwd, config.componentsDir),
1277
+ importBase: config.importBase,
1278
+ force
1279
+ })
1280
+ );
1281
+ context.stdout(`${await reportWrites(cwd, records, context, !values["no-format"])}
1282
+ ${nextSteps(config)}`);
1283
+ return 0;
1284
+ }
1285
+
1286
+ // src/cli/main.ts
1287
+ var COMMANDS = /* @__PURE__ */ new Map([
1288
+ ["init", { run: initCommand, help: INIT_HELP }],
1289
+ ["add", { run: addCommand, help: ADD_HELP }],
1290
+ ["check", { run: checkCommand, help: CHECK_HELP }]
1291
+ ]);
1292
+ var isHelpFlag = (arg) => arg === "--help" || arg === "-h";
1293
+ async function dispatch(argv, context) {
1294
+ const [name, ...args] = argv;
1295
+ if (name === "--version" || name === "-v") {
1296
+ context.stdout((await readPackageInfo()).version);
1297
+ return 0;
1298
+ }
1299
+ if (isHelpFlag(name)) {
1300
+ context.stdout(GENERAL_HELP);
1301
+ return 0;
1302
+ }
1303
+ const command = name === void 0 ? void 0 : COMMANDS.get(name);
1304
+ if (command === void 0) {
1305
+ context.stderr(name === void 0 ? GENERAL_HELP : `Unknown command "${name}".
1306
+
1307
+ ${GENERAL_HELP}`);
1308
+ return 1;
1309
+ }
1310
+ if (args.some(isHelpFlag)) {
1311
+ context.stdout(command.help);
1312
+ return 0;
1313
+ }
1314
+ return command.run(args, context);
1315
+ }
1316
+ async function runCli(argv, context) {
1317
+ try {
1318
+ return await dispatch(argv, context);
1319
+ } catch (error) {
1320
+ if (!(error instanceof CliError)) throw error;
1321
+ context.stderr(`error: ${error.message}`);
1322
+ return 1;
1323
+ }
1324
+ }
1325
+
1326
+ // src/cli/index.ts
1327
+ var prompter = process.stdin.isTTY ? createPrompter() : void 0;
1328
+ process.exitCode = await runCli(process.argv.slice(2), {
1329
+ cwd: process.cwd(),
1330
+ stdout: (line) => process.stdout.write(`${line}
1331
+ `),
1332
+ stderr: (line) => process.stderr.write(`${line}
1333
+ `),
1334
+ ...prompter && { prompt: prompter.ask }
1335
+ }).finally(() => prompter?.close());