diff --git a/apps/daemon/src/deploy.ts b/apps/daemon/src/deploy.ts index 2c9d20f4a7..951aad84e7 100644 --- a/apps/daemon/src/deploy.ts +++ b/apps/daemon/src/deploy.ts @@ -105,6 +105,13 @@ export async function buildDeployFileSet(projectsRoot, projectId, entryName, opt base: entryBase, })); + // Inline `` text that lives inside a + // `', + ); + expect(refs).toEqual([]); + }); + + it('rewrites url() and @import refs in css content relative to baseDir', () => { + expect( + rewriteCssReferences( + '@import "theme.css";body{background:url("bg.png")}', + 'sub', + ), + ).toBe('@import "sub/theme.css";body{background:url("sub/bg.png")}'); + }); + + it('keeps remote, data, and absolute css refs intact when rewriting', () => { + expect( + rewriteCssReferences( + 'body{background:url("https://cdn.test/a.png");--data:url(data:image/png,abc);--root:url("/abs.png")}', + 'sub', + ), + ).toBe( + 'body{background:url("https://cdn.test/a.png");--data:url(data:image/png,abc);--root:url("/abs.png")}', + ); + }); + + it('bundles assets referenced from inline ', + ); + await writeFile(path.join(dir, 'theme.css'), 'body{color:red}'); + await writeFile(path.join(dir, 'assets', 'bg.png'), 'bg'); + await writeFile(path.join(dir, 'fonts', 'custom.woff2'), 'font'); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'index.html'); + + expect(files.map((f) => f.file).sort()).toEqual([ + 'assets/bg.png', + 'fonts/custom.woff2', + 'index.html', + 'theme.css', + ]); + }); + + it('bundles assets referenced from style="" attributes', async () => { + const { projectsRoot, projectId, dir } = await setupProject(); + await mkdir(path.join(dir, 'assets')); + await writeFile( + path.join(dir, 'index.html'), + '
x
', + ); + await writeFile(path.join(dir, 'assets', 'hero.png'), 'hero'); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'index.html'); + + expect(files.map((f) => f.file).sort()).toEqual(['assets/hero.png', 'index.html']); + }); + + it('rewrites inline ', + ); + await writeFile(path.join(dir, 'sub', 'assets', 'bg.png'), 'bg'); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'sub/page.html'); + const index = files.find((f) => f.file === 'index.html'); + + expect(files.map((f) => f.file).sort()).toEqual(['index.html', 'sub/assets/bg.png']); + expect(index?.data.toString('utf8')).toContain('url("sub/assets/bg.png")'); + }); + + it('rewrites style="" url() refs when entry is in a subdirectory', async () => { + const { projectsRoot, projectId, dir } = await setupProject(); + await mkdir(path.join(dir, 'sub'), { recursive: true }); + await writeFile( + path.join(dir, 'sub', 'page.html'), + "
x
", + ); + await writeFile(path.join(dir, 'sub', 'hero.png'), 'hero'); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'sub/page.html'); + const index = files.find((f) => f.file === 'index.html'); + + expect(files.map((f) => f.file).sort()).toEqual(['index.html', 'sub/hero.png']); + expect(index?.data.toString('utf8')).toContain("url('sub/hero.png')"); + }); + + it('reports inline ', + ); + + await expect( + buildDeployFileSet(projectsRoot, projectId, 'index.html'), + ).rejects.toMatchObject({ + details: { missing: ['assets/missing.png'] }, + }); + }); + + it('extracts and rewrites url() refs from ', + ); + await writeFile(path.join(dir, 'sub', 'assets', 'icon.svg'), ''); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'sub/page.html'); + const index = files.find((f) => f.file === 'index.html'); + + expect(files.map((f) => f.file).sort()).toEqual(['index.html', 'sub/assets/icon.svg']); + expect(index?.data.toString('utf8')).toContain('url("sub/assets/icon.svg")'); + }); + + it('does not rewrite \';'; + await writeFile(path.join(dir, 'sub', 'page.html'), html); + + const files = await buildDeployFileSet(projectsRoot, projectId, 'sub/page.html'); + const index = files.find((f) => f.file === 'index.html'); + + // The fake ';", + ); + }); + + it('does not rewrite -->

x

'; + expect(rewriteEntryHtmlReferences(html, 'sub')).toBe(html); + }); + + it('runs in linear time on pathological unclosed url(', () => { + const huge = '('.repeat(100_000); + const input = `body{background:url${huge}}`; + const startExtract = Date.now(); + const refs = extractCssReferences(input); + expect(Date.now() - startExtract).toBeLessThan(500); + expect(refs).toEqual([]); + + const startRewrite = Date.now(); + expect(rewriteCssReferences(input, 'sub')).toBe(input); + expect(Date.now() - startRewrite).toBeLessThan(500); + }); }); describe('deployment link readiness', () => {