@mhosaic/feedback-cli 0.20.0 → 0.22.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.
- package/dist/bin.js +6 -1
- package/dist/bin.js.map +1 -1
- package/dist/build-RCLWV2WL.js +565 -0
- package/dist/build-RCLWV2WL.js.map +1 -0
- package/dist/check-FOJPEE4Q.js +342 -0
- package/dist/check-FOJPEE4Q.js.map +1 -0
- package/dist/chunk-AZQSQJNQ.js +83 -0
- package/dist/chunk-AZQSQJNQ.js.map +1 -0
- package/dist/chunk-IHJPCMYF.js +126 -0
- package/dist/chunk-IHJPCMYF.js.map +1 -0
- package/dist/chunk-SSLQOK2Z.js +456 -0
- package/dist/chunk-SSLQOK2Z.js.map +1 -0
- package/dist/config-JE3QRZVH.js +8 -0
- package/dist/config-JE3QRZVH.js.map +1 -0
- package/dist/generate-VFRQNPOI.js +14 -0
- package/dist/generate-VFRQNPOI.js.map +1 -0
- package/dist/{install-skill-QJ4ZDVVR.js → install-skill-PO5YSXWY.js} +4 -2
- package/dist/{install-skill-QJ4ZDVVR.js.map → install-skill-PO5YSXWY.js.map} +1 -1
- package/dist/qa-YR4GCV6T.js +43 -0
- package/dist/qa-YR4GCV6T.js.map +1 -0
- package/dist/sitemap-react-QTEAUKN5.js +330 -0
- package/dist/sitemap-react-QTEAUKN5.js.map +1 -0
- package/dist/sitemap-vue-EJDQTCYM.js +243 -0
- package/dist/sitemap-vue-EJDQTCYM.js.map +1 -0
- package/package.json +4 -3
- package/skills/integrate-feedback/SKILL.md +7 -0
- package/skills/integrate-qa-meter/SKILL.md +201 -0
- package/skills/integrate-qa-meter/references/qa-meter-config.md +168 -0
- package/skills/integrate-qa-meter/references/qa-meter-troubleshooting.md +37 -0
- package/skills/integrate-qa-meter/references/qa-meter.config.example.json +20 -0
- package/skills/integrate-qa-meter/references/qa-meter.config.example.react.json +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/generate.ts"],"sourcesContent":["/**\n * `qa generate` — static analysis of Playwright specs into generated suite YAML.\n *\n * Parses (never executes) the fixtures file, the referenced page-objects, and\n * every spec via the TypeScript compiler API, then emits one generated suite per\n * test directory: one `test_case` per `test()`, pages inferred from the\n * page-object fixtures each test destructures (or overridden by a\n * `<prefix>:pages` header). Output is fully deterministic — every collection is\n * sorted lexicographically so two runs serialize identically.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport ts from 'typescript'\nimport { stringify } from 'yaml'\nimport { loadQaConfig, type QaConfig } from './config'\nimport { normalizePattern } from './sitemap-core'\nimport type { GeneratedSuiteDoc, SuitePage, SuiteTestCase } from './suite-format'\n\nconst HEADER_COMMENT =\n '# GENERATED by qa-meter — do not hand-edit. Regenerate with: mhosaic-feedback qa generate'\n\n// ───────────────────────────────────────────────────────── helpers ──────────\n\n/** Read the CLI package version (stamped into `meta.generated_by`). */\nfunction cliVersion(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n // src/qa/generate.ts → ../../package.json ; dist/* → ../package.json. Walk up.\n for (let dir = here, i = 0; i < 5; i++, dir = dirname(dir)) {\n const candidate = join(dir, 'package.json')\n if (existsSync(candidate)) {\n try {\n const pkg = JSON.parse(readFileSync(candidate, 'utf8')) as { name?: string; version?: string }\n if (pkg.name === '@mhosaic/feedback-cli' && typeof pkg.version === 'string') return pkg.version\n } catch {\n // keep walking\n }\n }\n }\n return '0.0.0'\n}\n\n/** Slug a title: lowercase, strip accents, non-alphanumerics → `-`, collapse/trim. */\nfunction slug(input: string): string {\n return input\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/** Stable registry key for a route pattern (e.g. `/widgets/:widgetId/edit` → `widgets-widgetId-edit`). */\nfunction patternKey(pattern: string): string {\n const key = pattern\n .replace(/[:*]/g, '')\n .split('/')\n .filter(Boolean)\n .map((seg) => slug(seg))\n .filter(Boolean)\n .join('-')\n return key.length > 0 ? key : 'root'\n}\n\n/** Read & parse a TS source file into an AST (no Program, no type-checking). */\nfunction parseFile(file: string): ts.SourceFile {\n const text = readFileSync(file, 'utf8')\n return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, /*setParentNodes*/ true, ts.ScriptKind.TS)\n}\n\n/** Resolve a relative module specifier to an on-disk `.ts` file path. */\nfunction resolveImport(fromFile: string, spec: string): string | null {\n if (!spec.startsWith('.')) return null\n const base = resolve(dirname(fromFile), spec)\n for (const candidate of [`${base}.ts`, join(base, 'index.ts')]) {\n if (existsSync(candidate)) return candidate\n }\n return null\n}\n\n/** Map an imported identifier name → its source file, from a file's import declarations. */\nfunction importMap(sf: ts.SourceFile): Map<string, string> {\n const map = new Map<string, string>()\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n const target = resolveImport(sf.fileName, stmt.moduleSpecifier.text)\n if (!target) continue\n const bindings = stmt.importClause?.namedBindings\n if (bindings && ts.isNamedImports(bindings)) {\n for (const el of bindings.elements) map.set(el.name.text, target)\n }\n if (stmt.importClause?.name) map.set(stmt.importClause.name.text, target)\n }\n return map\n}\n\n// ─────────────────────────────────────────────── step 1: fixtures map ───────\n\n/** `fixtureName → page-object class name` from the `base.extend({...})` call. */\nfunction fixtureClasses(sf: ts.SourceFile): Map<string, string> {\n const out = new Map<string, string>()\n\n /** Is this a `…extend(...)` / `…extend<...>(...)` call? */\n const isExtendCall = (node: ts.Node): node is ts.CallExpression =>\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'extend'\n\n let objLit: ts.ObjectLiteralExpression | undefined\n const findExtend = (node: ts.Node): void => {\n if (objLit) return\n if (isExtendCall(node)) {\n const arg = node.arguments[0]\n if (arg && ts.isObjectLiteralExpression(arg)) objLit = arg\n }\n ts.forEachChild(node, findExtend)\n }\n findExtend(sf)\n if (!objLit) return out\n\n for (const prop of objLit.properties) {\n // A fixture property is `async name({ … }, use) { … }` (method) or `name: async (…) => …`.\n let name: string | undefined\n let body: ts.Node | undefined\n if (ts.isMethodDeclaration(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {\n name = prop.name.text\n body = prop.body\n } else if (\n ts.isPropertyAssignment(prop) &&\n (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&\n (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))\n ) {\n name = prop.name.text\n body = prop.initializer.body\n }\n if (!name || !body) continue\n\n // Find `use(new <Class>(...))` inside the body.\n let className: string | undefined\n const findUse = (node: ts.Node): void => {\n if (className) return\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n node.expression.text === 'use' &&\n node.arguments[0] &&\n ts.isNewExpression(node.arguments[0]) &&\n ts.isIdentifier(node.arguments[0].expression)\n ) {\n className = node.arguments[0].expression.text\n }\n ts.forEachChild(node, findUse)\n }\n findUse(body)\n if (className) out.set(name, className)\n }\n return out\n}\n\n// ──────────────────────────────────── step 2: page-object goto patterns ─────\n\n/** Extract the route pattern from a page-object class's `goto` method, or null. */\nfunction gotoPattern(sf: ts.SourceFile, className: string): string | null {\n let cls: ts.ClassDeclaration | undefined\n for (const stmt of sf.statements) {\n if (ts.isClassDeclaration(stmt) && stmt.name?.text === className) cls = stmt\n }\n if (!cls) return null\n\n let method: ts.MethodDeclaration | undefined\n for (const member of cls.members) {\n if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === 'goto') {\n method = member\n }\n }\n if (!method?.body) return null\n\n // Parameter names, in order, to resolve `${expr}` spans that aren't bare identifiers.\n const params = method.parameters\n .map((p) => (ts.isIdentifier(p.name) ? p.name.text : undefined))\n .filter((n): n is string => !!n)\n\n // Find `this.page.goto(<arg>)`.\n let arg: ts.Expression | undefined\n const findGoto = (node: ts.Node): void => {\n if (arg) return\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments.length > 0\n ) {\n arg = node.arguments[0]\n }\n ts.forEachChild(node, findGoto)\n }\n findGoto(method.body)\n if (!arg) return null\n\n if (ts.isStringLiteralLike(arg)) return normalizePattern(arg.text)\n\n if (ts.isTemplateExpression(arg)) {\n let raw = arg.head.text\n arg.templateSpans.forEach((span, i) => {\n let name: string\n if (ts.isIdentifier(span.expression)) {\n name = span.expression.text\n } else {\n name = params[i] ?? 'param'\n }\n raw += `:${name}${span.literal.text}`\n })\n return normalizePattern(raw)\n }\n return null\n}\n\n// ─────────────────────────────────────────────────── step 3: specs ──────────\n\ninterface SpecCase {\n title: string\n /** destructured fixture property keys, in source order */\n fixtures: string[]\n /** literal `page.goto('/x')` patterns found in the body */\n gotos: string[]\n}\n\ninterface HeaderAnnotations {\n pages?: string[]\n scenario?: string\n}\n\n/** Parse the leading comment block for `<prefix>:pages` / `<prefix>:scenario` (+ legacy fallback). */\nfunction parseHeader(text: string, prefix: string): HeaderAnnotations {\n const out: HeaderAnnotations = {}\n const lines = text.split(/\\r?\\n/)\n const comments: string[] = []\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed === '') continue\n if (trimmed.startsWith('//')) {\n comments.push(trimmed.replace(/^\\/\\/\\s?/, ''))\n continue\n }\n if (trimmed.startsWith('/*') || trimmed.startsWith('*')) {\n comments.push(trimmed.replace(/^\\/\\*+\\s?/, '').replace(/^\\*+\\s?/, '').replace(/\\*+\\/\\s*$/, ''))\n continue\n }\n break // first non-comment, non-blank line ends the header\n }\n\n const escaped = prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const read = (pfx: string): void => {\n for (const c of comments) {\n const pagesMatch = c.match(new RegExp(`${pfx}:pages\\\\s+(.+)$`))\n if (pagesMatch?.[1] && out.pages === undefined) {\n out.pages = pagesMatch[1].split(',').map((p) => p.trim()).filter(Boolean)\n }\n const scenarioMatch = c.match(new RegExp(`${pfx}:scenario\\\\s+(\\\\S+)`))\n if (scenarioMatch?.[1] && out.scenario === undefined) out.scenario = scenarioMatch[1]\n }\n }\n read(escaped)\n // legacy read-only fallback when the configured prefix form is absent\n read('@testgen')\n return out\n}\n\n/** Collect destructured property KEYS from a `test()` arrow's `({ a, b: alias })` param. */\nfunction destructuredKeys(fn: ts.ArrowFunction | ts.FunctionExpression): string[] {\n const param = fn.parameters[0]\n if (!param || !ts.isObjectBindingPattern(param.name)) return []\n const keys: string[] = []\n for (const el of param.name.elements) {\n // propertyName is the source key when aliased (`b: alias`); else `name` is the key.\n const keyNode = el.propertyName ?? el.name\n if (ts.isIdentifier(keyNode)) keys.push(keyNode.text)\n else if (ts.isStringLiteral(keyNode)) keys.push(keyNode.text)\n }\n return keys\n}\n\n/** Find literal `page.goto('/x')` string patterns inside a test body. */\nfunction literalGotos(body: ts.Node): string[] {\n const out: string[] = []\n const visit = (node: ts.Node): void => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'goto' &&\n node.arguments[0] &&\n ts.isStringLiteralLike(node.arguments[0])\n ) {\n out.push(normalizePattern(node.arguments[0].text))\n }\n ts.forEachChild(node, visit)\n }\n visit(body)\n return out\n}\n\n/**\n * Is `node` a test-defining call — `test(...)`, `test.only(...)`, or\n * `test.skip(...)` — i.e. one that REGISTERS a single test? Excludes structural\n * helpers (`test.describe`, `test.beforeEach`, `test.step`, …) which do not.\n */\nfunction isTestCall(node: ts.CallExpression): boolean {\n const callee = node.expression\n if (ts.isIdentifier(callee)) return callee.text === 'test'\n if (\n ts.isPropertyAccessExpression(callee) &&\n ts.isIdentifier(callee.expression) &&\n callee.expression.text === 'test'\n ) {\n return callee.name.text === 'only' || callee.name.text === 'skip' || callee.name.text === 'fixme'\n }\n return false\n}\n\n/** Outcome of scanning a spec AST: emittable cases + reports of dynamic-title tests. */\ninterface SpecScan {\n cases: SpecCase[]\n /** Approx descriptions of `test()` calls we found but couldn't emit (non-static title). */\n dynamicTitles: string[]\n}\n\n/**\n * Walk the WHOLE spec AST and collect every `test()`/`test.only()`/`test.skip()`\n * call (so tests inside `test.describe(() => …)` blocks, loops, or other wrappers\n * are found — not just direct top-level statements). A test with a non-static\n * title (template literal or identifier) cannot yield a stable external_key /\n * test_title that matches a Playwright result, so it is NOT emitted but is\n * REPORTED via `dynamicTitles` — silent loss is never acceptable.\n */\nfunction specCases(sf: ts.SourceFile): SpecScan {\n const cases: SpecCase[] = []\n const dynamicTitles: string[] = []\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node) && isTestCall(node)) {\n const titleArg = node.arguments[0]\n const fnArg = node.arguments[1]\n if (titleArg && ts.isStringLiteralLike(titleArg)) {\n if (fnArg && (ts.isArrowFunction(fnArg) || ts.isFunctionExpression(fnArg))) {\n const fixtures = destructuredKeys(fnArg)\n const gotos = fnArg.body ? literalGotos(fnArg.body) : []\n cases.push({ title: titleArg.text, fixtures, gotos })\n } else {\n // String title but no analyzable body (e.g. a bare reference) — still emittable.\n cases.push({ title: titleArg.text, fixtures: [], gotos: [] })\n }\n } else if (titleArg) {\n // Non-static title: template literal, identifier, call, etc. Report, don't emit.\n dynamicTitles.push(approxTitle(titleArg))\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(sf)\n\n // `cases` keep AST traversal order (deterministic per file); the suite build\n // re-sorts across specs. `dynamicTitles` are sorted for stable reporting.\n dynamicTitles.sort((a, b) => a.localeCompare(b))\n return { cases, dynamicTitles }\n}\n\n/** A short, human-readable approximation of a non-static `test()` title argument. */\nfunction approxTitle(node: ts.Node): string {\n const text = node.getText().replace(/\\s+/g, ' ').trim()\n const kind = ts.isTemplateExpression(node)\n ? 'template literal'\n : ts.isIdentifier(node)\n ? 'identifier'\n : 'non-static'\n return `${text.slice(0, 80)} (${kind} title)`\n}\n\n/** List spec files under `e2eDir` matching the `tests/**/*.spec.ts`-style glob. */\nfunction specFiles(e2eDir: string, testsGlob: string): string[] {\n // The glob is `<baseDir>/**/<suffix>` — derive base dir and a filename test.\n const starIdx = testsGlob.indexOf('**')\n const baseRel = starIdx >= 0 ? testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(testsGlob)\n const suffixMatch = testsGlob.match(/\\*([^*/]+)$/)\n const suffix = suffixMatch?.[1] ?? '.spec.ts'\n const baseAbs = join(e2eDir, baseRel)\n\n const out: string[] = []\n const walk = (dir: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })\n } catch {\n return\n }\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.name.endsWith(suffix)) out.push(full)\n }\n }\n walk(baseAbs)\n return out.sort((a, b) => a.localeCompare(b))\n}\n\n// ──────────────────────────────────────────────── orchestration ─────────────\n\n/**\n * Reporting attached to a generation run so that NO spec can vanish silently.\n */\nexport interface GenerateReport {\n /** Per-section generated suites, keyed by section. */\n suites: Map<string, GeneratedSuiteDoc>\n /** Emitted cases that could not be mapped to a page: `<spec> :: <title>`. */\n unmappable: string[]\n /** `test()` calls skipped because their title is not a static string literal: `<spec> :: <approx>`. */\n dynamicTitles: string[]\n /**\n * Spec files matched by the glob that produced ZERO emittable cases (e.g. a\n * factory `messageCrudTest(...)` whose `test()` lives in an imported base).\n * These are NOT in any suite, so they must be surfaced.\n */\n zeroCaseSpecs: string[]\n}\n\n/**\n * Pure generation. Besides the per-section suites, returns three reporting\n * lists so the caller can warn loudly: unmappable cases, dynamic-title tests\n * that were skipped, and spec files that yielded no extractable cases.\n */\nexport function generateSuites(cfg: QaConfig): GenerateReport {\n const version = cliVersion()\n const e2eDir = cfg.e2eDir\n const testsBase = (() => {\n const starIdx = cfg.testsGlob.indexOf('**')\n return starIdx >= 0 ? cfg.testsGlob.slice(0, starIdx).replace(/\\/+$/, '') : dirname(cfg.testsGlob)\n })()\n\n // Step 1 — fixtures map: fixtureName → page-object file path.\n const fixtureToFile = new Map<string, string>()\n const fixturesAbs = join(e2eDir, cfg.fixturesFile)\n if (existsSync(fixturesAbs)) {\n const sf = parseFile(fixturesAbs)\n const imports = importMap(sf)\n for (const [fixtureName, className] of fixtureClasses(sf)) {\n const target = imports.get(className)\n if (target) fixtureToFile.set(fixtureName, target)\n }\n }\n\n // Step 2 — fixtureName → route pattern (parse each referenced page-object once).\n const fixtureClassMap = existsSync(fixturesAbs) ? fixtureClasses(parseFile(fixturesAbs)) : new Map()\n const patternByFile = new Map<string, ts.SourceFile>()\n const fixtureToPattern = new Map<string, string>()\n for (const [fixtureName, file] of fixtureToFile) {\n const className = fixtureClassMap.get(fixtureName)\n if (!className || !existsSync(file)) continue\n let sf = patternByFile.get(file)\n if (!sf) {\n sf = parseFile(file)\n patternByFile.set(file, sf)\n }\n const pattern = gotoPattern(sf, className)\n if (pattern) fixtureToPattern.set(fixtureName, pattern)\n }\n\n const unmappable: string[] = []\n const dynamicTitles: string[] = []\n const zeroCaseSpecs: string[] = []\n // section → accumulated cases + page registry\n interface Section {\n cases: SuiteTestCase[]\n pages: Map<string, SuitePage>\n }\n const sections = new Map<string, Section>()\n\n const files = specFiles(e2eDir, cfg.testsGlob)\n for (const file of files) {\n const specRel = relative(e2eDir, file).split('\\\\').join('/')\n const specBasename = basenameNoSpec(specRel)\n const section = sectionFor(specRel, testsBase)\n const text = readFileSync(file, 'utf8')\n const header = parseHeader(text, cfg.annotationPrefix)\n const sf = parseFile(file)\n const scan = specCases(sf)\n const cases = scan.cases\n\n for (const approx of scan.dynamicTitles) dynamicTitles.push(`${specRel} :: ${approx}`)\n // A spec matched by the glob that produced no emittable cases must be surfaced\n // — its tests live elsewhere (factory/imported base) or use only dynamic titles.\n if (cases.length === 0) zeroCaseSpecs.push(specRel)\n\n if (!sections.has(section)) sections.set(section, { cases: [], pages: new Map() })\n const sec = sections.get(section)!\n\n cases.forEach((spec, index) => {\n let primary: string | undefined\n let traversed: string[] = []\n let pagesSource: 'inferred' | 'annotation' = 'inferred'\n\n if (header.pages && header.pages.length > 0) {\n // Annotation replaces inference for every case in this spec.\n const normalized = header.pages.map((p) => normalizePattern(p))\n primary = normalized[0]\n traversed = normalized.slice(1)\n pagesSource = 'annotation'\n } else {\n // Inference from destructured fixtures, then literal gotos in the body.\n const inferred: string[] = []\n for (const fx of spec.fixtures) {\n const pat = fixtureToPattern.get(fx)\n if (pat) inferred.push(pat)\n }\n for (const g of spec.gotos) inferred.push(g)\n if (inferred.length > 0) {\n primary = inferred[0]\n traversed = inferred.slice(1)\n }\n }\n\n const externalKey = header.scenario ?? `${specRel}::${slug(spec.title)}`\n\n let appliesTo: string[] = []\n if (primary) {\n const key = registerPage(sec.pages, primary, specRel, spec.title)\n appliesTo = [key]\n } else {\n unmappable.push(`${specRel} :: ${spec.title}`)\n }\n\n const testCase: SuiteTestCase = {\n id: `${specBasename}-${index}`,\n title: spec.title,\n suite: specBasename,\n status: 'covered',\n automation: { type: 'playwright', spec: specRel, test_title: spec.title },\n applies_to: appliesTo,\n ...(traversed.length > 0 ? { traverses: traversed.map((pattern) => ({ pattern })) } : {}),\n external_key: externalKey,\n pages_source: pagesSource,\n }\n sec.cases.push(testCase)\n })\n }\n\n // Build the sorted, deterministic suites.\n const suites = new Map<string, GeneratedSuiteDoc>()\n for (const section of [...sections.keys()].sort((a, b) => a.localeCompare(b))) {\n const sec = sections.get(section)!\n // A section with no emittable cases is not a suite — its specs are already\n // surfaced via zeroCaseSpecs/dynamicTitles. Don't write an empty YAML file.\n if (sec.cases.length === 0) continue\n const pages: Record<string, SuitePage> = {}\n for (const key of [...sec.pages.keys()].sort((a, b) => a.localeCompare(b))) {\n pages[key] = sec.pages.get(key)!\n }\n const test_cases = [...sec.cases].sort((a, b) => {\n const specA = a.automation?.spec ?? ''\n const specB = b.automation?.spec ?? ''\n if (specA !== specB) return specA.localeCompare(specB)\n return a.id.localeCompare(b.id)\n })\n suites.set(section, {\n meta: {\n section,\n format: 1,\n origin: 'generated',\n generated_by: `qa-meter@${version}`,\n },\n pages,\n test_cases,\n })\n }\n\n return {\n suites,\n unmappable,\n dynamicTitles: dynamicTitles.sort((a, b) => a.localeCompare(b)),\n zeroCaseSpecs: zeroCaseSpecs.sort((a, b) => a.localeCompare(b)),\n }\n}\n\n/** Register a primary pattern in the section registry; first case for a pattern wins. */\nfunction registerPage(\n registry: Map<string, SuitePage>,\n pattern: string,\n spec: string,\n testTitle: string,\n): string {\n const key = patternKey(pattern)\n if (!registry.has(key)) {\n registry.set(key, { pattern, spec, test_title: testTitle })\n }\n return key\n}\n\n/** `tests/widgets/list.spec.ts` → `list`. */\nfunction basenameNoSpec(specRel: string): string {\n const base = specRel.split('/').pop() ?? specRel\n return base.replace(/\\.spec\\.ts$/, '').replace(/\\.ts$/, '')\n}\n\n/** Section = the immediate parent dir name under the tests base (`tests/widgets/list.spec.ts` → `widgets`). */\nfunction sectionFor(specRel: string, testsBase: string): string {\n let rel = specRel\n const prefix = `${testsBase}/`\n if (rel.startsWith(prefix)) rel = rel.slice(prefix.length)\n const parts = rel.split('/')\n if (parts.length > 1 && parts[0]) return parts[0]\n return testsBase.split('/').filter(Boolean).pop() ?? 'tests'\n}\n\n// ─────────────────────────────────────────────── serialization ──────────────\n\n/** Serialize a generated suite the way `runGenerate` writes it (header + YAML). */\nexport function serializeSuite(doc: GeneratedSuiteDoc): string {\n const yaml = stringify(doc, { lineWidth: 0 })\n return `${HEADER_COMMENT}\\n${yaml}`\n}\n\n// ─────────────────────────────────────────────────── CLI entry ──────────────\n\nexport async function runGenerate(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n const { suites, unmappable, dynamicTitles, zeroCaseSpecs } = generateSuites(cfg)\n\n const outDir = join(cfg.suitesDir, 'generated')\n mkdirSync(outDir, { recursive: true })\n\n const sections = [...suites.keys()].sort((a, b) => a.localeCompare(b))\n for (const section of sections) {\n const doc = suites.get(section)!\n const file = join(outDir, `${section}-test-suite.yaml`)\n writeFileSync(file, serializeSuite(doc), 'utf8')\n }\n\n const totalCases = sections.reduce((n, s) => n + suites.get(s)!.test_cases.length, 0)\n process.stderr.write(\n `qa generate: ${sections.length} suite(s), ${totalCases} case(s) → ${relative(process.cwd(), outDir) || outDir}\\n`,\n )\n if (unmappable.length > 0) {\n process.stderr.write(`qa generate: ${unmappable.length} unmappable test(s):\\n`)\n for (const entry of unmappable) process.stderr.write(` warning: unmappable — ${entry}\\n`)\n }\n if (zeroCaseSpecs.length > 0) {\n process.stderr.write(\n `qa generate: ${zeroCaseSpecs.length} spec(s) produced no extractable cases ` +\n `(factory/loop/dynamic-title) — add @mhosaic:pages/@mhosaic:scenario or a string title:\\n`,\n )\n for (const spec of zeroCaseSpecs) process.stderr.write(` warning: zero-case spec — ${spec}\\n`)\n }\n if (dynamicTitles.length > 0) {\n process.stderr.write(\n `qa generate: ${dynamicTitles.length} test(s) skipped for a non-static title ` +\n `(give them a string literal title or a @mhosaic:scenario id):\\n`,\n )\n for (const entry of dynamicTitles) process.stderr.write(` warning: dynamic-title — ${entry}\\n`)\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
generateSuites,
|
|
4
|
+
runGenerate,
|
|
5
|
+
serializeSuite
|
|
6
|
+
} from "./chunk-SSLQOK2Z.js";
|
|
7
|
+
import "./chunk-AZQSQJNQ.js";
|
|
8
|
+
import "./chunk-IHJPCMYF.js";
|
|
9
|
+
export {
|
|
10
|
+
generateSuites,
|
|
11
|
+
runGenerate,
|
|
12
|
+
serializeSuite
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=generate-VFRQNPOI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -82,10 +82,12 @@ async function runInstallSkill(argv) {
|
|
|
82
82
|
`);
|
|
83
83
|
process.stdout.write(` 2. ${kleur.gray("Onboard a new host app: ")}${kleur.cyan("/integrate-feedback")}
|
|
84
84
|
`);
|
|
85
|
-
process.stdout.write(` 3. ${kleur.gray("
|
|
85
|
+
process.stdout.write(` 3. ${kleur.gray("Add the QA Meter (test-coverage FAB) to a host app: ")}${kleur.cyan("/integrate-qa-meter")}
|
|
86
|
+
`);
|
|
87
|
+
process.stdout.write(` 4. ${kleur.gray("Triage + fix reports: ")}${kleur.cyan("/feedback-pull")} ${kleur.gray("\u2192")} ${kleur.cyan("/feedback-fix")} ${kleur.gray("\u2192")} ${kleur.cyan("/feedback-watch-merges")} ${kleur.gray("\u2192")} ${kleur.cyan("/feedback-close")}
|
|
86
88
|
`);
|
|
87
89
|
}
|
|
88
90
|
export {
|
|
89
91
|
runInstallSkill
|
|
90
92
|
};
|
|
91
|
-
//# sourceMappingURL=install-skill-
|
|
93
|
+
//# sourceMappingURL=install-skill-PO5YSXWY.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/install-skill.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport kleur from 'kleur'\n\ninterface Args {\n force: boolean\n dryRun: boolean\n dest: string\n}\n\nfunction parseArgs(argv: string[]): Args {\n const out: Args = { force: false, dryRun: false, dest: join(homedir(), '.claude', 'skills') }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--force' || a === '-f') out.force = true\n else if (a === '--dry-run') out.dryRun = true\n else if (a === '--dest') out.dest = argv[++i] ?? out.dest\n }\n return out\n}\n\n/**\n * The CLI ships the skill directory inside the npm package. At build time\n * tsup emits dist/bin.js; at runtime we walk up to find the package root\n * and resolve `skills/` from there. This works for global installs (npx)\n * and for symlinked monorepo dev installs.\n */\nfunction findSkillsSource(): string | null {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin lives at dist/bin.js — package root is one level up\n const candidates = [\n join(here, '..', 'skills'),\n join(here, '..', '..', 'skills'),\n ]\n for (const c of candidates) {\n if (existsSync(c)) return c\n }\n return null\n}\n\nfunction copyRecursive(src: string, dest: string, force: boolean, dryRun: boolean): { copied: number; skipped: number } {\n let copied = 0\n let skipped = 0\n if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true })\n for (const entry of readdirSync(src)) {\n const s = join(src, entry)\n const d = join(dest, entry)\n const st = statSync(s)\n if (st.isDirectory()) {\n const r = copyRecursive(s, d, force, dryRun)\n copied += r.copied\n skipped += r.skipped\n } else {\n if (existsSync(d) && !force) {\n skipped++\n continue\n }\n if (!dryRun) {\n if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true })\n writeFileSync(d, readFileSync(s))\n }\n copied++\n }\n }\n return { copied, skipped }\n}\n\nexport async function runInstallSkill(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const src = findSkillsSource()\n\n process.stdout.write(kleur.bold('\\n📦 Installing Mhosaic Feedback Claude Code skill\\n\\n'))\n\n if (src === null) {\n process.stderr.write(kleur.red('Could not locate bundled skills/ directory.\\n'))\n process.stderr.write(kleur.gray('Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\\n'))\n process.exitCode = 1\n return\n }\n\n process.stdout.write(kleur.gray(`Source: ${src}\\n`))\n process.stdout.write(kleur.gray(`Destination: ${args.dest}\\n`))\n if (args.dryRun) process.stdout.write(kleur.yellow('(dry run — no files will be written)\\n'))\n process.stdout.write('\\n')\n\n const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun)\n\n process.stdout.write(kleur.green(`✓ ${copied} file(s) ${args.dryRun ? 'would be ' : ''}copied\\n`))\n if (skipped > 0) {\n process.stdout.write(kleur.yellow(`⚠ ${skipped} file(s) skipped (already exist; use --force to overwrite)\\n`))\n }\n process.stdout.write('\\n')\n process.stdout.write(kleur.bold('Next:\\n'))\n process.stdout.write(` 1. ${kleur.gray('Restart Claude Code if you have it open — skills are discovered at session start.')}\\n`)\n process.stdout.write(` 2. ${kleur.gray('Onboard a new host app: ')}${kleur.cyan('/integrate-feedback')}\\n`)\n process.stdout.write(` 3. ${kleur.gray('Triage + fix reports: ')}${kleur.cyan('/feedback-pull')} ${kleur.gray('→')} ${kleur.cyan('/feedback-fix')} ${kleur.gray('→')} ${kleur.cyan('/feedback-watch-merges')} ${kleur.gray('→')} ${kleur.cyan('/feedback-close')}\\n`)\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/commands/install-skill.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport kleur from 'kleur'\n\ninterface Args {\n force: boolean\n dryRun: boolean\n dest: string\n}\n\nfunction parseArgs(argv: string[]): Args {\n const out: Args = { force: false, dryRun: false, dest: join(homedir(), '.claude', 'skills') }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--force' || a === '-f') out.force = true\n else if (a === '--dry-run') out.dryRun = true\n else if (a === '--dest') out.dest = argv[++i] ?? out.dest\n }\n return out\n}\n\n/**\n * The CLI ships the skill directory inside the npm package. At build time\n * tsup emits dist/bin.js; at runtime we walk up to find the package root\n * and resolve `skills/` from there. This works for global installs (npx)\n * and for symlinked monorepo dev installs.\n */\nfunction findSkillsSource(): string | null {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin lives at dist/bin.js — package root is one level up\n const candidates = [\n join(here, '..', 'skills'),\n join(here, '..', '..', 'skills'),\n ]\n for (const c of candidates) {\n if (existsSync(c)) return c\n }\n return null\n}\n\nfunction copyRecursive(src: string, dest: string, force: boolean, dryRun: boolean): { copied: number; skipped: number } {\n let copied = 0\n let skipped = 0\n if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true })\n for (const entry of readdirSync(src)) {\n const s = join(src, entry)\n const d = join(dest, entry)\n const st = statSync(s)\n if (st.isDirectory()) {\n const r = copyRecursive(s, d, force, dryRun)\n copied += r.copied\n skipped += r.skipped\n } else {\n if (existsSync(d) && !force) {\n skipped++\n continue\n }\n if (!dryRun) {\n if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true })\n writeFileSync(d, readFileSync(s))\n }\n copied++\n }\n }\n return { copied, skipped }\n}\n\nexport async function runInstallSkill(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const src = findSkillsSource()\n\n process.stdout.write(kleur.bold('\\n📦 Installing Mhosaic Feedback Claude Code skill\\n\\n'))\n\n if (src === null) {\n process.stderr.write(kleur.red('Could not locate bundled skills/ directory.\\n'))\n process.stderr.write(kleur.gray('Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\\n'))\n process.exitCode = 1\n return\n }\n\n process.stdout.write(kleur.gray(`Source: ${src}\\n`))\n process.stdout.write(kleur.gray(`Destination: ${args.dest}\\n`))\n if (args.dryRun) process.stdout.write(kleur.yellow('(dry run — no files will be written)\\n'))\n process.stdout.write('\\n')\n\n const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun)\n\n process.stdout.write(kleur.green(`✓ ${copied} file(s) ${args.dryRun ? 'would be ' : ''}copied\\n`))\n if (skipped > 0) {\n process.stdout.write(kleur.yellow(`⚠ ${skipped} file(s) skipped (already exist; use --force to overwrite)\\n`))\n }\n process.stdout.write('\\n')\n process.stdout.write(kleur.bold('Next:\\n'))\n process.stdout.write(` 1. ${kleur.gray('Restart Claude Code if you have it open — skills are discovered at session start.')}\\n`)\n process.stdout.write(` 2. ${kleur.gray('Onboard a new host app: ')}${kleur.cyan('/integrate-feedback')}\\n`)\n process.stdout.write(` 3. ${kleur.gray('Add the QA Meter (test-coverage FAB) to a host app: ')}${kleur.cyan('/integrate-qa-meter')}\\n`)\n process.stdout.write(` 4. ${kleur.gray('Triage + fix reports: ')}${kleur.cyan('/feedback-pull')} ${kleur.gray('→')} ${kleur.cyan('/feedback-fix')} ${kleur.gray('→')} ${kleur.cyan('/feedback-watch-merges')} ${kleur.gray('→')} ${kleur.cyan('/feedback-close')}\\n`)\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,WAAW,cAAc,aAAa,UAAU,qBAAqB;AAC1F,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAE9B,OAAO,WAAW;AAQlB,SAAS,UAAU,MAAsB;AACvC,QAAM,MAAY,EAAE,OAAO,OAAO,QAAQ,OAAO,MAAM,KAAK,QAAQ,GAAG,WAAW,QAAQ,EAAE;AAC5F,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,aAAa,MAAM,KAAM,KAAI,QAAQ;AAAA,aACtC,MAAM,YAAa,KAAI,SAAS;AAAA,aAChC,MAAM,SAAU,KAAI,OAAO,KAAK,EAAE,CAAC,KAAK,IAAI;AAAA,EACvD;AACA,SAAO;AACT;AAQA,SAAS,mBAAkC;AACzC,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEnD,QAAM,aAAa;AAAA,IACjB,KAAK,MAAM,MAAM,QAAQ;AAAA,IACzB,KAAK,MAAM,MAAM,MAAM,QAAQ;AAAA,EACjC;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,WAAW,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,cAAc,KAAa,MAAc,OAAgB,QAAsD;AACtH,MAAI,SAAS;AACb,MAAI,UAAU;AACd,MAAI,CAAC,UAAU,CAAC,WAAW,IAAI,EAAG,WAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACrE,aAAW,SAAS,YAAY,GAAG,GAAG;AACpC,UAAM,IAAI,KAAK,KAAK,KAAK;AACzB,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,UAAM,KAAK,SAAS,CAAC;AACrB,QAAI,GAAG,YAAY,GAAG;AACpB,YAAM,IAAI,cAAc,GAAG,GAAG,OAAO,MAAM;AAC3C,gBAAU,EAAE;AACZ,iBAAW,EAAE;AAAA,IACf,OAAO;AACL,UAAI,WAAW,CAAC,KAAK,CAAC,OAAO;AAC3B;AACA;AAAA,MACF;AACA,UAAI,CAAC,QAAQ;AACX,YAAI,CAAC,WAAW,QAAQ,CAAC,CAAC,EAAG,WAAU,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACtE,sBAAc,GAAG,aAAa,CAAC,CAAC;AAAA,MAClC;AACA;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAEA,eAAsB,gBAAgB,MAA+B;AACnE,QAAM,OAAO,UAAU,IAAI;AAC3B,QAAM,MAAM,iBAAiB;AAE7B,UAAQ,OAAO,MAAM,MAAM,KAAK,+DAAwD,CAAC;AAEzF,MAAI,QAAQ,MAAM;AAChB,YAAQ,OAAO,MAAM,MAAM,IAAI,+CAA+C,CAAC;AAC/E,YAAQ,OAAO,MAAM,MAAM,KAAK,8DAA8D,CAAC;AAC/F,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,MAAM,KAAK,gBAAgB,GAAG;AAAA,CAAI,CAAC;AACxD,UAAQ,OAAO,MAAM,MAAM,KAAK,gBAAgB,KAAK,IAAI;AAAA,CAAI,CAAC;AAC9D,MAAI,KAAK,OAAQ,SAAQ,OAAO,MAAM,MAAM,OAAO,6CAAwC,CAAC;AAC5F,UAAQ,OAAO,MAAM,IAAI;AAEzB,QAAM,EAAE,QAAQ,QAAQ,IAAI,cAAc,KAAK,KAAK,MAAM,KAAK,OAAO,KAAK,MAAM;AAEjF,UAAQ,OAAO,MAAM,MAAM,MAAM,UAAK,MAAM,YAAY,KAAK,SAAS,cAAc,EAAE;AAAA,CAAU,CAAC;AACjG,MAAI,UAAU,GAAG;AACf,YAAQ,OAAO,MAAM,MAAM,OAAO,UAAK,OAAO;AAAA,CAA8D,CAAC;AAAA,EAC/G;AACA,UAAQ,OAAO,MAAM,IAAI;AACzB,UAAQ,OAAO,MAAM,MAAM,KAAK,SAAS,CAAC;AAC1C,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,wFAAmF,CAAC;AAAA,CAAI;AAChI,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,0BAA0B,CAAC,GAAG,MAAM,KAAK,qBAAqB,CAAC;AAAA,CAAI;AAC3G,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,sDAAsD,CAAC,GAAG,MAAM,KAAK,qBAAqB,CAAC;AAAA,CAAI;AACvI,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,wBAAwB,CAAC,GAAG,MAAM,KAAK,gBAAgB,CAAC,IAAI,MAAM,KAAK,QAAG,CAAC,IAAI,MAAM,KAAK,eAAe,CAAC,IAAI,MAAM,KAAK,QAAG,CAAC,IAAI,MAAM,KAAK,wBAAwB,CAAC,IAAI,MAAM,KAAK,QAAG,CAAC,IAAI,MAAM,KAAK,iBAAiB,CAAC;AAAA,CAAI;AACvQ;","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/qa/index.ts
|
|
4
|
+
async function runQa(args) {
|
|
5
|
+
const [sub, ...rest] = args;
|
|
6
|
+
const commands = {
|
|
7
|
+
sitemap: async () => ({ run: dispatchSitemap }),
|
|
8
|
+
generate: async () => ({ run: (await import("./generate-VFRQNPOI.js")).runGenerate }),
|
|
9
|
+
build: async () => ({ run: (await import("./build-RCLWV2WL.js")).runBuild }),
|
|
10
|
+
check: async () => ({ run: (await import("./check-FOJPEE4Q.js")).runCheck }),
|
|
11
|
+
refresh: async () => ({
|
|
12
|
+
run: async (a) => {
|
|
13
|
+
const { loadQaConfig } = await import("./config-JE3QRZVH.js");
|
|
14
|
+
const cfg = loadQaConfig(process.cwd());
|
|
15
|
+
if (cfg.sitemap?.framework) await dispatchSitemap(a);
|
|
16
|
+
await (await import("./generate-VFRQNPOI.js")).runGenerate(a);
|
|
17
|
+
await (await import("./build-RCLWV2WL.js")).runBuild(a);
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
};
|
|
21
|
+
const entry = commands[sub ?? ""];
|
|
22
|
+
if (!entry) {
|
|
23
|
+
process.stderr.write("Usage: mhosaic-feedback qa <sitemap|generate|build|check|refresh> [--strict] [--version <app_version>]\n");
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await (await entry()).run(rest);
|
|
28
|
+
}
|
|
29
|
+
async function dispatchSitemap(args) {
|
|
30
|
+
const { loadQaConfig } = await import("./config-JE3QRZVH.js");
|
|
31
|
+
const framework = loadQaConfig(process.cwd()).sitemap?.framework;
|
|
32
|
+
if (framework === "vue-router") return (await import("./sitemap-vue-EJDQTCYM.js")).runSitemap(args);
|
|
33
|
+
if (framework === "react-router") return (await import("./sitemap-react-QTEAUKN5.js")).runReactSitemap(args);
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`qa sitemap: unsupported framework ${framework ? `'${framework}'` : "(none set)"} (supported: vue-router, react-router). Set config.sitemap.framework.
|
|
36
|
+
`
|
|
37
|
+
);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
runQa
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=qa-YR4GCV6T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/index.ts"],"sourcesContent":["export async function runQa(args: string[]): Promise<void> {\n const [sub, ...rest] = args\n const commands: Record<string, () => Promise<{ run: (a: string[]) => Promise<void> }>> = {\n sitemap: async () => ({ run: dispatchSitemap }),\n generate: async () => ({ run: (await import('./generate')).runGenerate }),\n build: async () => ({ run: (await import('./build')).runBuild }),\n check: async () => ({ run: (await import('./check')).runCheck }),\n refresh: async () => ({\n run: async (a) => {\n const { loadQaConfig } = await import('./config')\n const cfg = loadQaConfig(process.cwd())\n if (cfg.sitemap?.framework) await dispatchSitemap(a)\n await (await import('./generate')).runGenerate(a)\n await (await import('./build')).runBuild(a)\n },\n }),\n }\n const entry = commands[sub ?? '']\n if (!entry) {\n process.stderr.write('Usage: mhosaic-feedback qa <sitemap|generate|build|check|refresh> [--strict] [--version <app_version>]\\n')\n process.exitCode = 1\n return\n }\n await (await entry()).run(rest)\n}\n\n/** Route `qa sitemap` to the adapter named by config.sitemap.framework. */\nasync function dispatchSitemap(args: string[]): Promise<void> {\n const { loadQaConfig } = await import('./config')\n const framework = loadQaConfig(process.cwd()).sitemap?.framework\n if (framework === 'vue-router') return (await import('./sitemap-vue')).runSitemap(args)\n if (framework === 'react-router') return (await import('./sitemap-react')).runReactSitemap(args)\n process.stderr.write(\n `qa sitemap: unsupported framework ${framework ? `'${framework}'` : '(none set)'} ` +\n `(supported: vue-router, react-router). Set config.sitemap.framework.\\n`,\n )\n process.exitCode = 1\n}\n"],"mappings":";;;AAAA,eAAsB,MAAM,MAA+B;AACzD,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,QAAM,WAAmF;AAAA,IACvF,SAAS,aAAa,EAAE,KAAK,gBAAgB;AAAA,IAC7C,UAAU,aAAa,EAAE,MAAM,MAAM,OAAO,wBAAY,GAAG,YAAY;AAAA,IACvE,OAAO,aAAa,EAAE,MAAM,MAAM,OAAO,qBAAS,GAAG,SAAS;AAAA,IAC9D,OAAO,aAAa,EAAE,MAAM,MAAM,OAAO,qBAAS,GAAG,SAAS;AAAA,IAC9D,SAAS,aAAa;AAAA,MACpB,KAAK,OAAO,MAAM;AAChB,cAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAU;AAChD,cAAM,MAAM,aAAa,QAAQ,IAAI,CAAC;AACtC,YAAI,IAAI,SAAS,UAAW,OAAM,gBAAgB,CAAC;AACnD,eAAO,MAAM,OAAO,wBAAY,GAAG,YAAY,CAAC;AAChD,eAAO,MAAM,OAAO,qBAAS,GAAG,SAAS,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,OAAO,EAAE;AAChC,MAAI,CAAC,OAAO;AACV,YAAQ,OAAO,MAAM,0GAA0G;AAC/H,YAAQ,WAAW;AACnB;AAAA,EACF;AACA,SAAO,MAAM,MAAM,GAAG,IAAI,IAAI;AAChC;AAGA,eAAe,gBAAgB,MAA+B;AAC5D,QAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAU;AAChD,QAAM,YAAY,aAAa,QAAQ,IAAI,CAAC,EAAE,SAAS;AACvD,MAAI,cAAc,aAAc,SAAQ,MAAM,OAAO,2BAAe,GAAG,WAAW,IAAI;AACtF,MAAI,cAAc,eAAgB,SAAQ,MAAM,OAAO,6BAAiB,GAAG,gBAAgB,IAAI;AAC/F,UAAQ,OAAO;AAAA,IACb,qCAAqC,YAAY,IAAI,SAAS,MAAM,YAAY;AAAA;AAAA,EAElF;AACA,UAAQ,WAAW;AACrB;","names":[]}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildSitemap
|
|
4
|
+
} from "./chunk-AZQSQJNQ.js";
|
|
5
|
+
import {
|
|
6
|
+
loadQaConfig
|
|
7
|
+
} from "./chunk-IHJPCMYF.js";
|
|
8
|
+
|
|
9
|
+
// src/qa/sitemap-react.ts
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import ts from "typescript";
|
|
13
|
+
function isCatchAllPath(path) {
|
|
14
|
+
return path === "*" || path === "/*" || path !== void 0 && path.endsWith("/*");
|
|
15
|
+
}
|
|
16
|
+
function deriveName(pathToken) {
|
|
17
|
+
const segs = (pathToken ?? "").split("/").filter(Boolean);
|
|
18
|
+
return segs.at(-1) ?? "home";
|
|
19
|
+
}
|
|
20
|
+
function mapReactRoutes(roots) {
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const visit = (node, parentPath) => {
|
|
23
|
+
if (node.unresolved) {
|
|
24
|
+
warnings.push(
|
|
25
|
+
`a route could not be statically resolved (${node.unresolved}) \u2014 its subtree is missing from the sitemap`
|
|
26
|
+
);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (isCatchAllPath(node.path)) return null;
|
|
30
|
+
const hasChildren = !!node.children && node.children.length > 0;
|
|
31
|
+
if (node.isNavigate && !hasChildren) return null;
|
|
32
|
+
const childRaws = [];
|
|
33
|
+
for (const child of node.children ?? []) {
|
|
34
|
+
const mapped = visit(child, node.index ? parentPath : node.path);
|
|
35
|
+
if (mapped) childRaws.push(mapped);
|
|
36
|
+
}
|
|
37
|
+
if (!node.index && node.path === void 0 && childRaws.length === 0) return null;
|
|
38
|
+
const raw = {};
|
|
39
|
+
if (node.index) {
|
|
40
|
+
raw.path = "";
|
|
41
|
+
raw.name = deriveName(parentPath);
|
|
42
|
+
} else if (node.path !== void 0) {
|
|
43
|
+
raw.path = node.path;
|
|
44
|
+
raw.name = deriveName(node.path);
|
|
45
|
+
}
|
|
46
|
+
if (childRaws.length) raw.children = childRaws;
|
|
47
|
+
return raw;
|
|
48
|
+
};
|
|
49
|
+
const routes = [];
|
|
50
|
+
for (const node of roots) {
|
|
51
|
+
const mapped = visit(node, void 0);
|
|
52
|
+
if (mapped) routes.push(mapped);
|
|
53
|
+
}
|
|
54
|
+
return { routes, warnings };
|
|
55
|
+
}
|
|
56
|
+
function routerLocalNames(sf) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const st of sf.statements) {
|
|
59
|
+
if (!ts.isImportDeclaration(st)) continue;
|
|
60
|
+
const spec = st.moduleSpecifier;
|
|
61
|
+
if (!ts.isStringLiteral(spec)) continue;
|
|
62
|
+
if (spec.text !== "react-router" && spec.text !== "react-router-dom") continue;
|
|
63
|
+
const named = st.importClause?.namedBindings;
|
|
64
|
+
if (!named || !ts.isNamedImports(named)) continue;
|
|
65
|
+
for (const el of named.elements) {
|
|
66
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
67
|
+
if (imported === "Route") out.route = el.name.text;
|
|
68
|
+
else if (imported === "Routes") out.routes = el.name.text;
|
|
69
|
+
else if (imported === "Navigate") out.navigate = el.name.text;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function jsxStringAttr(init) {
|
|
75
|
+
if (!init) return null;
|
|
76
|
+
if (ts.isStringLiteral(init)) return init.text;
|
|
77
|
+
if (ts.isJsxExpression(init) && init.expression) {
|
|
78
|
+
const e = init.expression;
|
|
79
|
+
if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function jsxElementRootTag(init, sf) {
|
|
84
|
+
if (!init || !ts.isJsxExpression(init) || !init.expression) return null;
|
|
85
|
+
return jsxRootTag(init.expression, sf);
|
|
86
|
+
}
|
|
87
|
+
function jsxRootTag(expr, sf) {
|
|
88
|
+
let e = expr;
|
|
89
|
+
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
90
|
+
if (ts.isJsxElement(e)) return e.openingElement.tagName.getText(sf);
|
|
91
|
+
if (ts.isJsxSelfClosingElement(e)) return e.tagName.getText(sf);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function readJsxRoutes(sf) {
|
|
95
|
+
const warnings = [];
|
|
96
|
+
const local = routerLocalNames(sf);
|
|
97
|
+
const isTag = (n, name) => {
|
|
98
|
+
if (!name) return false;
|
|
99
|
+
if (ts.isJsxElement(n)) return n.openingElement.tagName.getText(sf) === name;
|
|
100
|
+
if (ts.isJsxSelfClosingElement(n)) return n.tagName.getText(sf) === name;
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
const attrsOf = (n) => ts.isJsxElement(n) ? n.openingElement.attributes.properties : n.attributes.properties;
|
|
104
|
+
const readRoute = (n) => {
|
|
105
|
+
const route = {};
|
|
106
|
+
for (const a of attrsOf(n)) {
|
|
107
|
+
if (!ts.isJsxAttribute(a)) continue;
|
|
108
|
+
const name = a.name.getText(sf);
|
|
109
|
+
if (name === "index") route.index = true;
|
|
110
|
+
else if (name === "path") {
|
|
111
|
+
const v = jsxStringAttr(a.initializer);
|
|
112
|
+
if (v === null) route.unresolved = "dynamic-path";
|
|
113
|
+
else route.path = v;
|
|
114
|
+
} else if (name === "element") {
|
|
115
|
+
if (jsxElementRootTag(a.initializer, sf) === local.navigate) route.isNavigate = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (ts.isJsxElement(n)) {
|
|
119
|
+
const kids = [];
|
|
120
|
+
for (const child of n.children) {
|
|
121
|
+
if (isTag(child, local.route)) kids.push(readRoute(child));
|
|
122
|
+
}
|
|
123
|
+
if (kids.length) route.children = kids;
|
|
124
|
+
}
|
|
125
|
+
return route;
|
|
126
|
+
};
|
|
127
|
+
const roots = [];
|
|
128
|
+
const visit = (n) => {
|
|
129
|
+
if (isTag(n, local.routes) && ts.isJsxElement(n)) {
|
|
130
|
+
for (const child of n.children) {
|
|
131
|
+
if (isTag(child, local.route)) roots.push(readRoute(child));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(n, visit);
|
|
136
|
+
};
|
|
137
|
+
visit(sf);
|
|
138
|
+
if (!roots.length) warnings.push("no <Routes>/<Route> elements found");
|
|
139
|
+
return { roots, warnings };
|
|
140
|
+
}
|
|
141
|
+
var ROUTER_CREATORS = /* @__PURE__ */ new Set([
|
|
142
|
+
"createBrowserRouter",
|
|
143
|
+
"createMemoryRouter",
|
|
144
|
+
"createHashRouter",
|
|
145
|
+
"useRoutes"
|
|
146
|
+
]);
|
|
147
|
+
function propName(name, sf) {
|
|
148
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
149
|
+
return name.getText(sf);
|
|
150
|
+
}
|
|
151
|
+
function literalString(expr) {
|
|
152
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function objectToReactRoute(el, navigateName, sf) {
|
|
156
|
+
if (ts.isSpreadElement(el)) return { unresolved: "spread" };
|
|
157
|
+
if (!ts.isObjectLiteralExpression(el)) return { unresolved: "dynamic" };
|
|
158
|
+
const route = {};
|
|
159
|
+
for (const prop of el.properties) {
|
|
160
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
161
|
+
const key = propName(prop.name, sf);
|
|
162
|
+
if (key === "path") {
|
|
163
|
+
const v = literalString(prop.initializer);
|
|
164
|
+
if (v === null) route.unresolved = "dynamic-path";
|
|
165
|
+
else route.path = v;
|
|
166
|
+
} else if (key === "index") {
|
|
167
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) route.index = true;
|
|
168
|
+
} else if (key === "element") {
|
|
169
|
+
if (jsxRootTag(prop.initializer, sf) === navigateName) route.isNavigate = true;
|
|
170
|
+
} else if (key === "children") {
|
|
171
|
+
if (ts.isArrayLiteralExpression(prop.initializer)) {
|
|
172
|
+
route.children = prop.initializer.elements.map((c) => objectToReactRoute(c, navigateName, sf));
|
|
173
|
+
} else {
|
|
174
|
+
route.unresolved = "dynamic-children";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return route;
|
|
179
|
+
}
|
|
180
|
+
function readDataRoutes(sf) {
|
|
181
|
+
const warnings = [];
|
|
182
|
+
const navigateName = routerLocalNames(sf).navigate;
|
|
183
|
+
let array;
|
|
184
|
+
const findArray = (n) => {
|
|
185
|
+
if (array) return;
|
|
186
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {
|
|
187
|
+
const arg0 = n.arguments[0];
|
|
188
|
+
if (arg0 && ts.isArrayLiteralExpression(arg0)) {
|
|
189
|
+
array = arg0;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
ts.forEachChild(n, findArray);
|
|
194
|
+
};
|
|
195
|
+
findArray(sf);
|
|
196
|
+
if (!array) {
|
|
197
|
+
for (const st of sf.statements) {
|
|
198
|
+
if (!ts.isVariableStatement(st)) continue;
|
|
199
|
+
for (const d of st.declarationList.declarations) {
|
|
200
|
+
const init = d.initializer;
|
|
201
|
+
if (ts.isIdentifier(d.name) && d.name.text === "routes" && init && ts.isArrayLiteralExpression(init)) {
|
|
202
|
+
array = init;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!array) {
|
|
208
|
+
warnings.push("no createBrowserRouter([\u2026]) call or `routes` array found");
|
|
209
|
+
return { roots: [], warnings };
|
|
210
|
+
}
|
|
211
|
+
const roots = array.elements.map((el) => objectToReactRoute(el, navigateName, sf));
|
|
212
|
+
return { roots, warnings };
|
|
213
|
+
}
|
|
214
|
+
var ROUTE_FILE_SIGNALS = /createBrowserRouter|createMemoryRouter|createHashRouter|useRoutes|<Routes\b/;
|
|
215
|
+
function scanForRouteFiles(dir) {
|
|
216
|
+
const out = [];
|
|
217
|
+
const walk = (d) => {
|
|
218
|
+
let entries;
|
|
219
|
+
try {
|
|
220
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
225
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
226
|
+
const full = join(d, e.name);
|
|
227
|
+
if (e.isDirectory()) {
|
|
228
|
+
walk(full);
|
|
229
|
+
} else if (/\.(tsx|jsx|ts)$/.test(e.name) && !/\.(test|spec)\.[tj]sx?$/.test(e.name)) {
|
|
230
|
+
try {
|
|
231
|
+
if (ROUTE_FILE_SIGNALS.test(readFileSync(full, "utf8"))) out.push(full);
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
walk(dir);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function resolveEntry(opts, warnings) {
|
|
241
|
+
if (opts.entry) {
|
|
242
|
+
if (existsSync(opts.entry) && statSync(opts.entry).isFile()) return opts.entry;
|
|
243
|
+
warnings.push(`entry file not found: ${opts.entry}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (!opts.routerDir) {
|
|
247
|
+
warnings.push("react-router adapter needs `entry` or `routerDir` in config.sitemap");
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const candidates = scanForRouteFiles(opts.routerDir);
|
|
251
|
+
const first = candidates[0];
|
|
252
|
+
if (!first) {
|
|
253
|
+
warnings.push(`no router file (with <Routes> or createBrowserRouter) under ${opts.routerDir}`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (candidates.length > 1) {
|
|
257
|
+
warnings.push(
|
|
258
|
+
`multiple candidate router files under ${opts.routerDir}; using ${first} \u2014 set sitemap.entry to disambiguate`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return first;
|
|
262
|
+
}
|
|
263
|
+
function detectStyle(sf) {
|
|
264
|
+
let isData = false;
|
|
265
|
+
const visit = (n) => {
|
|
266
|
+
if (isData) return;
|
|
267
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {
|
|
268
|
+
isData = true;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
ts.forEachChild(n, visit);
|
|
272
|
+
};
|
|
273
|
+
visit(sf);
|
|
274
|
+
return isData ? "data" : "jsx";
|
|
275
|
+
}
|
|
276
|
+
function extractWithWarnings(opts) {
|
|
277
|
+
const warnings = [];
|
|
278
|
+
const entry = resolveEntry(opts, warnings);
|
|
279
|
+
if (!entry) return { payload: { format: 1, pages: [] }, warnings };
|
|
280
|
+
const source = readFileSync(entry, "utf8");
|
|
281
|
+
const sf = ts.createSourceFile(entry, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
282
|
+
const style = opts.routerStyle ?? detectStyle(sf);
|
|
283
|
+
const read = style === "data" ? readDataRoutes(sf) : readJsxRoutes(sf);
|
|
284
|
+
warnings.push(...read.warnings);
|
|
285
|
+
const mapped = mapReactRoutes(read.roots);
|
|
286
|
+
warnings.push(...mapped.warnings);
|
|
287
|
+
return { payload: buildSitemap(mapped.routes), warnings };
|
|
288
|
+
}
|
|
289
|
+
function extractReactRouterSitemap(opts) {
|
|
290
|
+
return extractWithWarnings(opts).payload;
|
|
291
|
+
}
|
|
292
|
+
async function runReactSitemap(_args) {
|
|
293
|
+
const cfg = loadQaConfig(process.cwd());
|
|
294
|
+
if (!cfg.sitemap || cfg.sitemap.framework !== "react-router") {
|
|
295
|
+
process.stderr.write(
|
|
296
|
+
'qa sitemap: config.sitemap.framework must be "react-router". Add a `sitemap` block with `framework: "react-router"`, `entry` (or `routerDir`), and `file`.\n'
|
|
297
|
+
);
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!cfg.sitemap.entry && !cfg.sitemap.routerDir) {
|
|
302
|
+
process.stderr.write("qa sitemap: react-router adapter needs `entry` (or `routerDir`) in config.sitemap.\n");
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { payload, warnings } = extractWithWarnings({
|
|
307
|
+
...cfg.sitemap.entry ? { entry: cfg.sitemap.entry } : {},
|
|
308
|
+
...cfg.sitemap.routerDir ? { routerDir: cfg.sitemap.routerDir } : {},
|
|
309
|
+
...cfg.sitemap.routerStyle ? { routerStyle: cfg.sitemap.routerStyle } : {}
|
|
310
|
+
});
|
|
311
|
+
const outFile = cfg.sitemap.file;
|
|
312
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
313
|
+
writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}
|
|
314
|
+
`, "utf8");
|
|
315
|
+
process.stderr.write(`qa sitemap \u2192 ${outFile} (${payload.pages.length} pages)
|
|
316
|
+
`);
|
|
317
|
+
for (const warning of warnings) process.stderr.write(` warning: ${warning}
|
|
318
|
+
`);
|
|
319
|
+
}
|
|
320
|
+
export {
|
|
321
|
+
deriveName,
|
|
322
|
+
extractReactRouterSitemap,
|
|
323
|
+
extractWithWarnings,
|
|
324
|
+
isCatchAllPath,
|
|
325
|
+
mapReactRoutes,
|
|
326
|
+
readDataRoutes,
|
|
327
|
+
readJsxRoutes,
|
|
328
|
+
runReactSitemap
|
|
329
|
+
};
|
|
330
|
+
//# sourceMappingURL=sitemap-react-QTEAUKN5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/qa/sitemap-react.ts"],"sourcesContent":["/**\n * react-router adapter for `qa sitemap` — STATIC AST extraction.\n *\n * Reads React Router v7 *declarative* route declarations straight from the\n * TypeScript syntax tree (no module evaluation, no vm, no React). Two readers:\n * - JSX: <Routes><Route path=… /> (e.g. apur/frontend App.tsx)\n * - object: createBrowserRouter([ { path, … } ]) (e.g. mhosaic-core router.tsx)\n * Both emit a normalized ReactRoute tree, mapped to the framework-agnostic\n * RawRoute tree consumed by sitemap-core.buildSitemap.\n *\n * Honest about its limits (spec 2026-06-15 §7): non-literal paths and\n * dynamically-composed object routes are WARNED, never silently dropped.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport ts from 'typescript'\nimport { buildSitemap, type RawRoute, type SitemapPayload } from './sitemap-core'\nimport { loadQaConfig } from './config'\n\n/** Normalized, framework-shaped route node produced by a reader. */\nexport interface ReactRoute {\n /** The route's OWN path token, as authored ('app', 'reports/:id', '/login'). */\n path?: string\n /** `<Route index/>` or `{ index: true }`. */\n index?: boolean\n /** The route's `element` is a root `<Navigate/>` (a redirect). */\n isNavigate?: boolean\n children?: ReactRoute[]\n /** Set when the reader saw a node it could not statically resolve. */\n unresolved?: string\n}\n\n// ───────────────────────────────── mapping (§4) ─────────────────────────────\n\n/** True for a react-router catch-all/splat path ('*' or '…/*'). */\nexport function isCatchAllPath(path: string | undefined): boolean {\n return path === '*' || path === '/*' || (path !== undefined && path.endsWith('/*'))\n}\n\n/** Last '/'-segment of a path token; '' / '/' / undefined → 'home'. */\nexport function deriveName(pathToken: string | undefined): string {\n const segs = (pathToken ?? '').split('/').filter(Boolean)\n return segs.at(-1) ?? 'home'\n}\n\n/**\n * Map a normalized ReactRoute tree to the RawRoute tree + collect honesty\n * warnings. Rules (spec §4): drop catch-all / `<Navigate>`-only; `index` → empty\n * path with the parent's derived name; pathless layout (no path, has children) →\n * no name (core marks it `structural`); real pages get a path-derived name.\n */\nexport function mapReactRoutes(roots: ReadonlyArray<ReactRoute>): {\n routes: RawRoute[]\n warnings: string[]\n} {\n const warnings: string[] = []\n\n const visit = (node: ReactRoute, parentPath: string | undefined): RawRoute | null => {\n if (node.unresolved) {\n warnings.push(\n `a route could not be statically resolved (${node.unresolved}) — its subtree is missing from the sitemap`,\n )\n return null\n }\n if (isCatchAllPath(node.path)) return null\n const hasChildren = !!node.children && node.children.length > 0\n if (node.isNavigate && !hasChildren) return null\n\n const childRaws: RawRoute[] = []\n for (const child of node.children ?? []) {\n // index children are named from the nearest pathed ancestor; pass our own\n // path down (undefined for a pathless layout).\n const mapped = visit(child, node.index ? parentPath : node.path)\n if (mapped) childRaws.push(mapped)\n }\n\n // A degenerate node (no path, not index, no surviving children) contributes nothing.\n if (!node.index && node.path === undefined && childRaws.length === 0) return null\n\n const raw: RawRoute = {}\n if (node.index) {\n raw.path = ''\n raw.name = deriveName(parentPath)\n } else if (node.path !== undefined) {\n raw.path = node.path\n raw.name = deriveName(node.path)\n }\n // else: pathless layout → leave path & name undefined (structural in core).\n if (childRaws.length) raw.children = childRaws\n return raw\n }\n\n const routes: RawRoute[] = []\n for (const node of roots) {\n const mapped = visit(node, undefined)\n if (mapped) routes.push(mapped)\n }\n return { routes, warnings }\n}\n\n// ─────────────────────────────────── AST helpers ────────────────────────────\n\ninterface RouterLocals { route?: string; routes?: string; navigate?: string }\n\n/** Local identifiers bound to react-router(-dom) `Route`/`Routes`/`Navigate`. */\nfunction routerLocalNames(sf: ts.SourceFile): RouterLocals {\n const out: RouterLocals = {}\n for (const st of sf.statements) {\n if (!ts.isImportDeclaration(st)) continue\n const spec = st.moduleSpecifier\n if (!ts.isStringLiteral(spec)) continue\n if (spec.text !== 'react-router' && spec.text !== 'react-router-dom') continue\n const named = st.importClause?.namedBindings\n if (!named || !ts.isNamedImports(named)) continue\n for (const el of named.elements) {\n const imported = (el.propertyName ?? el.name).text\n if (imported === 'Route') out.route = el.name.text\n else if (imported === 'Routes') out.routes = el.name.text\n else if (imported === 'Navigate') out.navigate = el.name.text\n }\n }\n return out\n}\n\n/** Static string value of a JSX attribute initializer, or null if non-literal. */\nfunction jsxStringAttr(init: ts.JsxAttributeValue | undefined): string | null {\n if (!init) return null\n if (ts.isStringLiteral(init)) return init.text\n if (ts.isJsxExpression(init) && init.expression) {\n const e = init.expression\n if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text\n }\n return null\n}\n\n/** Root JSX tag name of an `element={…}` attribute initializer, or null. */\nfunction jsxElementRootTag(init: ts.JsxAttributeValue | undefined, sf: ts.SourceFile): string | null {\n if (!init || !ts.isJsxExpression(init) || !init.expression) return null\n return jsxRootTag(init.expression, sf)\n}\n\n/** Root JSX tag of an expression (unwrapping parentheses); null if not JSX. */\nfunction jsxRootTag(expr: ts.Expression, sf: ts.SourceFile): string | null {\n let e: ts.Expression = expr\n while (ts.isParenthesizedExpression(e)) e = e.expression\n if (ts.isJsxElement(e)) return e.openingElement.tagName.getText(sf)\n if (ts.isJsxSelfClosingElement(e)) return e.tagName.getText(sf)\n return null\n}\n\n// ─────────────────────────────────── JSX reader ─────────────────────────────\n\n/** Parse `<Routes><Route …/>` JSX into a ReactRoute tree. */\nexport function readJsxRoutes(sf: ts.SourceFile): { roots: ReactRoute[]; warnings: string[] } {\n const warnings: string[] = []\n const local = routerLocalNames(sf)\n\n const isTag = (n: ts.Node, name: string | undefined): boolean => {\n if (!name) return false\n if (ts.isJsxElement(n)) return n.openingElement.tagName.getText(sf) === name\n if (ts.isJsxSelfClosingElement(n)) return n.tagName.getText(sf) === name\n return false\n }\n const attrsOf = (n: ts.JsxElement | ts.JsxSelfClosingElement): ts.NodeArray<ts.JsxAttributeLike> =>\n ts.isJsxElement(n) ? n.openingElement.attributes.properties : n.attributes.properties\n\n const readRoute = (n: ts.JsxElement | ts.JsxSelfClosingElement): ReactRoute => {\n const route: ReactRoute = {}\n for (const a of attrsOf(n)) {\n if (!ts.isJsxAttribute(a)) continue\n const name = a.name.getText(sf)\n if (name === 'index') route.index = true\n else if (name === 'path') {\n const v = jsxStringAttr(a.initializer)\n if (v === null) route.unresolved = 'dynamic-path'\n else route.path = v\n } else if (name === 'element') {\n if (jsxElementRootTag(a.initializer, sf) === local.navigate) route.isNavigate = true\n }\n }\n if (ts.isJsxElement(n)) {\n const kids: ReactRoute[] = []\n for (const child of n.children) {\n if (isTag(child, local.route)) kids.push(readRoute(child as ts.JsxElement | ts.JsxSelfClosingElement))\n }\n if (kids.length) route.children = kids\n }\n return route\n }\n\n const roots: ReactRoute[] = []\n const visit = (n: ts.Node): void => {\n if (isTag(n, local.routes) && ts.isJsxElement(n)) {\n for (const child of n.children) {\n if (isTag(child, local.route)) roots.push(readRoute(child as ts.JsxElement | ts.JsxSelfClosingElement))\n }\n return // consumed this <Routes> subtree\n }\n ts.forEachChild(n, visit)\n }\n visit(sf)\n\n if (!roots.length) warnings.push('no <Routes>/<Route> elements found')\n return { roots, warnings }\n}\n\n// ─────────────────────────────────── object reader ──────────────────────────\n\nconst ROUTER_CREATORS = new Set([\n 'createBrowserRouter', 'createMemoryRouter', 'createHashRouter', 'useRoutes',\n])\n\n/** Property-name text of an object-literal key. */\nfunction propName(name: ts.PropertyName, sf: ts.SourceFile): string {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text\n return name.getText(sf)\n}\n\n/** Static string value of an expression, or null if non-literal. */\nfunction literalString(expr: ts.Expression): string | null {\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text\n return null\n}\n\n/** One array element → a ReactRoute (or an `unresolved` marker). */\nfunction objectToReactRoute(\n el: ts.Expression,\n navigateName: string | undefined,\n sf: ts.SourceFile,\n): ReactRoute {\n if (ts.isSpreadElement(el)) return { unresolved: 'spread' }\n if (!ts.isObjectLiteralExpression(el)) return { unresolved: 'dynamic' }\n\n const route: ReactRoute = {}\n for (const prop of el.properties) {\n if (!ts.isPropertyAssignment(prop)) continue\n const key = propName(prop.name, sf)\n if (key === 'path') {\n const v = literalString(prop.initializer)\n if (v === null) route.unresolved = 'dynamic-path'\n else route.path = v\n } else if (key === 'index') {\n if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) route.index = true\n } else if (key === 'element') {\n if (jsxRootTag(prop.initializer, sf) === navigateName) route.isNavigate = true\n } else if (key === 'children') {\n if (ts.isArrayLiteralExpression(prop.initializer)) {\n route.children = prop.initializer.elements.map((c) => objectToReactRoute(c, navigateName, sf))\n } else {\n route.unresolved = 'dynamic-children'\n }\n }\n }\n return route\n}\n\n/** Parse `createBrowserRouter([…])` / a `routes` array into a ReactRoute tree. */\nexport function readDataRoutes(sf: ts.SourceFile): { roots: ReactRoute[]; warnings: string[] } {\n const warnings: string[] = []\n const navigateName = routerLocalNames(sf).navigate\n\n let array: ts.ArrayLiteralExpression | undefined\n const findArray = (n: ts.Node): void => {\n if (array) return\n if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {\n const arg0 = n.arguments[0]\n if (arg0 && ts.isArrayLiteralExpression(arg0)) {\n array = arg0\n return\n }\n }\n ts.forEachChild(n, findArray)\n }\n findArray(sf)\n\n if (!array) {\n for (const st of sf.statements) {\n if (!ts.isVariableStatement(st)) continue\n for (const d of st.declarationList.declarations) {\n const init = d.initializer\n if (ts.isIdentifier(d.name) && d.name.text === 'routes' && init && ts.isArrayLiteralExpression(init)) {\n array = init\n }\n }\n }\n }\n\n if (!array) {\n warnings.push('no createBrowserRouter([…]) call or `routes` array found')\n return { roots: [], warnings }\n }\n\n const roots = array.elements.map((el) => objectToReactRoute(el, navigateName, sf))\n return { roots, warnings }\n}\n\n// ─────────────────────────────────── extractor ──────────────────────────────\n\nexport interface ReactRouterAdapterOptions {\n /** The file holding the route tree (absolute). Preferred. */\n entry?: string\n /** Dir to scan for the entry when `entry` is omitted (absolute). */\n routerDir?: string\n /** Force a reader; otherwise auto-detect from the parsed file. */\n routerStyle?: 'jsx' | 'data'\n}\n\ninterface ExtractResult { payload: SitemapPayload; warnings: string[] }\n\nconst ROUTE_FILE_SIGNALS = /createBrowserRouter|createMemoryRouter|createHashRouter|useRoutes|<Routes\\b/\n\n/** Recursively list candidate router files under `dir` (sorted; tests excluded). */\nfunction scanForRouteFiles(dir: string): string[] {\n const out: string[] = []\n const walk = (d: string): void => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = readdirSync(d, { withFileTypes: true })\n } catch {\n return\n }\n for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n if (e.name === 'node_modules' || e.name.startsWith('.')) continue\n const full = join(d, e.name)\n if (e.isDirectory()) {\n walk(full)\n } else if (/\\.(tsx|jsx|ts)$/.test(e.name) && !/\\.(test|spec)\\.[tj]sx?$/.test(e.name)) {\n try {\n if (ROUTE_FILE_SIGNALS.test(readFileSync(full, 'utf8'))) out.push(full)\n } catch {\n // ignore unreadable files\n }\n }\n }\n }\n walk(dir)\n return out\n}\n\n/** Resolve the entry file from opts, or null (with a warning). */\nfunction resolveEntry(opts: ReactRouterAdapterOptions, warnings: string[]): string | null {\n if (opts.entry) {\n if (existsSync(opts.entry) && statSync(opts.entry).isFile()) return opts.entry\n warnings.push(`entry file not found: ${opts.entry}`)\n return null\n }\n if (!opts.routerDir) {\n warnings.push('react-router adapter needs `entry` or `routerDir` in config.sitemap')\n return null\n }\n const candidates = scanForRouteFiles(opts.routerDir)\n const first = candidates[0]\n if (!first) {\n warnings.push(`no router file (with <Routes> or createBrowserRouter) under ${opts.routerDir}`)\n return null\n }\n if (candidates.length > 1) {\n warnings.push(\n `multiple candidate router files under ${opts.routerDir}; using ${first} — set sitemap.entry to disambiguate`,\n )\n }\n return first\n}\n\n/** Auto-detect the reader from the parsed file: a creator call ⇒ 'data', else 'jsx'. */\nfunction detectStyle(sf: ts.SourceFile): 'jsx' | 'data' {\n let isData = false\n const visit = (n: ts.Node): void => {\n if (isData) return\n if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && ROUTER_CREATORS.has(n.expression.text)) {\n isData = true\n return\n }\n ts.forEachChild(n, visit)\n }\n visit(sf)\n return isData ? 'data' : 'jsx'\n}\n\n/** Internal worker: payload + honesty/IO warnings. */\nexport function extractWithWarnings(opts: ReactRouterAdapterOptions): ExtractResult {\n const warnings: string[] = []\n const entry = resolveEntry(opts, warnings)\n if (!entry) return { payload: { format: 1, pages: [] }, warnings }\n\n const source = readFileSync(entry, 'utf8')\n const sf = ts.createSourceFile(entry, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)\n\n const style = opts.routerStyle ?? detectStyle(sf)\n const read = style === 'data' ? readDataRoutes(sf) : readJsxRoutes(sf)\n warnings.push(...read.warnings)\n\n const mapped = mapReactRoutes(read.roots)\n warnings.push(...mapped.warnings)\n\n return { payload: buildSitemap(mapped.routes), warnings }\n}\n\n/** Statically extract the react-router sitemap payload (idempotent). */\nexport function extractReactRouterSitemap(opts: ReactRouterAdapterOptions): SitemapPayload {\n return extractWithWarnings(opts).payload\n}\n\n// ─────────────────────────────────── CLI entry ──────────────────────────────\n\n/** CLI: `qa sitemap` for react-router — load config, extract, write, report. */\nexport async function runReactSitemap(_args: string[]): Promise<void> {\n const cfg = loadQaConfig(process.cwd())\n if (!cfg.sitemap || cfg.sitemap.framework !== 'react-router') {\n process.stderr.write(\n 'qa sitemap: config.sitemap.framework must be \"react-router\". ' +\n 'Add a `sitemap` block with `framework: \"react-router\"`, `entry` (or `routerDir`), and `file`.\\n',\n )\n process.exitCode = 1\n return\n }\n if (!cfg.sitemap.entry && !cfg.sitemap.routerDir) {\n process.stderr.write('qa sitemap: react-router adapter needs `entry` (or `routerDir`) in config.sitemap.\\n')\n process.exitCode = 1\n return\n }\n\n const { payload, warnings } = extractWithWarnings({\n ...(cfg.sitemap.entry ? { entry: cfg.sitemap.entry } : {}),\n ...(cfg.sitemap.routerDir ? { routerDir: cfg.sitemap.routerDir } : {}),\n ...(cfg.sitemap.routerStyle ? { routerStyle: cfg.sitemap.routerStyle } : {}),\n })\n\n const outFile = cfg.sitemap.file\n mkdirSync(dirname(outFile), { recursive: true })\n writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\\n`, 'utf8')\n\n process.stderr.write(`qa sitemap → ${outFile} (${payload.pages.length} pages)\\n`)\n for (const warning of warnings) process.stderr.write(` warning: ${warning}\\n`)\n}\n"],"mappings":"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","names":[]}
|