[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 16interface CollectedPackage { 17 /** Bare specifiers this package is imported under (e.g. `vue`, `@vue/runtime-dom`). */ 18 specifiers: Set<string> 19 /** Output chunk basename, e.g. `vue` -> emitted as `_nuxt/vue.js`. */ 20 chunk: string 21} 22 23function bareSpecifier(chunk: string): string { 24 return `coschunk-${chunk}` 25} 26 27export default defineNuxtModule<ModuleOptions>({ 28 meta: { 29 name: 'nuxt-cos', 30 configKey: 'cos', 31 }, 32 defaults: { 33 packages: [/^(?:vue$|@vue\/)/], 34 }, 35 setup(options, nuxt) { 36 if (nuxt.options.dev) { 37 return 38 } 39 40 const resolver = createResolver(import.meta.url) 41 const packages = options.packages.map(p => typeof p === 'string' ? new RegExp(`^${p}$`) : p) 42 43 let scriptContent = '' 44 45 nuxt.options.nitro.virtual ||= {} 46 nuxt.options.nitro.virtual['virtual:cos-loader'] = () => `export default ${JSON.stringify(scriptContent)}` 47 48 addServerPlugin(resolver.resolve('./runtime/server/plugins/inject')) 49 50 const collected = new Map<string, CollectedPackage>() 51 const usedChunkNames = new Set<string>() 52 53 function chunkNameFor(specifier: string): string { 54 let index = 0 55 let name: string 56 do { 57 name = (specifier + (index ? `-${index}` : '')).replace(/[^a-z0-9]/gi, '-').replace(/(^-+)|(-+$)/g, '') 58 index++ 59 } while (usedChunkNames.has(name)) 60 usedChunkNames.add(name) 61 return name 62 } 63 64 addVitePlugin(() => ({ 65 name: 'nuxt-cos', 66 enforce: 'pre', 67 resolveId: { 68 order: 'pre', 69 async handler(id, importer, resolveOptions) { 70 if (!packages.some(p => p.test(id))) { 71 return 72 } 73 74 const resolved = await this.resolve(id, importer, { ...resolveOptions, skipSelf: true }) 75 if (!resolved) { 76 return 77 } 78 79 let pkg = collected.get(resolved.id) 80 if (!pkg) { 81 pkg = { specifiers: new Set(), chunk: chunkNameFor(id) } 82 collected.set(resolved.id, pkg) 83 } 84 pkg.specifiers.add(id) 85 86 return { id: bareSpecifier(pkg.chunk), external: true } 87 }, 88 }, 89 async generateBundle(_outputOptions, bundle) { 90 const externalIds = [...collected.keys()] 91 // Map every bare specifier any managed package may emit to its chunk. 92 const specifierToChunk = new Map<string, string>() 93 for (const pkg of collected.values()) { 94 for (const specifier of pkg.specifiers) { 95 specifierToChunk.set(specifier, pkg.chunk) 96 } 97 } 98 99 const managed: CosManifest['chunks'] = {} 100 101 for (const [input, pkg] of collected) { 102 const builder = await rolldown({ 103 input, 104 platform: 'browser', 105 treeshake: false, 106 external: externalIds.filter(id => id !== input), 107 }) 108 const { output } = await builder.generate({ file: `${pkg.chunk}.js`, codeSplitting: false }) 109 await builder.close() 110 111 let code = output[0].code 112 for (const [specifier, chunk] of specifierToChunk) { 113 code = rewriteSpecifier(code, specifier, bareSpecifier(chunk)) 114 } 115 // Imports rolldown kept as resolved absolute paths to other managed packages. 116 for (const otherId of externalIds) { 117 const chunk = collected.get(otherId)!.chunk 118 code = rewriteSpecifier(code, otherId, bareSpecifier(chunk)) 119 } 120 121 const fileName = `_nuxt/${pkg.chunk}.js` 122 const hash = createHash('sha256').update(code).digest('hex') 123 managed[bareSpecifier(pkg.chunk)] = { file: `${pkg.chunk}.js`, hash } 124 bundle[fileName] = { 125 type: 'asset', 126 fileName, 127 name: pkg.chunk, 128 names: [pkg.chunk], 129 originalFileName: null, 130 originalFileNames: [], 131 needsCodeReference: false, 132 source: code, 133 } 134 } 135 136 let entry: string | undefined 137 for (const file of Object.values(bundle)) { 138 if (file.type !== 'chunk') { 139 continue 140 } 141 for (const [specifier, chunk] of specifierToChunk) { 142 file.code = rewriteSpecifier(file.code, specifier, bareSpecifier(chunk)) 143 } 144 if (file.isEntry) { 145 entry = bareSpecifier(file.fileName) 146 managed[bareSpecifier(file.fileName)] ??= { 147 file: file.fileName.replace(/^_nuxt\//, ''), 148 hash: createHash('sha256').update(file.code).digest('hex'), 149 } 150 } 151 } 152 153 if (!entry) { 154 return 155 } 156 157 const manifest: CosManifest = { base: '/_nuxt/', entry, chunks: managed } 158 scriptContent = `(${runCosLoader.toString()})(${JSON.stringify(manifest)})` 159 }, 160 }), { client: true, server: false }) 161 }, 162}) 163 164function rewriteSpecifier(code: string, from: string, to: string): string { 165 const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 166 const fromImport = new RegExp(`((?:import|export)\\b[^;'"\\n]*?from\\s*|import\\s*|export\\s*\\*\\s*from\\s*)(["'])${escaped}\\2`, 'g') 167 const bareImport = new RegExp(`(\\bimport\\s*)(["'])${escaped}\\2`, 'g') 168 const dynamic = new RegExp(`(\\bimport\\s*\\(\\s*)(["'])${escaped}\\2(\\s*\\))`, 'g') 169 return code 170 .replace(dynamic, `$1$2${to}$2$3`) 171 .replace(fromImport, `$1$2${to}$2`) 172 .replace(bareImport, `$1$2${to}$2`) 173}