@haystackeditor/cli 0.24.1 → 0.25.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.
Files changed (51) hide show
  1. package/dist/assets/capture/capture.cb4204fcc997d8e8.js +2 -0
  2. package/dist/assets/capture/release.json +4 -0
  3. package/dist/assets/telemetry/runtime.cjs +829 -1254
  4. package/dist/capture/adapters/client-routes.js +383 -0
  5. package/dist/capture/adapters/django.js +134 -0
  6. package/dist/capture/adapters/files.js +77 -0
  7. package/dist/capture/adapters/index.js +74 -0
  8. package/dist/capture/adapters/jsx-edit.js +81 -0
  9. package/dist/capture/adapters/next-build.js +113 -0
  10. package/dist/capture/adapters/next.js +494 -0
  11. package/dist/capture/adapters/nuxt.js +199 -0
  12. package/dist/capture/adapters/rails.js +178 -0
  13. package/dist/capture/adapters/react-router.js +439 -0
  14. package/dist/capture/adapters/sveltekit.js +109 -0
  15. package/dist/capture/adapters/types.js +4 -0
  16. package/dist/capture/adapters/vite.js +135 -0
  17. package/dist/capture/app-config.js +107 -0
  18. package/dist/capture/consent.js +127 -0
  19. package/dist/capture/csp.js +332 -0
  20. package/dist/capture/html.js +74 -0
  21. package/dist/capture/js-ast.js +400 -0
  22. package/dist/capture/manifest.js +95 -0
  23. package/dist/capture/project.js +177 -0
  24. package/dist/capture/route-pattern.js +119 -0
  25. package/dist/capture/script-release.js +47 -0
  26. package/dist/capture/tag.js +74 -0
  27. package/dist/capture/url-rewrites.js +232 -0
  28. package/dist/capture-step.js +56 -0
  29. package/dist/commands/capture-brief.js +92 -0
  30. package/dist/commands/capture-contract.js +46 -0
  31. package/dist/commands/capture-manifest.js +86 -0
  32. package/dist/commands/init-capture.js +426 -0
  33. package/dist/commands/init-telemetry.js +1028 -0
  34. package/dist/commands/init.js +78 -5
  35. package/dist/commands/server-telemetry-contract.d.ts +66 -0
  36. package/dist/commands/server-telemetry-contract.js +127 -0
  37. package/dist/commands/telemetry-token.js +238 -0
  38. package/dist/commands/telemetry.d.ts +161 -8
  39. package/dist/commands/telemetry.js +940 -158
  40. package/dist/commands/verify-onboarding.js +21 -1
  41. package/dist/commands/verify.js +56 -9
  42. package/dist/index.js +85 -6
  43. package/dist/schema.js +2 -2
  44. package/dist/telemetry/next-loader.cjs +66 -9
  45. package/dist/telemetry/next.d.ts +11 -3
  46. package/dist/telemetry/next.js +95 -15
  47. package/dist/telemetry/typed-source.d.ts +47 -0
  48. package/dist/telemetry/typed-source.js +379 -0
  49. package/package.json +4 -2
  50. package/schemas/init.v1.json +63 -4
  51. package/schemas/pre-verify.v1.json +60 -3
