mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Live mode had no TanStack coverage: a TanStack Start user hit disconnects and static previews because there is no static index.html to inject and no adapter for the SSR root document. - New tanstack-adapter.mjs, modeled on the SvelteKit/Nuxt adapters: detects a TanStack Start project (@tanstack/react-start + src/routes/__root.tsx) and patches the __root document to mount a generated dev-only React component (src/impeccable/ImpeccableLiveRoot) that appends the live bundle on the client after hydration, carrying the ?token= param via buildLiveScriptSrc. Patch/unpatch round-trips byte-for-byte and is idempotent; refuses to clobber an unmanaged file at the component path. - Wire detection into live-inject.mjs (insert + remove + gitignore), ordered so SvelteKit/Nuxt win and a plain TanStack Router SPA falls through to the baseline Vite index.html path. - tanstack-router-vite fixture (baseline, no adapter) and tanstack-start fixture (SSR adapter), both with runtime blocks. Both pass the full live-e2e cycle (handshake, steer, pick, Go, cycle, accept, carbonize, reloadProbe). - Unit tests for detection + patch round-trip + apply/remove; tanstack-start branches in framework-fixtures.test.mjs; live.md framework table + adapter note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
176 lines
7.1 KiB
JavaScript
176 lines
7.1 KiB
JavaScript
/**
|
|
* Unit tests for the TanStack Start live-mode adapter.
|
|
* Run with: node --test tests/live-tanstack-adapter.test.mjs
|
|
*/
|
|
|
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
detectTanStackStartProject,
|
|
applyTanStackLiveAdapter,
|
|
removeTanStackLiveAdapter,
|
|
patchTanStackRoot,
|
|
unpatchTanStackRoot,
|
|
buildTanStackLiveRootComponent,
|
|
} from '../skill/scripts/live/tanstack-adapter.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const ROOT_TSX = `import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router';
|
|
|
|
export const Route = createRootRoute({
|
|
shellComponent: RootDocument,
|
|
});
|
|
|
|
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<HeadContent />
|
|
</head>
|
|
<body>
|
|
{children}
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
`;
|
|
|
|
function scaffold(tmp, { ext = 'tsx', rootBody = ROOT_TSX, startPackage = '@tanstack/react-start' } = {}) {
|
|
mkdirSync(join(tmp, 'src', 'routes'), { recursive: true });
|
|
writeFileSync(join(tmp, 'package.json'), JSON.stringify({
|
|
name: 'app',
|
|
dependencies: { '@tanstack/react-router': '^1', [startPackage]: '^1' },
|
|
}));
|
|
writeFileSync(join(tmp, 'src', 'routes', `__root.${ext}`), rootBody);
|
|
}
|
|
|
|
describe('tanstack-adapter — detection', () => {
|
|
let tmp;
|
|
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-tanstack-')); });
|
|
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
|
|
|
it('detects a TanStack Start project from package + root route', () => {
|
|
scaffold(tmp);
|
|
const project = detectTanStackStartProject(tmp);
|
|
assert.equal(project.rootRoute, 'src/routes/__root.tsx');
|
|
assert.equal(project.componentFile, 'src/impeccable/ImpeccableLiveRoot.tsx');
|
|
assert.equal(project.componentImport, '../impeccable/ImpeccableLiveRoot');
|
|
});
|
|
|
|
it('mirrors the root-route extension for the mount component (jsx)', () => {
|
|
scaffold(tmp, { ext: 'jsx' });
|
|
const project = detectTanStackStartProject(tmp);
|
|
assert.equal(project.rootRoute, 'src/routes/__root.jsx');
|
|
assert.equal(project.componentFile, 'src/impeccable/ImpeccableLiveRoot.jsx');
|
|
});
|
|
|
|
it('detects @tanstack/solid-start and @tanstack/start too', () => {
|
|
scaffold(tmp, { startPackage: '@tanstack/solid-start' });
|
|
assert.ok(detectTanStackStartProject(tmp));
|
|
});
|
|
|
|
it('returns null without the Start package (plain TanStack Router SPA)', () => {
|
|
mkdirSync(join(tmp, 'src', 'routes'), { recursive: true });
|
|
writeFileSync(join(tmp, 'package.json'), JSON.stringify({
|
|
dependencies: { '@tanstack/react-router': '^1' },
|
|
}));
|
|
writeFileSync(join(tmp, 'src', 'routes', '__root.tsx'), ROOT_TSX);
|
|
assert.equal(detectTanStackStartProject(tmp), null);
|
|
});
|
|
|
|
it('returns null without a root route file', () => {
|
|
writeFileSync(join(tmp, 'package.json'), JSON.stringify({
|
|
dependencies: { '@tanstack/react-start': '^1' },
|
|
}));
|
|
assert.equal(detectTanStackStartProject(tmp), null);
|
|
});
|
|
});
|
|
|
|
describe('tanstack-adapter — patch/unpatch round-trip', () => {
|
|
it('inserts the import + mount component before <Scripts />', () => {
|
|
const patched = patchTanStackRoot(ROOT_TSX, '../impeccable/ImpeccableLiveRoot');
|
|
assert.match(patched, /import ImpeccableLiveRoot from '\.\.\/impeccable\/ImpeccableLiveRoot';/);
|
|
assert.match(patched, /\{\/\* impeccable-live-tanstack-start \*\/\}/);
|
|
assert.match(patched, /<ImpeccableLiveRoot \/>/);
|
|
// component renders before <Scripts />
|
|
assert.ok(patched.indexOf('<ImpeccableLiveRoot />') < patched.indexOf('<Scripts />'));
|
|
});
|
|
|
|
it('round-trips byte-for-byte (patch then unpatch)', () => {
|
|
const patched = patchTanStackRoot(ROOT_TSX, '../impeccable/ImpeccableLiveRoot');
|
|
assert.notEqual(patched, ROOT_TSX);
|
|
assert.equal(unpatchTanStackRoot(patched), ROOT_TSX);
|
|
});
|
|
|
|
it('is idempotent (double patch adds one import + one mount)', () => {
|
|
const once = patchTanStackRoot(ROOT_TSX, '../impeccable/ImpeccableLiveRoot');
|
|
const twice = patchTanStackRoot(once, '../impeccable/ImpeccableLiveRoot');
|
|
assert.equal(twice, once);
|
|
assert.equal((twice.match(/<ImpeccableLiveRoot \/>/g) || []).length, 1);
|
|
assert.equal((twice.match(/^import ImpeccableLiveRoot/gm) || []).length, 1);
|
|
});
|
|
|
|
it('falls back to </body> when <Scripts /> is absent', () => {
|
|
const noScripts = ROOT_TSX.replace(/\s*<Scripts \/>/, '');
|
|
const patched = patchTanStackRoot(noScripts, '../impeccable/ImpeccableLiveRoot');
|
|
assert.match(patched, /<ImpeccableLiveRoot \/>/);
|
|
assert.ok(patched.indexOf('<ImpeccableLiveRoot />') < patched.indexOf('</body>'));
|
|
assert.equal(unpatchTanStackRoot(patched), noScripts);
|
|
});
|
|
|
|
it('builds a client-only mount component carrying the token', () => {
|
|
const body = buildTanStackLiveRootComponent(8123, 'tok-xyz');
|
|
assert.match(body, /http:\/\/localhost:8123\/live\.js\?token=tok-xyz/);
|
|
assert.match(body, /useEffect/);
|
|
assert.match(body, /typeof document === 'undefined'/);
|
|
assert.match(body, /data-impeccable-live-tanstack/);
|
|
});
|
|
});
|
|
|
|
describe('tanstack-adapter — apply/remove on disk', () => {
|
|
let tmp;
|
|
beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-tanstack-')); });
|
|
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
|
|
|
it('apply writes the component + patches root, remove restores byte-for-byte', () => {
|
|
scaffold(tmp);
|
|
const original = readFileSync(join(tmp, 'src/routes/__root.tsx'), 'utf-8');
|
|
|
|
const applied = applyTanStackLiveAdapter({ cwd: tmp, port: 9100, token: 'T1' });
|
|
assert.equal(applied.adapter, 'tanstack-start');
|
|
assert.equal(applied.inserted, true);
|
|
assert.ok(existsSync(join(tmp, 'src/impeccable/ImpeccableLiveRoot.tsx')));
|
|
assert.match(readFileSync(join(tmp, 'src/routes/__root.tsx'), 'utf-8'), /ImpeccableLiveRoot/);
|
|
assert.match(
|
|
readFileSync(join(tmp, 'src/impeccable/ImpeccableLiveRoot.tsx'), 'utf-8'),
|
|
/localhost:9100\/live\.js\?token=T1/,
|
|
);
|
|
|
|
const removed = removeTanStackLiveAdapter({ cwd: tmp });
|
|
assert.equal(removed.removed, true);
|
|
assert.equal(existsSync(join(tmp, 'src/impeccable/ImpeccableLiveRoot.tsx')), false);
|
|
assert.equal(existsSync(join(tmp, 'src/impeccable')), false, 'empty managed dir pruned');
|
|
assert.equal(readFileSync(join(tmp, 'src/routes/__root.tsx'), 'utf-8'), original);
|
|
});
|
|
|
|
it('refuses to clobber an unmanaged file at the component path', () => {
|
|
scaffold(tmp);
|
|
mkdirSync(join(tmp, 'src/impeccable'), { recursive: true });
|
|
writeFileSync(join(tmp, 'src/impeccable/ImpeccableLiveRoot.tsx'), 'export const mine = 1;\n');
|
|
const result = applyTanStackLiveAdapter({ cwd: tmp, port: 9100, token: 'T1' });
|
|
assert.equal(result.error, 'tanstack_component_conflict');
|
|
// unmanaged file untouched
|
|
assert.equal(
|
|
readFileSync(join(tmp, 'src/impeccable/ImpeccableLiveRoot.tsx'), 'utf-8'),
|
|
'export const mine = 1;\n',
|
|
);
|
|
});
|
|
});
|