@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/ids.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic content ID proposals: `<page>.<section>.<role>` built from the
|
|
3
|
+
* route, the enclosing component or landmark, and the element's role.
|
|
4
|
+
* Never from line numbers or array indexes. When naming is uncertain the
|
|
5
|
+
* candidate is flagged for review instead of accepting a poor ID.
|
|
6
|
+
*/
|
|
7
|
+
import { isContentId } from "@getrefino/core";
|
|
8
|
+
const STOP_WORDS = new Set(["a", "an", "the", "and", "or", "of", "to", "in", "on", "for", "with", "your", "our", "is", "are", "that", "this", "it", "at", "by", "from", "you", "we", "i", "can", "do", "does", "how", "what", "will", "my", "me", "be", "when", "where", "why", "as"]);
|
|
9
|
+
export function camel(words) {
|
|
10
|
+
const clean = words.map((word) => word.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean);
|
|
11
|
+
if (clean.length === 0)
|
|
12
|
+
return "";
|
|
13
|
+
return clean
|
|
14
|
+
.map((word, index) => {
|
|
15
|
+
const lower = word.toLowerCase();
|
|
16
|
+
return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
17
|
+
})
|
|
18
|
+
.join("");
|
|
19
|
+
}
|
|
20
|
+
export function slugFromText(text, maxWords = 3) {
|
|
21
|
+
const words = text
|
|
22
|
+
.replace(/['’]/g, "")
|
|
23
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const significant = words.filter((word) => !STOP_WORDS.has(word.toLowerCase()));
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const unique = (significant.length > 0 ? significant : words).filter((word) => {
|
|
28
|
+
const key = word.toLowerCase();
|
|
29
|
+
if (seen.has(key))
|
|
30
|
+
return false;
|
|
31
|
+
seen.add(key);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
return camel(unique.slice(0, maxWords));
|
|
35
|
+
}
|
|
36
|
+
/** Last meaningful segment of a site path: "/features/privacy" -> "privacy", "/#how-it-works" -> "howItWorks", "/" -> "home". */
|
|
37
|
+
export function pathSegment(path) {
|
|
38
|
+
const cleaned = path.replace(/^\/#?/, "").replace(/#/g, "/");
|
|
39
|
+
const last = cleaned.split("/").filter(Boolean).pop() ?? "home";
|
|
40
|
+
return slugFromText(last.replace(/[-_.]+/g, " "), 3) || "home";
|
|
41
|
+
}
|
|
42
|
+
function segmentFromIdentifier(name) {
|
|
43
|
+
const words = name
|
|
44
|
+
.replace(/[-_]+/g, " ")
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
46
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
return camel(words);
|
|
50
|
+
}
|
|
51
|
+
const COMPONENT_ALIASES = {
|
|
52
|
+
nav: "nav",
|
|
53
|
+
navbar: "nav",
|
|
54
|
+
navigation: "nav",
|
|
55
|
+
header: "nav",
|
|
56
|
+
siteheader: "nav",
|
|
57
|
+
topbar: "nav",
|
|
58
|
+
footer: "footer",
|
|
59
|
+
sitefooter: "footer",
|
|
60
|
+
hero: "hero",
|
|
61
|
+
faq: "faq",
|
|
62
|
+
faqs: "faq",
|
|
63
|
+
cta: "cta",
|
|
64
|
+
calltoaction: "cta",
|
|
65
|
+
ctaband: "cta",
|
|
66
|
+
testimonial: "testimonial",
|
|
67
|
+
testimonials: "testimonial",
|
|
68
|
+
pricing: "pricing",
|
|
69
|
+
features: "features",
|
|
70
|
+
feature: "features",
|
|
71
|
+
};
|
|
72
|
+
const PAGE_COMPONENT = /^(home|homepage|index|page|app|root|main|layout|rootlayout|default)(page|screen|view)?$/i;
|
|
73
|
+
export function componentSegment(component) {
|
|
74
|
+
if (!component)
|
|
75
|
+
return null;
|
|
76
|
+
const stripped = component.replace(/(Section|Component|Block|Widget|Container|Wrapper|Page|Screen|View|Route)$/, "") || component;
|
|
77
|
+
if (PAGE_COMPONENT.test(stripped) || PAGE_COMPONENT.test(component))
|
|
78
|
+
return null;
|
|
79
|
+
const alias = COMPONENT_ALIASES[stripped.toLowerCase()];
|
|
80
|
+
if (alias)
|
|
81
|
+
return alias;
|
|
82
|
+
const segment = segmentFromIdentifier(stripped);
|
|
83
|
+
return segment || null;
|
|
84
|
+
}
|
|
85
|
+
export function routeSegment(route, scope) {
|
|
86
|
+
if (scope === "shared")
|
|
87
|
+
return "shared";
|
|
88
|
+
if (!route || route === "/")
|
|
89
|
+
return "home";
|
|
90
|
+
const parts = route
|
|
91
|
+
.split("/")
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.map((part) => segmentFromIdentifier(part.replace(/^\[+\.*|\]+$/g, "")))
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
return parts.join(".") || "home";
|
|
96
|
+
}
|
|
97
|
+
/** Map component names to the native element they stand in for. */
|
|
98
|
+
export function normalizeElement(element) {
|
|
99
|
+
if (!element)
|
|
100
|
+
return "";
|
|
101
|
+
const bare = element.replace(/^.*\./, "");
|
|
102
|
+
if (/^(a|A|Anchor)$/.test(bare) || /Link$/.test(bare))
|
|
103
|
+
return "a";
|
|
104
|
+
if (/^(button|Btn)$/.test(bare) || /Button$/.test(bare))
|
|
105
|
+
return "button";
|
|
106
|
+
if (/^(Heading|Title|H1|H2)$/.test(bare))
|
|
107
|
+
return "h2";
|
|
108
|
+
if (/^(Text|Paragraph|P|Body)$/.test(bare))
|
|
109
|
+
return "p";
|
|
110
|
+
return bare.toLowerCase() === bare ? bare : bare;
|
|
111
|
+
}
|
|
112
|
+
/** Components named after a typographic role (`<Eyebrow>`, `<Lead>`, `<Note>`). */
|
|
113
|
+
const COMPONENT_ROLES = [
|
|
114
|
+
[/^(Eyebrow|Kicker|Overline|Pretitle|SuperTitle)$/i, "eyebrow"],
|
|
115
|
+
[/^(Tagline)$/i, "tagline"],
|
|
116
|
+
[/^(Subtitle|Subheading|Lead|Intro|Description)$/i, "subtitle"],
|
|
117
|
+
[/^(Note|Caption|FinePrint|Hint|Disclaimer|Footnote)$/i, "note"],
|
|
118
|
+
[/^(Badge|Pill|Tag|Chip)$/i, "badge"],
|
|
119
|
+
[/^(Quote|Testimonial|Blockquote)$/i, "quote"],
|
|
120
|
+
[/^(Label)$/i, "label"],
|
|
121
|
+
];
|
|
122
|
+
function componentRole(element) {
|
|
123
|
+
if (!element)
|
|
124
|
+
return null;
|
|
125
|
+
const bare = element.replace(/^.*\./, "");
|
|
126
|
+
if (!/^[A-Z]/.test(bare))
|
|
127
|
+
return null;
|
|
128
|
+
for (const [pattern, role] of COMPONENT_ROLES)
|
|
129
|
+
if (pattern.test(bare))
|
|
130
|
+
return role;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function isButtonLike(candidate) {
|
|
134
|
+
const className = candidate.context.className ?? "";
|
|
135
|
+
const element = normalizeElement(candidate.element);
|
|
136
|
+
return element === "button" || /(^|[\s_-])(btn|button|cta)([\s_-]|$)/i.test(className) || (element === "a" && /(^|[\s_-])(btn|button|cta)/i.test(className));
|
|
137
|
+
}
|
|
138
|
+
const CLASS_ROLES = [
|
|
139
|
+
[/(^|[\s_-])(eyebrow|kicker|overline|pretitle|pre-title)([\s_-]|$)/i, "eyebrow"],
|
|
140
|
+
[/(^|[\s_-])(tagline)([\s_-]|$)/i, "tagline"],
|
|
141
|
+
[/(^|[\s_-])(subtitle|sub-title|subheading|lead|intro|description|desc)([\s_-]|$)/i, "subtitle"],
|
|
142
|
+
[/(^|[\s_-])(note|fine-print|fineprint|disclaimer|caption|hint)([\s_-]|$)/i, "note"],
|
|
143
|
+
[/(^|[\s_-])(badge|pill|tag|chip)([\s_-]|$)/i, "badge"],
|
|
144
|
+
[/(^|[\s_-])(quote|testimonial)([\s_-]|$)/i, "quote"],
|
|
145
|
+
[/(^|[\s_-])(author|name)([\s_-]|$)/i, "name"],
|
|
146
|
+
[/(^|[\s_-])(role|job-title|position)([\s_-]|$)/i, "role"],
|
|
147
|
+
[/(^|[\s_-])(price|amount)([\s_-]|$)/i, "price"],
|
|
148
|
+
[/(^|[\s_-])(copyright)([\s_-]|$)/i, "copyright"],
|
|
149
|
+
// Typographic scale utilities (text-display, text-h3, text-body) say what an element is.
|
|
150
|
+
[/(^|\s)(text-)?(display|headline)(\s|$)/i, "title"],
|
|
151
|
+
[/(^|\s)text-h[1-6](\s|$)/i, "heading"],
|
|
152
|
+
[/(^|\s)text-body(\s|$)/i, "body"],
|
|
153
|
+
];
|
|
154
|
+
function classRole(className) {
|
|
155
|
+
if (!className)
|
|
156
|
+
return null;
|
|
157
|
+
for (const [pattern, role] of CLASS_ROLES)
|
|
158
|
+
if (pattern.test(className))
|
|
159
|
+
return role;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function isShortLabel(text) {
|
|
163
|
+
return text.split(/\s+/).length <= 3 && text.length <= 32;
|
|
164
|
+
}
|
|
165
|
+
function roleFor(candidate) {
|
|
166
|
+
const element = normalizeElement(candidate.element);
|
|
167
|
+
const text = candidate.text;
|
|
168
|
+
const inNav = candidate.context.ancestors.includes("nav") || candidate.context.landmark === "nav" || candidate.component?.toLowerCase().includes("nav") === true;
|
|
169
|
+
const inFooter = candidate.context.ancestors.includes("footer") || candidate.context.landmark === "footer";
|
|
170
|
+
const inFaq = /faq/i.test(candidate.context.landmark ?? "") || /faq/i.test(candidate.component ?? "");
|
|
171
|
+
if (candidate.source === "jsx-attribute" || candidate.source === "data-object") {
|
|
172
|
+
return { role: candidate.key ?? "text", fromText: false };
|
|
173
|
+
}
|
|
174
|
+
if (isButtonLike(candidate))
|
|
175
|
+
return { role: "cta", fromText: false };
|
|
176
|
+
const fromComponent = componentRole(candidate.element);
|
|
177
|
+
if (fromComponent)
|
|
178
|
+
return { role: fromComponent, fromText: false };
|
|
179
|
+
const hinted = classRole(candidate.context.className);
|
|
180
|
+
if (hinted && ["span", "p", "div", "small", "em", "strong"].includes(element))
|
|
181
|
+
return { role: hinted, fromText: false };
|
|
182
|
+
switch (element) {
|
|
183
|
+
case "h1":
|
|
184
|
+
return { role: "title", fromText: false };
|
|
185
|
+
case "h2":
|
|
186
|
+
return { role: "title", fromText: false };
|
|
187
|
+
case "h3":
|
|
188
|
+
case "h4":
|
|
189
|
+
case "h5":
|
|
190
|
+
case "h6":
|
|
191
|
+
return { role: "heading", fromText: false };
|
|
192
|
+
case "p":
|
|
193
|
+
return { role: "body", fromText: false };
|
|
194
|
+
case "blockquote":
|
|
195
|
+
case "q":
|
|
196
|
+
return { role: "quote", fromText: false };
|
|
197
|
+
case "figcaption":
|
|
198
|
+
return { role: "caption", fromText: false };
|
|
199
|
+
case "label":
|
|
200
|
+
case "legend":
|
|
201
|
+
return { role: "label", fromText: false };
|
|
202
|
+
case "small":
|
|
203
|
+
return { role: "note", fromText: false };
|
|
204
|
+
case "dt":
|
|
205
|
+
return { role: inFaq ? "question" : "term", fromText: false };
|
|
206
|
+
case "dd":
|
|
207
|
+
return { role: inFaq ? "answer" : "definition", fromText: false };
|
|
208
|
+
case "summary":
|
|
209
|
+
return { role: "question", fromText: false };
|
|
210
|
+
case "a":
|
|
211
|
+
case "li":
|
|
212
|
+
case "span":
|
|
213
|
+
case "th":
|
|
214
|
+
case "td":
|
|
215
|
+
case "strong":
|
|
216
|
+
case "em":
|
|
217
|
+
case "b":
|
|
218
|
+
case "i":
|
|
219
|
+
if ((inNav || inFooter || element === "a" || element === "li") && isShortLabel(text))
|
|
220
|
+
return { role: slugFromText(text), fromText: true };
|
|
221
|
+
return { role: element === "a" ? "link" : "text", fromText: false };
|
|
222
|
+
default:
|
|
223
|
+
return { role: "text", fromText: false };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function itemSegment(candidate) {
|
|
227
|
+
const { itemKey, itemTitle } = candidate.context;
|
|
228
|
+
if (itemKey)
|
|
229
|
+
return { segment: segmentFromIdentifier(itemKey) || null, derivedFromText: false };
|
|
230
|
+
if (itemTitle)
|
|
231
|
+
return { segment: slugFromText(itemTitle, 2) || null, derivedFromText: true };
|
|
232
|
+
return { segment: null, derivedFromText: false };
|
|
233
|
+
}
|
|
234
|
+
function baseIdFor(candidate) {
|
|
235
|
+
const page = routeSegment(candidate.context.route, candidate.scope);
|
|
236
|
+
const pageLeaf = page.slice(page.lastIndexOf(".") + 1).toLowerCase();
|
|
237
|
+
const componentRaw = componentSegment(candidate.component);
|
|
238
|
+
// A page component named after its route (About, PricingPage) adds nothing.
|
|
239
|
+
const component = componentRaw && componentRaw.toLowerCase() === pageLeaf ? null : componentRaw;
|
|
240
|
+
const rawLandmark = candidate.context.landmark ? segmentFromIdentifier(candidate.context.landmark) : null;
|
|
241
|
+
const landmark = rawLandmark ? (COMPONENT_ALIASES[rawLandmark.toLowerCase()] ?? rawLandmark) : null;
|
|
242
|
+
const sectionCandidates = [component, landmark].filter((value) => value !== null && value.toLowerCase() !== pageLeaf);
|
|
243
|
+
const section = sectionCandidates[0] ?? null;
|
|
244
|
+
const second = sectionCandidates[1] ?? null;
|
|
245
|
+
const redundant = !second ||
|
|
246
|
+
!section ||
|
|
247
|
+
second === section ||
|
|
248
|
+
second.toLowerCase().includes(section.toLowerCase()) ||
|
|
249
|
+
section.toLowerCase().includes(second.toLowerCase()) ||
|
|
250
|
+
/^(nav|header|footer|section|aside|article|main|card|grid|list|item)$/.test(second);
|
|
251
|
+
const extraSection = redundant ? null : second;
|
|
252
|
+
if (candidate.source === "data-object") {
|
|
253
|
+
// Named after the component that maps the array, then the array when it adds information
|
|
254
|
+
// (a footer that maps GUIDES and COMPANY_LINKS), then the item, then the field.
|
|
255
|
+
const collectionIdentifier = candidate.context.collection ? segmentFromIdentifier(candidate.context.collection) : null;
|
|
256
|
+
const collectionRaw = collectionIdentifier ? (COMPONENT_ALIASES[collectionIdentifier.toLowerCase()] ?? collectionIdentifier) : null;
|
|
257
|
+
const overlaps = collectionRaw && section && (collectionRaw.toLowerCase().includes(section.toLowerCase()) || section.toLowerCase().includes(collectionRaw.toLowerCase()));
|
|
258
|
+
// An array imported from a data module may be rendered by several components (nav links in
|
|
259
|
+
// the header and the footer); the module's own collection name is its stable section, whichever
|
|
260
|
+
// component happens to map it.
|
|
261
|
+
const sectionForData = candidate.context.collectionImported && collectionRaw ? collectionRaw : section;
|
|
262
|
+
const collection = overlaps || sectionForData === collectionRaw ? null : collectionRaw;
|
|
263
|
+
const item = itemSegment(candidate);
|
|
264
|
+
const parts = [page, sectionForData, collection, item.segment, candidate.key].filter((value) => Boolean(value));
|
|
265
|
+
const id = parts.join(".");
|
|
266
|
+
if (!item.segment)
|
|
267
|
+
return { id: null, review: true, reason: "Data item has no id/key/slug or title to name it by." };
|
|
268
|
+
return { id, review: item.derivedFromText, reason: item.derivedFromText ? `Item key "${item.segment}" was derived from its text; give it a semantic key (edit the id, then apply it to every field of the item).` : null };
|
|
269
|
+
}
|
|
270
|
+
if (candidate.source === "jsx-attribute") {
|
|
271
|
+
const owner = candidate.element ? segmentFromIdentifier(candidate.element.replace(/^.*\./, "")) : null;
|
|
272
|
+
const item = itemSegment(candidate);
|
|
273
|
+
const ownerSegment = owner && owner !== section ? owner : null;
|
|
274
|
+
// The item key is only needed to tell several same-named components apart; a lone
|
|
275
|
+
// <SectionHeader title="…"> is simply section.sectionHeader.title.
|
|
276
|
+
const needsItem = candidate.context.siblingCount > 1 || (item.segment !== null && !item.derivedFromText);
|
|
277
|
+
const parts = [page, section, extraSection, ownerSegment, needsItem ? item.segment : null, candidate.key].filter((value) => Boolean(value));
|
|
278
|
+
const id = parts.join(".");
|
|
279
|
+
const reviewItem = needsItem && item.derivedFromText;
|
|
280
|
+
return { id, review: reviewItem, reason: reviewItem ? `Element key "${item.segment}" was derived from its text; rename it to something semantic.` : null };
|
|
281
|
+
}
|
|
282
|
+
const role = roleFor(candidate);
|
|
283
|
+
// FAQ-style pairs are keyed by their question, and texts inside a figure / list item /
|
|
284
|
+
// article by that item's first short label, so each group stays together.
|
|
285
|
+
const pairKey = candidate.context.itemTitle ? slugFromText(candidate.context.itemTitle, 3) : null;
|
|
286
|
+
const parts = [page, section, extraSection, pairKey, role.role].filter((value) => Boolean(value));
|
|
287
|
+
return { id: parts.join("."), review: false, reason: null };
|
|
288
|
+
}
|
|
289
|
+
function isShortHeadingLabel(text) {
|
|
290
|
+
return text.split(/\s+/).length <= 3 && text.length <= 32;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Assign IDs to all candidates, resolving collisions semantically where
|
|
294
|
+
* possible (second CTA -> secondaryCta) and flagging the rest for review.
|
|
295
|
+
*/
|
|
296
|
+
export function proposeIds(candidates) {
|
|
297
|
+
const results = [];
|
|
298
|
+
const bases = candidates.map((raw) => ({ raw, base: baseIdFor(raw) }));
|
|
299
|
+
// Several short headings sharing one base id (footer columns, a row of feature titles)
|
|
300
|
+
// are told apart by their own label: shared.footer.company.title, not title3.
|
|
301
|
+
const headingGroups = new Map();
|
|
302
|
+
for (const { raw, base } of bases) {
|
|
303
|
+
if (!base.id || !/\.(title|heading)$/.test(base.id) || raw.source !== "jsx-text")
|
|
304
|
+
continue;
|
|
305
|
+
headingGroups.set(base.id, (headingGroups.get(base.id) ?? 0) + 1);
|
|
306
|
+
}
|
|
307
|
+
const labelledHeadings = new Set();
|
|
308
|
+
for (const [id, count] of headingGroups) {
|
|
309
|
+
if (count > 1 && bases.every(({ raw, base }) => base.id !== id || isShortHeadingLabel(raw.text)))
|
|
310
|
+
labelledHeadings.add(id);
|
|
311
|
+
}
|
|
312
|
+
/** How many times each base id was proposed. */
|
|
313
|
+
const proposed = new Map();
|
|
314
|
+
/** Final ids handed out. */
|
|
315
|
+
const used = new Set();
|
|
316
|
+
for (const { raw, base } of bases) {
|
|
317
|
+
if (!base.id || !isContentId(base.id)) {
|
|
318
|
+
results.push({ raw, id: null, idReason: base.reason ?? `Could not build a valid ID from "${raw.text.slice(0, 40)}".`, review: true });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
let id = base.id;
|
|
322
|
+
let review = base.review;
|
|
323
|
+
let reason = base.reason;
|
|
324
|
+
if (labelledHeadings.has(id) && raw.source === "jsx-text") {
|
|
325
|
+
const leaf = id.slice(id.lastIndexOf(".") + 1);
|
|
326
|
+
const prefix = id.slice(0, id.lastIndexOf("."));
|
|
327
|
+
const label = slugFromText(raw.text, 3);
|
|
328
|
+
if (label && isContentId(`${prefix}.${label}.${leaf}`))
|
|
329
|
+
id = `${prefix}.${label}.${leaf}`;
|
|
330
|
+
}
|
|
331
|
+
const count = proposed.get(id) ?? 0;
|
|
332
|
+
proposed.set(id, count + 1);
|
|
333
|
+
if (count > 0) {
|
|
334
|
+
const leaf = id.slice(id.lastIndexOf(".") + 1);
|
|
335
|
+
const prefix = id.slice(0, id.lastIndexOf("."));
|
|
336
|
+
const hrefKey = raw.source === "jsx-text" && raw.context.href && (leaf === "cta" || leaf === "link") ? pathSegment(raw.context.href) : null;
|
|
337
|
+
// Alternative states of one element ("Saving…" / "Save") are not a second
|
|
338
|
+
// call to action or a subtitle; they are the same slot, so they keep an
|
|
339
|
+
// ordinal and the reviewer names them.
|
|
340
|
+
const alternative = raw.context.ambiguous === true;
|
|
341
|
+
if (leaf === "cta" && count === 1 && !alternative) {
|
|
342
|
+
id = `${prefix}.secondaryCta`;
|
|
343
|
+
}
|
|
344
|
+
else if (hrefKey && hrefKey !== "home" && isContentId(`${prefix}.${hrefKey}${leaf === "cta" ? "Cta" : "Link"}`)) {
|
|
345
|
+
// A third call to action or another long link: the path it points to is its stable identity.
|
|
346
|
+
id = `${prefix}.${hrefKey}${leaf === "cta" ? "Cta" : "Link"}`;
|
|
347
|
+
}
|
|
348
|
+
else if (leaf === "title" && count === 1 && !alternative) {
|
|
349
|
+
id = `${prefix}.subtitle`;
|
|
350
|
+
review = true;
|
|
351
|
+
reason = `Second title-level heading in the same section; confirm "${id}" is the right name.`;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
id = `${prefix}.${leaf}${count + 1}`;
|
|
355
|
+
review = true;
|
|
356
|
+
reason = `Repeated "${leaf}" in the same section; the ordinal suffix is a placeholder, choose a semantic name.`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
while (used.has(id)) {
|
|
360
|
+
id = `${id}x`;
|
|
361
|
+
review = true;
|
|
362
|
+
}
|
|
363
|
+
used.add(id);
|
|
364
|
+
results.push({ raw, id, idReason: reason, review });
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { inspectRepository, appDirectory, packageManagerInstall, packageManagerRun } from "./inspect.js";
|
|
2
|
+
export type { InspectOptions } from "./inspect.js";
|
|
3
|
+
export { scanCopy, scanFile, decodeJsxEntities, isUtilityClass } from "./scan.js";
|
|
4
|
+
export type { ScanOptions } from "./scan.js";
|
|
5
|
+
export { proposeIds, slugFromText, componentSegment, routeSegment } from "./ids.js";
|
|
6
|
+
export { buildPlan, planMigration, planContent, defaultContentFile, integrationPoints, hostIntegration, hostLabel, resolveRefinoSite, verificationCommands, PLAN_DIR, PLAN_FILE, INSTRUCTIONS_FILE, CONFIG_FILE, BOILERPLATE_DIR, TOOL_NAME, TOOL_VERSION, dependencyRange, } from "./plan.js";
|
|
7
|
+
export { renderAgentInstructions } from "./agent.js";
|
|
8
|
+
export { applyPlan } from "./apply.js";
|
|
9
|
+
export type { ApplyOptions, RefinoConfig } from "./apply.js";
|
|
10
|
+
export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
|
|
11
|
+
export type { TemplateFile } from "./templates.js";
|
|
12
|
+
export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, hostedFiles } from "./templates-hosted.js";
|
|
13
|
+
export type { RefinoSiteConfig } from "./templates-hosted.js";
|
|
14
|
+
export { verifyIntegration } from "./verify.js";
|
|
15
|
+
export type { VerifyOptions } from "./verify.js";
|
|
16
|
+
export type { ApplyResult, CandidateStatus, CheckStatus, CopyCandidate, CopyClassification, DetectedPackage, Framework, GitState, HostIntegration, HostKind, Language, MigrationPlan, PackageManager, PlanOptions, PlannedFile, RefinoSite, RepositoryInspection, RouteInfo, Router, ScanResult, ScriptSet, VerificationCheck, VerificationResult, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { inspectRepository, appDirectory, packageManagerInstall, packageManagerRun } from "./inspect.js";
|
|
2
|
+
export { scanCopy, scanFile, decodeJsxEntities, isUtilityClass } from "./scan.js";
|
|
3
|
+
export { proposeIds, slugFromText, componentSegment, routeSegment } from "./ids.js";
|
|
4
|
+
export { buildPlan, planMigration, planContent, defaultContentFile, integrationPoints, hostIntegration, hostLabel, resolveRefinoSite, verificationCommands, PLAN_DIR, PLAN_FILE, INSTRUCTIONS_FILE, CONFIG_FILE, BOILERPLATE_DIR, TOOL_NAME, TOOL_VERSION, dependencyRange, } from "./plan.js";
|
|
5
|
+
export { renderAgentInstructions } from "./agent.js";
|
|
6
|
+
export { applyPlan } from "./apply.js";
|
|
7
|
+
export { boilerplateFiles, ENV_EXAMPLE_BLOCK } from "./templates.js";
|
|
8
|
+
export { DEFAULT_REFINO_APP_URL, REFINO_SITE_ID_PATTERN, hostedFiles } from "./templates-hosted.js";
|
|
9
|
+
export { verifyIntegration } from "./verify.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PackageManager, RepositoryInspection } from "./types.js";
|
|
2
|
+
export interface InspectOptions {
|
|
3
|
+
/** Path of the app inside a monorepo, relative to root. Auto-detected otherwise. */
|
|
4
|
+
readonly app?: string;
|
|
5
|
+
}
|
|
6
|
+
/** The `packages:` entries of a pnpm-workspace.yaml (block list or inline array). */
|
|
7
|
+
export declare function pnpmWorkspacePackages(yaml: string): string[];
|
|
8
|
+
/** The runtime packages a migrated site depends on, and the names they had before the Refino rename. */
|
|
9
|
+
export declare const REFINO_PACKAGES: readonly ["@getrefino/core", "@getrefino/react", "@getrefino/github"];
|
|
10
|
+
export declare const LEGACY_PACKAGES: readonly ["@inline-copy/core", "@inline-copy/react", "@inline-copy/github"];
|
|
11
|
+
export declare const CONFIG_FILE_NAME = "refino.config.json";
|
|
12
|
+
export declare const LEGACY_CONFIG_FILE_NAME = "inline-copy.config.json";
|
|
13
|
+
export declare function inspectRepository(rootPath: string, options?: InspectOptions): RepositoryInspection;
|
|
14
|
+
/** Absolute path of the app directory for an inspection. */
|
|
15
|
+
export declare function appDirectory(inspection: RepositoryInspection): string;
|
|
16
|
+
export declare function packageManagerRun(pm: PackageManager, script: string): string;
|
|
17
|
+
/** Run a binary from node_modules through the package manager. */
|
|
18
|
+
export declare function packageManagerExec(pm: PackageManager, command: string): string;
|
|
19
|
+
export declare function packageManagerInstall(pm: PackageManager): string;
|