polen
Version:
A framework for delightful GraphQL developer portals
129 lines (115 loc) • 3.89 kB
text/typescript
import jsesc from 'jsesc'
// import { id,or,exclude } from 'rolldown/filter'
import {} from '@rolldown/pluginutils'
import type { Plugin } from 'vite'
export interface Codec {
stringify(value: any): string
parse(text: string): any
}
export interface VitePluginJsonImportsOptions {
/**
* Use a custom codec.
*/
codec?: {
/**
* Import path for the custom codec to use in generated code.
*
* Must be a package name or use a path alias.
*
* Examples:
* - '@my-org/my-codec' (npm package)
* - '#utils/codec' (package subpath)
* - 'my-codec' (npm package)
* - '~/utils/codec' (if ~ is aliased in vite config)
*/
importPath: string
/**
* Custom codec for validation and runtime parsing
* The codec.parse() is used at build time to validate the JSON
* The codec will be imported in the generated code for runtime parsing
* @default JSON
*/
validate?: Codec
/**
* The export (its name) to import.
*
* @default 'default'
*/
importExport?: string
}
filter?: {
/**
* Module types to process (the file extension without the dot).
* @default ['json']
*/
moduleTypes?: string[]
/**
* Picomatch patterns to include
* @default includes all files with configured extensions
*/
id?: {
include?: string | string[]
/**
* Picomatch patterns to exclude
* @default excludes node_modules and files with \0 prefix
*/
exclude?: string | string[]
}
}
/**
* Plugin name. Useful to customize if providing a custom codec e.g. `superjson`.
*
* @default 'json'
*/
name?: string
}
export const create = (options: VitePluginJsonImportsOptions = {}): Plugin => {
const codec = options.codec?.validate ?? JSON
const codecImportExport = options.codec?.importExport ?? `default`
const pluginName = options.name ?? `json-imports`
// Always exclude \0 prefixed modules
const baseExcludePatterns: (string | RegExp)[] = [/^\0/]
const moduleTypes = options.filter?.moduleTypes ?? [`json`]
const isCustomCodec = Boolean(options.codec?.importPath)
// Check if we should handle this file based on extension
const shouldHandle = (id: string) => {
// Exclude internal modules with \0 prefix
if (id.startsWith(`\0`)) return false
// Check if it has one of our configured extensions
return moduleTypes.some(type => id.endsWith(`.${type}`))
}
return {
name: pluginName,
enforce: `pre` as const,
// Transform files based on extension
transform(code, id) {
if (!shouldHandle(id)) return
try {
// Skip validation for superjson since the codec validates the envelope format
if (options.codec?.validate && !options.codec.importPath?.includes(`superjson`)) {
codec.parse(code)
}
if (!isCustomCodec) {
// For native JSON, directly export as JavaScript object literal
return `export default ${code}`
} else {
// For custom codecs, we need to parse at runtime
if (!options.codec?.importPath) {
throw new Error(`codec.importPath is required when using a custom codec`)
}
const importIdentifier = codecImportExport === `default` ? `codec` : codecImportExport
const importStatement = codecImportExport === `default`
? `import ${importIdentifier} from '${options.codec.importPath}'`
: `import { ${codecImportExport} } from '${options.codec.importPath}'`
return `
${importStatement}
const data = ${importIdentifier}.parse('${jsesc(code)}')
export { data as default }`
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
this.error(`Failed to parse JSON in ${id}: ${message}`)
}
},
}
}