@getrefino/onboarding 0.1.0-rc.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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/agent.d.ts +8 -0
- package/dist/agent.js +219 -0
- package/dist/apply.d.ts +23 -0
- package/dist/apply.js +142 -0
- package/dist/dataflow.d.ts +96 -0
- package/dist/dataflow.js +333 -0
- package/dist/fs.d.ts +20 -0
- package/dist/fs.js +131 -0
- package/dist/graph.d.ts +45 -0
- package/dist/graph.js +299 -0
- package/dist/ids.d.ts +20 -0
- package/dist/ids.js +367 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +9 -0
- package/dist/inspect.d.ts +19 -0
- package/dist/inspect.js +471 -0
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +447 -0
- package/dist/scan.d.ts +67 -0
- package/dist/scan.js +1486 -0
- package/dist/templates-hosted.d.ts +34 -0
- package/dist/templates-hosted.js +332 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +671 -0
- package/dist/types.d.ts +286 -0
- package/dist/types.js +5 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.js +403 -0
- package/package.json +54 -0
package/dist/graph.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source graph: parse files with the TypeScript compiler, resolve local
|
|
3
|
+
* imports, discover routes per framework, and compute which files belong
|
|
4
|
+
* to which route (or are shared chrome).
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import ts from "typescript";
|
|
8
|
+
import { exists, isDirectory, isFile, readJson, readText, rel, walkFiles } from "./fs.js";
|
|
9
|
+
const PAGE_EXTENSIONS = ["tsx", "jsx", "ts", "js"];
|
|
10
|
+
function findWithExtensions(base) {
|
|
11
|
+
for (const ext of PAGE_EXTENSIONS) {
|
|
12
|
+
const candidate = `${base}.${ext}`;
|
|
13
|
+
if (isFile(candidate))
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function nextAppRoutes(appDir, routesDir) {
|
|
19
|
+
const routes = [];
|
|
20
|
+
const layouts = [];
|
|
21
|
+
const dir = join(appDir, routesDir);
|
|
22
|
+
for (const file of walkFiles(dir)) {
|
|
23
|
+
const relative = rel(dir, file);
|
|
24
|
+
const segments = relative.split("/");
|
|
25
|
+
const name = segments.pop();
|
|
26
|
+
const match = name.match(/^(page|layout|template|not-found|error|loading|default)\.(tsx|jsx|ts|js)$/);
|
|
27
|
+
if (!match)
|
|
28
|
+
continue;
|
|
29
|
+
const kind = match[1];
|
|
30
|
+
const urlSegments = segments
|
|
31
|
+
.filter((segment) => !segment.startsWith("(") && !segment.startsWith("@") && !segment.startsWith("_"))
|
|
32
|
+
.map((segment) => segment);
|
|
33
|
+
const path = `/${urlSegments.join("/")}`.replace(/\/+/g, "/");
|
|
34
|
+
if (kind === "page")
|
|
35
|
+
routes.push({ path: path === "" ? "/" : path, file: rel(appDir, file) });
|
|
36
|
+
else if (kind === "layout" || kind === "template")
|
|
37
|
+
layouts.push(rel(appDir, file));
|
|
38
|
+
}
|
|
39
|
+
routes.sort((a, b) => (a.path === "/" ? -1 : b.path === "/" ? 1 : a.path.localeCompare(b.path)));
|
|
40
|
+
return { routes, layouts };
|
|
41
|
+
}
|
|
42
|
+
function nextPagesRoutes(appDir, routesDir) {
|
|
43
|
+
const routes = [];
|
|
44
|
+
const layouts = [];
|
|
45
|
+
const dir = join(appDir, routesDir);
|
|
46
|
+
for (const file of walkFiles(dir)) {
|
|
47
|
+
const relative = rel(dir, file);
|
|
48
|
+
if (relative.startsWith("api/"))
|
|
49
|
+
continue;
|
|
50
|
+
const withoutExt = relative.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
51
|
+
const name = withoutExt.split("/").pop();
|
|
52
|
+
if (name === "_app" || name === "_document") {
|
|
53
|
+
if (name === "_app")
|
|
54
|
+
layouts.push(rel(appDir, file));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (name.startsWith("_"))
|
|
58
|
+
continue;
|
|
59
|
+
const path = `/${withoutExt.replace(/(^|\/)index$/, "")}`.replace(/\/+/g, "/") || "/";
|
|
60
|
+
routes.push({ path: path === "" ? "/" : path, file: rel(appDir, file) });
|
|
61
|
+
}
|
|
62
|
+
routes.sort((a, b) => (a.path === "/" ? -1 : b.path === "/" ? 1 : a.path.localeCompare(b.path)));
|
|
63
|
+
return { routes, layouts };
|
|
64
|
+
}
|
|
65
|
+
function findEntry(appDir, sourceDir) {
|
|
66
|
+
const bases = ["main", "index", "App", "app"];
|
|
67
|
+
for (const base of bases) {
|
|
68
|
+
const found = findWithExtensions(join(appDir, sourceDir, base));
|
|
69
|
+
if (found)
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** Parse a file once; JSX is enabled for every extension so .js files with JSX work. */
|
|
75
|
+
export function parseSource(file, text) {
|
|
76
|
+
const kind = file.endsWith(".ts") ? ts.ScriptKind.TS : file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs") ? ts.ScriptKind.JSX : file.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TSX;
|
|
77
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
|
|
78
|
+
}
|
|
79
|
+
export function readPathAliases(appDir) {
|
|
80
|
+
const configPath = ["tsconfig.json", "jsconfig.json"].map((name) => join(appDir, name)).find((path) => isFile(path));
|
|
81
|
+
const config = configPath ? readJson(configPath) : null;
|
|
82
|
+
const baseUrl = resolve(appDir, config?.compilerOptions?.baseUrl ?? ".");
|
|
83
|
+
const paths = config?.compilerOptions?.paths ?? {};
|
|
84
|
+
return { baseUrl, paths };
|
|
85
|
+
}
|
|
86
|
+
export function resolveImport(fromFile, specifier, appDir, aliases) {
|
|
87
|
+
let target = null;
|
|
88
|
+
if (specifier.startsWith(".")) {
|
|
89
|
+
target = resolve(dirname(fromFile), specifier);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const [pattern, targets] of Object.entries(aliases.paths)) {
|
|
93
|
+
const prefix = pattern.replace(/\*$/, "");
|
|
94
|
+
if (!specifier.startsWith(prefix))
|
|
95
|
+
continue;
|
|
96
|
+
const rest = specifier.slice(prefix.length);
|
|
97
|
+
const first = targets[0];
|
|
98
|
+
if (!first)
|
|
99
|
+
continue;
|
|
100
|
+
target = resolve(aliases.baseUrl, first.replace(/\*$/, "") + rest);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (!target && (specifier.startsWith("@/") || specifier.startsWith("~/"))) {
|
|
104
|
+
const rest = specifier.slice(2);
|
|
105
|
+
const srcCandidate = resolve(appDir, "src", rest);
|
|
106
|
+
target = exists(resolve(appDir, "src")) && (findWithExtensions(srcCandidate) || isDirectory(srcCandidate)) ? srcCandidate : resolve(appDir, rest);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!target)
|
|
110
|
+
return null;
|
|
111
|
+
if (isFile(target))
|
|
112
|
+
return target;
|
|
113
|
+
return findWithExtensions(target) ?? findWithExtensions(join(target, "index"));
|
|
114
|
+
}
|
|
115
|
+
export function localImports(source, file, appDir, aliases) {
|
|
116
|
+
const results = [];
|
|
117
|
+
const visit = (node) => {
|
|
118
|
+
let specifier = null;
|
|
119
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier))
|
|
120
|
+
specifier = node.moduleSpecifier.text;
|
|
121
|
+
else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier))
|
|
122
|
+
specifier = node.moduleSpecifier.text;
|
|
123
|
+
else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0]))
|
|
124
|
+
specifier = node.arguments[0].text;
|
|
125
|
+
if (specifier) {
|
|
126
|
+
const resolved = resolveImport(file, specifier, appDir, aliases);
|
|
127
|
+
if (resolved && resolved.startsWith(appDir) && !resolved.includes("/node_modules/"))
|
|
128
|
+
results.push(resolved);
|
|
129
|
+
}
|
|
130
|
+
ts.forEachChild(node, visit);
|
|
131
|
+
};
|
|
132
|
+
visit(source);
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
function containsJsx(source) {
|
|
136
|
+
let found = false;
|
|
137
|
+
const visit = (node) => {
|
|
138
|
+
if (found)
|
|
139
|
+
return;
|
|
140
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
|
|
141
|
+
found = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
ts.forEachChild(node, visit);
|
|
145
|
+
};
|
|
146
|
+
visit(source);
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
/** Files reachable from a start file through local imports, including itself. */
|
|
150
|
+
export function reachableFiles(start, appDir, aliases, cache, options = {}) {
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
const stack = [start];
|
|
153
|
+
while (stack.length > 0) {
|
|
154
|
+
const file = stack.pop();
|
|
155
|
+
if (seen.has(file))
|
|
156
|
+
continue;
|
|
157
|
+
let entry = cache.get(file);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
const text = readText(file);
|
|
160
|
+
const source = text ? parseSource(file, text) : null;
|
|
161
|
+
entry = { imports: source ? localImports(source, file, appDir, aliases) : [], jsx: source ? containsJsx(source) : false };
|
|
162
|
+
cache.set(file, entry);
|
|
163
|
+
}
|
|
164
|
+
if (options.renderTreeOnly && file !== start && !entry.jsx)
|
|
165
|
+
continue;
|
|
166
|
+
seen.add(file);
|
|
167
|
+
for (const next of entry.imports)
|
|
168
|
+
if (!seen.has(next))
|
|
169
|
+
stack.push(next);
|
|
170
|
+
}
|
|
171
|
+
return seen;
|
|
172
|
+
}
|
|
173
|
+
/** react-router: map <Route path="/x" element={<Foo/>}> to the component's file. */
|
|
174
|
+
function reactRouterRoutes(entry, appDir, aliases) {
|
|
175
|
+
const cache = new Map();
|
|
176
|
+
const files = reachableFiles(entry, appDir, aliases, cache);
|
|
177
|
+
const routes = [];
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const text = readText(file);
|
|
180
|
+
if (!text || !text.includes("Route"))
|
|
181
|
+
continue;
|
|
182
|
+
const source = parseSource(file, text);
|
|
183
|
+
const importedFrom = new Map();
|
|
184
|
+
for (const statement of source.statements) {
|
|
185
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
186
|
+
continue;
|
|
187
|
+
const clause = statement.importClause;
|
|
188
|
+
const resolved = resolveImport(file, statement.moduleSpecifier.text, appDir, aliases);
|
|
189
|
+
if (!clause || !resolved)
|
|
190
|
+
continue;
|
|
191
|
+
if (clause.name)
|
|
192
|
+
importedFrom.set(clause.name.text, resolved);
|
|
193
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
194
|
+
for (const element of clause.namedBindings.elements)
|
|
195
|
+
importedFrom.set(element.name.text, resolved);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const visit = (node) => {
|
|
199
|
+
if ((ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) && ts.isIdentifier(node.tagName) && node.tagName.text === "Route") {
|
|
200
|
+
let path = null;
|
|
201
|
+
let component = null;
|
|
202
|
+
let index = false;
|
|
203
|
+
for (const attr of node.attributes.properties) {
|
|
204
|
+
if (!ts.isJsxAttribute(attr) || !ts.isIdentifier(attr.name))
|
|
205
|
+
continue;
|
|
206
|
+
const name = attr.name.text;
|
|
207
|
+
if (name === "path" && attr.initializer && ts.isStringLiteral(attr.initializer))
|
|
208
|
+
path = attr.initializer.text;
|
|
209
|
+
if (name === "index")
|
|
210
|
+
index = true;
|
|
211
|
+
if ((name === "element" || name === "Component" || name === "component") && attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
212
|
+
const expression = attr.initializer.expression;
|
|
213
|
+
if (ts.isJsxSelfClosingElement(expression) && ts.isIdentifier(expression.tagName))
|
|
214
|
+
component = expression.tagName.text;
|
|
215
|
+
else if (ts.isJsxElement(expression) && ts.isIdentifier(expression.openingElement.tagName))
|
|
216
|
+
component = expression.openingElement.tagName.text;
|
|
217
|
+
else if (ts.isIdentifier(expression))
|
|
218
|
+
component = expression.text;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (index && !path)
|
|
222
|
+
path = "/";
|
|
223
|
+
if (path && component) {
|
|
224
|
+
const target = importedFrom.get(component);
|
|
225
|
+
routes.push({ path: path.startsWith("/") ? path : `/${path}`, file: rel(appDir, target ?? file) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
ts.forEachChild(node, visit);
|
|
229
|
+
};
|
|
230
|
+
visit(source);
|
|
231
|
+
}
|
|
232
|
+
routes.sort((a, b) => (a.path === "/" ? -1 : b.path === "/" ? 1 : a.path.localeCompare(b.path)));
|
|
233
|
+
return routes;
|
|
234
|
+
}
|
|
235
|
+
export function discoverRoutes(appDir, framework, sourceDir) {
|
|
236
|
+
if (framework === "next") {
|
|
237
|
+
const appRoutesDir = ["app", "src/app"].find((dir) => isDirectory(join(appDir, dir)));
|
|
238
|
+
const pagesRoutesDir = ["pages", "src/pages"].find((dir) => isDirectory(join(appDir, dir)));
|
|
239
|
+
if (appRoutesDir) {
|
|
240
|
+
const { routes, layouts } = nextAppRoutes(appDir, appRoutesDir);
|
|
241
|
+
return { router: "next-app", routesDir: appRoutesDir, routes, layouts, entry: null };
|
|
242
|
+
}
|
|
243
|
+
if (pagesRoutesDir) {
|
|
244
|
+
const { routes, layouts } = nextPagesRoutes(appDir, pagesRoutesDir);
|
|
245
|
+
return { router: "next-pages", routesDir: pagesRoutesDir, routes, layouts, entry: null };
|
|
246
|
+
}
|
|
247
|
+
return { router: "unknown", routesDir: null, routes: [], layouts: [], entry: null };
|
|
248
|
+
}
|
|
249
|
+
if (framework === "vite-react" || framework === "react") {
|
|
250
|
+
const entry = findEntry(appDir, sourceDir);
|
|
251
|
+
if (!entry)
|
|
252
|
+
return { router: "unknown", routesDir: null, routes: [], layouts: [], entry: null };
|
|
253
|
+
const aliases = readPathAliases(appDir);
|
|
254
|
+
const pkg = readJson(join(appDir, "package.json"));
|
|
255
|
+
const usesRouter = Boolean(pkg?.dependencies?.["react-router"] || pkg?.dependencies?.["react-router-dom"] || pkg?.devDependencies?.["react-router-dom"]);
|
|
256
|
+
if (usesRouter) {
|
|
257
|
+
const routes = reactRouterRoutes(entry, appDir, aliases);
|
|
258
|
+
if (routes.length > 0)
|
|
259
|
+
return { router: "react-router", routesDir: null, routes, layouts: [rel(appDir, entry)], entry: rel(appDir, entry) };
|
|
260
|
+
}
|
|
261
|
+
const appFile = findWithExtensions(join(appDir, sourceDir, "App")) ?? entry;
|
|
262
|
+
return { router: "single-page", routesDir: null, routes: [{ path: "/", file: rel(appDir, appFile) }], layouts: [], entry: rel(appDir, entry) };
|
|
263
|
+
}
|
|
264
|
+
return { router: "unknown", routesDir: null, routes: [], layouts: [], entry: null };
|
|
265
|
+
}
|
|
266
|
+
/** Compute which route(s) each source file belongs to, and which files are shared chrome. */
|
|
267
|
+
export function computeFileScopes(appDir, routes, layouts) {
|
|
268
|
+
const aliases = readPathAliases(appDir);
|
|
269
|
+
const cache = new Map();
|
|
270
|
+
const byFile = new Map();
|
|
271
|
+
const shared = new Set();
|
|
272
|
+
const routeFiles = new Set(routes.map((route) => resolve(appDir, route.file)));
|
|
273
|
+
const layoutFiles = new Set(layouts.map((layout) => resolve(appDir, layout)));
|
|
274
|
+
for (const layout of layoutFiles) {
|
|
275
|
+
for (const file of reachableFiles(layout, appDir, aliases, cache, { renderTreeOnly: true })) {
|
|
276
|
+
// A layout's import graph includes the page files it wraps only on
|
|
277
|
+
// Pages Router (_app imports nothing); route files never count as shared.
|
|
278
|
+
if (!routeFiles.has(file))
|
|
279
|
+
shared.add(file);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const route of routes) {
|
|
283
|
+
const start = resolve(appDir, route.file);
|
|
284
|
+
for (const file of reachableFiles(start, appDir, aliases, cache, { renderTreeOnly: true })) {
|
|
285
|
+
if (layoutFiles.has(file))
|
|
286
|
+
continue;
|
|
287
|
+
let set = byFile.get(file);
|
|
288
|
+
if (!set)
|
|
289
|
+
byFile.set(file, (set = new Set()));
|
|
290
|
+
set.add(route.path);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const fromLayouts = new Set(shared);
|
|
294
|
+
for (const [file, set] of byFile) {
|
|
295
|
+
if (set.size > 1 && !routeFiles.has(file))
|
|
296
|
+
shared.add(file);
|
|
297
|
+
}
|
|
298
|
+
return { byFile, shared, fromLayouts };
|
|
299
|
+
}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RawCandidate } from "./scan.js";
|
|
2
|
+
export interface ProposedId {
|
|
3
|
+
readonly raw: RawCandidate;
|
|
4
|
+
readonly id: string | null;
|
|
5
|
+
readonly idReason: string | null;
|
|
6
|
+
readonly review: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function camel(words: readonly string[]): string;
|
|
9
|
+
export declare function slugFromText(text: string, maxWords?: number): string;
|
|
10
|
+
/** Last meaningful segment of a site path: "/features/privacy" -> "privacy", "/#how-it-works" -> "howItWorks", "/" -> "home". */
|
|
11
|
+
export declare function pathSegment(path: string): string;
|
|
12
|
+
export declare function componentSegment(component: string | null): string | null;
|
|
13
|
+
export declare function routeSegment(route: string | null, scope: string): string;
|
|
14
|
+
/** Map component names to the native element they stand in for. */
|
|
15
|
+
export declare function normalizeElement(element: string | null): string;
|
|
16
|
+
/**
|
|
17
|
+
* Assign IDs to all candidates, resolving collisions semantically where
|
|
18
|
+
* possible (second CTA -> secondaryCta) and flagging the rest for review.
|
|
19
|
+
*/
|
|
20
|
+
export declare function proposeIds(candidates: readonly RawCandidate[]): ProposedId[];
|