Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 86 additions & 22 deletions src/mark-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<head>`, then `<body>`. A marker the
* developer placed themselves is adopted rather than duplicated.
* Anchors as the first child of `<head>`, then `<body>`, 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)) {
Expand All @@ -196,37 +269,28 @@ 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 =>
[REGION_OPEN, ...buildSourceMarkerSnippet(framework).split('\n'), REGION_CLOSE]
.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]*)<head>[ \t]*$/m, /^([ \t]*)<body>[ \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 {
Expand All @@ -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 };
Expand Down
208 changes: 208 additions & 0 deletions tests/mark-build.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -183,6 +184,38 @@ describe('ensureMarkerInJsxShell', () => {
const doc = (body: string): string =>
['export const Root = () => (', ' <html>', ' <head>', ' </head>', ' <body>', body, ' </body>', ' </html>', ');'].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 = () => (',
' <html>',
' <head>',
' <title>Example</title>',
' </head>',
' <body>',
' <main />',
' <script',
' src="https://cdn.patchstack.com/patchstack-widget.js"',
' data-site-uuid={patchstackConfig.siteUuid}',
' defer',
' />',
' </body>',
' </html>',
');',
].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');
Expand All @@ -196,13 +229,173 @@ describe('ensureMarkerInJsxShell', () => {
expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('</head>'));
});

it('finds the end of an attributed multiline head tag', () => {
const attributedHead = [
'const count = 1;',
'export const Root = () => (',
' <html>',
' <head',
' data-label="one > zero"',
' data-active={count > 0}',
' >',
' <title>Example</title>',
' </head>',
' </html>',
');',
].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('<title>'));
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</title></head>',
' </html>',
');',
].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('<head>'));
expect(source.indexOf('__PATCHSTACK_PROD__')).toBeLessThan(source.indexOf('<title>'));
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;
expect((twice.match(/__PATCHSTACK_PROD__/g) ?? []).length).toBe(1);
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');
Expand Down Expand Up @@ -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);
});
});
Loading