import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { normalizeArtifactRuntimeImports } from '../../src/artifacts/runtime-compat.js'; import { writeProjectFile } from '../../src/projects.js'; const brokenReactMotionHtml = `
`; describe('artifact runtime compatibility normalizer', () => { it('rewrites vanilla Motion UMD to Framer Motion UMD when React Motion hooks are used', () => { const normalized = normalizeArtifactRuntimeImports('landing.html', brokenReactMotionHtml); expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('https://unpkg.com/motion@11.11.13/dist/motion.js'); }); it('preserves non-HTML files and HTML that does not use Motion React hooks', () => { expect(normalizeArtifactRuntimeImports('notes.md', brokenReactMotionHtml)).toBe(brokenReactMotionHtml); const vanillaMotionHtml = ''; expect(normalizeArtifactRuntimeImports('animation.html', vanillaMotionHtml)).toBe(vanillaMotionHtml); }); it('removes stale integrity from rewritten script tags', () => { const html = brokenReactMotionHtml.replace( 'src="https://unpkg.com/motion@11.11.13/dist/motion.js"', 'src="https://unpkg.com/motion@11.11.13/dist/motion.js" integrity="sha384-stale"', ); const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('sha384-stale'); }); it('aliases the FramerMotion global for framer-motion UMD artifacts', () => { const html = ` `; const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('window.FramerMotion = window.FramerMotion || window.Motion;'); expect(normalized.indexOf('framer-motion@11.11.17')).toBeLessThan(normalized.indexOf('window.FramerMotion')); }); it('normalizes content before writeProjectFile persists it', async () => { const projectsRoot = await mkdtemp(path.join(tmpdir(), 'od-runtime-compat-')); try { await writeProjectFile(projectsRoot, 'project-1', 'landing.html', Buffer.from(brokenReactMotionHtml, 'utf8')); const saved = await readFile(path.join(projectsRoot, 'project-1', 'landing.html'), 'utf8'); expect(saved).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(saved).not.toContain('https://unpkg.com/motion@11.11.13/dist/motion.js'); } finally { await rm(projectsRoot, { recursive: true, force: true }); } }); // Port-path regression coverage for orbit-style React prototypes. This pattern leans on // the full Motion hook set (useScroll/useTransform/useAnimationFrame/useMotionValue), which only the // framer-motion React UMD build exposes. A single-file prototype that reaches for the vanilla // motion.js DOM bundle throws `useScroll is not a function` and renders blank — this is the artifact // the normalizer must repair at the write boundary. const orbitPrototypeHtml = `
`; it('repairs an orbit prototype that uses the full Motion hook set', () => { const normalized = normalizeArtifactRuntimeImports('orbit-prototype.html', orbitPrototypeHtml) as string; expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('/dist/motion.js'); // The destructured `Motion` global now resolves to the React build that actually carries the hooks. expect(normalized).toContain('useAnimationFrame'); }); it('rewrites the minified vanilla Motion bundle as well', () => { const html = brokenReactMotionHtml.replace('dist/motion.js', 'dist/motion.min.js'); const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('/dist/motion.min.js'); }); it('repairs a prototype that loads the wrong bundle and reads the FramerMotion global', () => { const html = brokenReactMotionHtml.replace('const { motion, useScroll, useTransform } = Motion;', 'const { motion, useScroll, useTransform } = FramerMotion;'); const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('/dist/motion.js'); expect(normalized).toContain('window.FramerMotion = window.FramerMotion || window.Motion;'); }); it('repairs the right package name but wrong file (framer-motion@.../dist/motion.js)', () => { const html = brokenReactMotionHtml.replace( 'https://unpkg.com/motion@11.11.13/dist/motion.js', 'https://unpkg.com/framer-motion@11.11.13/dist/motion.js', ); const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('/dist/motion.js'); }); it('repairs the vanilla bundle served from jsdelivr and preserves the host', () => { const html = brokenReactMotionHtml.replace( 'https://unpkg.com/motion@11.11.13/dist/motion.js', 'https://cdn.jsdelivr.net/npm/motion@11.11.13/dist/motion.js', ); const normalized = normalizeArtifactRuntimeImports('landing.html', html) as string; expect(normalized).toContain('https://cdn.jsdelivr.net/npm/framer-motion@11.11.13/dist/framer-motion.js'); expect(normalized).not.toContain('/dist/motion.js'); }); it('leaves the correct framer-motion.js bundle untouched (no false rewrite)', () => { const html = brokenReactMotionHtml.replace( 'https://unpkg.com/motion@11.11.13/dist/motion.js', 'https://unpkg.com/framer-motion@11.11.13/dist/framer-motion.js', ); // Already correct → returned verbatim (idempotent), the vanilla-file matcher must not touch it. expect(normalizeArtifactRuntimeImports('landing.html', html)).toBe(html); }); });