[READ-ONLY] Mirror of https://github.com/danielroe/cross-origin-storage. Load shared dependencies from Cross-Origin Storage (COS).
cross-origin-storage experimental nuxt vite vite-plugin
0

Configure Feed

Select the types of activity you want to include in your feed.

1import { createHash } from 'node:crypto' 2import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync, globSync } from 'node:fs' 3import { fileURLToPath } from 'node:url' 4import { join } from 'node:path' 5import { afterAll, beforeAll, describe, expect, it } from 'vitest' 6import { build } from 'vite' 7import { cosPlugin } from '../src/vite' 8import type { Alias } from 'vite' 9 10// Build inside the project tree so fixtures resolve packages from the project 11// node_modules rather than a detached temp dir. 12const scratchRoot = fileURLToPath(new URL('./.plugin-scratch', import.meta.url)) 13const nodeModules = fileURLToPath(new URL('../node_modules', import.meta.url)) 14 15function resolvePkg(glob: string): string { 16 const match = globSync(glob, { cwd: nodeModules })[0] 17 if (!match) { 18 throw new Error(`fixture dependency not found: ${glob}`) 19 } 20 return join(nodeModules, match) 21} 22 23interface Built { 24 outDir: string 25 assetsDir: string 26 cosChunks: () => string[] 27 specifiersOf: (file: string) => string[] 28 html: () => string 29} 30 31async function buildApp(entry: string, packages: Array<string | RegExp>, alias: Alias[]): Promise<Built> { 32 mkdirSync(scratchRoot, { recursive: true }) 33 const root = mkdtempSync(join(scratchRoot, 'app-')) 34 const outDir = join(root, 'dist') 35 const assetsDir = join(outDir, 'assets') 36 mkdirSync(join(root, 'src'), { recursive: true }) 37 writeFileSync( 38 join(root, 'index.html'), 39 '<!doctype html><html><head></head><body><script type="module" src="/src/main.js"></script></body></html>', 40 ) 41 writeFileSync(join(root, 'src/main.js'), entry) 42 43 await build({ 44 root, 45 logLevel: 'error', 46 resolve: { alias }, 47 plugins: [cosPlugin({ packages })], 48 build: { outDir, emptyOutDir: true, rollupOptions: { input: join(root, 'index.html') } }, 49 }) 50 51 return { 52 outDir, 53 assetsDir, 54 cosChunks: () => readdirSync(assetsDir).filter(f => /^[a-f0-9]{64}\.js$/.test(f)), 55 specifiersOf: (file) => { 56 const code = readFileSync(join(assetsDir, file), 'utf8') 57 return [...new Set([...code.matchAll(/(?:from|import)\s*["']([^"']+)["']/g)].map(m => m[1]!))] 58 }, 59 html: () => readFileSync(join(outDir, 'index.html'), 'utf8'), 60 } 61} 62 63afterAll(() => { 64 rmSync(scratchRoot, { recursive: true, force: true }) 65}) 66 67describe('cosPlugin with vue', () => { 68 let app: Built 69 70 beforeAll(async () => { 71 app = await buildApp( 72 'import { ref } from "vue"\ndocument.body.dataset.count = String(ref(0).value)\n', 73 [/^(?:vue$|@vue\/)/], 74 [{ find: 'vue', replacement: resolvePkg('.pnpm/vue@*/node_modules/vue/dist/vue.runtime.esm-bundler.js') }], 75 ) 76 }, 120_000) 77 78 it('emits content-addressed chunks whose names match their bytes', () => { 79 expect(app.cosChunks().length).toBeGreaterThanOrEqual(1) 80 for (const file of app.cosChunks()) { 81 const hash = createHash('sha256').update(readFileSync(join(app.assetsDir, file))).digest('hex') 82 expect(hash).toBe(file.replace('.js', '')) 83 } 84 }) 85 86 it('rewrites managed imports to content-addressed specifiers', () => { 87 for (const file of app.cosChunks()) { 88 for (const specifier of app.specifiersOf(file)) { 89 expect(specifier).toMatch(/^cos1:[a-f0-9]{64}$/) 90 } 91 } 92 }) 93 94 it('injects the loader into index.html and removes the default entry script', () => { 95 const html = app.html() 96 expect(html).toContain('<script id="cos-loader">') 97 expect(html).toMatch(/cos1:[a-f0-9]{64}/) 98 expect(html).not.toMatch(/<script type="module"[^>]*src="[^"]*\.js"/) 99 }) 100 101 it('derives the base path from the vite config', () => { 102 expect(app.html()).toMatch(/"base":"\/assets\/"/) 103 }) 104}) 105 106describe('cosPlugin with a non-vue package graph (unhead + hookable)', () => { 107 let app: Built 108 109 beforeAll(async () => { 110 // unhead imports hookable transitively; managing only `unhead` should still 111 // externalise hookable into its own shared chunk via auto-collection, 112 // exactly as @vue/shared is for the vue graph. This proves the algorithm is 113 // package-agnostic, not vue-shaped, and that transitive deps are collected. 114 app = await buildApp( 115 'import { createHead } from "unhead/client"\ndocument.title = String(!!createHead)\n', 116 ['unhead/client'], 117 [ 118 { find: /^unhead\/client$/, replacement: resolvePkg('.pnpm/unhead@*/node_modules/unhead/dist/client.mjs') }, 119 { find: /^hookable$/, replacement: resolvePkg('.pnpm/hookable@*/node_modules/hookable/dist/index.mjs') }, 120 ], 121 ) 122 }, 120_000) 123 124 it('auto-collects transitive deps the app never imported directly', () => { 125 // The app imports only `unhead/client`, yet hookable (a transitive dep) and 126 // unhead's internal shared chunks each become their own managed chunk. 127 expect(app.cosChunks().length).toBeGreaterThan(1) 128 for (const file of app.cosChunks()) { 129 const hash = createHash('sha256').update(readFileSync(join(app.assetsDir, file))).digest('hex') 130 expect(hash).toBe(file.replace('.js', '')) 131 } 132 }) 133 134 it('externalises shared deps into leaf chunks rather than inlining them', () => { 135 // A leaf imports no managed chunk; if deps were inlined there would be no 136 // leaves, and a chunk with deps depends on those leaves. 137 const leaves = app.cosChunks().filter(f => app.specifiersOf(f).length === 0) 138 expect(leaves.length).toBeGreaterThanOrEqual(1) 139 140 const dependants = app.cosChunks().filter(f => app.specifiersOf(f).length > 0) 141 expect(dependants.length).toBeGreaterThanOrEqual(1) 142 }) 143 144 it('references dependencies only by content-addressed specifier', () => { 145 for (const file of app.cosChunks()) { 146 for (const specifier of app.specifiersOf(file)) { 147 expect(specifier).toMatch(/^cos1:[a-f0-9]{64}$/) 148 } 149 } 150 }) 151})