[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 { defineNuxtModule, addServerPlugin, addVitePlugin, createResolver } from '@nuxt/kit' 3import { rolldown } from 'rolldown' 4import { runCosLoader } from './runtime/loader' 5import type { CosManifest } from './runtime/loader' 6 7export interface ModuleOptions { 8 /** 9 * Packages to extract into standalone Cross-Origin Storage chunks. 10 * Each entry is matched against the imported module specifier; a plain 11 * string is treated as an exact match. 12 */ 13 packages: Array<string | RegExp> 14} 15 16/** 17 * Recipe version embedded in every content-addressed specifier, meant to be bumped 18 * whenever the build recipe (bundler version, options, define replacements) 19 * changes in a way that alters emitted bytes, so chunks built under different 20 * recipes cannot silently collide on the same SHA-256. 21 */ 22const RECIPE = 'cos1' 23 24function contentSpecifier(hash: string): string { 25 return `${RECIPE}:${hash}` 26} 27 28export default defineNuxtModule<ModuleOptions>({ 29 meta: { 30 name: 'nuxt-cos', 31 configKey: 'cos', 32 }, 33 defaults: { 34 packages: [/^(?:vue$|@vue\/)/], 35 }, 36 setup(options, nuxt) { 37 if (nuxt.options.dev) { 38 return 39 } 40 41 const resolver = createResolver(import.meta.url) 42 const packages = options.packages.map(p => typeof p === 'string' ? new RegExp(`^${p}$`) : p) 43 44 let scriptContent = '' 45 46 nuxt.options.nitro.virtual ||= {} 47 nuxt.options.nitro.virtual['virtual:cos-loader'] = () => `export default ${JSON.stringify(scriptContent)}` 48 49 addServerPlugin(resolver.resolve('./runtime/server/plugins/inject')) 50 51 const collected = new Set<string>() 52 53 addVitePlugin(() => ({ 54 name: 'nuxt-cos', 55 enforce: 'pre', 56 resolveId: { 57 order: 'pre', 58 async handler(id, importer, resolveOptions) { 59 if (!packages.some(p => p.test(id))) { 60 return 61 } 62 63 const resolved = await this.resolve(id, importer, { ...resolveOptions, skipSelf: true }) 64 if (!resolved) { 65 return 66 } 67 68 collected.add(resolved.id) 69 70 // Externalise under a synthetic specifier so it never clashes with the 71 // real module id elsewhere in the app graph. It is rewritten to a 72 // content-addressed specifier in `generateBundle`, once every managed 73 // chunk has been hashed bottom-up. 74 return { id: `cos-ext:${resolved.id}`, external: true } 75 }, 76 }, 77 async generateBundle(_outputOptions, bundle) { 78 const ids = [...collected] 79 const idSet = new Set(ids) 80 81 // Build each managed package once, externalising its siblings. The raw 82 // output keeps sibling imports as their resolved absolute ids, which 83 // double as the dependency edges between managed chunks. 84 const raw = new Map<string, { code: string, deps: string[] }>() 85 for (const input of ids) { 86 const builder = await rolldown({ 87 input, 88 platform: 'browser', 89 treeshake: false, 90 external: ids.filter(id => id !== input), 91 }) 92 const { output } = await builder.generate({ file: 'chunk.js', codeSplitting: false, minify: true }) 93 await builder.close() 94 95 const code = output[0].code 96 const deps = [...new Set([...code.matchAll(/(?:from|import)\s*["']([^"']+)["']/g)].map(m => m[1]!))] 97 .filter(spec => idSet.has(spec)) 98 raw.set(input, { code, deps }) 99 } 100 101 // Hash bottom-up: a chunk's specifier for a dependency is that 102 // dependency's content hash, so a chunk can only be hashed once all of 103 // its dependencies have been. The npm graph for these packages is a DAG. 104 const hashes = new Map<string, string>() 105 const managed: CosManifest['chunks'] = {} 106 107 const visit = (id: string, stack: string[]): string => { 108 const existing = hashes.get(id) 109 if (existing) { 110 return existing 111 } 112 if (stack.includes(id)) { 113 throw new Error(`[nuxt-cos] dependency cycle between managed packages: ${[...stack, id].join(' -> ')}`) 114 } 115 116 const { code, deps } = raw.get(id)! 117 let resolved = code 118 for (const dep of deps) { 119 resolved = rewriteSpecifier(resolved, dep, contentSpecifier(visit(dep, [...stack, id]))) 120 } 121 122 const hash = createHash('sha256').update(resolved).digest('hex') 123 const fileName = `_nuxt/${hash}.js` 124 hashes.set(id, hash) 125 managed[contentSpecifier(hash)] = { file: `${hash}.js`, hash } 126 bundle[fileName] = { 127 type: 'asset', 128 fileName, 129 name: hash, 130 names: [hash], 131 originalFileName: null, 132 originalFileNames: [], 133 needsCodeReference: false, 134 source: resolved, 135 } 136 return hash 137 } 138 139 for (const id of ids) { 140 visit(id, []) 141 } 142 143 let entry: CosManifest['entry'] | undefined 144 for (const file of Object.values(bundle)) { 145 if (file.type !== 'chunk') { 146 continue 147 } 148 for (const id of ids) { 149 file.code = rewriteSpecifier(file.code, `cos-ext:${id}`, contentSpecifier(hashes.get(id)!)) 150 } 151 if (file.isEntry) { 152 // the entry is app-specific and should not be content-addressed 153 entry = { specifier: `${RECIPE}:entry`, file: file.fileName.replace(/^_nuxt\//, '') } 154 } 155 } 156 157 if (!entry) { 158 return 159 } 160 161 const manifest: CosManifest = { base: '/_nuxt/', entry, chunks: managed } 162 scriptContent = `(${runCosLoader.toString()})(${JSON.stringify(manifest)})` 163 }, 164 }), { client: true, server: false }) 165 }, 166}) 167 168function rewriteSpecifier(code: string, from: string, to: string): string { 169 const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 170 const fromImport = new RegExp(`((?:import|export)\\b[^;'"\\n]*?from\\s*|import\\s*|export\\s*\\*\\s*from\\s*)(["'])${escaped}\\2`, 'g') 171 const bareImport = new RegExp(`(\\bimport\\s*)(["'])${escaped}\\2`, 'g') 172 const dynamic = new RegExp(`(\\bimport\\s*\\(\\s*)(["'])${escaped}\\2(\\s*\\))`, 'g') 173 return code 174 .replace(dynamic, `$1$2${to}$2$3`) 175 .replace(fromImport, `$1$2${to}$2`) 176 .replace(bareImport, `$1$2${to}$2`) 177}