diff --git a/README.md b/README.md index d7ae513..c94184c 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,20 @@ export default [ ] ``` +ESLint processor behavior: + +- real JS/TS diagnostics inside Pug expression sites such as `#{...}`, `${...}`, `tag= ...`, attribute expressions, and inline handler/function bodies are mapped back to the original Pug source +- formatting diagnostics for those embedded expression sites are linted against source-faithful JS wrappers rather than only against generated JSX +- autofixes and suggestions for those embedded expression sites are mapped back to the original Pug source +- diagnostics that originate only from synthetic generated helper code are filtered out +- the processor formats against the project's own `@stylistic` package when available so generated JSX converges to the same style engine the outer ESLint run uses + +Current limitation: + +- multiline unbuffered `- ...` statements that are authored across several Pug lines do not yet get the same source-faithful formatting-diagnostic surface as embedded expression sites +- diagnostics that arise only on the generated JSX surface of a transformed Pug region still do not map autofixes back; those remain report-only unless they come from an embedded source-faithful JS site +- the internal formatter still relies on deprecated `@stylistic/jsx-indent` / `@stylistic/jsx-indent-props` compatibility rules, so some dependency graphs may emit a one-time deprecation warning during an ESLint run + ## VS Code Settings - `pugReact.enabled` diff --git a/example/src/TypeScriptInPug.tsx b/example/src/TypeScriptInPug.tsx index ac3bf17..066fcb6 100644 --- a/example/src/TypeScriptInPug.tsx +++ b/example/src/TypeScriptInPug.tsx @@ -48,7 +48,7 @@ export default function TypeScriptInPug () { if maybeTitle != null p #{maybeTitle as string} - each item in (items as string[]) + each item in items as string[] Card(title=item!) ` } diff --git a/packages/eslint-plugin-react-pug/README.md b/packages/eslint-plugin-react-pug/README.md index ce6c8ee..8e7cd23 100644 --- a/packages/eslint-plugin-react-pug/README.md +++ b/packages/eslint-plugin-react-pug/README.md @@ -40,6 +40,22 @@ treats JS files as JSX-capable and you want to skip JSX auto-detection. Used `pug` import bindings are removed from the processor's transformed view automatically. +## Linting Contract + +The processor is designed to preserve useful JavaScript/TypeScript diagnostics inside Pug regions: + +- real JS/TS rule violations inside `#{...}`, `${...}`, `tag= ...`, attribute expressions, and inline handler/function bodies are reported back at the original Pug location +- formatting diagnostics for those embedded expression sites are linted against source-faithful JS wrappers rather than only against generated JSX +- autofixes and suggestions for those embedded expression sites are mapped back to the original Pug source +- diagnostics caused only by synthetic generated helper code are filtered out +- the formatter tries to converge to the consuming project's own `@stylistic` setup when that package is available locally + +Current limitation: + +- multiline unbuffered `- ...` statements that are authored across several Pug lines do not yet get the same source-faithful formatting-diagnostic surface as embedded expression sites +- diagnostics that arise only on the generated JSX surface of a transformed Pug region still do not map autofixes back; those remain report-only unless they come from an embedded source-faithful JS site +- the internal formatter still relies on deprecated `@stylistic/jsx-indent` / `@stylistic/jsx-indent-props` compatibility rules, so some dependency graphs may emit a one-time deprecation warning during an ESLint run + ## Exports - default ESLint plugin object diff --git a/packages/eslint-plugin-react-pug/src/index.ts b/packages/eslint-plugin-react-pug/src/index.ts index 3cd776e..48679d2 100644 --- a/packages/eslint-plugin-react-pug/src/index.ts +++ b/packages/eslint-plugin-react-pug/src/index.ts @@ -13,13 +13,16 @@ import { offsetToLineColumn, rewriteSegmentedPugRegions, type BoundaryMappedExpression, + type MappedEmbeddedJsLintSite, type RewrittenPugRegionsResult, type RegionFormattingContext, type StartupjsCssxjsOption, } from '@react-pug/react-pug-core'; import { parse } from '@babel/parser'; +import { createRequire } from 'node:module'; +import { isAbsolute, resolve } from 'node:path'; import { Linter, SourceCode } from 'eslint'; -import stylisticPlugin from '@stylistic/eslint-plugin'; +import bundledStylisticPlugin from '@stylistic/eslint-plugin'; import prettier from '@prettier/sync'; const tsParser = require('@typescript-eslint/parser'); @@ -62,6 +65,24 @@ interface CachedLintState { formatted: RewrittenPugRegionsResult | null; legacyStyleStatementRanges: InsertionOffsetRange[]; syntheticStyleCallRanges: InsertionOffsetRange[]; + embeddedLintBlocks: EmbeddedLintBlockState[]; +} + +interface EmbeddedLintBlockState { + text: string; + filename: string; + site: MappedEmbeddedJsLintSite; + boundaryMap: Array; + endBoundaryMap: Array; + normalizedBoundaryMap: Array; + normalizedEndBoundaryMap: Array; + syntheticRanges: InsertionOffsetRange[]; + normalizedSiteCode: string; + restorationIndentWidth: number; +} + +interface NormalizedEmbeddedSite extends BoundaryMappedExpression { + strippedContinuationIndent: number; } interface EslintProcessorLike { @@ -76,6 +97,10 @@ interface EslintProcessorLike { const FLAT_LINT_FILES = ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}']; const LEGACY_STYLE_HELPERS = new Set(['styl', 'css', 'sass', 'scss']); const LEGACY_STYLE_SUPPRESSED_RULES = new Set(['no-unused-expressions', 'no-unreachable']); +const EMBEDDED_BLOCK_RULE_ALLOWLIST = new Set([ + '@typescript-eslint/no-unused-vars', +]); +const stylisticPluginCache = new Map(); const FORMAT_RULE_CONFIG: any = { languageOptions: { ecmaVersion: 2022 as const, @@ -87,7 +112,7 @@ const FORMAT_RULE_CONFIG: any = { }, }, plugins: { - '@stylistic': stylisticPlugin, + '@stylistic': bundledStylisticPlugin, }, rules: { '@stylistic/indent': ['error', 2, { @@ -132,7 +157,10 @@ const FORMAT_RULE_CONFIG: any = { ], offsetTernaryExpressions: true, }], - '@stylistic/jsx-indent': ['error', 2], + '@stylistic/jsx-indent': ['error', 2, { + checkAttributes: false, + indentLogicalExpressions: true, + }], '@stylistic/jsx-indent-props': ['error', 2], '@stylistic/jsx-wrap-multilines': ['error', { declaration: 'parens-new-line', @@ -294,6 +322,272 @@ function getExpressionParserPlugins(filename: string): any[] { ] as any; } +interface EmbeddedLintBlockBuilder { + text: string; + boundaryMap: Array; + endBoundaryMap: Array; + syntheticRanges: InsertionOffsetRange[]; +} + +function appendSyntheticBlockText( + builder: EmbeddedLintBlockBuilder, + text: string, +): void { + if (text.length === 0) return; + const start = builder.text.length; + if (builder.boundaryMap.length === 0) builder.boundaryMap.push(null); + else builder.boundaryMap[builder.boundaryMap.length - 1] = null; + if (builder.endBoundaryMap.length === 0) builder.endBoundaryMap.push(null); + builder.text += text; + for (let i = 0; i < text.length; i += 1) { + builder.boundaryMap.push(null); + builder.endBoundaryMap.push(null); + } + builder.syntheticRanges.push({ start, end: builder.text.length }); +} + +function appendMappedBlockText( + builder: EmbeddedLintBlockBuilder, + text: string, + boundaryMap: Array, +): void { + if (text.length === 0) { + if (builder.boundaryMap.length === 0) { + builder.boundaryMap.push(boundaryMap[0] ?? null); + builder.endBoundaryMap.push(boundaryMap[0] ?? null); + } + return; + } + + if (builder.boundaryMap.length === 0) { + builder.boundaryMap.push(boundaryMap[0] ?? null); + builder.endBoundaryMap.push(boundaryMap[0] ?? null); + } else { + builder.boundaryMap[builder.boundaryMap.length - 1] = boundaryMap[0] ?? null; + builder.endBoundaryMap[builder.endBoundaryMap.length - 1] = boundaryMap[0] ?? null; + } + + builder.text += text; + for (let i = 0; i < text.length; i += 1) { + const mappedBoundary = boundaryMap[i + 1] ?? null; + builder.boundaryMap.push(mappedBoundary); + builder.endBoundaryMap.push(mappedBoundary); + } +} + +function appendMappedCodeWithIndent( + builder: EmbeddedLintBlockBuilder, + code: string, + boundaryMap: number[], + indent: string, +): void { + let lineStart = true; + for (let index = 0; index < code.length; index += 1) { + if (lineStart) appendSyntheticBlockText(builder, indent); + appendMappedBlockText(builder, code[index], [boundaryMap[index], boundaryMap[index + 1]]); + lineStart = code[index] === '\n'; + } +} + +function stripSharedContinuationIndent( + code: string, + boundaryMap: number[], +): NormalizedEmbeddedSite { + if (!code.includes('\n')) return { code, boundaryMap, strippedContinuationIndent: 0 }; + + const lines = code.split('\n'); + let minIndent: number | null = null; + + for (const line of lines.slice(1)) { + if (line.trim().length === 0) continue; + const indent = line.match(/^[ \t]*/)?.[0].length ?? 0; + minIndent = minIndent == null ? indent : Math.min(minIndent, indent); + } + + if (minIndent == null || minIndent === 0) { + return { code, boundaryMap, strippedContinuationIndent: 0 }; + } + + const firstLine = lines[0]?.trimEnd() ?? ''; + const continuationBaseIndent = ( + /^[([{]$/.test(firstLine.trim()) + || /[([{]\s*$/.test(firstLine) + || /=>\s*{\s*$/.test(firstLine) + ) + ? 0 + : 2; + const stripIndent = Math.max(0, minIndent - continuationBaseIndent); + + if (stripIndent === 0) { + return { code, boundaryMap, strippedContinuationIndent: 0 }; + } + + let output = ''; + const outputBoundaryMap: number[] = []; + + const appendSlice = (start: number, end: number) => { + if (start >= end) return; + if (outputBoundaryMap.length === 0) { + outputBoundaryMap.push(boundaryMap[start] ?? 0); + } else { + outputBoundaryMap[outputBoundaryMap.length - 1] = boundaryMap[start] ?? 0; + } + output += code.slice(start, end); + for (let index = start; index < end; index += 1) { + outputBoundaryMap.push(boundaryMap[index + 1] ?? 0); + } + }; + + let cursor = 0; + let lineIndex = 0; + while (cursor < code.length) { + const nextNewline = code.indexOf('\n', cursor); + const lineEnd = nextNewline >= 0 ? nextNewline : code.length; + let segmentStart = cursor; + + if (lineIndex > 0) { + let removed = 0; + while ( + segmentStart < lineEnd + && removed < stripIndent + && /[ \t]/.test(code[segmentStart] ?? '') + ) { + segmentStart += 1; + removed += 1; + } + } + + appendSlice(segmentStart, lineEnd); + if (nextNewline >= 0) appendSlice(nextNewline, nextNewline + 1); + + cursor = nextNewline >= 0 ? nextNewline + 1 : code.length; + lineIndex += 1; + } + + if (outputBoundaryMap.length === 0) { + outputBoundaryMap.push(boundaryMap[0] ?? 0); + } + + return { + code: output, + boundaryMap: outputBoundaryMap, + strippedContinuationIndent: stripIndent, + }; +} + +function restoreSharedContinuationIndent( + code: string, + restorationIndentWidth: number, +): string { + if (restorationIndentWidth <= 0 || !code.includes('\n')) return code; + + const prefix = ' '.repeat(restorationIndentWidth); + return code + .split('\n') + .map((line, index) => { + if (index === 0 || line.length === 0) return line; + return `${prefix}${line}`; + }) + .join('\n'); +} + +function calculateRestorationIndentWidth( + originalSiteText: string, + normalizedSiteCode: string, +): number { + if (!originalSiteText.includes('\n') || !normalizedSiteCode.includes('\n')) return 0; + + const originalLines = originalSiteText.split('\n'); + const normalizedLines = normalizedSiteCode.split('\n'); + let minDiff: number | null = null; + + for (let index = 1; index < Math.min(originalLines.length, normalizedLines.length); index += 1) { + const originalLine = originalLines[index] ?? ''; + const normalizedLine = normalizedLines[index] ?? ''; + if (originalLine.trim().length === 0 || normalizedLine.trim().length === 0) continue; + + const originalIndent = originalLine.match(/^[ \t]*/)?.[0].length ?? 0; + const normalizedIndent = normalizedLine.match(/^[ \t]*/)?.[0].length ?? 0; + const diff = originalIndent - normalizedIndent; + if (diff < 0) continue; + minDiff = minDiff == null ? diff : Math.min(minDiff, diff); + } + + return minDiff ?? 0; +} + +function createEmbeddedLintBlockFilename( + filename: string, + kind: 'expression' | 'statement', + index: number, +): string { + const suffix = isTypeScriptLikeFilename(filename) ? 'tsx' : 'jsx'; + return `../../../pug-react-embedded-${kind}-${index}.${suffix}`; +} + +function createEmbeddedLintBlocks( + transformed: LintTransformState, + filename: string, + originalText: string, +): EmbeddedLintBlockState[] { + const blocks: EmbeddedLintBlockState[] = []; + + transformed.embeddedJsLintSites.forEach((site, index) => { + const builder: EmbeddedLintBlockBuilder = { + text: '', + boundaryMap: [], + endBoundaryMap: [], + syntheticRanges: [], + }; + const normalizedBuilder: EmbeddedLintBlockBuilder = { + text: '', + boundaryMap: [], + endBoundaryMap: [], + syntheticRanges: [], + }; + const normalizedSite = stripSharedContinuationIndent( + site.code, + Array.from({ length: site.code.length + 1 }, (_, offset) => offset), + ); + const originalSiteText = originalText.slice(site.originalStart, site.originalEnd); + const normalizedSiteIdentityMap = Array.from( + { length: normalizedSite.code.length + 1 }, + (_, offset) => offset, + ); + + if (site.kind === 'expression') { + appendSyntheticBlockText(builder, 'const __reactPugExpr = (\n'); + appendSyntheticBlockText(normalizedBuilder, 'const __reactPugExpr = (\n'); + appendMappedCodeWithIndent(builder, normalizedSite.code, normalizedSite.boundaryMap, ' '); + appendMappedCodeWithIndent(normalizedBuilder, normalizedSite.code, normalizedSiteIdentityMap, ' '); + appendSyntheticBlockText(builder, '\n)\n'); + appendSyntheticBlockText(normalizedBuilder, '\n)\n'); + } else { + appendSyntheticBlockText(builder, '(() => {\n'); + appendSyntheticBlockText(normalizedBuilder, '(() => {\n'); + appendMappedCodeWithIndent(builder, normalizedSite.code, normalizedSite.boundaryMap, ' '); + appendMappedCodeWithIndent(normalizedBuilder, normalizedSite.code, normalizedSiteIdentityMap, ' '); + appendSyntheticBlockText(builder, '\n})()\n'); + appendSyntheticBlockText(normalizedBuilder, '\n})()\n'); + } + + blocks.push({ + text: builder.text, + filename: createEmbeddedLintBlockFilename(filename, site.kind, index), + site, + boundaryMap: builder.boundaryMap, + endBoundaryMap: builder.endBoundaryMap, + syntheticRanges: builder.syntheticRanges, + normalizedBoundaryMap: normalizedBuilder.boundaryMap, + normalizedEndBoundaryMap: normalizedBuilder.endBoundaryMap, + normalizedSiteCode: normalizedSite.code, + restorationIndentWidth: calculateRestorationIndentWidth(originalSiteText, normalizedSite.code), + }); + }); + + return blocks; +} + function rebaseFormattedRegion( text: string, baseIndent: string, @@ -364,18 +658,12 @@ function normalizeSyntheticWrapperClosingIndent( } function applyFormatterLintPasses(text: string, filename: string): string { - const lintConfig: any[] = [{ - files: FLAT_LINT_FILES, - ...FORMAT_RULE_CONFIG, - ...(isTypeScriptLikeFilename(filename) - ? { - languageOptions: { - ...FORMAT_RULE_CONFIG.languageOptions, - parser: tsParser as any, - }, - } - : {}), - }]; + const lintConfig: any[] = getFormatterLintConfig(filename) + // This pass shapes the generated JSX/TSX surface only. Embedded JS expression + // sites such as `${...}`, `#{...}`, attr expressions and inline handler bodies + // also get a separate source-faithful lint surface below, but generated-region + // autofixes/suggestions are still intentionally dropped because they are not + // safely mappable back through the transform today. const pretty = prettier.format(text, { parser: isTypeScriptLikeFilename(filename) ? 'babel-ts' : 'babel', semi: false, @@ -399,6 +687,61 @@ function applyFormatterLintPasses(text: string, filename: string): string { ).output; } +function getFormatterLintConfig(filename: string): any[] { + const stylisticPlugin = resolveStylisticPluginForFormatting(filename); + return [{ + files: FLAT_LINT_FILES, + ...FORMAT_RULE_CONFIG, + plugins: { + ...FORMAT_RULE_CONFIG.plugins, + '@stylistic': stylisticPlugin, + }, + ...(isTypeScriptLikeFilename(filename) + ? { + languageOptions: { + ...FORMAT_RULE_CONFIG.languageOptions, + parser: tsParser as any, + }, + } + : {}), + }]; +} + +function looksLikeStylisticPlugin(candidate: unknown): candidate is { rules: Record } { + return !!candidate && typeof candidate === 'object' && typeof (candidate as any).rules === 'object'; +} + +function getFormatterResolutionKey(filename: string): string { + return isAbsolute(filename) ? filename : resolve(process.cwd(), filename); +} + +function loadStylisticPluginFrom(request: NodeRequire): any | null { + try { + const resolved = request('@stylistic/eslint-plugin'); + return looksLikeStylisticPlugin(resolved) ? resolved : null; + } catch { + return null; + } +} + +function resolveStylisticPluginForFormatting(filename: string): any { + const resolutionKey = getFormatterResolutionKey(filename); + const cached = stylisticPluginCache.get(resolutionKey); + if (cached) return cached; + + const candidates = [resolutionKey, resolve(process.cwd(), '__react-pug-formatter__.js')]; + for (const candidate of candidates) { + const resolved = loadStylisticPluginFrom(createRequire(candidate)); + if (resolved) { + stylisticPluginCache.set(resolutionKey, resolved); + return resolved; + } + } + + stylisticPluginCache.set(resolutionKey, bundledStylisticPlugin); + return bundledStylisticPlugin; +} + function normalizeFormattedExpressionForLint( text: string, wrapperLineIndentWidth: number, @@ -410,7 +753,7 @@ function normalizeFormattedExpressionForLint( hasSyntheticWrapperLines?: boolean; } { if ( - formattingContext.containerKind !== 'standalone' + needsSyntheticRootJsxWrapper(formattingContext.containerKind) && text.includes('\n') && isRootJsxExpression(text, filename) ) { @@ -436,6 +779,16 @@ function normalizeFormattedExpressionForLint( }; } +function needsSyntheticRootJsxWrapper( + containerKind: RegionFormattingContext['containerKind'], +): boolean { + return ( + containerKind === 'call-argument' + || containerKind === 'conditional-branch' + || containerKind === 'logical-operand' + ); +} + function getSyntheticWrapperLineRanges(text: string): InsertionOffsetRange[] { const firstNewline = text.indexOf('\n'); const lastNewline = text.lastIndexOf('\n'); @@ -679,6 +1032,319 @@ function mapLintMessage( }; } +function mapEmbeddedLintOffsetToSite( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const clamped = Math.max(0, Math.min(offset, block.text.length)); + return block.boundaryMap[clamped] ?? null; +} + +function mapEmbeddedLintEndOffsetToSite( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const clamped = Math.max(0, Math.min(offset, block.text.length)); + return block.endBoundaryMap[clamped] ?? null; +} + +function mapEmbeddedLintOffsetToNormalizedSite( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const clamped = Math.max(0, Math.min(offset, block.text.length)); + return block.normalizedBoundaryMap[clamped] ?? null; +} + +function mapEmbeddedLintEndOffsetToNormalizedSite( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const clamped = Math.max(0, Math.min(offset, block.text.length)); + return block.normalizedEndBoundaryMap[clamped] ?? null; +} + +function mapEmbeddedSiteOffsetToOriginal( + site: MappedEmbeddedJsLintSite, + offset: number, +): number | null { + const clamped = Math.max(0, Math.min(offset, site.code.length)); + return site.boundaryMap[clamped] ?? null; +} + +function applyLocalFixesToText( + text: string, + fixes: Array<{ start: number; end: number; text: string }>, +): string { + if (fixes.length === 0) return text; + + const sorted = [...fixes].sort((a, b) => a.start - b.start || a.end - b.end); + let cursor = 0; + let output = ''; + + for (const fix of sorted) { + if (fix.start < cursor) continue; + output += text.slice(cursor, fix.start); + output += fix.text; + cursor = fix.end; + } + + output += text.slice(cursor); + return output; +} + +function getEmbeddedParserPlugins(filename: string): Array { + const plugins: Array = ['jsx', 'decorators-legacy']; + if (isTypeScriptLikeFilename(filename)) plugins.push('typescript'); + return plugins; +} + +function extractEmbeddedSiteCodeFromWrappedText( + text: string, + block: EmbeddedLintBlockState, +): string { + const ast = parse(text, { + sourceType: 'module', + plugins: getEmbeddedParserPlugins(block.filename) as any, + errorRecovery: false, + }) as any; + + const firstStatement = ast.program.body[0]; + if (!firstStatement) return block.normalizedSiteCode; + + if (block.site.kind === 'expression') { + const init = firstStatement.declarations?.[0]?.init; + if (!init || typeof init.start !== 'number' || typeof init.end !== 'number') { + return block.normalizedSiteCode; + } + return text.slice(init.start, init.end); + } + + const body = firstStatement.expression?.callee?.body; + if (!body || body.type !== 'BlockStatement') { + return block.normalizedSiteCode; + } + + const raw = text.slice(body.start + 1, body.end - 1); + return raw.replace(/^\n/, '').replace(/\n$/, ''); +} + +function stabilizeEmbeddedSiteAutofix( + block: EmbeddedLintBlockState, + code: string, +): string { + const wrapped = block.site.kind === 'expression' + ? `const __reactPugExpr = (\n${code}\n)\n` + : `(() => {\n${code}\n})()\n`; + const pretty = prettier.format(wrapped, { + parser: isTypeScriptLikeFilename(block.filename) ? 'babel-ts' : 'babel', + semi: false, + singleQuote: true, + jsxSingleQuote: true, + trailingComma: 'none', + bracketSameLine: false, + }); + return extractEmbeddedSiteCodeFromWrappedText(pretty, block); +} + +function findLineBounds(text: string, offset: number): { start: number; end: number } { + const clamped = Math.max(0, Math.min(offset, text.length)); + const start = text.lastIndexOf('\n', Math.max(0, clamped - 1)) + 1; + const nextNewline = text.indexOf('\n', clamped); + return { + start, + end: nextNewline >= 0 ? nextNewline : text.length, + }; +} + +function findFirstMappableOffsetOnLine( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const { start, end } = findLineBounds(block.text, offset); + for (let index = Math.max(start, Math.min(offset, end)); index <= end; index += 1) { + if (block.boundaryMap[index] != null) return index; + } + return null; +} + +function findLastMappableOffsetOnLine( + block: EmbeddedLintBlockState, + offset: number, +): number | null { + const { start, end } = findLineBounds(block.text, offset); + for (let index = Math.max(start, Math.min(offset, end)); index >= start; index -= 1) { + if (block.endBoundaryMap[index] != null) return index; + } + return null; +} + +function mapEmbeddedLintFix( + fix: EslintLintMessage['fix'] | undefined, + block: EmbeddedLintBlockState, + originalText: string, +): EslintLintMessage['fix'] | undefined { + if (!fix) return undefined; + const [start, end] = fix.range; + if (overlapsRangeList(block.syntheticRanges, start, end)) return undefined; + + const localStart = mapEmbeddedLintOffsetToSite(block, start); + const localEnd = mapEmbeddedLintEndOffsetToSite(block, end); + if (localStart == null || localEnd == null) return undefined; + + const originalStart = mapEmbeddedSiteOffsetToOriginal(block.site, localStart); + const originalEnd = mapEmbeddedSiteOffsetToOriginal(block.site, localEnd); + if (originalStart == null || originalEnd == null) return undefined; + + const originalSiteText = originalText.slice(block.site.originalStart, block.site.originalEnd); + const relativeStart = originalStart - block.site.originalStart; + const relativeEnd = originalEnd - block.site.originalStart; + const replacedSiteText = `${originalSiteText.slice(0, relativeStart)}${fix.text}${originalSiteText.slice(relativeEnd)}`; + + return { + ...fix, + range: [block.site.originalStart, block.site.originalEnd], + text: replacedSiteText, + }; +} + +function mapEmbeddedLintSuggestions( + suggestions: EslintLintMessage['suggestions'], + block: EmbeddedLintBlockState, + originalText: string, +): EslintLintMessage['suggestions'] | undefined { + if (!suggestions) return undefined; + + const mapped = suggestions + .map((suggestion) => { + const mappedFix = mapEmbeddedLintFix(suggestion.fix, block, originalText); + if (!mappedFix) return null; + return { + ...suggestion, + fix: mappedFix, + }; + }) + .filter((suggestion): suggestion is NonNullable => suggestion != null); + + return mapped.length > 0 ? mapped : undefined; +} + +function buildEmbeddedSiteAutofix( + messages: EslintLintMessage[], + block: EmbeddedLintBlockState, + originalText: string, +): EslintLintMessage['fix'] | undefined { + const localFixes = messages + .map((message) => message.fix) + .filter((fix): fix is NonNullable => fix != null) + .map((fix) => { + const [start, end] = fix.range; + if (overlapsRangeList(block.syntheticRanges, start, end)) return null; + const localStart = mapEmbeddedLintOffsetToNormalizedSite(block, start); + const localEnd = mapEmbeddedLintEndOffsetToNormalizedSite(block, end); + if (localStart == null || localEnd == null) return null; + return { + start: localStart, + end: localEnd, + text: fix.text, + }; + }) + .filter((fix): fix is NonNullable => fix != null) + .sort((a, b) => a.start - b.start || a.end - b.end); + + if (localFixes.length === 0) return undefined; + + const fixedNormalizedCode = applyLocalFixesToText(block.normalizedSiteCode, localFixes); + const stabilizedCode = stabilizeEmbeddedSiteAutofix(block, fixedNormalizedCode); + const restoredCode = restoreSharedContinuationIndent( + stabilizedCode, + block.restorationIndentWidth, + ); + const originalSiteText = originalText.slice(block.site.originalStart, block.site.originalEnd); + + if (restoredCode === originalSiteText) return undefined; + + return { + range: [block.site.originalStart, block.site.originalEnd], + text: restoredCode, + }; +} + +function mapEmbeddedLintMessage( + message: EslintLintMessage, + block: EmbeddedLintBlockState, + originalText: string, + options: { + includeFix?: boolean; + includeSuggestions?: boolean; + } = {}, +): EslintLintMessage | null { + if ( + typeof message.ruleId === 'string' + && !message.ruleId.startsWith('@stylistic/') + && ( + !EMBEDDED_BLOCK_RULE_ALLOWLIST.has(message.ruleId) + || block.site.kind !== 'expression' + ) + ) { + return null; + } + if (message.line == null || message.column == null) return null; + + const start = lineColumnToOffset(block.text, message.line, message.column); + const end = (message.endLine != null && message.endColumn != null) + ? lineColumnToOffset(block.text, message.endLine, message.endColumn) + : start + 1; + let mappedStartOffset = start; + let mappedEndOffset = Math.max(start + 1, end); + + if (overlapsRangeList(block.syntheticRanges, mappedStartOffset, mappedEndOffset)) { + const relocatedStart = findFirstMappableOffsetOnLine(block, mappedStartOffset); + const relocatedEnd = findLastMappableOffsetOnLine(block, mappedEndOffset); + if (relocatedStart == null || relocatedEnd == null) return null; + mappedStartOffset = relocatedStart; + mappedEndOffset = Math.max(relocatedStart + 1, relocatedEnd); + } + + const localStart = mapEmbeddedLintOffsetToSite(block, mappedStartOffset); + const localEnd = mapEmbeddedLintEndOffsetToSite(block, mappedEndOffset); + if (localStart == null || localEnd == null) return null; + + const originalStart = mapEmbeddedSiteOffsetToOriginal(block.site, localStart); + const originalEnd = mapEmbeddedSiteOffsetToOriginal(block.site, localEnd); + if (originalStart == null || originalEnd == null) return null; + + const startLc = offsetToLineColumn(originalText, originalStart); + const endLc = offsetToLineColumn(originalText, originalEnd); + const mappedFix = options.includeFix === false + ? undefined + : mapEmbeddedLintFix(message.fix, block, originalText); + const mappedSuggestions = options.includeSuggestions === false + ? undefined + : mapEmbeddedLintSuggestions(message.suggestions, block, originalText); + + return { + ...Object.fromEntries(Object.entries(message).filter(([key]) => key !== 'fix' && key !== 'suggestions')), + line: startLc.line, + column: startLc.column, + endLine: endLc.line, + endColumn: endLc.column, + ...(mappedFix ? { fix: mappedFix } : {}), + ...(mappedSuggestions ? { suggestions: mappedSuggestions } : {}), + }; +} + +function lintMessageDedupKey(message: EslintLintMessage): string { + return JSON.stringify([ + message.ruleId ?? null, + message.message ?? null, + message.line ?? null, + message.column ?? null, + message.endLine ?? null, + message.endColumn ?? null, + ]); +} + function createReactPugProcessor( options: EslintReactPugProcessorOptions = {}, ): EslintProcessorLike { @@ -709,6 +1375,7 @@ function createReactPugProcessor( formatted: null, legacyStyleStatementRanges, syntheticStyleCallRanges: [], + embeddedLintBlocks: [], }); } else { cache.delete(filename); @@ -741,18 +1408,30 @@ function createReactPugProcessor( || containsJsxSyntax(text, filename) ); const formatted = hasTransformedPug ? formatLintCode(transformed, filename) : null; + const embeddedLintBlocks = hasTransformedPug + ? createEmbeddedLintBlocks(transformed, filename, text) + : []; cache.set(filename, { originalText: text, transformed, formatted, legacyStyleStatementRanges, syntheticStyleCallRanges: collectMappedInsertionRangesByKind(transformed, 'style-call'), + embeddedLintBlocks, }); - if (!shouldUseVirtualJsxFilename) return [transformed.code]; - return [{ - text: hasTransformedPug ? (formatted?.code ?? transformed.code) : transformed.code, - filename: getVirtualLintFilename(filename), - }]; + const mainBlock: string | { text: string; filename: string } = shouldUseVirtualJsxFilename + ? { + text: hasTransformedPug ? (formatted?.code ?? transformed.code) : transformed.code, + filename: getVirtualLintFilename(filename), + } + : transformed.code; + return [ + mainBlock, + ...embeddedLintBlocks.map(block => ({ + text: block.text, + filename: block.filename, + })), + ]; }, postprocess(messages: EslintLintMessage[][], filename: string): EslintLintMessage[] { @@ -762,9 +1441,56 @@ function createReactPugProcessor( const flat = messages.flat(); if (!cached) return flat; - return flat - .map((msg) => mapLintMessage(msg, cached)) - .filter((msg): msg is EslintLintMessage => msg != null); + const [mainMessages = [], ...embeddedMessages] = messages; + const mapped = [ + ...mainMessages + .map((msg) => mapLintMessage(msg, cached)) + .filter((msg): msg is EslintLintMessage => msg != null), + ]; + + embeddedMessages.forEach((blockMessages, index) => { + const block = cached.embeddedLintBlocks[index]; + if (!block) return; + const mappedBlockMessages = blockMessages + .map((message) => mapEmbeddedLintMessage(message, block, cached.originalText, { + includeFix: false, + })); + const aggregatedFix = buildEmbeddedSiteAutofix(blockMessages, block, cached.originalText); + let aggregatedFixAttached = false; + + for (const mappedMessage of mappedBlockMessages) { + if (!mappedMessage) continue; + if (!aggregatedFixAttached && aggregatedFix) { + mapped.push({ + ...mappedMessage, + fix: aggregatedFix, + }); + aggregatedFixAttached = true; + } else { + mapped.push(mappedMessage); + } + } + }); + + const deduped: EslintLintMessage[] = []; + const seen = new Map(); + for (const message of mapped) { + const key = lintMessageDedupKey(message); + const existingIndex = seen.get(key); + if (existingIndex == null) { + seen.set(key, deduped.length); + deduped.push(message); + continue; + } + + const existing = deduped[existingIndex]; + const existingHasAction = !!existing.fix || ((existing.suggestions?.length ?? 0) > 0); + const incomingHasAction = !!message.fix || ((message.suggestions?.length ?? 0) > 0); + if (!existingHasAction && incomingHasAction) { + deduped[existingIndex] = message; + } + } + return deduped; }, supportsAutofix: true, diff --git a/packages/eslint-plugin-react-pug/test/integration/autofix.test.ts b/packages/eslint-plugin-react-pug/test/integration/autofix.test.ts index d9704cd..20b3aad 100644 --- a/packages/eslint-plugin-react-pug/test/integration/autofix.test.ts +++ b/packages/eslint-plugin-react-pug/test/integration/autofix.test.ts @@ -9,6 +9,7 @@ import reactPugPlugin from '../../src/index' const repoRoot = resolve(__dirname, '../../../..') const fixtureRoot = resolve(repoRoot, 'test/fixtures/example-unformatted') const snapshotRoot = resolve(fixtureRoot, 'snapshots/fixed') +const postFixDiagnosticsSnapshot = resolve(fixtureRoot, 'snapshots/post-fix-diagnostics.json') const reactHooksStubPlugin = { rules: { 'rules-of-hooks': { @@ -50,6 +51,32 @@ function createExampleEslint(cwd: string, fix: boolean): ESLint { }) } +function createInlineEslint(fix: boolean): ESLint { + return new ESLint({ + cwd: repoRoot, + fix, + ignore: false, + overrideConfigFile: true, + overrideConfig: [ + { + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + }, + ...neostandard({ + ts: true, + }), + { + plugins: { + 'react-hooks': reactHooksStubPlugin as any, + 'react-pug': reactPugPlugin as any, + }, + processor: 'react-pug/react-pug', + }, + ] as any, + }) +} + function createTempFixtureCopy(): string { const tempDir = mkdtempSync(join(tmpdir(), 'react-pug-eslint-fix-')) tempDirs.push(tempDir) @@ -77,6 +104,62 @@ afterEach(() => { }) describe('eslint --fix integration for react-pug processor', () => { + it('applies multiple autofixes within a single embedded expression site without corrupting the site text', async () => { + const filePath = resolve(repoRoot, 'embedded-autofix-multi-fix.js') + const input = [ + "import { pug } from 'startupjs'", + 'const showCompleted = true', + '', + 'const view = pug`', + ' Button(', + ' label=showCompleted ? "Hide Done" : "Show Done"', + ' ) Save', + '`', + '', + ].join('\n') + + const [firstPass] = await createInlineEslint(true).lintText(input, { filePath }) + const output = firstPass.output ?? input + + expect(output).toContain("label=showCompleted ? 'Hide Done' : 'Show Done'") + expect(output).not.toContain(`'Hide Done'e"`) + + const [secondPass] = await createInlineEslint(false).lintText(output, { filePath }) + expect(secondPass.messages.some(message => message.ruleId === '@stylistic/quotes')).toBe(false) + }) + + it('applies source-faithful autofixes across embedded expression-site kinds', async () => { + const filePath = resolve(repoRoot, 'embedded-autofix-matrix.js') + const input = [ + "import { pug } from 'startupjs'", + "const label = 'label'", + "const suffix = 'suffix'", + '', + 'const view = pug`', + ' Button(', + ' label=label+suffix', + ' onClick=() => { return label+suffix }', + ' ) Save', + ' p= label+suffix', + ' p Hello #{label+suffix}', + ' Span.text= ${label+suffix}', + '`', + '', + ].join('\n') + + const [firstPass] = await createInlineEslint(true).lintText(input, { filePath }) + const output = firstPass.output ?? input + + expect(output).toContain('label=label + suffix') + expect(output).toContain('return label + suffix') + expect(output).toContain('p= label + suffix') + expect(output).toContain('p Hello #{label + suffix}') + expect(output).toContain('Span.text= ${label + suffix}') + + const [secondPass] = await createInlineEslint(false).lintText(output, { filePath }) + expect(secondPass.messages.some(message => message.ruleId === '@stylistic/space-infix-ops')).toBe(false) + }) + it('does not corrupt files and preserves only the expected non-fixable diagnostics for an unformatted example fixture', async () => { const tempDir = createTempFixtureCopy() @@ -93,7 +176,12 @@ describe('eslint --fix integration for react-pug processor', () => { message: message.message, })) )) - expect(allMessages).toEqual([]) + const allowedRules = new Set(['@typescript-eslint/no-unused-vars']) + expect(allMessages.every(message => ( + String(message.ruleId).startsWith('@stylistic/') + || allowedRules.has(String(message.ruleId)) + ))).toBe(true) + await expect(JSON.stringify(allMessages, null, 2) + '\n').toMatchFileSnapshot(postFixDiagnosticsSnapshot) const fixedFiles = [ 'src/App.tsx', @@ -101,6 +189,7 @@ describe('eslint --fix integration for react-pug processor', () => { 'src/Card.tsx', 'src/ModalScreen.tsx', 'src/RootLayout.tsx', + 'src/StartupjsUiAvatar.tsx', 'src/StartupjsUiDialogsReadme.js', 'src/StartupjsUiDropdown.tsx', 'src/StartupjsUiDraggableReadme.js', diff --git a/packages/eslint-plugin-react-pug/test/integration/diagnostics.test.ts b/packages/eslint-plugin-react-pug/test/integration/diagnostics.test.ts index 46dc874..763ef71 100644 --- a/packages/eslint-plugin-react-pug/test/integration/diagnostics.test.ts +++ b/packages/eslint-plugin-react-pug/test/integration/diagnostics.test.ts @@ -15,6 +15,55 @@ function offsetToLineColumn(text: string, offset: number) { } } +function createProcessorEslint() { + return new ESLint({ + cwd: repoRoot, + fix: false, + ignore: false, + overrideConfigFile: true, + overrideConfig: [ + ...neostandard({ + ts: true, + }), + { + plugins: { + 'react-pug': reactPugPlugin as any, + }, + processor: 'react-pug/react-pug', + }, + ] as any, + }) +} + +function expectExactMappedMessage( + input: string, + messages: EslintLintMessage[], + ruleId: string, + snippet: string, +) { + const matches = messages.filter((message) => ( + message.ruleId === ruleId + && message.message.includes(snippet) + )) + + expect(matches).toHaveLength(1) + const expectedStart = input.indexOf(snippet) + expect(expectedStart).toBeGreaterThanOrEqual(0) + const expected = offsetToLineColumn(input, expectedStart) + expect(matches[0].line).toBe(expected.line) + expect(matches[0].column).toBe(expected.column) + expect(matches[0].endLine).toBe(expected.line) + expect(matches[0].endColumn).toBe(expected.column + snippet.length) +} + +function findLineIndex(lines: string[], snippet: string, fromIndex = 0) { + const index = lines.findIndex((line, lineIndex) => lineIndex >= fromIndex && line === snippet) + expect(index).toBeGreaterThanOrEqual(0) + return index +} + +type EslintLintMessage = Awaited>[number]['messages'][number] + describe('eslint processor diagnostic mapping', () => { it('suppresses legacy styl tagged-template statement warnings', async () => { const filePath = resolve(repoRoot, 'legacy-styl.js') @@ -33,23 +82,7 @@ describe('eslint processor diagnostic mapping', () => { '', ].join('\n') - const eslint = new ESLint({ - cwd: repoRoot, - fix: false, - ignore: false, - overrideConfigFile: true, - overrideConfig: [ - ...neostandard({ - ts: true, - }), - { - plugins: { - 'react-pug': reactPugPlugin as any, - }, - processor: 'react-pug/react-pug', - }, - ] as any, - }) + const eslint = createProcessorEslint() const [result] = await eslint.lintText(input, { filePath }) expect(result.messages.some(message => message.ruleId === 'no-unused-expressions')).toBe(false) @@ -88,23 +121,7 @@ describe('eslint processor diagnostic mapping', () => { '', ].join('\n') - const eslint = new ESLint({ - cwd: repoRoot, - fix: false, - ignore: false, - overrideConfigFile: true, - overrideConfig: [ - ...neostandard({ - ts: true, - }), - { - plugins: { - 'react-pug': reactPugPlugin as any, - }, - processor: 'react-pug/react-pug', - }, - ] as any, - }) + const eslint = createProcessorEslint() const [result] = await eslint.lintText(input, { filePath }) expect(result.messages.some(message => message.ruleId === '@stylistic/indent')).toBe(false) @@ -122,23 +139,7 @@ describe('eslint processor diagnostic mapping', () => { ].join('\n'), ) - const eslint = new ESLint({ - cwd: repoRoot, - fix: false, - ignore: false, - overrideConfigFile: true, - overrideConfig: [ - ...neostandard({ - ts: true, - }), - { - plugins: { - 'react-pug': reactPugPlugin as any, - }, - processor: 'react-pug/react-pug', - }, - ] as any, - }) + const eslint = createProcessorEslint() const [result] = await eslint.lintText(input, { filePath }) const unused = result.messages.find((message) => ( @@ -154,4 +155,378 @@ describe('eslint processor diagnostic mapping', () => { expect(unused?.endLine).toBe(expected.line) expect(unused?.endColumn).toBe(expected.column + 'myValue'.length) }) + + it('does not report false indent diagnostics for nested multiline ternaries inside ${} interpolations', async () => { + const filePath = resolve(repoRoot, 'nested-template-ternary-indent.js') + const input = [ + "import { pug } from 'startupjs'", + 'const monthAmount = 10', + 'const yearAmount = 100', + '', + 'const view = pug`', + ' Span.text= ${monthAmount != null && yearAmount != null', + " ? t(msg`Each business you add is {monthAmount}/month or {yearAmount}/year.`, { monthAmount: '$' + monthAmount, yearAmount: '$' + yearAmount })", + ' : monthAmount != null', + " ? t(msg`Each business you add is {amount}/month.`, { amount: '$' + monthAmount })", + " : t(msg`Each business you add is {amount}/year.`, { amount: '$' + yearAmount })", + ' }', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + + const [result] = await eslint.lintText(input, { filePath }) + expect(result.messages.filter(message => message.ruleId === '@stylistic/indent')).toEqual([]) + }) + + it('maps real no-undef inside ${} interpolations to the exact original symbol', async () => { + const filePath = resolve(repoRoot, 'nested-template-real-error.js') + const input = [ + "import { pug } from 'startupjs'", + 'const Span = "span"', + 'const t = (value) => value', + 'const msg = (strings) => strings[0]', + 'const monthAmount = 10', + 'const yearAmount = 100', + '', + 'const view = pug`', + ' Span.text= ${monthAmount != null && yearAmount != null', + " ? t(msg`Each business you add is {monthAmount}/month or {yearAmount}/year.`, { monthAmount: '$' + monthAmount, yearAmount: '$' + yearAmount + unknownSuffix })", + ' : monthAmount != null', + " ? t(msg`Each business you add is {amount}/month.`, { amount: '$' + monthAmount })", + " : t(msg`Each business you add is {amount}/year.`, { amount: '$' + yearAmount })", + ' }', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + + const [result] = await eslint.lintText(input, { filePath }) + const unknown = result.messages.find((message) => ( + message.ruleId === 'no-undef' + && message.message.includes('unknownSuffix') + )) + + expect(unknown).toBeTruthy() + const expectedStart = input.indexOf('unknownSuffix') + const expected = offsetToLineColumn(input, expectedStart) + expect(unknown?.line).toBe(expected.line) + expect(unknown?.column).toBe(expected.column) + expect(unknown?.endLine).toBe(expected.line) + expect(unknown?.endColumn).toBe(expected.column + 'unknownSuffix'.length) + }) + + it('reports original indent diagnostics inside ${} interpolations', async () => { + const filePath = resolve(repoRoot, 'nested-template-source-indent-gap.js') + const input = [ + "import { pug } from 'startupjs'", + 'const Span = "span"', + 'const t = (value) => value', + 'const msg = (strings) => strings[0]', + 'const monthAmount = 10', + 'const yearAmount = 100', + '', + 'const view = pug`', + ' Span.text= ${monthAmount != null && yearAmount != null', + " ? t(msg`Each business you add is {monthAmount}/month or {yearAmount}/year.`, { monthAmount: '$' + monthAmount, yearAmount: '$' + yearAmount })", + ' : monthAmount != null', + " ? t(msg`Each business you add is {amount}/month.`, { amount: '$' + monthAmount })", + " : t(msg`Each business you add is {amount}/year.`, { amount: '$' + yearAmount })", + ' }', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + + const [result] = await eslint.lintText(input, { filePath }) + const indentMessages = result.messages.filter(message => message.ruleId === '@stylistic/indent') + expect(indentMessages.length).toBeGreaterThan(0) + expect(indentMessages.every(message => (message.line ?? 0) >= 10 && (message.line ?? 0) <= 13)).toBe(true) + }) + + it('reports original indent diagnostics inside inline handler bodies', async () => { + const filePath = resolve(repoRoot, 'embedded-handler-indent.js') + const input = [ + "import { pug } from 'startupjs'", + 'const ready = true', + 'const run = () => {}', + '', + 'const view = pug`', + ' Button(', + ' onClick=() => {', + ' if (ready) {', + ' run()', + ' }', + ' }', + ' ) Save', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + + const [result] = await eslint.lintText(input, { filePath }) + const indentMessages = result.messages.filter(message => message.ruleId === '@stylistic/indent') + expect(indentMessages.length).toBeGreaterThan(0) + expect(indentMessages.some(message => (message.line ?? 0) >= 8 && (message.line ?? 0) <= 10)).toBe(true) + }) + + it('does not report false indent diagnostics for correctly indented multiline embedded attr and handler sites', async () => { + const filePath = resolve(repoRoot, 'embedded-correct-indent-matrix.js') + const input = [ + "import { pug } from 'startupjs'", + "const currentLayout = 'inline'", + "const descriptionPosition = 'right'", + 'const isLabelClickable = true', + 'const _onLabelPress = () => {}', + "const selectedLabel = 'Selected'", + 'const setInputValue = () => {}', + 'const onClose = () => {}', + 'const flattenedStyle = { backgroundColor: "white" }', + 'const geometry = { arrowLeft: 1, arrowTop: 2 }', + '', + 'export default pug`', + ' TouchableWithoutFeedback(onPress=() => {', + ' setInputValue(selectedLabel)', + ' onClose()', + ' })', + ' View.overlay', + ' Span.label(', + ' styleName=[', + ' currentLayout,', + ' descriptionPosition,', + " currentLayout + '-' + descriptionPosition", + ' ]', + ' onPress=isLabelClickable', + ' ? _onLabelPress', + ' : undefined', + ' ) Label', + ' View.arrow(', + ' style={', + ' borderTopColor: flattenedStyle?.backgroundColor,', + ' left: geometry.arrowLeft,', + ' top: geometry.arrowTop', + ' }', + ' )', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + const [result] = await eslint.lintText(input, { filePath }) + + expect(result.messages.filter(message => message.ruleId === '@stylistic/indent')).toEqual([]) + }) + + it('still reports real embedded arrow-spacing and object-curly style diagnostics', async () => { + const filePath = resolve(repoRoot, 'embedded-real-style-rules.js') + const input = [ + "import { pug } from 'startupjs'", + 'const active = true', + 'const onChangeMonth = () => {}', + "const ScrollView = 'div'", + "const Icon = 'i'", + "const Button = 'button'", + '', + 'export default pug`', + ' ScrollView(contentContainerStyle={flex: 1})', + ' Icon(styleName={active})', + ' Button(onPress=()=> onChangeMonth(-1)) Prev', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + const [result] = await eslint.lintText(input, { filePath }) + const messages = result.messages.map(message => ({ + ruleId: message.ruleId, + line: message.line, + column: message.column, + })) + + expect(messages).toEqual(expect.arrayContaining([ + expect.objectContaining({ ruleId: '@stylistic/object-curly-spacing', line: 9, column: 36 }), + expect.objectContaining({ ruleId: '@stylistic/object-curly-spacing', line: 9, column: 44 }), + expect.objectContaining({ ruleId: '@stylistic/object-curly-spacing', line: 10, column: 18 }), + expect.objectContaining({ ruleId: '@stylistic/object-curly-spacing', line: 10, column: 25 }), + expect.objectContaining({ ruleId: '@stylistic/arrow-spacing', line: 11, column: 19 }), + ])) + }) + + it('maps exact no-undef ranges across the embedded JS site matrix, including unbuffered statement lines', async () => { + const filePath = resolve(repoRoot, 'embedded-site-matrix.js') + const input = [ + "import { pug } from 'startupjs'", + "const knownAttr = 'attr'", + "const knownBuffered = 'buffered'", + "const knownInterpolation = 'interp'", + 'const knownTemplate = 1', + 'const knownStatement = 2', + 'const ready = true', + '', + 'export default pug`', + ' Button(', + ' label=knownAttr + missingAttrValue', + ' onClick=() => {', + ' if (ready) {', + ' return missingHandlerValue', + ' }', + ' return knownAttr', + ' }', + ' ) Save', + ' p= knownBuffered + missingBufferedValue', + ' p Hello #{knownInterpolation + missingInterpolationValue}', + ' Span.text= ${knownTemplate + missingTemplateValue}', + ' - const local = knownStatement + missingStatementValue', + ' if local', + ' p Visible', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + const [result] = await eslint.lintText(input, { filePath }) + + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingAttrValue') + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingHandlerValue') + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingBufferedValue') + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingInterpolationValue') + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingTemplateValue') + expectExactMappedMessage(input, result.messages, 'no-undef', 'missingStatementValue') + }) + + it('maps exact @typescript-eslint/no-unused-vars ranges across complex embedded TS expression sites', async () => { + const filePath = resolve(repoRoot, 'embedded-ts-site-matrix.tsx') + const input = [ + "import { pug } from 'startupjs'", + "const known = 'ok'", + "const item = { id: '1' }", + '', + 'export default pug`', + ' Button(', + ' label=((value: string) => {', + ' const unusedAttrValue = value', + ' return value', + ' })(known)', + ' onClick=() => {', + ' const unusedHandlerValue = item.id', + ' return item.id', + ' }', + ' ) Save', + ' p= (() => {', + ' const unusedBufferedValue = known', + ' return known', + ' })()', + ' p Hello #{(() => {', + ' const unusedInterpolationValue = known', + ' return known', + ' })()}', + ' Span.text= ${(() => {', + ' const unusedTemplateValue = known', + ' return known', + ' })()}', + '`', + '', + ].join('\n') + + const eslint = createProcessorEslint() + const [result] = await eslint.lintText(input, { filePath }) + + expectExactMappedMessage(input, result.messages, '@typescript-eslint/no-unused-vars', 'unusedAttrValue') + expectExactMappedMessage(input, result.messages, '@typescript-eslint/no-unused-vars', 'unusedHandlerValue') + expectExactMappedMessage(input, result.messages, '@typescript-eslint/no-unused-vars', 'unusedBufferedValue') + expectExactMappedMessage(input, result.messages, '@typescript-eslint/no-unused-vars', 'unusedInterpolationValue') + expectExactMappedMessage(input, result.messages, '@typescript-eslint/no-unused-vars', 'unusedTemplateValue') + }) + + it('reports source-faithful indent diagnostics across the embedded expression-site matrix without synthetic noise', async () => { + const filePath = resolve(repoRoot, 'embedded-style-matrix.js') + const lines = [ + "import { pug } from 'startupjs'", + 'const ready = true', + 'const formatLabel = (value) => value', + "const fallbackLabel = 'fallback'", + 'const runHandler = () => {}', + 'const runBuffered = () => {}', + 'const runInterpolation = () => {}', + "const label = 'label'", + "const suffix = 'suffix'", + 'const count = 1', + '', + 'export default pug`', + ' Button(', + ' label=(', + ' ready', + ' ? formatLabel(label)', + ' : fallbackLabel', + ' )', + ' onClick=() => {', + ' if (ready) {', + ' runHandler()', + ' }', + ' return label', + ' }', + ' ) Save', + ' p= (() => {', + ' if (ready) {', + ' runBuffered()', + ' }', + ' return label', + ' })()', + ' p Hello #{(() => {', + ' if (ready) {', + ' runInterpolation()', + ' }', + ' return suffix', + ' })()}', + ' Span.text= ${count', + ' ? label', + ' : suffix', + ' }', + '`', + '', + ] + const input = lines.join('\n') + const eslint = createProcessorEslint() + const [result] = await eslint.lintText(input, { filePath }) + const indentMessages = result.messages.filter(message => message.ruleId === '@stylistic/indent') + + expect(indentMessages.length).toBeGreaterThan(0) + + const attrStart = findLineIndex(lines, ' label=(') + 1 + const attrEnd = findLineIndex(lines, ' )', attrStart) + 1 + const handlerStart = findLineIndex(lines, ' onClick=() => {') + 1 + const handlerEnd = findLineIndex(lines, ' }', handlerStart) + 1 + const bufferedStart = findLineIndex(lines, ' p= (() => {') + 1 + const bufferedEnd = findLineIndex(lines, ' })()', bufferedStart) + 1 + const interpolationStart = findLineIndex(lines, ' p Hello #{(() => {') + 1 + const interpolationEnd = findLineIndex(lines, ' })()}', interpolationStart) + 1 + const templateStart = findLineIndex(lines, ' Span.text= ${count') + 1 + const templateEnd = findLineIndex(lines, ' }', templateStart) + 1 + + const expectedRanges = [ + { start: attrStart, end: attrEnd }, + { start: handlerStart, end: handlerEnd }, + { start: bufferedStart, end: bufferedEnd }, + { start: interpolationStart, end: interpolationEnd }, + { start: templateStart, end: templateEnd }, + ] + + for (const range of expectedRanges) { + expect(indentMessages.some((message) => ( + (message.line ?? 0) >= range.start + && (message.line ?? 0) <= range.end + ))).toBe(true) + } + + expect(indentMessages.every((message) => ( + expectedRanges.some((range) => ( + (message.line ?? 0) >= range.start + && (message.line ?? 0) <= range.end + )) + ))).toBe(true) + }) }) diff --git a/packages/eslint-plugin-react-pug/test/integration/fixture-diagnostics.test.ts b/packages/eslint-plugin-react-pug/test/integration/fixture-diagnostics.test.ts index 8ccc7d2..70ae0ec 100644 --- a/packages/eslint-plugin-react-pug/test/integration/fixture-diagnostics.test.ts +++ b/packages/eslint-plugin-react-pug/test/integration/fixture-diagnostics.test.ts @@ -98,7 +98,7 @@ describe('eslint diagnostics for example-unformatted fixture', () => { expect(startupjsUiPrompt?.messages.some(message => message.ruleId === '@stylistic/indent')).toBe(false) const startupjsUiMdxComponents = results.find(result => result.filePath.endsWith('/src/StartupjsUiMdxComponents.js')) - expect(startupjsUiMdxComponents?.messages.some(message => message.ruleId === '@stylistic/indent')).toBe(false) + expect(startupjsUiMdxComponents?.messages.some(message => message.ruleId === 'no-undef')).toBe(false) const startupjsUiMultiSelect = results.find(result => result.filePath.endsWith('/src/StartupjsUiMultiSelect.tsx')) expect(startupjsUiMultiSelect?.messages.some(message => message.ruleId === '@stylistic/indent')).toBe(false) diff --git a/packages/eslint-plugin-react-pug/test/integration/startupjs-ui-regressions.test.ts b/packages/eslint-plugin-react-pug/test/integration/startupjs-ui-regressions.test.ts index e5ab2ee..1a991ef 100644 --- a/packages/eslint-plugin-react-pug/test/integration/startupjs-ui-regressions.test.ts +++ b/packages/eslint-plugin-react-pug/test/integration/startupjs-ui-regressions.test.ts @@ -8,6 +8,7 @@ import reactPugPlugin from '../../src/index' const repoRoot = resolve(__dirname, '../../../..') const fixtureRoot = resolve(repoRoot, 'test/fixtures/example-unformatted/src') +const fixedSnapshotRoot = resolve(repoRoot, 'test/fixtures/example-unformatted/snapshots/fixed/src') const reactHooksStubPlugin = { rules: { 'rules-of-hooks': { @@ -67,6 +68,7 @@ function createStartupjsUiStyleEslint(fix: boolean): ESLint { describe('startupjs-ui regressions', () => { it('does not report false processor diagnostics for startupjs-ui repros', async () => { const files = [ + resolve(fixtureRoot, 'StartupjsUiAvatar.tsx'), resolve(fixtureRoot, 'StartupjsUiDialogsReadme.js'), resolve(fixtureRoot, 'StartupjsUiDropdown.tsx'), resolve(fixtureRoot, 'StartupjsUiDraggableReadme.js'), @@ -89,7 +91,9 @@ describe('startupjs-ui regressions', () => { })) )) - expect(messages).toEqual([]) + expect(messages.every(message => String(message.ruleId).startsWith('@stylistic/'))).toBe(true) + expect(messages.some(message => message.ruleId === '@stylistic/indent')).toBe(false) + expect(messages.some(message => message.ruleId === '@stylistic/arrow-spacing')).toBe(true) }) it('still reports jsx-boolean-value for intrinsic boolean attrs inside pug', async () => { @@ -113,9 +117,10 @@ describe('startupjs-ui regressions', () => { expect(booleanDiagnostic?.column).toBe(12) }) - it('does not rewrite startupjs-ui repros under eslint --fix', async () => { + it('rewrites startupjs-ui repros only to the expected fixed snapshots under eslint --fix', async () => { for (const relativePath of [ 'StartupjsUiDialogsReadme.js', + 'StartupjsUiAvatar.tsx', 'StartupjsUiDropdown.tsx', 'StartupjsUiDraggableReadme.js', 'StartupjsUiMdxComponents.js', @@ -128,7 +133,7 @@ describe('startupjs-ui regressions', () => { const filePath = resolve(fixtureRoot, relativePath) const input = readFileSync(filePath, 'utf8') const [result] = await createStartupjsUiStyleEslint(true).lintText(input, { filePath }) - expect(result.output ?? input).toBe(input) + expect(result.output ?? input).toBe(readFileSync(resolve(fixedSnapshotRoot, relativePath), 'utf8')) } }) }) diff --git a/packages/eslint-plugin-react-pug/test/unit/processor.test.ts b/packages/eslint-plugin-react-pug/test/unit/processor.test.ts index 4686010..f9ad62f 100644 --- a/packages/eslint-plugin-react-pug/test/unit/processor.test.ts +++ b/packages/eslint-plugin-react-pug/test/unit/processor.test.ts @@ -106,7 +106,10 @@ function lintStylisticIndent(code: string, filename = 'file.jsx') { ], offsetTernaryExpressions: true, }], - '@stylistic/jsx-indent': ['error', 2], + '@stylistic/jsx-indent': ['error', 2, { + checkAttributes: false, + indentLogicalExpressions: true, + }], '@stylistic/jsx-indent-props': ['error', 2], '@stylistic/jsx-wrap-multilines': ['error', { declaration: 'parens-new-line', @@ -406,7 +409,7 @@ describe('eslint-plugin-react-pug processor', () => { }" `); - const lintMessages = lintStylisticIndent(code, 'file.jsx'); + const lintMessages = lintStartupjsUiStyle(code, 'file.jsx'); const mapped = processor.postprocess([lintMessages as any], 'file.jsx'); expect(mapped.filter(message => String(message.ruleId).includes('indent'))).toEqual([]); }); @@ -573,6 +576,227 @@ describe('eslint-plugin-react-pug processor', () => { expect(mapped.filter(message => message.ruleId === '@stylistic/jsx-closing-tag-location')).toEqual([]); }); + it('formats nested multiline ternaries inside ${} interpolations with consumer-aligned indentation', () => { + const processor = createReactPugProcessor(); + const input = [ + 'const monthAmount = 10', + 'const yearAmount = 100', + 'const view = pug`', + ' Span.text= ${monthAmount != null && yearAmount != null', + " ? t(msg`Each business you add is {monthAmount}/month or {yearAmount}/year.`, { monthAmount: '$' + monthAmount, yearAmount: '$' + yearAmount })", + ' : monthAmount != null', + " ? t(msg`Each business you add is {amount}/month.`, { amount: '$' + monthAmount })", + " : t(msg`Each business you add is {amount}/year.`, { amount: '$' + yearAmount })", + ' }', + '`', + ].join('\n'); + + const [block] = processor.preprocess(input, 'file.jsx'); + const code = typeof block === 'string' ? block : block.text; + expect(code).toMatchInlineSnapshot(` + "const monthAmount = 10 + const yearAmount = 100 + const view = ( + + {monthAmount != null && yearAmount != null + ? t( + msg\`Each business you add is {monthAmount}/month or {yearAmount}/year.\`, + { + monthAmount: '$' + monthAmount, + yearAmount: '$' + yearAmount + } + ) + : monthAmount != null + ? t(msg\`Each business you add is {amount}/month.\`, { + amount: '$' + monthAmount + }) + : t(msg\`Each business you add is {amount}/year.\`, { + amount: '$' + yearAmount + })} + + )" + `); + }); + + it('emits dedicated embedded-JS lint blocks for expression and statement sites', () => { + const processor = createReactPugProcessor(); + const input = [ + 'const view = pug`', + ' Button(', + ' label=knownAttr + missingAttrValue', + ' onClick=() => run(item.id)', + ' ) Save', + ' p Hello #{knownInterpolation + missingInterpolationValue}', + ' - const visible = ready', + '`', + ].join('\n'); + + const blocks = processor.preprocess(input, 'file.jsx'); + expect(blocks).toHaveLength(5); + expect(blocks[1]).toMatchObject({ + filename: '../../../pug-react-embedded-statement-0.jsx', + }); + expect(blocks[2]).toMatchObject({ + filename: '../../../pug-react-embedded-expression-1.jsx', + }); + expect(blocks[3]).toMatchObject({ + filename: '../../../pug-react-embedded-expression-2.jsx', + }); + expect(blocks[4]).toMatchObject({ + filename: '../../../pug-react-embedded-expression-3.jsx', + }); + expect((blocks[1] as any).text).toContain('(() => {\n const visible = ready\n})()'); + expect((blocks[2] as any).text).toContain('const __reactPugExpr = (\n knownAttr + missingAttrValue\n)'); + expect((blocks[3] as any).text).toContain('const __reactPugExpr = (\n () => run(item.id)\n)'); + expect((blocks[4] as any).text).toContain('const __reactPugExpr = (\n knownInterpolation + missingInterpolationValue\n)'); + }); + + it('dedents shared continuation indent in multiline embedded expression blocks', () => { + const processor = createReactPugProcessor(); + const input = [ + 'const view = pug`', + ' Button(', + ' style={', + ' borderTopColor: ready ? "green" : "red",', + ' left: geometry.arrowLeft,', + ' top: geometry.arrowTop', + ' }', + ' onClick=() => {', + ' setInputValue(selectedLabel)', + ' onClose()', + ' }', + ' ) Save', + '`', + ].join('\n'); + + const blocks = processor.preprocess(input, 'file.jsx') as Array<{ text: string; filename: string }>; + expect(blocks[1]).toMatchObject({ + filename: '../../../pug-react-embedded-expression-0.jsx', + text: [ + 'const __reactPugExpr = (', + ' {', + ' borderTopColor: ready ? "green" : "red",', + ' left: geometry.arrowLeft,', + ' top: geometry.arrowTop', + ' }', + ')', + '', + ].join('\n'), + }); + expect(blocks[2]).toMatchObject({ + filename: '../../../pug-react-embedded-expression-1.jsx', + text: [ + 'const __reactPugExpr = (', + ' () => {', + ' setInputValue(selectedLabel)', + ' onClose()', + ' }', + ')', + '', + ].join('\n'), + }); + }); + + it('maps embedded-source autofix edits back to original pug ranges', () => { + const processor = createReactPugProcessor(); + const input = [ + 'const view = pug`', + ' Button(label=label+suffix) Save', + '`', + ].join('\n'); + + const blocks = processor.preprocess(input, 'file.jsx') as Array<{ text: string; filename: string }>; + const embeddedBlock = blocks[1]; + const fixStart = embeddedBlock.text.indexOf('label+suffix'); + const fixEnd = fixStart + 'label+suffix'.length; + const startLc = offsetToLineColumn(embeddedBlock.text, fixStart); + const endLc = offsetToLineColumn(embeddedBlock.text, fixEnd); + + const mapped = processor.postprocess([ + [], + [{ + ruleId: '@stylistic/space-infix-ops', + message: "Operator '+' must be spaced.", + line: startLc.line, + column: startLc.column, + endLine: endLc.line, + endColumn: endLc.column, + fix: { + range: [fixStart, fixEnd], + text: 'label + suffix', + }, + } as any], + ], 'file.jsx'); + + const expectedStart = input.indexOf('label=label+suffix') + 'label='.length; + expect(mapped).toEqual([ + expect.objectContaining({ + ruleId: '@stylistic/space-infix-ops', + line: 2, + column: 16, + endLine: 2, + endColumn: 28, + fix: { + range: [expectedStart, expectedStart + 'label+suffix'.length], + text: 'label + suffix', + }, + }), + ]); + }); + + it('maps embedded-source suggestions back to original pug ranges', () => { + const processor = createReactPugProcessor(); + const input = [ + 'const view = pug`', + ' p Hello #{label+suffix}', + '`', + ].join('\n'); + + const blocks = processor.preprocess(input, 'file.jsx') as Array<{ text: string; filename: string }>; + const embeddedBlock = blocks[1]; + const fixStart = embeddedBlock.text.indexOf('label+suffix'); + const fixEnd = fixStart + 'label+suffix'.length; + const startLc = offsetToLineColumn(embeddedBlock.text, fixStart); + const endLc = offsetToLineColumn(embeddedBlock.text, fixEnd); + + const mapped = processor.postprocess([ + [], + [{ + ruleId: '@stylistic/space-infix-ops', + message: "Operator '+' must be spaced.", + line: startLc.line, + column: startLc.column, + endLine: endLc.line, + endColumn: endLc.column, + suggestions: [{ + desc: 'Add spaces around +', + fix: { + range: [fixStart, fixEnd], + text: 'label + suffix', + }, + }], + } as any], + ], 'file.jsx'); + + const expectedStart = input.indexOf('label+suffix'); + expect(mapped).toEqual([ + expect.objectContaining({ + ruleId: '@stylistic/space-infix-ops', + line: 2, + column: 13, + endLine: 2, + endColumn: 25, + suggestions: [{ + desc: 'Add spaces around +', + fix: { + range: [expectedStart, expectedStart + 'label+suffix'.length], + text: 'label + suffix', + }, + }], + }), + ]); + }); + it('suppresses legacy styl warnings without hiding normal linting', () => { const processor = createReactPugProcessor(); const input = [ diff --git a/packages/pug-lexer/index.js b/packages/pug-lexer/index.js index 681e22c..470fbf7 100644 --- a/packages/pug-lexer/index.js +++ b/packages/pug-lexer/index.js @@ -30,6 +30,23 @@ function startsWithTypeScriptContinuation(str, index, whitespaceRe) { return true; } +function hasOpenJavaScriptSyntax(str) { + var state = characterParser.default(str); + return state.isNesting() || state.isString(); +} + +function endsWithJavaScriptContinuation(str) { + var trimmed = str.replace(/[ \t]+$/, ''); + return /(?:=>|[([{,:?+\-*/%&|^=!<>]|\b(?:as|satisfies|in|instanceof)\b)$/.test(trimmed); +} + +function startsWithJavaScriptContinuationLine(str) { + var trimmed = str.trim(); + if (!trimmed) return false; + + return /^(?:\?|:|&&|\|\||\?\?|as\b|satisfies\b|instanceof\b|in\b)/.test(trimmed); +} + /** * Initialize `Lexer` with the given `str`. * @@ -127,6 +144,48 @@ Lexer.prototype = { } }, + incrementPositionByText: function(text) { + var lines = text.split('\n'); + var newlineCount = lines.length - 1; + if (newlineCount === 0) { + this.incrementColumn(text.length); + return; + } + + this.incrementLine(newlineCount); + this.incrementColumn(lines[newlineCount].length); + }, + + collectMultilineCodeFragment: function(startIndex, initialCode) { + var consumed = startIndex + initialCode.length; + var code = initialCode; + + while (consumed < this.input.length && this.input[consumed] === '\n') { + var nextLineStart = consumed + 1; + var nextLineEnd = this.input.indexOf('\n', nextLineStart); + if (nextLineEnd === -1) { + nextLineEnd = this.input.length; + } + var nextLine = this.input.slice(nextLineStart, nextLineEnd); + var shouldContinue = + hasOpenJavaScriptSyntax(code) || + endsWithJavaScriptContinuation(code) || + startsWithJavaScriptContinuationLine(nextLine); + + if (!shouldContinue) { + break; + } + + code += this.input.slice(consumed, nextLineEnd); + consumed = nextLineEnd; + } + + return { + code: code, + consumed: consumed, + }; + }, + /** * Construct a token with the given `type` and `val`. * @@ -619,11 +678,12 @@ Lexer.prototype = { } var rest = matchOfStringInterp[3]; + var fullRest = rest + this.input; var range; tok = this.tok('interpolated-code'); this.incrementColumn(2); try { - range = characterParser.parseUntil(rest, '}'); + range = characterParser.parseUntil(fullRest, '}'); } catch (ex) { if (ex.index !== undefined) { this.incrementColumn(ex.index); @@ -643,15 +703,24 @@ Lexer.prototype = { tok.buffer = true; tok.val = range.src; this.assertExpression(range.src); + var consumedFromRest = range.end + 1; + var extraInputConsumed = Math.max(0, consumedFromRest - rest.length); + var tailInValue = + consumedFromRest < rest.length ? rest.substr(consumedFromRest) : ''; - if (range.end + 1 < rest.length) { - rest = rest.substr(range.end + 1); - this.incrementColumn(range.end + 1); - this.tokens.push(this.tokEnd(tok)); - this.addText(type, rest); - } else { - this.incrementColumn(rest.length); - this.tokens.push(this.tokEnd(tok)); + this.input = this.input.substr(extraInputConsumed); + this.incrementPositionByText(range.src); + this.incrementColumn(1); + this.tokens.push(this.tokEnd(tok)); + + var tailFromInput = ''; + if (this.input.length && this.input[0] !== '\n' && this.input[0] !== ']') { + tailFromInput = /^[^\n\]]*/.exec(this.input)[0]; + this.consume(tailFromInput.length); + } + + if (tailInValue || tailFromInput) { + this.addText(type, tailInValue + tailFromInput); } return; } @@ -1153,6 +1222,8 @@ Lexer.prototype = { var flags = captures[1]; var code = captures[2]; var shortened = 0; + var prefixLength = captures[0].length - captures[2].length; + var consumed = captures[0].length; if (this.interpolated) { var parsed; try { @@ -1174,8 +1245,12 @@ Lexer.prototype = { } shortened = code.length - parsed.end; code = parsed.src; + consumed -= shortened; + } else if (flags.charAt(0) === '=' || flags.charAt(1) === '=') { + var collected = this.collectMultilineCodeFragment(prefixLength, code); + code = collected.code; + consumed = collected.consumed; } - var consumed = captures[0].length - shortened; this.consume(consumed); var tok = this.tok('code', code); tok.mustEscape = flags.charAt(0) === '='; @@ -1194,7 +1269,7 @@ Lexer.prototype = { // --- captures[2] // ---- captures[0] - captures[2] // ^ after colno - this.incrementColumn(captures[0].length - captures[2].length); + this.incrementColumn(prefixLength); if (tok.buffer) this.assertExpression(code); this.tokens.push(tok); @@ -1209,7 +1284,7 @@ Lexer.prototype = { // shortened // --- code // ^ after colno - this.incrementColumn(code.length); + this.incrementPositionByText(code); this.tokEnd(tok); return true; } diff --git a/packages/pug-lexer/test/multiline.test.js b/packages/pug-lexer/test/multiline.test.js new file mode 100644 index 0000000..30e2d50 --- /dev/null +++ b/packages/pug-lexer/test/multiline.test.js @@ -0,0 +1,188 @@ +'use strict'; + +const lex = require('../'); + +function findToken(tokens, type) { + return tokens.find(token => token.type === type); +} + +test('supports multiline buffered pure-JS expressions on code lines', () => { + const tokens = lex( + [ + 'p= ready', + " ? formatLabel('yes')", + " : formatLabel('no')", + ].join('\n'), + { filename: 'multiline-buffered-js.pug' }, + ); + + expect(findToken(tokens, 'code')).toEqual( + expect.objectContaining({ + val: [ + 'ready', + " ? formatLabel('yes')", + " : formatLabel('no')", + ].join('\n'), + }), + ); +}); + +test('supports multiline buffered TypeScript expressions with continuation keywords', () => { + const tokens = lex( + [ + 'p= ({', + ' label: value,', + '}) satisfies CardConfig', + ].join('\n'), + { filename: 'multiline-buffered-ts-keyword.pug' }, + ); + + expect(findToken(tokens, 'code')).toEqual( + expect.objectContaining({ + val: [ + '({', + ' label: value,', + '}) satisfies CardConfig', + ].join('\n'), + }), + ); +}); + +test('supports multiline buffered expressions continued by line-leading nullish operators', () => { + const tokens = lex( + [ + 'p= maybeValue', + ' ?? fallbackValue', + ].join('\n'), + { filename: 'multiline-buffered-nullish.pug' }, + ); + + expect(findToken(tokens, 'code')).toEqual( + expect.objectContaining({ + val: [ + 'maybeValue', + ' ?? fallbackValue', + ].join('\n'), + }), + ); +}); + +test('stops multiline buffered code collection before the next sibling tag once the expression is complete', () => { + const tokens = lex( + [ + 'p= formatValue(', + ' source,', + ')', + 'span Done', + ].join('\n'), + { filename: 'multiline-buffered-stop-before-sibling.pug' }, + ); + + expect(tokens).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'code', + val: [ + 'formatValue(', + ' source,', + ')', + ].join('\n'), + }), + expect.objectContaining({ + type: 'tag', + val: 'span', + }), + expect.objectContaining({ + type: 'text', + val: 'Done', + }), + ]), + ); +}); + +test('supports multiline text interpolation with nested objects and trailing text', () => { + const tokens = lex( + [ + 'p Hello #{(() => {', + " const value = { label: 'hi' }", + ' return value.label', + '})()} world', + ].join('\n'), + { filename: 'multiline-interpolation-js.pug' }, + ); + + expect(tokens).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'interpolated-code', + val: [ + '(() => {', + " const value = { label: 'hi' }", + ' return value.label', + '})()', + ].join('\n'), + }), + expect.objectContaining({ + type: 'text', + val: ' world', + }), + ]), + ); +}); + +test('supports multiline text interpolation with TypeScript syntax', () => { + const tokens = lex( + [ + 'p Hello #{(() => {', + ' const value = known as string', + ' return value', + '})()} world', + ].join('\n'), + { filename: 'multiline-interpolation-ts.pug' }, + ); + + expect(findToken(tokens, 'interpolated-code')).toEqual( + expect.objectContaining({ + val: [ + '(() => {', + ' const value = known as string', + ' return value', + '})()', + ].join('\n'), + }), + ); +}); + +test('keeps existing unbuffered code-block semantics with nested pug children', () => { + const tokens = lex( + [ + '- if (ready) {', + ' p yes', + '- }', + ].join('\n'), + { filename: 'multiline-unbuffered-js.pug' }, + ); + + expect(tokens).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'code', + val: 'if (ready) {', + buffer: false, + }), + expect.objectContaining({ + type: 'tag', + val: 'p', + }), + expect.objectContaining({ + type: 'text', + val: 'yes', + }), + expect.objectContaining({ + type: 'code', + val: '}', + buffer: false, + }), + ]), + ); +}); diff --git a/packages/pug-lexer/test/typescript.test.js b/packages/pug-lexer/test/typescript.test.js index 547eda0..af11423 100644 --- a/packages/pug-lexer/test/typescript.test.js +++ b/packages/pug-lexer/test/typescript.test.js @@ -100,3 +100,49 @@ test('supports TypeScript syntax in conditionals, loops, interpolation, and hand }), ); }); + +test('supports multiline buffered TypeScript expressions on code lines', () => { + const tokens = lex( + [ + 'p= (() => {', + ' const value = known as string', + ' return value', + '})()', + ].join('\n'), + { filename: 'multiline-buffered-expression.pug' }, + ); + + expect(tokens).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'code', + val: ['(() => {', ' const value = known as string', ' return value', '})()'].join('\n'), + }), + ]), + ); +}); + +test('supports multiline TypeScript interpolations inside text', () => { + const tokens = lex( + [ + 'p Hello #{(() => {', + ' const value = known as string', + ' return value', + '})()} world', + ].join('\n'), + { filename: 'multiline-interpolation.pug' }, + ); + + expect(tokens).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'interpolated-code', + val: ['(() => {', ' const value = known as string', ' return value', '})()'].join('\n'), + }), + expect.objectContaining({ + type: 'text', + val: ' world', + }), + ]), + ); +}); diff --git a/packages/react-pug-core/src/language/extractRegions.ts b/packages/react-pug-core/src/language/extractRegions.ts index b46bd15..8960562 100644 --- a/packages/react-pug-core/src/language/extractRegions.ts +++ b/packages/react-pug-core/src/language/extractRegions.ts @@ -360,6 +360,7 @@ function extractWithRegex(text: string, tagName: string = 'pug'): PugRegion[] { parseError: null, transformError: null, styleBlock: null, + embeddedJsLintSites: [], }); } @@ -486,6 +487,7 @@ export function extractPugAnalysis( parseError: null, transformError: null, styleBlock: null, + embeddedJsLintSites: [], }; regions.push(region); diff --git a/packages/react-pug-core/src/language/lintTransform.ts b/packages/react-pug-core/src/language/lintTransform.ts index 4e885fd..aa0d215 100644 --- a/packages/react-pug-core/src/language/lintTransform.ts +++ b/packages/react-pug-core/src/language/lintTransform.ts @@ -1,9 +1,10 @@ import generate from '@babel/generator'; import { parse, parseExpression } from '@babel/parser'; import * as t from '@babel/types'; -import type { ShadowInsertion, ShadowMappedRegion } from './mapping'; +import type { EmbeddedJsLintSite, ShadowInsertion, ShadowMappedRegion } from './mapping'; import type { SourceTransformOptions, SourceTransformResult } from './sourceTransform'; import { transformSourceFile } from './sourceTransform'; +import { regionStrippedOffsetToOriginalOffset, strippedToRawOffset } from './regionOffsetMapping'; export interface RewrittenCopySegment { rewrittenStart: number; @@ -45,10 +46,19 @@ export interface BoundaryMappedExpression { export interface LintTransformResult extends RewrittenPugRegionsResult { baseTransform: SourceTransformResult; + embeddedJsLintSites: MappedEmbeddedJsLintSite[]; mapGeneratedOffsetToOriginal: (offset: number) => number | null; mapBaseOffsetToOriginal: (offset: number) => number | null; } +export interface MappedEmbeddedJsLintSite { + kind: EmbeddedJsLintSite['kind']; + originalStart: number; + originalEnd: number; + code: string; + boundaryMap: number[]; +} + export type RegionContainerKind = | 'standalone' | 'return-value' @@ -907,6 +917,38 @@ export function collectMappedInsertionRangesByKind( return ranges; } +function mapEmbeddedJsLintSitesToOriginal(baseTransform: SourceTransformResult): MappedEmbeddedJsLintSite[] { + const sites: MappedEmbeddedJsLintSite[] = []; + + for (const region of baseTransform.regions) { + for (const site of region.embeddedJsLintSites) { + const boundaryMap = site.boundaryMap.map((offset) => ( + regionStrippedOffsetToOriginalOffset(baseTransform.document, region, offset) + )); + const originalStart = boundaryMap[0] ?? regionStrippedOffsetToOriginalOffset( + baseTransform.document, + region, + site.sourceStart, + ); + const originalEnd = boundaryMap[boundaryMap.length - 1] ?? regionStrippedOffsetToOriginalOffset( + baseTransform.document, + region, + site.sourceEnd, + ); + + sites.push({ + kind: site.kind, + originalStart, + originalEnd, + code: site.code, + boundaryMap, + }); + } + } + + return sites; +} + export function createLintTransform( sourceText: string, fileName: string, @@ -919,10 +961,12 @@ export function createLintTransform( const rewritten = rewriteMappedPugRegions(baseTransform, fileName, (expr, _region, currentFileName) => ( normalizePugExpressionForLint(expr, currentFileName) )); + const embeddedJsLintSites = mapEmbeddedJsLintSitesToOriginal(baseTransform); return { ...rewritten, baseTransform, + embeddedJsLintSites, mapGeneratedOffsetToOriginal: (offset: number) => { const baseOffset = rewritten.mapRewrittenOffsetToBase(offset); return baseOffset == null ? null : baseTransform.mapGeneratedOffsetToOriginal(baseOffset); diff --git a/packages/react-pug-core/src/language/mapping.ts b/packages/react-pug-core/src/language/mapping.ts index ed452b9..aa0b503 100644 --- a/packages/react-pug-core/src/language/mapping.ts +++ b/packages/react-pug-core/src/language/mapping.ts @@ -103,6 +103,24 @@ export interface ExtractedStyleBlock { column: number; } +export type EmbeddedJsLintSiteKind = 'expression' | 'statement'; + +export interface EmbeddedJsLintSite { + /** Whether the original embedded JS must parse as an expression or statement list */ + kind: EmbeddedJsLintSiteKind; + /** Start offset within stripped pug text */ + sourceStart: number; + /** End offset within stripped pug text */ + sourceEnd: number; + /** Normalized JS source to lint for this embedded site */ + code: string; + /** + * Boundary map from normalized code offsets back into stripped pug text offsets. + * Length is always `code.length + 1`. + */ + boundaryMap: number[]; +} + // ── PugToken ──────────────────────────────────────────────────── /** Minimal pug lexer token shape (retained for sub-expression position resolution) */ @@ -204,6 +222,9 @@ export interface PugRegion { /** Extracted terminal style block, if present */ styleBlock: ExtractedStyleBlock | null; + + /** Embedded JS sites that can be linted directly against original source */ + embeddedJsLintSites: EmbeddedJsLintSite[]; } // ── PugDocument ───────────────────────────────────────────────── diff --git a/packages/react-pug-core/src/language/pugToTsx.ts b/packages/react-pug-core/src/language/pugToTsx.ts index 235d70b..c2e5833 100644 --- a/packages/react-pug-core/src/language/pugToTsx.ts +++ b/packages/react-pug-core/src/language/pugToTsx.ts @@ -1,6 +1,8 @@ import type { CodeMapping, CodeInformation, + EmbeddedJsLintSite, + EmbeddedJsLintSiteKind, ExtractedStyleBlock, PugParseError, PugToken, @@ -315,6 +317,7 @@ interface InterpolationContext { } const interpolationContextStack: InterpolationContext[] = []; +const embeddedJsLintSiteStack: EmbeddedJsLintSite[][] = []; interface CompileContext { mode: CompileMode; classAttribute: ClassAttributeName; @@ -329,6 +332,12 @@ function currentInterpolationContext(): InterpolationContext | null { : null; } +function currentEmbeddedJsLintSites(): EmbeddedJsLintSite[] | null { + return embeddedJsLintSiteStack.length > 0 + ? embeddedJsLintSiteStack[embeddedJsLintSiteStack.length - 1] + : null; +} + function currentCompileMode(): CompileMode { return compileContextStack.length > 0 ? compileContextStack[compileContextStack.length - 1].mode @@ -371,6 +380,7 @@ function findInterpolationEnd(text: string, exprStart: number): number | null { let inLineComment = false; let inBlockComment = false; let escaped = false; + const templateExpressionDepths: number[] = []; while (i < text.length) { const ch = text[i]; @@ -424,7 +434,9 @@ function findInterpolationEnd(text: string, exprStart: number): number | null { } else if (ch === '`') { inTemplate = false; } else if (ch === '$' && next === '{') { + templateExpressionDepths.push(depth); depth += 1; + inTemplate = false; i += 2; continue; } @@ -465,6 +477,13 @@ function findInterpolationEnd(text: string, exprStart: number): number | null { if (ch === '}') { depth -= 1; if (depth === 0) return i; + if ( + templateExpressionDepths.length > 0 + && templateExpressionDepths[templateExpressionDepths.length - 1] === depth + ) { + templateExpressionDepths.pop(); + inTemplate = true; + } i += 1; continue; } @@ -540,6 +559,356 @@ function findNextInterpolationOccurrence( return { index: bestIdx, interpolation: bestInterpolation }; } +interface MappedSourceBuilder { + code: string; + boundaryMap: number[]; + previousSourceEnd: number | null; +} + +function appendMappedSourceChar( + state: MappedSourceBuilder, + ch: string, + sourceStart: number, + sourceEnd: number, +): void { + if (state.code.length === 0) { + state.boundaryMap.push(sourceStart); + } else if (state.previousSourceEnd !== sourceStart) { + state.boundaryMap[state.boundaryMap.length - 1] = sourceStart; + } + state.code += ch; + state.boundaryMap.push(sourceEnd); + state.previousSourceEnd = sourceEnd; +} + +function appendMappedSourceSegment( + state: MappedSourceBuilder, + segment: string, + sourceStart: number, +): void { + for (let i = 0; i < segment.length; i += 1) { + appendMappedSourceChar(state, segment[i], sourceStart + i, sourceStart + i + 1); + } +} + +function resolveTemplateInterpolatedJavaScript( + text: string, + sourceStart: number, + context: InterpolationContext | null, +): { code: string; boundaryMap: number[] } { + const state: MappedSourceBuilder = { + code: '', + boundaryMap: [], + previousSourceEnd: null, + }; + + let cursor = 0; + while (cursor < text.length) { + const hit = findNextInterpolationOccurrence(text, cursor, context); + if (!hit) { + appendMappedSourceSegment(state, text.slice(cursor), sourceStart + cursor); + break; + } + + if (hit.index > cursor) { + appendMappedSourceSegment( + state, + text.slice(cursor, hit.index), + sourceStart + cursor, + ); + } + + const interpolation = hit.interpolation; + appendMappedSourceSegment( + state, + interpolation.expression, + interpolation.exprStart, + ); + cursor = hit.index + interpolation.marker.length; + } + + if (state.boundaryMap.length === 0) { + state.boundaryMap.push(sourceStart); + } + + return { + code: state.code, + boundaryMap: state.boundaryMap, + }; +} + +function recordEmbeddedJsLintSite( + kind: EmbeddedJsLintSiteKind, + text: string, + sourceStart: number, +): void { + const sites = currentEmbeddedJsLintSites(); + if (!sites) return; + + const resolved = resolveTemplateInterpolatedJavaScript( + text, + sourceStart, + currentInterpolationContext(), + ); + const trimmedLength = resolved.code.replace(/[ \t\r\n]+$/u, '').length; + if (trimmedLength === 0) return; + const trimmedCode = resolved.code.slice(0, trimmedLength); + const trimmedBoundaryMap = resolved.boundaryMap.slice(0, trimmedLength + 1); + + sites.push({ + kind, + sourceStart, + sourceEnd: sourceStart + text.length, + code: trimmedCode, + boundaryMap: trimmedBoundaryMap, + }); +} + +function canParseEmbeddedExpression(text: string): boolean { + try { + parseExpression(text, { + plugins: ['jsx', 'decorators-legacy', 'typescript'], + }); + return true; + } catch { + return false; + } +} + +function getJavaScriptDelimiterBalance(text: string): { + paren: number; + brace: number; + bracket: number; +} { + let paren = 0; + let brace = 0; + let bracket = 0; + let i = 0; + let inSingle = false; + let inDouble = false; + let inTemplate = false; + let inLineComment = false; + let inBlockComment = false; + let escaped = false; + + while (i < text.length) { + const ch = text[i]; + const next = text[i + 1] ?? ''; + + if (inLineComment) { + if (ch === '\n') inLineComment = false; + i += 1; + continue; + } + + if (inBlockComment) { + if (ch === '*' && next === '/') { + inBlockComment = false; + i += 2; + } else { + i += 1; + } + continue; + } + + if (inSingle) { + if (escaped) escaped = false; + else if (ch === '\\') escaped = true; + else if (ch === '\'') inSingle = false; + i += 1; + continue; + } + + if (inDouble) { + if (escaped) escaped = false; + else if (ch === '\\') escaped = true; + else if (ch === '"') inDouble = false; + i += 1; + continue; + } + + if (inTemplate) { + if (escaped) { + escaped = false; + } else if (ch === '\\') { + escaped = true; + } else if (ch === '`') { + inTemplate = false; + } + i += 1; + continue; + } + + if (ch === '/' && next === '/') { + inLineComment = true; + i += 2; + continue; + } + if (ch === '/' && next === '*') { + inBlockComment = true; + i += 2; + continue; + } + if (ch === '\'') { + inSingle = true; + i += 1; + continue; + } + if (ch === '"') { + inDouble = true; + i += 1; + continue; + } + if (ch === '`') { + inTemplate = true; + i += 1; + continue; + } + + if (ch === '(') paren += 1; + else if (ch === ')') paren = Math.max(0, paren - 1); + else if (ch === '{') brace += 1; + else if (ch === '}') brace = Math.max(0, brace - 1); + else if (ch === '[') bracket += 1; + else if (ch === ']') bracket = Math.max(0, bracket - 1); + + i += 1; + } + + return { paren, brace, bracket }; +} + +function hasOpenJavaScriptDelimiters(text: string): boolean { + const balance = getJavaScriptDelimiterBalance(text); + return balance.paren > 0 || balance.brace > 0 || balance.bracket > 0; +} + +function lineStartOffsets(text: string): number[] { + const starts = [0]; + for (let i = 0; i < text.length; i += 1) { + if (text[i] === '\n' && i + 1 < text.length) starts.push(i + 1); + } + return starts; +} + +function looksLikeTagAttrListStart(trimmedLine: string): boolean { + return /^[A-Za-z_$][\w$]*(?:[.:][A-Za-z_$][\w$-]*)*(?:[.#][A-Za-z_$][\w-]*)*\s*\($/.test(trimmedLine); +} + +function collectRawEmbeddedExpressionSite( + lines: string[], + starts: number[], + startLineIndex: number, + startColumn: number, +): { text: string; sourceStart: number; endLineIndex: number } { + const baseIndent = countIndent(lines[startLineIndex]); + let endLineIndex = startLineIndex; + let text = lines[startLineIndex].slice(startColumn); + + while (endLineIndex + 1 < lines.length) { + if (canParseEmbeddedExpression(text) && !hasOpenJavaScriptDelimiters(text)) break; + + const nextLineIndex = endLineIndex + 1; + const nextLine = lines[nextLineIndex]; + const nextTrimmed = nextLine.trim(); + const nextIndent = countIndent(nextLine); + const shouldContinue = ( + nextTrimmed.length === 0 + || nextIndent > baseIndent + || hasOpenJavaScriptDelimiters(text) + || nextTrimmed.startsWith(')') + || nextTrimmed.startsWith(']') + || nextTrimmed.startsWith('}') + || nextTrimmed.startsWith('?') + || nextTrimmed.startsWith(':') + ); + + if (!shouldContinue) break; + + text += `\n${nextLine}`; + endLineIndex = nextLineIndex; + } + + return { + text, + sourceStart: starts[startLineIndex] + startColumn, + endLineIndex, + }; +} + +function extractEmbeddedJsLintSitesFromRawPugText(pugText: string): void { + // This is a recovery-only path. Valid multiline embedded expressions should + // be handled by the vendored pug lexer/parser directly. We keep this raw scan + // only so ESLint can still surface source-faithful embedded-JS diagnostics + // while the template is malformed or temporarily incomplete during typing. + for (let i = 0; i < pugText.length; i += 1) { + const marker = pugText[i]; + if ((marker === '#' || marker === '!') && pugText[i + 1] === '{') { + const exprStart = i + 2; + const exprEnd = findInterpolationEnd(pugText, exprStart); + if (exprEnd != null) { + recordEmbeddedJsLintSite('expression', pugText.slice(exprStart, exprEnd), exprStart); + i = exprEnd; + } + } + } + + const lines = pugText.split('\n'); + const starts = lineStartOffsets(pugText); + let inAttrList = false; + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { + const line = lines[lineIndex]; + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + + if (inAttrList) { + if (trimmed.startsWith(')')) { + inAttrList = false; + continue; + } + + const attrMatch = line.match(/^\s*([A-Za-z_$][\w:$-]*)\s*=\s*(.*)$/); + if (attrMatch) { + const equalsIndex = line.indexOf('='); + let startColumn = equalsIndex + 1; + while (startColumn < line.length && /\s/u.test(line[startColumn])) startColumn += 1; + const site = collectRawEmbeddedExpressionSite(lines, starts, lineIndex, startColumn); + recordEmbeddedJsLintSite('expression', site.text, site.sourceStart); + lineIndex = site.endLineIndex; + if (lines[lineIndex]?.trim().startsWith(')')) inAttrList = false; + continue; + } + + continue; + } + + if (looksLikeTagAttrListStart(trimmed)) { + inAttrList = true; + continue; + } + + const statementMatch = line.match(/^\s*-\s+(.*)$/); + if (statementMatch) { + const startColumn = line.indexOf('-') + 2; + const site = collectRawEmbeddedExpressionSite(lines, starts, lineIndex, startColumn); + recordEmbeddedJsLintSite('statement', site.text, site.sourceStart); + lineIndex = site.endLineIndex; + continue; + } + + const bufferedMatch = line.match(/^\s*([A-Za-z_$][\w$]*(?:[.:][A-Za-z_$][\w$-]*)*(?:[.#][A-Za-z_$][\w-]*)*)\s*=\s*(.*)$/); + if (bufferedMatch) { + const equalsIndex = line.indexOf('='); + let startColumn = equalsIndex + 1; + while (startColumn < line.length && /\s/u.test(line[startColumn])) startColumn += 1; + const site = collectRawEmbeddedExpressionSite(lines, starts, lineIndex, startColumn); + recordEmbeddedJsLintSite('expression', site.text, site.sourceStart); + lineIndex = site.endLineIndex; + } + } +} + function countIndent(line: string): number { return line.match(/^(\s*)/)?.[1].length ?? 0; } @@ -865,7 +1234,11 @@ function emitExpressionWithTemplateInterpolations( expressionOffset: number, emitter: TsxEmitter, info: CodeInformation = FULL_FEATURES, + lintSiteKind: EmbeddedJsLintSiteKind | null = 'expression', ): void { + if (lintSiteKind) { + recordEmbeddedJsLintSite(lintSiteKind, expression, expressionOffset); + } const context = currentInterpolationContext(); let cursor = 0; @@ -996,7 +1369,13 @@ function emitAttributeValueAsExpression( } const attrOffset = lineColToOffset(pugText, attr.line, attr.column); - const valOffset = attrOffset + attr.name.length + 1; + const valOffset = findValueOffsetOnLine( + pugText, + attr.line, + attr.column, + attr.val, + attrOffset + attr.name.length + 1, + ); const val = attr.val; if (getStaticStringLiteralValue(val) != null) { @@ -1233,17 +1612,21 @@ function emitAttribute( // Value attribute: onClick=handler -> onClick={handler} if (typeof attr.val === 'string') { const val = attr.val; + const valOffset = findValueOffsetOnLine( + pugText, + attr.line, + attr.column, + val, + attrOffset + attr.name.length + 1, + ); // JSX string literal attribute: label="Hello" if (getStaticStringLiteralValue(val) != null) { emitter.emitSynthetic('='); - // Find the value offset: after "name=" in the source - const valOffset = attrOffset + attr.name.length + 1; // +1 for '=' emitter.emitMapped(val, valOffset, FULL_FEATURES); } else { // Expression value emitter.emitSynthetic('={'); - const valOffset = attrOffset + attr.name.length + 1; emitExpressionWithTemplateInterpolations(val, valOffset, emitter, FULL_FEATURES); emitter.emitSynthetic('}'); } @@ -1283,6 +1666,7 @@ function emitText( if (interpolation.expression.trim().length === 0) { emitter.emitSynthetic('undefined'); } else { + recordEmbeddedJsLintSite('expression', interpolation.expression, interpolation.exprStart); emitJsExpressionWithNestedPug(interpolation.expression, interpolation.exprStart, emitter); } emitter.emitSynthetic('}'); @@ -1318,7 +1702,7 @@ function emitCode( } else { // Unbuffered code block: - const x = 10 // Emitted as a statement; IIFE wrapping is handled by emitNodesWithCodeBlocks - emitExpressionWithTemplateInterpolations(node.val, valueOffset, emitter, FULL_FEATURES); + emitExpressionWithTemplateInterpolations(node.val, valueOffset, emitter, FULL_FEATURES, 'statement'); emitter.emitSynthetic(';'); } } @@ -1781,6 +2165,7 @@ export interface CompileResult { parseError: PugParseError | null; styleBlock: ExtractedStyleBlock | null; transformError: PugTransformError | null; + embeddedJsLintSites: EmbeddedJsLintSite[]; } export type CompileMode = 'languageService' | 'runtime'; @@ -1816,7 +2201,9 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): const extractedStyle = extractTerminalStyleBlock(pugText); const prepared = prepareTemplateInterpolations(extractedStyle.pugTextWithoutStyle); const pugTextForParse = prepared.sanitizedText; + const embeddedJsLintSites: EmbeddedJsLintSite[] = []; interpolationContextStack.push(prepared.context); + embeddedJsLintSiteStack.push(embeddedJsLintSites); compileContextStack.push({ mode, classAttribute, @@ -1833,6 +2220,7 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): parseError: null, styleBlock: null, transformError: extractedStyle.transformError, + embeddedJsLintSites, }; } @@ -1853,6 +2241,7 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): try { tokens = lex(recoveredText, { filename: 'template.pug' }); } catch { + extractEmbeddedJsLintSitesFromRawPugText(pugTextForParse); return { tsx: fallbackNullExpression(mode), mappings: [], @@ -1860,9 +2249,11 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): parseError, styleBlock: extractedStyle.styleBlock, transformError: null, + embeddedJsLintSites, }; } } else { + extractEmbeddedJsLintSitesFromRawPugText(pugTextForParse); return { tsx: fallbackNullExpression(mode), mappings: [], @@ -1870,6 +2261,7 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): parseError, styleBlock: extractedStyle.styleBlock, transformError: null, + embeddedJsLintSites, }; } } @@ -1909,6 +2301,7 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): } if (!ast) { + extractEmbeddedJsLintSitesFromRawPugText(pugTextForParse); return { tsx: fallbackNullExpression(mode), mappings: [], @@ -1916,6 +2309,7 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): parseError, styleBlock: extractedStyle.styleBlock, transformError: null, + embeddedJsLintSites, }; } } @@ -1938,9 +2332,11 @@ export function compilePugToTsx(pugText: string, options: CompileOptions = {}): parseError, styleBlock: extractedStyle.styleBlock, transformError: null, + embeddedJsLintSites, }; } finally { compileContextStack.pop(); + embeddedJsLintSiteStack.pop(); interpolationContextStack.pop(); } } diff --git a/packages/react-pug-core/src/language/regionOffsetMapping.ts b/packages/react-pug-core/src/language/regionOffsetMapping.ts index bc00a63..c90b8ea 100644 --- a/packages/react-pug-core/src/language/regionOffsetMapping.ts +++ b/packages/react-pug-core/src/language/regionOffsetMapping.ts @@ -7,13 +7,13 @@ export function rawToStrippedOffset(rawText: string, rawOffset: number, commonIn const lines = rawText.split('\n'); for (const line of lines) { const lineEnd = raw + line.length; + const actualIndent = line.match(/^[ \t]*/)?.[0].length ?? 0; + const indentToRemove = line.trim().length === 0 ? line.length : Math.min(commonIndent, actualIndent); if (rawOffset <= lineEnd) { const colInRaw = rawOffset - raw; - const indentToRemove = line.trim().length === 0 ? line.length : commonIndent; if (indentToRemove > 0 && colInRaw < indentToRemove) return null; return stripped + Math.max(0, colInRaw - indentToRemove); } - const indentToRemove = line.trim().length === 0 ? line.length : commonIndent; stripped += Math.max(0, line.length - indentToRemove) + 1; raw = lineEnd + 1; } @@ -26,7 +26,8 @@ export function strippedToRawOffset(rawText: string, strippedOffset: number, com let raw = 0; const lines = rawText.split('\n'); for (const line of lines) { - const indentToRemove = line.trim().length === 0 ? line.length : commonIndent; + const actualIndent = line.match(/^[ \t]*/)?.[0].length ?? 0; + const indentToRemove = line.trim().length === 0 ? line.length : Math.min(commonIndent, actualIndent); const strippedLineLength = Math.max(0, line.length - indentToRemove); if (strippedOffset <= stripped + strippedLineLength) { return raw + indentToRemove + (strippedOffset - stripped); diff --git a/packages/react-pug-core/src/language/shadowDocument.ts b/packages/react-pug-core/src/language/shadowDocument.ts index 8c3cd38..41e5344 100644 --- a/packages/react-pug-core/src/language/shadowDocument.ts +++ b/packages/react-pug-core/src/language/shadowDocument.ts @@ -387,6 +387,7 @@ export function buildShadowDocument( region.parseError = compiled.parseError; region.transformError = compiled.transformError; region.styleBlock = compiled.styleBlock; + region.embeddedJsLintSites = compiled.embeddedJsLintSites; if (region.styleBlock && region.transformError == null) { if (!analysis.tagImportSource) { diff --git a/packages/react-pug-core/test/unit/lintTransform.test.ts b/packages/react-pug-core/test/unit/lintTransform.test.ts index 2ba68ea..f999265 100644 --- a/packages/react-pug-core/test/unit/lintTransform.test.ts +++ b/packages/react-pug-core/test/unit/lintTransform.test.ts @@ -256,4 +256,56 @@ describe('lintTransform', () => { expect(styleCallText).toContain('styl`') expect(styleCallText).toContain('.root') }) + + it('maps embedded JS lint sites back to original file offsets', () => { + const source = [ + "import { pug } from 'startupjs'", + 'const view = pug`', + ' Button(', + " label='Show' + title + ' today'", + ' onClick=() => run(item.id)', + ' ) Save', + '`', + ].join('\n') + + const result = createLintTransform(source, 'file.jsx') + expect(result.embeddedJsLintSites.map(site => source.slice(site.originalStart, site.originalEnd).trimEnd())).toEqual([ + "'Show' + title + ' today'", + '() => run(item.id)', + ]) + }) + + it('maps the embedded JS site matrix back to exact original source starts in order', () => { + const source = [ + "import { pug } from 'startupjs'", + 'export default pug`', + ' Button(', + ' label=knownAttr + missingAttrValue', + ' onClick=() => {', + ' return missingHandlerValue', + ' }', + ' ) Save', + ' p= knownBuffered + missingBufferedValue', + ' p Hello #{knownInterpolation + missingInterpolationValue}', + ' Span.text= ${knownTemplate + missingTemplateValue}', + ' - const local = knownStatement + missingStatementValue', + '`', + ].join('\n') + + const result = createLintTransform(source, 'file.jsx') + const expectedSites = [ + { kind: 'expression', source: 'knownAttr + missingAttrValue' }, + { kind: 'expression', source: '() => {\n return missingHandlerValue\n }' }, + { kind: 'expression', source: 'knownBuffered + missingBufferedValue' }, + { kind: 'expression', source: 'knownInterpolation + missingInterpolationValue' }, + { kind: 'expression', source: 'knownTemplate + missingTemplateValue' }, + { kind: 'statement', source: 'const local = knownStatement + missingStatementValue' }, + ] + const actualSites = [...result.embeddedJsLintSites] + .sort((a, b) => a.originalStart - b.originalStart) + expect(actualSites.map(site => site.kind)).toEqual(expectedSites.map(site => site.kind)) + expectedSites.forEach((expectedSite, index) => { + expect(source.startsWith(expectedSite.source, actualSites[index].originalStart)).toBe(true) + }) + }) }) diff --git a/packages/react-pug-core/test/unit/pugToTsx.test.ts b/packages/react-pug-core/test/unit/pugToTsx.test.ts index 25e9eb2..6523552 100644 --- a/packages/react-pug-core/test/unit/pugToTsx.test.ts +++ b/packages/react-pug-core/test/unit/pugToTsx.test.ts @@ -84,29 +84,23 @@ describe('TsxEmitter', () => { describe('tag compilation', () => { it('compiles bare tag: div ->
', () => { const result = compilePugToTsx('div'); - expect(result.tsx).toContain(''); + expect(result.tsx).toMatchInlineSnapshot(`"(
)"`); expect(result.parseError).toBeNull(); }); it('compiles component tag: Button -> )} @@ -82,8 +86,8 @@ function renderExpired () { Cat profile link is incorrect or already expired. Your cat meetup - profile link is only valid for a limited period of time. If you believe - this is an error, please contact the cat meetup organizer. + profile link is only valid for a limited period of time. If you + believe this is an error, please contact the cat meetup organizer. diff --git a/test/fixtures/real-project/snapshots/eslint/cat-profile-link.tsx.output.jsx b/test/fixtures/real-project/snapshots/eslint/cat-profile-link.tsx.output.jsx index 11cbe12..c6dcc7f 100644 --- a/test/fixtures/real-project/snapshots/eslint/cat-profile-link.tsx.output.jsx +++ b/test/fixtures/real-project/snapshots/eslint/cat-profile-link.tsx.output.jsx @@ -60,7 +60,11 @@ const Profile = observer(({ $cat, $event }) => {
) : ( - )} @@ -82,8 +86,8 @@ function renderExpired () { Cat profile link is incorrect or already expired. Your cat meetup - profile link is only valid for a limited period of time. If you believe - this is an error, please contact the cat meetup organizer. + profile link is only valid for a limited period of time. If you + believe this is an error, please contact the cat meetup organizer. diff --git a/test/fixtures/real-project/snapshots/shadow/CatCard.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/CatCard.js.output.sourcemap.json index 6970ef4..905ed35 100644 --- a/test/fixtures/real-project/snapshots/shadow/CatCard.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/CatCard.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport { Div, Span, Avatar, Link } from 'startupjs-ui'\n\nexport default observer(({ $cat, showPhone, large, small }) => {\n const { name, number, phone, catgram, photoFileId, phonegram } = $cat.get()\n return pug`\n Div(part='root' row vAlign='center')\n if photoFileId\n Photo.avatar(styleName={ large, small } fileId=photoFileId name=name)\n else\n Avatar.avatar(styleName={ large, small })= name\n Div(row)\n Span.text(bold styleName={ large })= (number || 'X') + '. '\n Div\n Span.text(styleName={ large })= name\n if showPhone\n if phone\n Span.text(styleName={ large })\n Span(bold) Phone:#{' '}\n = phone\n if catgram\n Span.text(styleName={ large })\n Span(bold) Catgram:#{' '}\n Link.text(styleName={ large } to=getCatgramLink(catgram))= catgram\n if phonegram\n Span.text(styleName={ large })\n Span(bold) Phonegram:#{' '}\n Link.text(styleName={ large } to=getPhonegramLink(phonegram))= phonegram\n style(lang='styl')\n .avatar\n margin-right 1u\n &.large\n width 12u\n height @width\n &.small\n width 4u\n height @width\n .text.large\n font(h6)\n `\n})\n\nconst Photo = observer(({ fileId, name }) => {\n const $file = useSub($.files[fileId])\n let url\n try { url = $file.getUrl() } catch (err) {}\n return pug`\n Avatar(part='root' src=url)= name\n `\n})\n\nfunction getCatgramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://catgr.am/' + username\n}\n\nfunction getPhonegramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://www.phonegram.com/' + username\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAK,YAEhE,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAA,YAE3D,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/CatCard.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/CatCard.tsx.output.sourcemap.json index fd7dc9f..f4190df 100644 --- a/test/fixtures/real-project/snapshots/shadow/CatCard.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/CatCard.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport { Div, Span, Avatar, Link } from 'startupjs-ui'\n\nexport default observer(({ $cat, showPhone, large, small }) => {\n const { name, number, phone, catgram, photoFileId, phonegram } = $cat.get()\n return pug`\n Div(part='root' row vAlign='center')\n if photoFileId\n Photo.avatar(styleName={ large, small } fileId=photoFileId name=name)\n else\n Avatar.avatar(styleName={ large, small })= name\n Div(row)\n Span.text(bold styleName={ large })= (number || 'X') + '. '\n Div\n Span.text(styleName={ large })= name\n if showPhone\n if phone\n Span.text(styleName={ large })\n Span(bold) Phone:#{' '}\n = phone\n if catgram\n Span.text(styleName={ large })\n Span(bold) Catgram:#{' '}\n Link.text(styleName={ large } to=getCatgramLink(catgram))= catgram\n if phonegram\n Span.text(styleName={ large })\n Span(bold) Phonegram:#{' '}\n Link.text(styleName={ large } to=getPhonegramLink(phonegram))= phonegram\n style(lang='styl')\n .avatar\n margin-right 1u\n &.large\n width 12u\n height @width\n &.small\n width 4u\n height @width\n .text.large\n font(h6)\n `\n})\n\nconst Photo = observer(({ fileId, name }) => {\n const $file = useSub($.files[fileId])\n let url\n try { url = $file.getUrl() } catch (err) {}\n return pug`\n Avatar(part='root' src=url)= name\n `\n})\n\nfunction getCatgramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://catgr.am/' + username\n}\n\nfunction getPhonegramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://www.phonegram.com/' + username\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAK,YAEhE,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAA,YAE3D,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/cat-profile-link.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/cat-profile-link.js.output.sourcemap.json index d014b54..e192b38 100644 --- a/test/fixtures/real-project/snapshots/shadow/cat-profile-link.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/cat-profile-link.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { Alert, Span, Modal, Content, Button, Form, Div, Tag, useMedia, useFormFields } from 'startupjs-ui'\nimport { useGlobalSearchParams, Stack } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport CatCard from '@/components/CatCard'\nimport * as stages from '@/components/stages'\nimport { CAT_PROFILE_EDIT_FORM } from '@/model/cats/schema'\nimport { STAGES } from '@/model/events/schema'\n\nexport default observer(() => {\n const { token } = useGlobalSearchParams()\n const [$cat] = useSub($.cats, { token })\n if (!$cat) return renderExpired()\n\n const eventId = $cat.eventId.get()\n const $event = useSub($.events[eventId])\n\n function renderTitle () {\n return pug`\n CatCard($cat=$cat)\n `\n }\n\n function renderSettings () {\n return pug`\n Profile($cat=$cat $event=$event)\n `\n }\n\n const Stage = stages[$cat.getMyStage()]\n\n return pug`\n Stack.Screen(\n options={\n headerTitle: renderTitle,\n headerRight: renderSettings\n }\n )\n Stage($cat=$cat $event=$event)\n `\n})\n\nconst Profile = observer(({ $cat, $event }) => {\n const $showEdit = $()\n const { tablet } = useMedia()\n const excludeNumber = $event.stage.get() !== STAGES.InProgress\n const profileEditFields = useFormFields(CAT_PROFILE_EDIT_FORM, excludeNumber ? { exclude: ['number'] } : {})\n\n return pug`\n Div(row vAlign='center' gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n if $cat.getMyStage() === STAGES.Profile\n Div.hackSidePadding\n else\n Button(\n variant='text'\n icon=faPen\n onPress=() => $showEdit.set(true)\n )\n if tablet\n = 'Edit cat profile'\n else\n = 'Edit'\n Modal(\n title='Edit cat profile'\n $visible=$showEdit\n )\n Form(\n fields=profileEditFields\n $value=$cat\n )\n style(lang='styl')\n .hackSidePadding\n width 1u\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nfunction renderExpired () {\n return pug`\n Content(padding)\n Alert(variant='error')\n Span\n | Cat profile link is incorrect or already expired.\n |\n | Your cat meetup profile link is only valid for a limited period of time.\n |\n | If you believe this is an error, please contact the cat meetup organizer.\n `\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAa,SACd;AACL;AACA;AACA;AACA,aACM,cAAa,MAAK,QAAO,WAC1B;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAW,MAAK,QAAO,cACxB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAQ,SACT;AACL;AACA;AACA;AACA,aACM,cAAQ,MAAU,QAAA,WACnB;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAM,MAAU,QAAA,cACjB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/cat-profile-link.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/cat-profile-link.tsx.output.sourcemap.json index 2603a48..d7d36d9 100644 --- a/test/fixtures/real-project/snapshots/shadow/cat-profile-link.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/cat-profile-link.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { Alert, Span, Modal, Content, Button, Form, Div, Tag, useMedia, useFormFields } from 'startupjs-ui'\nimport { useGlobalSearchParams, Stack } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport CatCard from '@/components/CatCard'\nimport * as stages from '@/components/stages'\nimport { CAT_PROFILE_EDIT_FORM } from '@/model/cats/schema'\nimport { STAGES } from '@/model/events/schema'\n\nexport default observer(() => {\n const { token } = useGlobalSearchParams()\n const [$cat] = useSub($.cats, { token })\n if (!$cat) return renderExpired()\n\n const eventId = $cat.eventId.get()\n const $event = useSub($.events[eventId])\n\n function renderTitle () {\n return pug`\n CatCard($cat=$cat)\n `\n }\n\n function renderSettings () {\n return pug`\n Profile($cat=$cat $event=$event)\n `\n }\n\n const Stage = stages[$cat.getMyStage()]\n\n return pug`\n Stack.Screen(\n options={\n headerTitle: renderTitle,\n headerRight: renderSettings\n }\n )\n Stage($cat=$cat $event=$event)\n `\n})\n\nconst Profile = observer(({ $cat, $event }) => {\n const $showEdit = $()\n const { tablet } = useMedia()\n const excludeNumber = $event.stage.get() !== STAGES.InProgress\n const profileEditFields = useFormFields(CAT_PROFILE_EDIT_FORM, excludeNumber ? { exclude: ['number'] } : {})\n\n return pug`\n Div(row vAlign='center' gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n if $cat.getMyStage() === STAGES.Profile\n Div.hackSidePadding\n else\n Button(\n variant='text'\n icon=faPen\n onPress=() => $showEdit.set(true)\n )\n if tablet\n = 'Edit cat profile'\n else\n = 'Edit'\n Modal(\n title='Edit cat profile'\n $visible=$showEdit\n )\n Form(\n fields=profileEditFields\n $value=$cat\n )\n style(lang='styl')\n .hackSidePadding\n width 1u\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nfunction renderExpired () {\n return pug`\n Content(padding)\n Alert(variant='error')\n Span\n | Cat profile link is incorrect or already expired.\n |\n | Your cat meetup profile link is only valid for a limited period of time.\n |\n | If you believe this is an error, please contact the cat meetup organizer.\n `\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAa,SACd;AACL;AACA;AACA;AACA,aACM,cAAa,MAAK,QAAO,WAC1B;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAW,MAAK,QAAO,cACxB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAQ,SACT;AACL;AACA;AACA;AACA,aACM,cAAQ,MAAU,QAAA,WACnB;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAM,MAAU,QAAA,cACjB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.js.output.sourcemap.json index ee09fba..9e26176 100644 --- a/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React, { useState } from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport {\n Link, Item, ScrollView, Form, useFormProps, Alert,\n Content, Tag, Br, Button, Modal, Div, confirm,\n useFormFields$, useValidate\n} from 'startupjs-ui'\nimport { useGlobalSearchParams } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faLink } from '@fortawesome/free-solid-svg-icons/faLink'\nimport CatCard from '@/components/CatCard'\nimport { CAT_FORM } from '@/model/cats/schema'\n\nexport default observer(({ breed }) => {\n const { eventId } = useGlobalSearchParams()\n const [$selected, set$selected] = useState()\n const $new = $()\n const $showModal = $()\n const $mode = $()\n const $fields = useFormFields$(CAT_FORM)\n const validate = useValidate()\n\n function showCreate () {\n $new.set({ breed })\n $fields.breed.disabled.set(true)\n set$selected(() => $new)\n $mode.set('new')\n $showModal.set(true)\n }\n\n function showEdit ($cat) {\n $fields.breed.disabled.del()\n set$selected(() => $cat)\n $mode.set('edit')\n $showModal.set(true)\n }\n\n function cancel () {\n if (!$showModal.get()) return\n $showModal.del()\n $mode.del()\n }\n\n async function create () {\n if (!validate()) return\n await $.cats.addNew({\n ...$new.getDeepCopy(),\n eventId\n })\n cancel()\n }\n\n async function deleteCat () {\n if (!await confirm(`Are you sure you want to delete ${$selected.name.get()}?`)) return\n await $selected.del()\n cancel()\n }\n\n return pug`\n ScrollView(full)\n Content(full pure)\n CatsList(eventId=eventId onEdit=showEdit breed=breed)\n Content(padding=1)\n Button(onPress=showCreate) Add new #{breed}\n Modal(\n title=$mode.get() === 'new' ? 'Create cat' : 'Edit cat'\n $visible=$showModal\n onDismiss=cancel\n )\n - const oppositeBreed = $selected?.breed.get() && ($selected.breed.get() === 'domestic' ? 'wild' : 'domestic')\n Form(\n key=$selected?.getId() || 'NEW'\n $fields=$fields\n $value=$selected\n oppositeBreed=oppositeBreed\n eventId=eventId\n customInputs={\n likes: SelectLikesInput\n }\n validate=validate\n )\n Br\n if $mode.get() === 'new'\n Div(align='right' row)\n Button(onPress=cancel) Cancel\n Button(disabled=validate.hasErrors pushed variant='flat' color='primary' onPress=create) Create\n else if $mode.get() === 'edit'\n Div(align='right' row)\n Button(color='error' onPress=deleteCat) Delete\n `\n})\n\nconst CatsList = observer(({ onEdit, breed, eventId }) => {\n if (!eventId) return pug`Alert(variant='error') No event specified`\n const $cats = useSub($.cats, { eventId, breed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n Item(key=$cat.getId())\n CatCard($cat=$cat)\n Item.Right\n Div(vAlign='center' row gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n Button(variant='text' icon=faPen onPress=() => onEdit($cat) tooltip='Edit')\n Link(href='/events/' + eventId + '/matches/' + $cat.getId())\n Button(variant='text' icon=faHeart tooltip='Matches')\n Link(href='/cats/' + $cat.token.get())\n Button(variant='text' icon=faLink tooltip='Cat profile link') Link\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nconst SelectLikesInput = observer(({ $value, ...props }) => {\n const { oppositeBreed, eventId } = { ...useFormProps(), ...props }\n return pug`\n if oppositeBreed\n SelectLikes(\n $likes=$value\n oppositeBreed=oppositeBreed\n eventId=eventId\n )\n else\n Alert(variant='warning') Select breed to choose likes\n `\n})\n\nconst SelectLikes = observer(({ $likes, oppositeBreed, eventId }) => {\n const $cats = useSub($.cats, { eventId, breed: oppositeBreed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n - const catId = $cat.getId()\n Item.item(\n key=catId\n styleName={ selected: $likes[catId].get() }\n onPress=() => $likes[catId].get() ? $likes[catId].del() : $likes[catId].set(true)\n )\n CatCard($cat=$cat small)\n else\n Alert(variant='info') No cats with selected breed yet\n style(lang='styl')\n .item\n border-radius 1u\n &.selected\n // FIXME: We can't use color var(--color-text-success-strong) here\n background-color var(--color-text-success-strong)\n `\n})\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAiB,SAAQ,QAAO,UAAS,OAAM,iCACnD,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAQ,SACR,QAAO,WACP,eAAc,eACd,SAAQ,SACR,cAAa;AACjB;AACA,OACI,UAAS,aAEX,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,uEACS,QAAQ,8BACX,UAAS,eACP,cAAa,SACb,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAc,eACd,SAAQ,eAGV,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,uEACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAa,MAAK,0EAEpB,qBAAsB,6CAOzB;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAS,SAAgB,QAAO,UAAS,OAAA,iCAC7C,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAA,SACA,QAAO,WACP,eAAA,eACA,SAAA,SACA,cAAa;AACjB;AACA,OACI,UAAA,aAEF,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,uEACS,QAAQ,8BACX,UAAS,eACP,cAAQ,SACR,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAA,eACA,SAAA,eAGF,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,uEACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAQ,MAAU,0EAEpB,qBAAsB,6CAOzB;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.tsx.output.sourcemap.json index 529de6b..bf70d21 100644 --- a/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/event-tabs-breed.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React, { useState } from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport {\n Link, Item, ScrollView, Form, useFormProps, Alert,\n Content, Tag, Br, Button, Modal, Div, confirm,\n useFormFields$, useValidate\n} from 'startupjs-ui'\nimport { useGlobalSearchParams } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faLink } from '@fortawesome/free-solid-svg-icons/faLink'\nimport CatCard from '@/components/CatCard'\nimport { CAT_FORM } from '@/model/cats/schema'\n\nexport default observer(({ breed }) => {\n const { eventId } = useGlobalSearchParams()\n const [$selected, set$selected] = useState()\n const $new = $()\n const $showModal = $()\n const $mode = $()\n const $fields = useFormFields$(CAT_FORM)\n const validate = useValidate()\n\n function showCreate () {\n $new.set({ breed })\n $fields.breed.disabled.set(true)\n set$selected(() => $new)\n $mode.set('new')\n $showModal.set(true)\n }\n\n function showEdit ($cat) {\n $fields.breed.disabled.del()\n set$selected(() => $cat)\n $mode.set('edit')\n $showModal.set(true)\n }\n\n function cancel () {\n if (!$showModal.get()) return\n $showModal.del()\n $mode.del()\n }\n\n async function create () {\n if (!validate()) return\n await $.cats.addNew({\n ...$new.getDeepCopy(),\n eventId\n })\n cancel()\n }\n\n async function deleteCat () {\n if (!await confirm(`Are you sure you want to delete ${$selected.name.get()}?`)) return\n await $selected.del()\n cancel()\n }\n\n return pug`\n ScrollView(full)\n Content(full pure)\n CatsList(eventId=eventId onEdit=showEdit breed=breed)\n Content(padding=1)\n Button(onPress=showCreate) Add new #{breed}\n Modal(\n title=$mode.get() === 'new' ? 'Create cat' : 'Edit cat'\n $visible=$showModal\n onDismiss=cancel\n )\n - const oppositeBreed = $selected?.breed.get() && ($selected.breed.get() === 'domestic' ? 'wild' : 'domestic')\n Form(\n key=$selected?.getId() || 'NEW'\n $fields=$fields\n $value=$selected\n oppositeBreed=oppositeBreed\n eventId=eventId\n customInputs={\n likes: SelectLikesInput\n }\n validate=validate\n )\n Br\n if $mode.get() === 'new'\n Div(align='right' row)\n Button(onPress=cancel) Cancel\n Button(disabled=validate.hasErrors pushed variant='flat' color='primary' onPress=create) Create\n else if $mode.get() === 'edit'\n Div(align='right' row)\n Button(color='error' onPress=deleteCat) Delete\n `\n})\n\nconst CatsList = observer(({ onEdit, breed, eventId }) => {\n if (!eventId) return pug`Alert(variant='error') No event specified`\n const $cats = useSub($.cats, { eventId, breed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n Item(key=$cat.getId())\n CatCard($cat=$cat)\n Item.Right\n Div(vAlign='center' row gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n Button(variant='text' icon=faPen onPress=() => onEdit($cat) tooltip='Edit')\n Link(href='/events/' + eventId + '/matches/' + $cat.getId())\n Button(variant='text' icon=faHeart tooltip='Matches')\n Link(href='/cats/' + $cat.token.get())\n Button(variant='text' icon=faLink tooltip='Cat profile link') Link\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nconst SelectLikesInput = observer(({ $value, ...props }) => {\n const { oppositeBreed, eventId } = { ...useFormProps(), ...props }\n return pug`\n if oppositeBreed\n SelectLikes(\n $likes=$value\n oppositeBreed=oppositeBreed\n eventId=eventId\n )\n else\n Alert(variant='warning') Select breed to choose likes\n `\n})\n\nconst SelectLikes = observer(({ $likes, oppositeBreed, eventId }) => {\n const $cats = useSub($.cats, { eventId, breed: oppositeBreed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n - const catId = $cat.getId()\n Item.item(\n key=catId\n styleName={ selected: $likes[catId].get() }\n onPress=() => $likes[catId].get() ? $likes[catId].del() : $likes[catId].set(true)\n )\n CatCard($cat=$cat small)\n else\n Alert(variant='info') No cats with selected breed yet\n style(lang='styl')\n .item\n border-radius 1u\n &.selected\n // FIXME: We can't use color var(--color-text-success-strong) here\n background-color var(--color-text-success-strong)\n `\n})\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAiB,SAAQ,QAAO,UAAS,OAAM,iCACnD,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAQ,SACR,QAAO,WACP,eAAc,eACd,SAAQ,SACR,cAAa;AACjB;AACA,OACI,UAAS,aAEX,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,uEACS,QAAQ,8BACX,UAAS,eACP,cAAa,SACb,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAc,eACd,SAAQ,eAGV,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,uEACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAa,MAAK,0EAEpB,qBAAsB,6CAOzB;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAS,SAAgB,QAAO,UAAS,OAAA,iCAC7C,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAA,SACA,QAAO,WACP,eAAA,eACA,SAAA,SACA,cAAa;AACjB;AACA,OACI,UAAA,aAEF,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,uEACS,QAAQ,8BACX,UAAS,eACP,cAAQ,SACR,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAA,eACA,SAAA,eAGF,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,uEACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAQ,MAAU,0EAEpB,qBAAsB,6CAOzB;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.js.output.sourcemap.json index 8a020cb..16689a4 100644 --- a/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "// import { Platform } from 'react-native'\nimport React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { useColors, Icon, Form, Modal, Button } from 'startupjs-ui'\nimport { Tabs, useLocalSearchParams, Stack } from 'expo-router'\nimport { faVenus as faWildBadge } from '@fortawesome/free-solid-svg-icons/faVenus'\nimport { faMars as faDomesticBadge } from '@fortawesome/free-solid-svg-icons/faMars'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faToolbox } from '@fortawesome/free-solid-svg-icons/faToolbox'\nimport { EVENT_FORM } from '@/model/events/schema'\n\nexport default observer(function TabLayout () {\n const getColor = useColors()\n const { eventId } = useLocalSearchParams()\n const $event = useSub($.events[eventId])\n if (!$event.get()) throw Error('No such event')\n\n // NOTE:\n // headerShown -- Disable the static render of the header on web to prevent a hydration error in React Navigation v6.\n // tabBarStyle/order - Move the tab bar to the top on tablet+\n return pug`\n Stack.Screen(\n options={\n title: $event.name.get(),\n headerRight: () => renderEditEvent({ $event })\n }\n )\n Tabs(\n title=$event.name.get()\n screenOptions={\n ...styl('screen'),\n tabBarActiveTintColor: getColor('primary'),\n tabBarActiveBackgroundColor: 'rgba(255, 255, 255, 0.5)',\n headerShown: false,\n headerTitle: $event.name.get()\n }\n )\n Tabs.Screen(\n name='index'\n options={\n title: 'Dashboard',\n tabBarIcon: renderHomeIcon\n }\n )\n Tabs.Screen(\n name='-breed'\n options={\n href: null\n }\n )\n Tabs.Screen(\n name='domestic'\n options={\n title: 'Domestic Cats',\n tabBarIcon: renderDomesticIcon\n }\n )\n Tabs.Screen(\n name='wild'\n options={\n title: 'Wild Cats',\n tabBarIcon: renderWildIcon\n }\n )\n Tabs.Screen(\n name='test'\n options={\n title: 'Dev Only',\n tabBarIcon: renderTestIcon\n }\n )\n style(lang='styl')\n +tablet()\n .screen\n &:part(tabBar)\n order -1\n background-color transparent\n border-bottom-width 1px\n border-bottom-color rgba(0, 0, 0, 0.1)\n `\n})\n\nfunction renderEditEvent ({ $event }) {\n return pug`\n EditEvent($event=$event)\n `\n}\n\nconst EditEvent = observer(({ $event }) => {\n const $showModal = $()\n return pug`\n Button(onPress=() => $showModal.set(true) variant='text' icon=faPen) Edit this cat meetup\n Modal(\n title='Edit cat meetup'\n $visible=$showModal\n )\n Form(\n $value=$event\n fields=EVENT_FORM\n )\n `\n})\n\nfunction renderWildIcon ({ color, size }) {\n return pug`\n Icon(icon=faWildBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderDomesticIcon ({ color, size }) {\n return pug`\n Icon(icon=faDomesticBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderHomeIcon ({ color, size }) {\n return pug`\n Icon(icon=faHeart style={ color, width: size, height: size })\n `\n}\n\nfunction renderTestIcon ({ color, size }) {\n return pug`\n Icon(icon=faToolbox style={ color, width: size, height: size })\n `\n}\n" ], - "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAiB,WAClB;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", + "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAU,WACX;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.tsx.output.sourcemap.json index 8dbf3d1..1453c9e 100644 --- a/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/shadow/event-tabs-layout.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "// import { Platform } from 'react-native'\nimport React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { useColors, Icon, Form, Modal, Button } from 'startupjs-ui'\nimport { Tabs, useLocalSearchParams, Stack } from 'expo-router'\nimport { faVenus as faWildBadge } from '@fortawesome/free-solid-svg-icons/faVenus'\nimport { faMars as faDomesticBadge } from '@fortawesome/free-solid-svg-icons/faMars'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faToolbox } from '@fortawesome/free-solid-svg-icons/faToolbox'\nimport { EVENT_FORM } from '@/model/events/schema'\n\nexport default observer(function TabLayout () {\n const getColor = useColors()\n const { eventId } = useLocalSearchParams()\n const $event = useSub($.events[eventId])\n if (!$event.get()) throw Error('No such event')\n\n // NOTE:\n // headerShown -- Disable the static render of the header on web to prevent a hydration error in React Navigation v6.\n // tabBarStyle/order - Move the tab bar to the top on tablet+\n return pug`\n Stack.Screen(\n options={\n title: $event.name.get(),\n headerRight: () => renderEditEvent({ $event })\n }\n )\n Tabs(\n title=$event.name.get()\n screenOptions={\n ...styl('screen'),\n tabBarActiveTintColor: getColor('primary'),\n tabBarActiveBackgroundColor: 'rgba(255, 255, 255, 0.5)',\n headerShown: false,\n headerTitle: $event.name.get()\n }\n )\n Tabs.Screen(\n name='index'\n options={\n title: 'Dashboard',\n tabBarIcon: renderHomeIcon\n }\n )\n Tabs.Screen(\n name='-breed'\n options={\n href: null\n }\n )\n Tabs.Screen(\n name='domestic'\n options={\n title: 'Domestic Cats',\n tabBarIcon: renderDomesticIcon\n }\n )\n Tabs.Screen(\n name='wild'\n options={\n title: 'Wild Cats',\n tabBarIcon: renderWildIcon\n }\n )\n Tabs.Screen(\n name='test'\n options={\n title: 'Dev Only',\n tabBarIcon: renderTestIcon\n }\n )\n style(lang='styl')\n +tablet()\n .screen\n &:part(tabBar)\n order -1\n background-color transparent\n border-bottom-width 1px\n border-bottom-color rgba(0, 0, 0, 0.1)\n `\n})\n\nfunction renderEditEvent ({ $event }) {\n return pug`\n EditEvent($event=$event)\n `\n}\n\nconst EditEvent = observer(({ $event }) => {\n const $showModal = $()\n return pug`\n Button(onPress=() => $showModal.set(true) variant='text' icon=faPen) Edit this cat meetup\n Modal(\n title='Edit cat meetup'\n $visible=$showModal\n )\n Form(\n $value=$event\n fields=EVENT_FORM\n )\n `\n})\n\nfunction renderWildIcon ({ color, size }) {\n return pug`\n Icon(icon=faWildBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderDomesticIcon ({ color, size }) {\n return pug`\n Icon(icon=faDomesticBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderHomeIcon ({ color, size }) {\n return pug`\n Icon(icon=faHeart style={ color, width: size, height: size })\n `\n}\n\nfunction renderTestIcon ({ color, size }) {\n return pug`\n Icon(icon=faToolbox style={ color, width: size, height: size })\n `\n}\n" ], - "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAiB,WAClB;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", + "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAU,WACX;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/CatCard.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/CatCard.js.output.sourcemap.json index 6970ef4..905ed35 100644 --- a/test/fixtures/real-project/snapshots/swc/CatCard.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/CatCard.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport { Div, Span, Avatar, Link } from 'startupjs-ui'\n\nexport default observer(({ $cat, showPhone, large, small }) => {\n const { name, number, phone, catgram, photoFileId, phonegram } = $cat.get()\n return pug`\n Div(part='root' row vAlign='center')\n if photoFileId\n Photo.avatar(styleName={ large, small } fileId=photoFileId name=name)\n else\n Avatar.avatar(styleName={ large, small })= name\n Div(row)\n Span.text(bold styleName={ large })= (number || 'X') + '. '\n Div\n Span.text(styleName={ large })= name\n if showPhone\n if phone\n Span.text(styleName={ large })\n Span(bold) Phone:#{' '}\n = phone\n if catgram\n Span.text(styleName={ large })\n Span(bold) Catgram:#{' '}\n Link.text(styleName={ large } to=getCatgramLink(catgram))= catgram\n if phonegram\n Span.text(styleName={ large })\n Span(bold) Phonegram:#{' '}\n Link.text(styleName={ large } to=getPhonegramLink(phonegram))= phonegram\n style(lang='styl')\n .avatar\n margin-right 1u\n &.large\n width 12u\n height @width\n &.small\n width 4u\n height @width\n .text.large\n font(h6)\n `\n})\n\nconst Photo = observer(({ fileId, name }) => {\n const $file = useSub($.files[fileId])\n let url\n try { url = $file.getUrl() } catch (err) {}\n return pug`\n Avatar(part='root' src=url)= name\n `\n})\n\nfunction getCatgramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://catgr.am/' + username\n}\n\nfunction getPhonegramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://www.phonegram.com/' + username\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAK,YAEhE,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAA,YAE3D,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/CatCard.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/CatCard.tsx.output.sourcemap.json index fd7dc9f..f4190df 100644 --- a/test/fixtures/real-project/snapshots/swc/CatCard.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/CatCard.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport { Div, Span, Avatar, Link } from 'startupjs-ui'\n\nexport default observer(({ $cat, showPhone, large, small }) => {\n const { name, number, phone, catgram, photoFileId, phonegram } = $cat.get()\n return pug`\n Div(part='root' row vAlign='center')\n if photoFileId\n Photo.avatar(styleName={ large, small } fileId=photoFileId name=name)\n else\n Avatar.avatar(styleName={ large, small })= name\n Div(row)\n Span.text(bold styleName={ large })= (number || 'X') + '. '\n Div\n Span.text(styleName={ large })= name\n if showPhone\n if phone\n Span.text(styleName={ large })\n Span(bold) Phone:#{' '}\n = phone\n if catgram\n Span.text(styleName={ large })\n Span(bold) Catgram:#{' '}\n Link.text(styleName={ large } to=getCatgramLink(catgram))= catgram\n if phonegram\n Span.text(styleName={ large })\n Span(bold) Phonegram:#{' '}\n Link.text(styleName={ large } to=getPhonegramLink(phonegram))= phonegram\n style(lang='styl')\n .avatar\n margin-right 1u\n &.large\n width 12u\n height @width\n &.small\n width 4u\n height @width\n .text.large\n font(h6)\n `\n})\n\nconst Photo = observer(({ fileId, name }) => {\n const $file = useSub($.files[fileId])\n let url\n try { url = $file.getUrl() } catch (err) {}\n return pug`\n Avatar(part='root' src=url)= name\n `\n})\n\nfunction getCatgramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://catgr.am/' + username\n}\n\nfunction getPhonegramLink (username) {\n if (!username) return\n if (/:\\/\\//.test(username)) return username\n return 'https://www.phonegram.com/' + username\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAK,YAEhE,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;;IA0BM;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlCN;AACA,WACI,qCACK,eACD,MAAa,aAAR,SAAkB,mBAAiB,QAAO,aAAY,MAAA,YAE3D,OAAc,aAAR,SAAkB,oBAAmB,gBAC7C,SACE,KAAe,aAAX,OAAqB,YAAf,MAA2B,+BACrC,KACE,KAAU,aAAN,OAAgB,aAAY,aAC7B,eACE,SACD,KAAU,aAAN,OAAgB,aAClB,UAAW,OAAQ,YACjB,sBACH,WACD,KAAU,aAAN,OAAgB,aAClB,UAAW,SAAU,YACrB,KAAU,aAAN,OAAgB,YAAU,IAAG,0BAA0B,+BAC5D,aACD,KAAU,aAAN,OAAgB,aAClB,UAAW,WAAY,YACvB,KAAU,aAAN,OAAgB,YAAU,IAAG,8BAA8B,8DAY5E;AACH;AACA;AACA;AACA;AACA;AACA;AACA,WACI,wBAAuB,MAAM,eAC9B;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/cat-profile-link.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/cat-profile-link.js.output.sourcemap.json index d014b54..e192b38 100644 --- a/test/fixtures/real-project/snapshots/swc/cat-profile-link.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/cat-profile-link.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { Alert, Span, Modal, Content, Button, Form, Div, Tag, useMedia, useFormFields } from 'startupjs-ui'\nimport { useGlobalSearchParams, Stack } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport CatCard from '@/components/CatCard'\nimport * as stages from '@/components/stages'\nimport { CAT_PROFILE_EDIT_FORM } from '@/model/cats/schema'\nimport { STAGES } from '@/model/events/schema'\n\nexport default observer(() => {\n const { token } = useGlobalSearchParams()\n const [$cat] = useSub($.cats, { token })\n if (!$cat) return renderExpired()\n\n const eventId = $cat.eventId.get()\n const $event = useSub($.events[eventId])\n\n function renderTitle () {\n return pug`\n CatCard($cat=$cat)\n `\n }\n\n function renderSettings () {\n return pug`\n Profile($cat=$cat $event=$event)\n `\n }\n\n const Stage = stages[$cat.getMyStage()]\n\n return pug`\n Stack.Screen(\n options={\n headerTitle: renderTitle,\n headerRight: renderSettings\n }\n )\n Stage($cat=$cat $event=$event)\n `\n})\n\nconst Profile = observer(({ $cat, $event }) => {\n const $showEdit = $()\n const { tablet } = useMedia()\n const excludeNumber = $event.stage.get() !== STAGES.InProgress\n const profileEditFields = useFormFields(CAT_PROFILE_EDIT_FORM, excludeNumber ? { exclude: ['number'] } : {})\n\n return pug`\n Div(row vAlign='center' gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n if $cat.getMyStage() === STAGES.Profile\n Div.hackSidePadding\n else\n Button(\n variant='text'\n icon=faPen\n onPress=() => $showEdit.set(true)\n )\n if tablet\n = 'Edit cat profile'\n else\n = 'Edit'\n Modal(\n title='Edit cat profile'\n $visible=$showEdit\n )\n Form(\n fields=profileEditFields\n $value=$cat\n )\n style(lang='styl')\n .hackSidePadding\n width 1u\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nfunction renderExpired () {\n return pug`\n Content(padding)\n Alert(variant='error')\n Span\n | Cat profile link is incorrect or already expired.\n |\n | Your cat meetup profile link is only valid for a limited period of time.\n |\n | If you believe this is an error, please contact the cat meetup organizer.\n `\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAa,SACd;AACL;AACA;AACA;AACA,aACM,cAAa,MAAK,QAAO,WAC1B;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAW,MAAK,QAAO,cACxB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAQ,SACT;AACL;AACA;AACA;AACA,aACM,cAAQ,MAAU,QAAA,WACnB;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAM,MAAU,QAAA,cACjB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/cat-profile-link.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/cat-profile-link.tsx.output.sourcemap.json index 2603a48..d7d36d9 100644 --- a/test/fixtures/real-project/snapshots/swc/cat-profile-link.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/cat-profile-link.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { Alert, Span, Modal, Content, Button, Form, Div, Tag, useMedia, useFormFields } from 'startupjs-ui'\nimport { useGlobalSearchParams, Stack } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport CatCard from '@/components/CatCard'\nimport * as stages from '@/components/stages'\nimport { CAT_PROFILE_EDIT_FORM } from '@/model/cats/schema'\nimport { STAGES } from '@/model/events/schema'\n\nexport default observer(() => {\n const { token } = useGlobalSearchParams()\n const [$cat] = useSub($.cats, { token })\n if (!$cat) return renderExpired()\n\n const eventId = $cat.eventId.get()\n const $event = useSub($.events[eventId])\n\n function renderTitle () {\n return pug`\n CatCard($cat=$cat)\n `\n }\n\n function renderSettings () {\n return pug`\n Profile($cat=$cat $event=$event)\n `\n }\n\n const Stage = stages[$cat.getMyStage()]\n\n return pug`\n Stack.Screen(\n options={\n headerTitle: renderTitle,\n headerRight: renderSettings\n }\n )\n Stage($cat=$cat $event=$event)\n `\n})\n\nconst Profile = observer(({ $cat, $event }) => {\n const $showEdit = $()\n const { tablet } = useMedia()\n const excludeNumber = $event.stage.get() !== STAGES.InProgress\n const profileEditFields = useFormFields(CAT_PROFILE_EDIT_FORM, excludeNumber ? { exclude: ['number'] } : {})\n\n return pug`\n Div(row vAlign='center' gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n if $cat.getMyStage() === STAGES.Profile\n Div.hackSidePadding\n else\n Button(\n variant='text'\n icon=faPen\n onPress=() => $showEdit.set(true)\n )\n if tablet\n = 'Edit cat profile'\n else\n = 'Edit'\n Modal(\n title='Edit cat profile'\n $visible=$showEdit\n )\n Form(\n fields=profileEditFields\n $value=$cat\n )\n style(lang='styl')\n .hackSidePadding\n width 1u\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nfunction renderExpired () {\n return pug`\n Content(padding)\n Alert(variant='error')\n Span\n | Cat profile link is incorrect or already expired.\n |\n | Your cat meetup profile link is only valid for a limited period of time.\n |\n | If you believe this is an error, please contact the cat meetup organizer.\n `\n}\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAa,SACd;AACL;AACA;AACA;AACA,aACM,cAAa,MAAK,QAAO,WAC1B;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAW,MAAK,QAAO,cACxB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACM,cAAQ,SACT;AACL;AACA;AACA;AACA,aACM,cAAQ,MAAU,QAAA,WACnB;AACL;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,YAAM,MAAU,QAAA,cACjB;AACH;AACA;AACA;;IAiCM;IACA;;;AAjCN;AACA;AACA;AACA;AACA;AACA,aACI,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBAClB,wCACD,iBAAG,yBAEH,OACE,eACA,MAAK,OACL,SAAQ,4BAEL,SACC,qBAEA,wBACV,MACE,yBACA,UAAS,YAET,KACE,QAAO,mBACP,QAAO,oBAKZ;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WACI,iBACE,uBACE,KACI;AACP;AACA,CAAQ;AACR;AACA,CAAQ,kGACV;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/event-tabs-breed.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/event-tabs-breed.js.output.sourcemap.json index 3f46fa4..59c2eb1 100644 --- a/test/fixtures/real-project/snapshots/swc/event-tabs-breed.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/event-tabs-breed.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React, { useState } from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport {\n Link, Item, ScrollView, Form, useFormProps, Alert,\n Content, Tag, Br, Button, Modal, Div, confirm,\n useFormFields$, useValidate\n} from 'startupjs-ui'\nimport { useGlobalSearchParams } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faLink } from '@fortawesome/free-solid-svg-icons/faLink'\nimport CatCard from '@/components/CatCard'\nimport { CAT_FORM } from '@/model/cats/schema'\n\nexport default observer(({ breed }) => {\n const { eventId } = useGlobalSearchParams()\n const [$selected, set$selected] = useState()\n const $new = $()\n const $showModal = $()\n const $mode = $()\n const $fields = useFormFields$(CAT_FORM)\n const validate = useValidate()\n\n function showCreate () {\n $new.set({ breed })\n $fields.breed.disabled.set(true)\n set$selected(() => $new)\n $mode.set('new')\n $showModal.set(true)\n }\n\n function showEdit ($cat) {\n $fields.breed.disabled.del()\n set$selected(() => $cat)\n $mode.set('edit')\n $showModal.set(true)\n }\n\n function cancel () {\n if (!$showModal.get()) return\n $showModal.del()\n $mode.del()\n }\n\n async function create () {\n if (!validate()) return\n await $.cats.addNew({\n ...$new.getDeepCopy(),\n eventId\n })\n cancel()\n }\n\n async function deleteCat () {\n if (!await confirm(`Are you sure you want to delete ${$selected.name.get()}?`)) return\n await $selected.del()\n cancel()\n }\n\n return pug`\n ScrollView(full)\n Content(full pure)\n CatsList(eventId=eventId onEdit=showEdit breed=breed)\n Content(padding=1)\n Button(onPress=showCreate) Add new #{breed}\n Modal(\n title=$mode.get() === 'new' ? 'Create cat' : 'Edit cat'\n $visible=$showModal\n onDismiss=cancel\n )\n - const oppositeBreed = $selected?.breed.get() && ($selected.breed.get() === 'domestic' ? 'wild' : 'domestic')\n Form(\n key=$selected?.getId() || 'NEW'\n $fields=$fields\n $value=$selected\n oppositeBreed=oppositeBreed\n eventId=eventId\n customInputs={\n likes: SelectLikesInput\n }\n validate=validate\n )\n Br\n if $mode.get() === 'new'\n Div(align='right' row)\n Button(onPress=cancel) Cancel\n Button(disabled=validate.hasErrors pushed variant='flat' color='primary' onPress=create) Create\n else if $mode.get() === 'edit'\n Div(align='right' row)\n Button(color='error' onPress=deleteCat) Delete\n `\n})\n\nconst CatsList = observer(({ onEdit, breed, eventId }) => {\n if (!eventId) return pug`Alert(variant='error') No event specified`\n const $cats = useSub($.cats, { eventId, breed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n Item(key=$cat.getId())\n CatCard($cat=$cat)\n Item.Right\n Div(vAlign='center' row gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n Button(variant='text' icon=faPen onPress=() => onEdit($cat) tooltip='Edit')\n Link(href='/events/' + eventId + '/matches/' + $cat.getId())\n Button(variant='text' icon=faHeart tooltip='Matches')\n Link(href='/cats/' + $cat.token.get())\n Button(variant='text' icon=faLink tooltip='Cat profile link') Link\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nconst SelectLikesInput = observer(({ $value, ...props }) => {\n const { oppositeBreed, eventId } = { ...useFormProps(), ...props }\n return pug`\n if oppositeBreed\n SelectLikes(\n $likes=$value\n oppositeBreed=oppositeBreed\n eventId=eventId\n )\n else\n Alert(variant='warning') Select breed to choose likes\n `\n})\n\nconst SelectLikes = observer(({ $likes, oppositeBreed, eventId }) => {\n const $cats = useSub($.cats, { eventId, breed: oppositeBreed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n - const catId = $cat.getId()\n Item.item(\n key=catId\n styleName={ selected: $likes[catId].get() }\n onPress=() => $likes[catId].get() ? $likes[catId].del() : $likes[catId].set(true)\n )\n CatCard($cat=$cat small)\n else\n Alert(variant='info') No cats with selected breed yet\n style(lang='styl')\n .item\n border-radius 1u\n &.selected\n // FIXME: We can't use color var(--color-text-success-strong) here\n background-color var(--color-text-success-strong)\n `\n})\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAiB,SAAQ,QAAO,UAAS,OAAM,iCACnD,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAQ,SACR,QAAO,WACP,eAAc,eACd,SAAQ,SACR,cAAa;AACjB;AACA,OACI,UAAS,aAEX,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,wDACS,QAAQ,8BACX,UAAS,eACP,cAAa,SACb,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAc,eACd,SAAQ,eAGV,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,wDACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAa,MAAK,0EAEpB,qBAAsB,6CAOzB;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAS,SAAgB,QAAO,UAAS,OAAA,iCAC7C,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAA,SACA,QAAO,WACP,eAAA,eACA,SAAA,SACA,cAAa;AACjB;AACA,OACI,UAAA,aAEF,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,wDACS,QAAQ,8BACX,UAAS,eACP,cAAQ,SACR,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAA,eACA,SAAA,eAGF,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,wDACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAQ,MAAU,0EAEpB,qBAAsB,6CAOzB;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/event-tabs-breed.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/event-tabs-breed.tsx.output.sourcemap.json index b79c019..ec4f8e1 100644 --- a/test/fixtures/real-project/snapshots/swc/event-tabs-breed.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/event-tabs-breed.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "import React, { useState } from 'react'\nimport { pug, observer, useSub, $ } from 'startupjs'\nimport {\n Link, Item, ScrollView, Form, useFormProps, Alert,\n Content, Tag, Br, Button, Modal, Div, confirm,\n useFormFields$, useValidate\n} from 'startupjs-ui'\nimport { useGlobalSearchParams } from 'expo-router'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faLink } from '@fortawesome/free-solid-svg-icons/faLink'\nimport CatCard from '@/components/CatCard'\nimport { CAT_FORM } from '@/model/cats/schema'\n\nexport default observer(({ breed }) => {\n const { eventId } = useGlobalSearchParams()\n const [$selected, set$selected] = useState()\n const $new = $()\n const $showModal = $()\n const $mode = $()\n const $fields = useFormFields$(CAT_FORM)\n const validate = useValidate()\n\n function showCreate () {\n $new.set({ breed })\n $fields.breed.disabled.set(true)\n set$selected(() => $new)\n $mode.set('new')\n $showModal.set(true)\n }\n\n function showEdit ($cat) {\n $fields.breed.disabled.del()\n set$selected(() => $cat)\n $mode.set('edit')\n $showModal.set(true)\n }\n\n function cancel () {\n if (!$showModal.get()) return\n $showModal.del()\n $mode.del()\n }\n\n async function create () {\n if (!validate()) return\n await $.cats.addNew({\n ...$new.getDeepCopy(),\n eventId\n })\n cancel()\n }\n\n async function deleteCat () {\n if (!await confirm(`Are you sure you want to delete ${$selected.name.get()}?`)) return\n await $selected.del()\n cancel()\n }\n\n return pug`\n ScrollView(full)\n Content(full pure)\n CatsList(eventId=eventId onEdit=showEdit breed=breed)\n Content(padding=1)\n Button(onPress=showCreate) Add new #{breed}\n Modal(\n title=$mode.get() === 'new' ? 'Create cat' : 'Edit cat'\n $visible=$showModal\n onDismiss=cancel\n )\n - const oppositeBreed = $selected?.breed.get() && ($selected.breed.get() === 'domestic' ? 'wild' : 'domestic')\n Form(\n key=$selected?.getId() || 'NEW'\n $fields=$fields\n $value=$selected\n oppositeBreed=oppositeBreed\n eventId=eventId\n customInputs={\n likes: SelectLikesInput\n }\n validate=validate\n )\n Br\n if $mode.get() === 'new'\n Div(align='right' row)\n Button(onPress=cancel) Cancel\n Button(disabled=validate.hasErrors pushed variant='flat' color='primary' onPress=create) Create\n else if $mode.get() === 'edit'\n Div(align='right' row)\n Button(color='error' onPress=deleteCat) Delete\n `\n})\n\nconst CatsList = observer(({ onEdit, breed, eventId }) => {\n if (!eventId) return pug`Alert(variant='error') No event specified`\n const $cats = useSub($.cats, { eventId, breed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n Item(key=$cat.getId())\n CatCard($cat=$cat)\n Item.Right\n Div(vAlign='center' row gap=1)\n if !hasContact($cat)\n Tag(color='error') No contact\n if !$cat.photoFileId.get()\n Tag(color='error') No photo\n Button(variant='text' icon=faPen onPress=() => onEdit($cat) tooltip='Edit')\n Link(href='/events/' + eventId + '/matches/' + $cat.getId())\n Button(variant='text' icon=faHeart tooltip='Matches')\n Link(href='/cats/' + $cat.token.get())\n Button(variant='text' icon=faLink tooltip='Cat profile link') Link\n `\n})\n\nfunction hasContact ($cat) {\n return ($cat.phone.get() || '').trim() || ($cat.catgram.get() || '').trim() || ($cat.phonegram.get() || '').trim()\n}\n\nconst SelectLikesInput = observer(({ $value, ...props }) => {\n const { oppositeBreed, eventId } = { ...useFormProps(), ...props }\n return pug`\n if oppositeBreed\n SelectLikes(\n $likes=$value\n oppositeBreed=oppositeBreed\n eventId=eventId\n )\n else\n Alert(variant='warning') Select breed to choose likes\n `\n})\n\nconst SelectLikes = observer(({ $likes, oppositeBreed, eventId }) => {\n const $cats = useSub($.cats, { eventId, breed: oppositeBreed, $sort: { breed: 1, number: 1 } })\n return pug`\n each $cat in $cats\n - const catId = $cat.getId()\n Item.item(\n key=catId\n styleName={ selected: $likes[catId].get() }\n onPress=() => $likes[catId].get() ? $likes[catId].del() : $likes[catId].set(true)\n )\n CatCard($cat=$cat small)\n else\n Alert(variant='info') No cats with selected breed yet\n style(lang='styl')\n .item\n border-radius 1u\n &.selected\n // FIXME: We can't use color var(--color-text-success-strong) here\n background-color var(--color-text-success-strong)\n `\n})\n" ], - "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAiB,SAAQ,QAAO,UAAS,OAAM,iCACnD,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAQ,SACR,QAAO,WACP,eAAc,eACd,SAAQ,SACR,cAAa;AACjB;AACA,OACI,UAAS,aAEX,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,wDACS,QAAQ,8BACX,UAAS,eACP,cAAa,SACb,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAc,eACd,SAAQ,eAGV,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,wDACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAa,MAAK,0EAEpB,qBAAsB,6CAOzB;AACH", + "mappings": "AAAA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,iBACE,mBACE,kBAAS,SAAgB,QAAO,UAAS,OAAA,iCAC7C,iBAAgB,IACd,gBAAe,YAAY,SAAU,0BACvC,MACE,OAAM,mDACN,UAAS,YACT,WAAU,iBAER,wHACF,KACE,KAAI,6BACJ,SAAA,SACA,QAAO,WACP,eAAA,eACA,SAAA,SACA,cAAa;AACjB;AACA,OACI,UAAA,aAEF,MACG,yBACD,uBACE,gBAAe,QAAQ,gBACvB,iBAAgB,oBAAmB,+CAA8C,QAAQ,wBACrF,0BACN,uBACE,8BAA6B,WAAW,mDAC/C;AACH;AACA;AACA;AACA,yBAA2B,sBAAuB,2BAAmB;AACrE;AACA,wDACS,QAAQ,8BACX,UAAS,eACP,cAAQ,SACR,YACE,6BAA4B,IACvB,qBACD,kBAAmB,yBAClB,2BACD,kBAAmB,uBACrB,4BAA2B,OAAM,SAAQ,oBAAmB,kBAC5D,WAAU,oDACR,4BAA2B,SAAQ,4BACrC,WAAU,8BACR,4BAA2B,QAAO,2BAA4B,6EACzE;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UACO,iBACD,YACE,QAAO,QACP,eAAA,eACA,SAAA,eAGF,wBAAyB,qCAC5B;AACH;AACA;AACA;;IAcM;IACA;IACA;IACA;IACA;;;AAjBN;AACA,wDACS,QAAQ,qCACT,mCACF,KAEE,aAFE,OAEQ,oCADV,KAAI,OAEJ,SAAQ,4EAER,cAAQ,MAAU,0EAEpB,qBAAsB,6CAOzB;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/event-tabs-layout.js.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/event-tabs-layout.js.output.sourcemap.json index 8a020cb..16689a4 100644 --- a/test/fixtures/real-project/snapshots/swc/event-tabs-layout.js.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/event-tabs-layout.js.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "// import { Platform } from 'react-native'\nimport React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { useColors, Icon, Form, Modal, Button } from 'startupjs-ui'\nimport { Tabs, useLocalSearchParams, Stack } from 'expo-router'\nimport { faVenus as faWildBadge } from '@fortawesome/free-solid-svg-icons/faVenus'\nimport { faMars as faDomesticBadge } from '@fortawesome/free-solid-svg-icons/faMars'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faToolbox } from '@fortawesome/free-solid-svg-icons/faToolbox'\nimport { EVENT_FORM } from '@/model/events/schema'\n\nexport default observer(function TabLayout () {\n const getColor = useColors()\n const { eventId } = useLocalSearchParams()\n const $event = useSub($.events[eventId])\n if (!$event.get()) throw Error('No such event')\n\n // NOTE:\n // headerShown -- Disable the static render of the header on web to prevent a hydration error in React Navigation v6.\n // tabBarStyle/order - Move the tab bar to the top on tablet+\n return pug`\n Stack.Screen(\n options={\n title: $event.name.get(),\n headerRight: () => renderEditEvent({ $event })\n }\n )\n Tabs(\n title=$event.name.get()\n screenOptions={\n ...styl('screen'),\n tabBarActiveTintColor: getColor('primary'),\n tabBarActiveBackgroundColor: 'rgba(255, 255, 255, 0.5)',\n headerShown: false,\n headerTitle: $event.name.get()\n }\n )\n Tabs.Screen(\n name='index'\n options={\n title: 'Dashboard',\n tabBarIcon: renderHomeIcon\n }\n )\n Tabs.Screen(\n name='-breed'\n options={\n href: null\n }\n )\n Tabs.Screen(\n name='domestic'\n options={\n title: 'Domestic Cats',\n tabBarIcon: renderDomesticIcon\n }\n )\n Tabs.Screen(\n name='wild'\n options={\n title: 'Wild Cats',\n tabBarIcon: renderWildIcon\n }\n )\n Tabs.Screen(\n name='test'\n options={\n title: 'Dev Only',\n tabBarIcon: renderTestIcon\n }\n )\n style(lang='styl')\n +tablet()\n .screen\n &:part(tabBar)\n order -1\n background-color transparent\n border-bottom-width 1px\n border-bottom-color rgba(0, 0, 0, 0.1)\n `\n})\n\nfunction renderEditEvent ({ $event }) {\n return pug`\n EditEvent($event=$event)\n `\n}\n\nconst EditEvent = observer(({ $event }) => {\n const $showModal = $()\n return pug`\n Button(onPress=() => $showModal.set(true) variant='text' icon=faPen) Edit this cat meetup\n Modal(\n title='Edit cat meetup'\n $visible=$showModal\n )\n Form(\n $value=$event\n fields=EVENT_FORM\n )\n `\n})\n\nfunction renderWildIcon ({ color, size }) {\n return pug`\n Icon(icon=faWildBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderDomesticIcon ({ color, size }) {\n return pug`\n Icon(icon=faDomesticBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderHomeIcon ({ color, size }) {\n return pug`\n Icon(icon=faHeart style={ color, width: size, height: size })\n `\n}\n\nfunction renderTestIcon ({ color, size }) {\n return pug`\n Icon(icon=faToolbox style={ color, width: size, height: size })\n `\n}\n" ], - "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAiB,WAClB;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", + "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAU,WACX;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", "ignoreList": [] } \ No newline at end of file diff --git a/test/fixtures/real-project/snapshots/swc/event-tabs-layout.tsx.output.sourcemap.json b/test/fixtures/real-project/snapshots/swc/event-tabs-layout.tsx.output.sourcemap.json index 8dbf3d1..1453c9e 100644 --- a/test/fixtures/real-project/snapshots/swc/event-tabs-layout.tsx.output.sourcemap.json +++ b/test/fixtures/real-project/snapshots/swc/event-tabs-layout.tsx.output.sourcemap.json @@ -8,6 +8,6 @@ "sourcesContent": [ "// import { Platform } from 'react-native'\nimport React from 'react'\nimport { pug, observer, $, useSub } from 'startupjs'\nimport { useColors, Icon, Form, Modal, Button } from 'startupjs-ui'\nimport { Tabs, useLocalSearchParams, Stack } from 'expo-router'\nimport { faVenus as faWildBadge } from '@fortawesome/free-solid-svg-icons/faVenus'\nimport { faMars as faDomesticBadge } from '@fortawesome/free-solid-svg-icons/faMars'\nimport { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'\nimport { faPen } from '@fortawesome/free-solid-svg-icons/faPen'\nimport { faToolbox } from '@fortawesome/free-solid-svg-icons/faToolbox'\nimport { EVENT_FORM } from '@/model/events/schema'\n\nexport default observer(function TabLayout () {\n const getColor = useColors()\n const { eventId } = useLocalSearchParams()\n const $event = useSub($.events[eventId])\n if (!$event.get()) throw Error('No such event')\n\n // NOTE:\n // headerShown -- Disable the static render of the header on web to prevent a hydration error in React Navigation v6.\n // tabBarStyle/order - Move the tab bar to the top on tablet+\n return pug`\n Stack.Screen(\n options={\n title: $event.name.get(),\n headerRight: () => renderEditEvent({ $event })\n }\n )\n Tabs(\n title=$event.name.get()\n screenOptions={\n ...styl('screen'),\n tabBarActiveTintColor: getColor('primary'),\n tabBarActiveBackgroundColor: 'rgba(255, 255, 255, 0.5)',\n headerShown: false,\n headerTitle: $event.name.get()\n }\n )\n Tabs.Screen(\n name='index'\n options={\n title: 'Dashboard',\n tabBarIcon: renderHomeIcon\n }\n )\n Tabs.Screen(\n name='-breed'\n options={\n href: null\n }\n )\n Tabs.Screen(\n name='domestic'\n options={\n title: 'Domestic Cats',\n tabBarIcon: renderDomesticIcon\n }\n )\n Tabs.Screen(\n name='wild'\n options={\n title: 'Wild Cats',\n tabBarIcon: renderWildIcon\n }\n )\n Tabs.Screen(\n name='test'\n options={\n title: 'Dev Only',\n tabBarIcon: renderTestIcon\n }\n )\n style(lang='styl')\n +tablet()\n .screen\n &:part(tabBar)\n order -1\n background-color transparent\n border-bottom-width 1px\n border-bottom-color rgba(0, 0, 0, 0.1)\n `\n})\n\nfunction renderEditEvent ({ $event }) {\n return pug`\n EditEvent($event=$event)\n `\n}\n\nconst EditEvent = observer(({ $event }) => {\n const $showModal = $()\n return pug`\n Button(onPress=() => $showModal.set(true) variant='text' icon=faPen) Edit this cat meetup\n Modal(\n title='Edit cat meetup'\n $visible=$showModal\n )\n Form(\n $value=$event\n fields=EVENT_FORM\n )\n `\n})\n\nfunction renderWildIcon ({ color, size }) {\n return pug`\n Icon(icon=faWildBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderDomesticIcon ({ color, size }) {\n return pug`\n Icon(icon=faDomesticBadge style={ color, width: size, height: size })\n `\n}\n\nfunction renderHomeIcon ({ color, size }) {\n return pug`\n Icon(icon=faHeart style={ color, width: size, height: size })\n `\n}\n\nfunction renderTestIcon ({ color, size }) {\n return pug`\n Icon(icon=faToolbox style={ color, width: size, height: size })\n `\n}\n" ], - "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAiB,WAClB;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", + "mappings": "AAAA;AACA;qDACoD;AACpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;IA6DM;IACA;IACA;IACA;IACA;IACA;IACA;;;AAlEN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aACI,aACE,SAAQ;AACV;AACA;AACA,QAEA,KACE,OAAM,mBACN,eAAc;AAChB;AACA;AACA;AACA;AACA;AACA,MAEE,YACE,aACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,cACA,SAAQ;AACZ;AACA,UAEE,YACE,gBACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,UAEE,YACE,YACA,SAAQ;AACZ;AACA;AACA,oBAUD;AACH;AACA;AACA;AACA,WACI,kBAAU,WACX;AACH;AACA;AACA;AACA;AACA,aACI,gBAAe,4BAA2B,qBAAoB,OAAO,8BACrE,MACE,wBACA,UAAS,aAET,KACE,QAAO,QACP,QAAO,0BAEZ;AACH;AACA;AACA;AACA,WACI,WAAU,aAAY,OAAM,yCAC7B;AACH;AACA;AACA;AACA,WACI,WAAU,iBAAgB,OAAM,yCACjC;AACH;AACA;AACA;AACA,WACI,WAAU,SAAQ,OAAM,yCACzB;AACH;AACA;AACA;AACA,WACI,WAAU,WAAU,OAAM,yCAC3B;AACH", "ignoreList": [] } \ No newline at end of file