@@ -0,0 +1,81 @@
1
+ /** Placing init's tags in a JSX document (Next.js layouts and _app, React Router / Remix root): exactly once, replacing
2
+ * an earlier version in place, and checked by parsing the result. */
3
+ import { applyEdits, jsxName, jsxStringAttribute, lineIndent, parseFile, startsLine, walk } from '../js-ast.js';
4
+ import { ourScriptKind } from '../tag.js';
5
+ export function jsxElementsNamed(ast, name) {
6
+ const found = [];
7
+ walk(ast.program, node => { if (node.type === 'JSXElement' && jsxName(node) === name)
8
+ found.push(node); });
9
+ return found;
10
+ }
11
+ /** init's own script elements: one of `components` (Script, script) whose src is one of init's files. */
12
+ export function ourJsxElements(ast, components) {
13
+ const found = [];
14
+ walk(ast.program, node => {
15
+ if (node.type !== 'JSXElement')
16
+ return;
17
+ const name = jsxName(node);
18
+ const src = name !== null && components.includes(name) ? jsxStringAttribute(node, 'src') : null;
19
+ if (src !== null && ourScriptKind(src) !== null)
20
+ found.push(node);
21
+ });
22
+ return found;
23
+ }
24
+ function lineRange(code, node) {
25
+ const lineStart = code.lastIndexOf('\n', node.start - 1) + 1;
26
+ const lineEnd = code.indexOf('\n', node.end);
27
+ const end = lineEnd === -1 ? code.length : lineEnd + 1;
28
+ return code.slice(lineStart, node.start).trim() === '' && code.slice(node.end, end).trim() === ''
29
+ ? { start: lineStart, end } : { start: node.start, end: node.end };
30
+ }
31
+ /**
32
+ * Edits that leave exactly `tags` as init's elements: none when they are already there; the first earlier element
33
+ * replaced in place and the others removed when they differ; else inserted before `closingStart` (a `</body>`, `</head>`
34
+ * or `</>`), one per line at the children's indentation when the closing tag starts its own line.
35
+ */
36
+ export function placeTags(code, ours, closingStart, tags, unit) {
37
+ if (ours.length > 0) {
38
+ const current = ours.map(node => code.slice(node.start, node.end));
39
+ if (current.length === tags.length && current.every((text, index) => text === tags[index]))
40
+ return [];
41
+ const indent = lineIndent(code, ours[0].start);
42
+ return [
43
+ { start: ours[0].start, end: ours[0].end, text: tags.join(`\n${indent}`) },
44
+ ...ours.slice(1).map(node => ({ ...lineRange(code, node), text: '' })),
45
+ ];
46
+ }
47
+ if (closingStart === null)
48
+ return { manual: 'there is no place for the tag' };
49
+ if (startsLine(code, closingStart)) {
50
+ const indent = lineIndent(code, closingStart) + unit;
51
+ const lineStart = code.lastIndexOf('\n', closingStart - 1) + 1;
52
+ return [{ start: lineStart, end: lineStart, text: tags.map(tag => `${indent}${tag}\n`).join('') }];
53
+ }
54
+ return [{ start: closingStart, end: closingStart, text: tags.join('') }];
55
+ }
56
+ /** Applies the edits and proves the result: it parses, and init's elements in it are exactly `tags`. */
57
+ export function checkedResult(code, file, edits, components, tags) {
58
+ const next = applyEdits(code, edits);
59
+ const ast = parseFile(next, file);
60
+ if (!ast)
61
+ return { manual: 'the edited file would not parse' };
62
+ const placed = ourJsxElements(ast, components).map(node => next.slice(node.start, node.end));
63
+ if (placed.length !== tags.length || placed.some((text, index) => text !== tags[index])) {
64
+ return { manual: 'the edit did not leave exactly one tag' };
65
+ }
66
+ return next;
67
+ }
68
+ /** `text` with every line after the first indented by one more `unit`, or null when the text holds a multi-line template
69
+ * literal or string (a JSX attribute string may span lines), whose content would change. JSX text is insensitive to the
70
+ * indentation of its lines. */
71
+ export function reindented(node, code, unit) {
72
+ let multilineTemplate = false;
73
+ walk(node, inner => {
74
+ if ((inner.type === 'TemplateLiteral' || inner.type === 'StringLiteral') && code.slice(inner.start, inner.end).includes('\n')) {
75
+ multilineTemplate = true;
76
+ }
77
+ });
78
+ if (multilineTemplate)
79
+ return null;
80
+ return code.slice(node.start, node.end).split('\n').map((line, index) => index === 0 || line === '' ? line : `${unit}${line}`).join('\n');
81
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * What a Next.js production build says about its routes (CAPTURE-V1 rule 4), read after `next build` from its own output,
3
+ * so wrappers, conditionals and environment-set options (`basePath: process.env.BASE_PATH`, `output: isVercel ? … : …`)
4
+ * are what this build actually used. Formats as Next 13–16 write them (packages/next/src/build/index.ts,
5
+ * generate-routes-manifest.ts, shared/lib/router/utils/app-paths.ts):
6
+ * <distDir>/routes-manifest.json { version: 3, basePath, i18n?, rewrites: { beforeFiles, afterFiles, fallback }
7
+ * (an array before Next 10), staticRoutes, dynamicRoutes, … }
8
+ * <distDir>/required-server-files.json { version: 1, config: the resolved config (pageExtensions, output, assetPrefix,
9
+ * basePath, i18n, …), appDir, relativeAppDir, files, ignore }
10
+ * <distDir>/app-path-routes-manifest.json { "/(group)/x/[id]/page": "/x/[id]", … } (app router)
11
+ * <distDir>/server/app-paths-manifest.json { "/(group)/x/[id]/page": "app/(group)/x/[id]/page.js", … } (app router)
12
+ * <distDir>/server/pages-manifest.json { "/about": "pages/about.js", "/_app": "pages/_app.js", … } (pages router)
13
+ * <distDir>/export-detail.json { version: 1, outDirectory, success } (export/index.ts): written by a static
14
+ * export (`output: 'export'`) inside the build, removed by any other build
15
+ * <distDir>/BUILD_ID
16
+ */
17
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
18
+ import { isAbsolute, join, relative } from 'node:path';
19
+ import { isDirectory, isFile } from './files.js';
20
+ /** Next's default pageExtensions (config-shared.ts), in its order of precedence. */
21
+ export const DEFAULT_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];
22
+ function json(file) {
23
+ if (!isFile(file))
24
+ return null;
25
+ return JSON.parse(readFileSync(file, 'utf8'));
26
+ }
27
+ /** The build directory: .next when it holds a build, else the one top-level directory that does (a custom distDir),
28
+ * the newest when several do (the build that just ran). */
29
+ function buildDir(appDir) {
30
+ const holdsBuild = (dir) => isFile(join(dir, 'routes-manifest.json')) && isFile(join(dir, 'BUILD_ID'));
31
+ if (holdsBuild(join(appDir, '.next')))
32
+ return join(appDir, '.next');
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(appDir);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ const builds = entries.filter(entry => entry !== 'node_modules').map(entry => join(appDir, entry))
41
+ .filter(dir => isDirectory(dir) && holdsBuild(dir))
42
+ .sort((a, b) => statSync(join(b, 'BUILD_ID')).mtimeMs - statSync(join(a, 'BUILD_ID')).mtimeMs);
43
+ return builds[0] ?? null;
44
+ }
45
+ /** Next's normalizeAppPath: route groups and @slots add no segment, and the trailing /page or /route goes. */
46
+ export function normalizeAppPath(key) {
47
+ const segments = key.split('/');
48
+ const kept = segments.filter((segment, index) => segment !== '' && !(segment.startsWith('(') && segment.endsWith(')'))
49
+ && !segment.startsWith('@') && !((segment === 'page' || segment === 'route') && index === segments.length - 1));
50
+ return `/${kept.join('/')}`;
51
+ }
52
+ function pageRewrites(rewrites, basePath) {
53
+ const list = Array.isArray(rewrites) ? rewrites
54
+ : rewrites && typeof rewrites === 'object'
55
+ ? ['beforeFiles', 'afterFiles', 'fallback'].flatMap(phase => rewrites[phase] ?? []) : [];
56
+ return list.filter(rewrite => {
57
+ if (typeof rewrite.destination !== 'string' || !rewrite.destination.startsWith('/'))
58
+ return false;
59
+ // Destinations carry the basePath; API routes, Next's own files and capture's own files (the origin-root routing a
60
+ // basePath build asks for) are not pages.
61
+ const path = basePath !== '' && rewrite.destination.startsWith(`${basePath}/`) ? rewrite.destination.slice(basePath.length) : rewrite.destination;
62
+ if (path === '/api' || ['/api/', '/_next/', '/_haystack/', '/.well-known/haystack-capture/'].some(prefix => path.startsWith(prefix)))
63
+ return false;
64
+ // Intercepting routes' own rewrites (matched on the Next-Url header) keep the URL's route.
65
+ const has = Array.isArray(rewrite.has) ? rewrite.has : [];
66
+ return !has.some(condition => condition.type === 'header' && typeof condition.key === 'string' && condition.key.toLowerCase() === 'next-url');
67
+ }).map(rewrite => ({ source: String(rewrite.source), destination: String(rewrite.destination) }));
68
+ }
69
+ /** The app's production build as Next wrote it, or why there is none to read. */
70
+ export function readNextBuild(appDir) {
71
+ const dir = buildDir(appDir);
72
+ if (dir === null)
73
+ return { manual: 'there is no Next.js build output (.next/routes-manifest.json) to read the routes from: run it after `next build`' };
74
+ const routes = json(join(dir, 'routes-manifest.json'));
75
+ if (routes === null)
76
+ return { manual: `${dir} has no routes-manifest.json` };
77
+ const required = json(join(dir, 'required-server-files.json'));
78
+ // The export this build wrote (every build removes the file before it exports, and a finished export says success),
79
+ // wherever it went: out/, or a custom distDir that `output: 'export'` turns into the export directory.
80
+ const exported = json(join(dir, 'export-detail.json'));
81
+ const exportPath = exported?.success === true && typeof exported.outDirectory === 'string'
82
+ ? (isAbsolute(exported.outDirectory) ? exported.outDirectory : join(appDir, exported.outDirectory)) : null;
83
+ const config = required?.config ?? {};
84
+ const appPathRoutes = json(join(dir, 'app-path-routes-manifest.json'));
85
+ const serverAppPaths = json(join(dir, 'server', 'app-paths-manifest.json'));
86
+ const appPaths = appPathRoutes ?? (serverAppPaths
87
+ ? Object.fromEntries(Object.keys(serverAppPaths).map(key => [key, normalizeAppPath(key)])) : null);
88
+ const locales = Array.isArray(routes.i18n?.locales) ? routes.i18n.locales.filter((locale) => typeof locale === 'string') : [];
89
+ const basePath = typeof routes.basePath === 'string' ? routes.basePath : '';
90
+ return {
91
+ dir,
92
+ basePath,
93
+ i18n: locales.length > 0 && typeof routes.i18n?.defaultLocale === 'string' ? { locales, defaultLocale: routes.i18n.defaultLocale } : null,
94
+ pageExtensions: Array.isArray(config.pageExtensions) && config.pageExtensions.every(item => typeof item === 'string')
95
+ ? config.pageExtensions : DEFAULT_EXTENSIONS,
96
+ exportDir: exportPath !== null && isDirectory(exportPath) ? relative(appDir, exportPath).split('\\').join('/') : null,
97
+ assetPrefix: typeof config.assetPrefix === 'string' ? config.assetPrefix : '',
98
+ appPaths,
99
+ pages: json(join(dir, 'server', 'pages-manifest.json')),
100
+ pageRewrites: pageRewrites(routes.rewrites, basePath),
101
+ };
102
+ }
103
+ /** The URL path the build's static files are served under (assetPrefix may be another origin, whose path still counts). */
104
+ export function staticPathPrefix(assetPrefix) {
105
+ if (assetPrefix === '')
106
+ return '';
107
+ let path = assetPrefix;
108
+ try {
109
+ path = new URL(assetPrefix).pathname;
110
+ }
111
+ catch { /* a path, not a URL */ }
112
+ return path.replace(/\/+$/, '');
113
+ }