From a942bdb90776e1e6d10610a7590b53193d5383ca Mon Sep 17 00:00:00 2001 From: Mario Tarosso Date: Wed, 2 Sep 2026 09:01:04 +0100 Subject: [PATCH] Fix production marker insertion in JSX roots --- src/mark-build.ts | 108 +++++++++++++++----- tests/mark-build.test.ts | 208 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 22 deletions(-) diff --git a/src/mark-build.ts b/src/mark-build.ts index 4ef5998f..4a912700 100644 --- a/src/mark-build.ts +++ b/src/mark-build.ts @@ -182,12 +182,85 @@ export interface SourceMarkerResult { action: SourceMarkerAction; } +interface JsxShellAnchor { + end: number; + indent: string; +} + +/** + * Find a literal JSX opening tag at the start of a line. JSX attributes may + * contain `>` inside strings or braced expressions, so the tag boundary needs + * a small lexical scan rather than a character class. Self-closing tags cannot + * carry the marker as a child and are skipped. + */ +function findJsxShellAnchor(source: string, tagName: 'head' | 'body'): JsxShellAnchor | null { + const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, 'gm'); + let candidate: RegExpExecArray | null; + + while ((candidate = candidates.exec(source)) !== null) { + let quote: "'" | '"' | '`' | null = null; + let escaped = false; + let braces = 0; + let blockComment = false; + let lineComment = false; + + for (let index = candidates.lastIndex; index < source.length; index += 1) { + const char = source[index]; + const next = source[index + 1]; + + if (lineComment) { + if (char === '\n') lineComment = false; + continue; + } + if (blockComment) { + if (char === '*' && next === '/') { + blockComment = false; + index += 1; + } + continue; + } + if (quote !== null) { + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === quote) { + quote = null; + } + continue; + } + + if (char === "'" || char === '"' || char === '`') { + quote = char; + } else if (braces > 0 && char === '/' && next === '*') { + blockComment = true; + index += 1; + } else if (braces > 0 && char === '/' && next === '/') { + lineComment = true; + index += 1; + } else if (char === '{') { + braces += 1; + } else if (char === '}') { + if (braces === 0) break; + braces -= 1; + } else if (char === '>' && braces === 0) { + const openingTag = source.slice(candidate.index, index + 1); + if (/\/\s*>$/.test(openingTag)) break; + return { end: index + 1, indent: candidate[1] ?? '' }; + } + } + } + + return null; +} + /** * Insert the production marker into a JSX root shell, idempotently. * - * Anchors on the widget tag when one is present, so the marker is ordered ahead - * of it in the document; otherwise on ``, then ``. A marker the - * developer placed themselves is adopted rather than duplicated. + * Anchors as the first child of ``, then ``, so the marker runs + * before the deferred widget without relying on how that widget is expressed + * in source. A marker the developer placed themselves is adopted rather than + * duplicated. */ export function ensureMarkerInJsxShell(source: string, framework: string | null): SourceMarkerResult { if (!hasJsxShell(framework)) { @@ -196,7 +269,7 @@ export function ensureMarkerInJsxShell(source: string, framework: string | null) const stripped = source.replace(REGION_RE, ''); if (stripped.includes(PROD_MARKER_GLOBAL)) { - return { source, action: 'manual' }; + return { source: stripped, action: 'manual' }; } const block = (indent: string): string => @@ -204,29 +277,20 @@ export function ensureMarkerInJsxShell(source: string, framework: string | null) .map((line) => `${indent}${line}`) .join('\n'); - const widget = stripped.match(/^([ \t]*).*patchstack-widget.*$/m); - if (widget?.index !== undefined) { - const indent = widget[1] ?? ''; - return { - source: `${stripped.slice(0, widget.index)}${block(indent)}\n${stripped.slice(widget.index)}`, - action: 'added', - }; - } + for (const tagName of ['head', 'body'] as const) { + const anchor = findJsxShellAnchor(stripped, tagName); + if (anchor === null) continue; - for (const anchor of [/^([ \t]*)[ \t]*$/m, /^([ \t]*)[ \t]*$/m]) { - const match = stripped.match(anchor); - if (match?.index === undefined) { - continue; - } - const end = match.index + match[0].length; - const indent = `${match[1] ?? ''} `; + const indent = `${anchor.indent} `; + const remainder = stripped.slice(anchor.end); + const separator = remainder.startsWith('\n') || remainder.startsWith('\r') ? '' : '\n'; return { - source: `${stripped.slice(0, end)}\n${block(indent)}${stripped.slice(end)}`, + source: `${stripped.slice(0, anchor.end)}\n${block(indent)}${separator}${remainder}`, action: 'added', }; } - return { source, action: 'no-anchor' }; + return { source: stripped, action: 'no-anchor' }; } export interface EnsureSourceMarkerResult extends SourceMarkerResult { @@ -250,7 +314,7 @@ export function ensureSourceMarker( const file = path.resolve(cwd, shell); const before = readFileSync(file, 'utf8'); const result = ensureMarkerInJsxShell(before, framework); - if (result.source !== before && result.action === 'added') { + if (result.source !== before) { writeFileSync(file, result.source); } return { ...result, shell }; diff --git a/tests/mark-build.test.ts b/tests/mark-build.test.ts index 5c82a57d..4e3b1fcf 100644 --- a/tests/mark-build.test.ts +++ b/tests/mark-build.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import ts from 'typescript'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { MARKER_ATTR, @@ -183,6 +184,38 @@ describe('ensureMarkerInJsxShell', () => { const doc = (body: string): string => ['export const Root = () => (', ' ', ' ', ' ', ' ', body, ' ', ' ', ');'].join('\n'); + const syntaxErrors = (source: string): readonly ts.Diagnostic[] => + (ts.transpileModule(source, { + fileName: '__root.tsx', + reportDiagnostics: true, + compilerOptions: { + jsx: ts.JsxEmit.ReactJSX, + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + }, + }).diagnostics ?? []).filter( + (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error, + ); + + const multilineWidgetDoc = [ + 'const patchstackConfig = { siteUuid: "00000000-0000-4000-8000-000000000000" };', + 'export const Root = () => (', + ' ', + ' ', + ' Example', + ' ', + ' ', + '
', + ' ', + ' ', + ' ', + ');', + ].join('\n'); + it('inserts the marker above the widget tag, so it runs first', () => { const { source, action } = ensureMarkerInJsxShell(doc(widgetLine), 'tanstack-start'); expect(action).toBe('added'); @@ -196,6 +229,108 @@ describe('ensureMarkerInJsxShell', () => { expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('')); }); + it('finds the end of an attributed multiline head tag', () => { + const attributedHead = [ + 'const count = 1;', + 'export const Root = () => (', + ' ', + ' 0}', + ' >', + ' Example', + ' ', + ' ', + ');', + ].join('\n'); + + const { source, action } = ensureMarkerInJsxShell(attributedHead, 'tanstack-start'); + + expect(action).toBe('added'); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeGreaterThan(source.indexOf('\n >')); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('')); + expect(syntaxErrors(source)).toEqual([]); + }); + + it('does not insert outside a self-closing shell tag', () => { + const selfClosing = [ + 'export const Root = () => (', + ' <html>', + ' <body />', + ' </html>', + ');', + ].join('\n'); + + const result = ensureMarkerInJsxShell(selfClosing, 'tanstack-start'); + + expect(result.action).toBe('no-anchor'); + expect(result.source).toBe(selfClosing); + }); + + it('is byte-idempotent when the opening tag has an inline child', () => { + const inline = [ + 'export const Root = () => (', + ' <html>', + ' <head><title>Example', + ' ', + ');', + ].join('\n'); + + const once = ensureMarkerInJsxShell(inline, 'tanstack-start').source; + const twice = ensureMarkerInJsxShell(once, 'tanstack-start').source; + + expect(twice).toBe(once); + expect(syntaxErrors(twice)).toEqual([]); + }); + + it('keeps a multiline widget tag syntactically valid', () => { + const { source, action } = ensureMarkerInJsxShell(multilineWidgetDoc, 'tanstack-start'); + + expect(action).toBe('added'); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeGreaterThan(source.indexOf('')); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('')); + expect(source).toContain( + '<script\n src="https://cdn.patchstack.com/patchstack-widget.js"', + ); + expect(syntaxErrors(source)).toEqual([]); + }); + + it('does not treat a widget URL in route metadata as a JSX anchor', () => { + const tanstackRoot = [ + 'export const Route = createRootRoute({', + ' head: () => ({', + ' scripts: [', + ' {', + ' src: "https://cdn.patchstack.com/patchstack-widget.js",', + ' defer: true,', + ' },', + ' ],', + ' }),', + ' component: RootDocument,', + '});', + 'function RootDocument() {', + ' return (', + ' <html>', + ' <head>', + ' <HeadContent />', + ' </head>', + ' <body />', + ' </html>', + ' );', + '}', + ].join('\n'); + + const { source, action } = ensureMarkerInJsxShell(tanstackRoot, 'tanstack-start'); + + expect(action).toBe('added'); + expect(source).toContain( + 'scripts: [\n {\n src: "https://cdn.patchstack.com/patchstack-widget.js",', + ); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeGreaterThan(source.indexOf('<head>')); + expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('<HeadContent />')); + expect(syntaxErrors(source)).toEqual([]); + }); + it('is idempotent — a re-run refreshes the block instead of stacking copies', () => { const once = ensureMarkerInJsxShell(doc(widgetLine), 'tanstack-start').source; const twice = ensureMarkerInJsxShell(once, 'tanstack-start').source; @@ -203,6 +338,64 @@ describe('ensureMarkerInJsxShell', () => { expect((twice.match(/#region patchstack/g) ?? []).length).toBe(1); }); + it('repairs a managed block inserted inside multiline script attributes', () => { + const misplacedBlock = [ + '{/* #region patchstack (managed by patchstack-connect — do not edit) */}', + ...buildSourceMarkerSnippet('tanstack-start').split('\n'), + '{/* #endregion patchstack */}', + ] + .map((line) => ` ${line}`) + .join('\n'); + const broken = multilineWidgetDoc.replace( + ' src="https://cdn.patchstack.com/patchstack-widget.js"', + `${misplacedBlock}\n src="https://cdn.patchstack.com/patchstack-widget.js"`, + ); + + expect(syntaxErrors(broken)).not.toEqual([]); + + const repaired = ensureMarkerInJsxShell(broken, 'tanstack-start'); + const rerun = ensureMarkerInJsxShell(repaired.source, 'tanstack-start'); + + expect(repaired.action).toBe('added'); + expect(repaired.source.indexOf('__PATCHSTACK_PROD__')).toBeGreaterThan( + repaired.source.indexOf('<head>'), + ); + expect(repaired.source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan( + repaired.source.indexOf('<title>'), + ); + expect((repaired.source.match(/#region patchstack/g) ?? []).length).toBe(1); + expect(syntaxErrors(repaired.source)).toEqual([]); + expect(rerun.source).toBe(repaired.source); + }); + + it('removes a misplaced managed block when no safe document anchor exists', () => { + const source = [ + 'export const head = {', + ' scripts: [', + ' {', + ' src: "https://cdn.patchstack.com/patchstack-widget.js",', + ' },', + ' ],', + '};', + ].join('\n'); + const misplacedBlock = [ + '{/* #region patchstack (managed by patchstack-connect — do not edit) */}', + ...buildSourceMarkerSnippet('tanstack-start').split('\n'), + '{/* #endregion patchstack */}', + ] + .map((line) => ` ${line}`) + .join('\n'); + const broken = source.replace( + ' src: "https://cdn.patchstack.com/patchstack-widget.js",', + `${misplacedBlock}\n src: "https://cdn.patchstack.com/patchstack-widget.js",`, + ); + + const result = ensureMarkerInJsxShell(broken, 'tanstack-start'); + + expect(result.action).toBe('no-anchor'); + expect(result.source).toBe(source); + }); + it('adopts a hand-placed marker rather than adding a second one', () => { const hand = doc(' {import.meta.env.PROD && <script>{"window.__PATCHSTACK_PROD__=true;"}</script>}'); const { source, action } = ensureMarkerInJsxShell(hand, 'tanstack-start'); @@ -252,4 +445,19 @@ describe('ensureSourceMarker', () => { expect(ensureSourceMarker(root, shell, 'tanstack-start').action).toBe('no-anchor'); expect(readFileSync(path.join(root, shell), 'utf8')).toBe('export const x = 1;\n'); }); + + it('persists cleanup when a stale managed block has no new anchor', () => { + const shell = 'root.tsx'; + const clean = 'export const x = 1;\n'; + const stale = [ + '{/* #region patchstack (managed by patchstack-connect — do not edit) */}', + buildSourceMarkerSnippet('tanstack-start'), + '{/* #endregion patchstack */}', + clean, + ].join('\n'); + writeFileSync(path.join(root, shell), stale); + + expect(ensureSourceMarker(root, shell, 'tanstack-start').action).toBe('no-anchor'); + expect(readFileSync(path.join(root, shell), 'utf8')).toBe(clean); + }); });