|  | 
|  | 1 | +import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; | 
|  | 2 | +import { sentryVitePlugin } from '@sentry/vite-plugin'; | 
|  | 3 | +import * as fs from 'fs'; | 
|  | 4 | +import * as path from 'path'; | 
|  | 5 | +// @ts-ignore -sorcery has no types :( | 
|  | 6 | +// eslint-disable-next-line import/default | 
|  | 7 | +import * as sorcery from 'sorcery'; | 
|  | 8 | +import type { Plugin } from 'vite'; | 
|  | 9 | + | 
|  | 10 | +const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = { | 
|  | 11 | +  // TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options | 
|  | 12 | +  include: ['build/server', 'build/client'], | 
|  | 13 | +}; | 
|  | 14 | + | 
|  | 15 | +// sorcery has no types, so these are some basic type definitions: | 
|  | 16 | +type Chain = { | 
|  | 17 | +  write(): Promise<void>; | 
|  | 18 | +  apply(): Promise<void>; | 
|  | 19 | +}; | 
|  | 20 | +type Sorcery = { | 
|  | 21 | +  load(filepath: string): Promise<Chain>; | 
|  | 22 | +}; | 
|  | 23 | + | 
|  | 24 | +type SentryVitePluginOptionsOptionalInclude = Omit<SentryVitePluginOptions, 'include'> & { | 
|  | 25 | +  include?: SentryVitePluginOptions['include']; | 
|  | 26 | +}; | 
|  | 27 | + | 
|  | 28 | +/** | 
|  | 29 | + * Creates a new Vite plugin that uses the unplugin-based Sentry Vite plugin to create | 
|  | 30 | + * releases and upload source maps to Sentry. | 
|  | 31 | + * | 
|  | 32 | + * Because the unplugin-based Sentry Vite plugin doesn't work ootb with SvelteKit, | 
|  | 33 | + * we need to add some additional stuff to make source maps work: | 
|  | 34 | + * | 
|  | 35 | + * - the `config` hook needs to be added to generate source maps | 
|  | 36 | + * - the `configResolved` hook tells us when to upload source maps. | 
|  | 37 | + *   We only want to upload once at the end, given that SvelteKit makes multiple builds | 
|  | 38 | + * - the `closeBundle` hook is used to flatten server source maps, which at the moment is necessary for SvelteKit. | 
|  | 39 | + *   After the maps are flattened, they're uploaded to Sentry as in the original plugin. | 
|  | 40 | + *   see: https://github.com/sveltejs/kit/discussions/9608 | 
|  | 41 | + * | 
|  | 42 | + * @returns the custom Sentry Vite plugin | 
|  | 43 | + */ | 
|  | 44 | +export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Plugin { | 
|  | 45 | +  const mergedOptions = { | 
|  | 46 | +    ...DEFAULT_PLUGIN_OPTIONS, | 
|  | 47 | +    ...options, | 
|  | 48 | +  }; | 
|  | 49 | +  const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); | 
|  | 50 | + | 
|  | 51 | +  const { debug } = mergedOptions; | 
|  | 52 | +  const { buildStart, resolveId, transform, renderChunk } = sentryPlugin; | 
|  | 53 | + | 
|  | 54 | +  let upload = true; | 
|  | 55 | + | 
|  | 56 | +  const customPlugin: Plugin = { | 
|  | 57 | +    name: 'sentry-vite-plugin-custom', | 
|  | 58 | +    apply: 'build', // only apply this plugin at build time | 
|  | 59 | +    enforce: 'post', | 
|  | 60 | + | 
|  | 61 | +    // These hooks are copied from the original Sentry Vite plugin. | 
|  | 62 | +    // They're mostly responsible for options parsing and release injection. | 
|  | 63 | +    buildStart, | 
|  | 64 | +    resolveId, | 
|  | 65 | +    renderChunk, | 
|  | 66 | +    transform, | 
|  | 67 | + | 
|  | 68 | +    // Modify the config to generate source maps | 
|  | 69 | +    config: config => { | 
|  | 70 | +      // eslint-disable-next-line no-console | 
|  | 71 | +      debug && console.log('[Source Maps Plugin] Enabeling source map generation'); | 
|  | 72 | +      return { | 
|  | 73 | +        ...config, | 
|  | 74 | +        build: { | 
|  | 75 | +          ...config.build, | 
|  | 76 | +          sourcemap: true, | 
|  | 77 | +        }, | 
|  | 78 | +      }; | 
|  | 79 | +    }, | 
|  | 80 | + | 
|  | 81 | +    configResolved: config => { | 
|  | 82 | +      // The SvelteKit plugins trigger additional builds within the main (SSR) build. | 
|  | 83 | +      // We just need a mechanism to upload source maps only once. | 
|  | 84 | +      // `config.build.ssr` is `true` for that first build and `false` in the other ones. | 
|  | 85 | +      // Hence we can use it as a switch to upload source maps only once in main build. | 
|  | 86 | +      if (!config.build.ssr) { | 
|  | 87 | +        upload = false; | 
|  | 88 | +      } | 
|  | 89 | +    }, | 
|  | 90 | + | 
|  | 91 | +    // We need to start uploading source maps later than in the original plugin | 
|  | 92 | +    // because SvelteKit is still doing some stuff at closeBundle. | 
|  | 93 | +    closeBundle: () => { | 
|  | 94 | +      if (!upload) { | 
|  | 95 | +        return; | 
|  | 96 | +      } | 
|  | 97 | + | 
|  | 98 | +      // TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options | 
|  | 99 | +      const outDir = path.resolve(process.cwd(), 'build'); | 
|  | 100 | + | 
|  | 101 | +      const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js')); | 
|  | 102 | +      // eslint-disable-next-line no-console | 
|  | 103 | +      debug && console.log('[Source Maps Plugin] Flattening source maps'); | 
|  | 104 | + | 
|  | 105 | +      jsFiles.forEach(async file => { | 
|  | 106 | +        try { | 
|  | 107 | +          await (sorcery as Sorcery).load(file).then(async chain => { | 
|  | 108 | +            if (!chain) { | 
|  | 109 | +              // We end up here, if we don't have a source map for the file. | 
|  | 110 | +              // This is fine, as we're not interested in files w/o source maps. | 
|  | 111 | +              return; | 
|  | 112 | +            } | 
|  | 113 | +            // This flattens the source map | 
|  | 114 | +            await chain.apply(); | 
|  | 115 | +            // Write it back to the original file | 
|  | 116 | +            await chain.write(); | 
|  | 117 | +          }); | 
|  | 118 | +        } catch (e) { | 
|  | 119 | +          // Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly | 
|  | 120 | +          // happening in Kit-internal files which is fine as they're not in-app. | 
|  | 121 | +          // This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist. | 
|  | 122 | +          const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open'); | 
|  | 123 | +          if (debug && !isKnownError) { | 
|  | 124 | +            // eslint-disable-next-line no-console | 
|  | 125 | +            console.error('[Source Maps Plugin] error while flattening', file, e); | 
|  | 126 | +          } | 
|  | 127 | +        } | 
|  | 128 | +      }); | 
|  | 129 | + | 
|  | 130 | +      // @ts-ignore - this hook exists on the plugin! | 
|  | 131 | +      sentryPlugin.writeBundle(); | 
|  | 132 | +    }, | 
|  | 133 | +  }; | 
|  | 134 | + | 
|  | 135 | +  return customPlugin; | 
|  | 136 | +} | 
|  | 137 | + | 
|  | 138 | +function getFiles(dir: string): string[] { | 
|  | 139 | +  if (!fs.existsSync(dir)) { | 
|  | 140 | +    return []; | 
|  | 141 | +  } | 
|  | 142 | +  const dirents = fs.readdirSync(dir, { withFileTypes: true }); | 
|  | 143 | +  // eslint-disable-next-line @typescript-eslint/ban-ts-comment | 
|  | 144 | +  // @ts-ignore | 
|  | 145 | +  const files: string[] = dirents.map(dirent => { | 
|  | 146 | +    const resFileOrDir = path.resolve(dir, dirent.name); | 
|  | 147 | +    return dirent.isDirectory() ? getFiles(resFileOrDir) : resFileOrDir; | 
|  | 148 | +  }); | 
|  | 149 | + | 
|  | 150 | +  return Array.prototype.concat(...files); | 
|  | 151 | +} | 
0 commit comments