1/**
2 * Utilities for assembling the Rollup plugin pipeline used by the Effect
3 * bundle-size tooling.
4 *
5 * This module is responsible for the bundler-specific work that turns local
6 * fixture entrypoints into comparable ESM output: resolving Effect package
7 * imports against each package's built `dist` files, replacing production
8 * environment checks, lowering TypeScript with esbuild, minifying with terser,
9 * and optionally adding a bundle visualizer. It is primarily used by the
10 * Rollup service when measuring gzipped fixture sizes or opening a
11 * visualization for bundle inspection.
12 *
13 * Keep plugin ordering intentional when changing this module. Local package
14 * resolution must run before normal node resolution so workspace imports are
15 * measured from built artifacts, esbuild must emit ESM for Rollup to continue
16 * tree-shaking, and terser mangling is disabled while visualizing so reported
17 * module names stay readable.
18 *
19 * @since 4.0.0
20 */
21import { nodeResolve } from "@rollup/plugin-node-resolve"
22import replace from "@rollup/plugin-replace"
23import terser from "@rollup/plugin-terser"
24import type * as Path from "effect/Path"
25import * as Predicate from "effect/Predicate"
26import type { Plugin } from "rollup"
27import esbuild from "rollup-plugin-esbuild"
28import { type PluginVisualizerOptions, visualizer } from "rollup-plugin-visualizer"
29
30const EFFECT_PACKAGE_REGEX = /^(@effect\/[\w-]+|effect)(\/.*)?$/
31const TYPE_SCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
32
33const toLocalDistPath = (pathService: Path.Path, packageDir: string, resolvedId: string): string => {
34 const srcDir = pathService.join(packageDir, "src")
35 const relative = pathService.relative(srcDir, resolvedId)
36 if (relative === "" || relative.startsWith("..") || pathService.isAbsolute(relative)) {
37 return resolvedId
38 }
39 const extension = pathService.extname(relative)
40 if (!TYPE_SCRIPT_EXTENSIONS.has(extension)) {
41 return resolvedId
42 }
43 return pathService.join(packageDir, "dist", relative.slice(0, -extension.length) + ".js")
44}
45
46/**
47 * Options for configuring Rollup plugins.
48 *
49 * @category options
50 * @since 4.0.0
51 */
52export interface PluginOptions {
53 readonly nodeTarget?: string | undefined
54 readonly minify?: boolean | undefined
55 readonly mangle?: boolean | undefined
56 readonly visualize?: boolean | undefined
57 readonly visualizations?: ReadonlyArray<VisualizationOutput> | undefined
58}
59
60/**
61 * Output generated by the Rollup visualizer plugin.
62 *
63 * @category options
64 * @since 4.0.0
65 */
66export interface VisualizationOutput {
67 readonly filename: string
68 readonly template: NonNullable<PluginVisualizerOptions["template"]>
69 readonly title?: string | undefined
70}
71
72interface ResolvedPluginOptions {
73 readonly nodeTarget: string
74 readonly minify: boolean
75 readonly mangle: boolean
76 readonly visualize: boolean
77 readonly visualizations: ReadonlyArray<VisualizationOutput>
78}
79
80const defaultPluginOptions: ResolvedPluginOptions = {
81 nodeTarget: "node20",
82 minify: true,
83 mangle: true,
84 visualize: false,
85 visualizations: []
86}
87
88/**
89 * Merges provided options with defaults.
90 */
91const resolvePluginOptions = (options: PluginOptions): ResolvedPluginOptions => ({
92 nodeTarget: options.nodeTarget ?? defaultPluginOptions.nodeTarget,
93 minify: options.minify ?? defaultPluginOptions.minify,
94 mangle: options.mangle ?? defaultPluginOptions.mangle,
95 visualize: options.visualize ?? defaultPluginOptions.visualize,
96 visualizations: options.visualizations ?? defaultPluginOptions.visualizations
97})
98
99/**
100 * Creates a custom Rollup plugin that resolves Effect package imports to their
101 * local dist directories.
102 *
103 * @category constructors
104 * @since 4.0.0
105 */
106export const createResolveLocalPackageImports = (pathService: Path.Path): Plugin => ({
107 name: "rollup-plugin-resolve-imports",
108 async resolveId(source, importer) {
109 const match = source.match(EFFECT_PACKAGE_REGEX)
110 if (Predicate.isNotNull(match)) {
111 const packageName = match[1]
112 const packageJson = await this.resolve(`${packageName}/package.json`, importer, { skipSelf: true })
113 if (packageJson === null) return null
114 const resolved = await this.resolve(source, importer, { skipSelf: true })
115 if (resolved === null) return null
116 return {
117 ...resolved,
118 id: toLocalDistPath(pathService, pathService.dirname(packageJson.id), resolved.id),
119 external: false
120 }
121 }
122 return null
123 }
124})
125
126/**
127 * Creates the full Rollup plugin pipeline for bundling.
128 *
129 * @category constructors
130 * @since 4.0.0
131 */
132export const createPlugins = (pathService: Path.Path, options: PluginOptions = {}): Array<Plugin> => {
133 const resolved = resolvePluginOptions(options)
134 const plugins: Array<Plugin> = [
135 createResolveLocalPackageImports(pathService),
136 nodeResolve(),
137 // @ts-expect-error see https://github.com/rollup/plugins/issues/1662
138 replace({
139 "process.env.NODE_ENV": JSON.stringify("production"),
140 preventAssignment: true
141 }),
142 esbuild({
143 target: resolved.nodeTarget,
144 format: "esm",
145 treeShaking: true
146 }),
147 // @ts-expect-error see https://github.com/rollup/plugins/issues/1662
148 terser({
149 format: { comments: false },
150 compress: resolved.minify,
151 mangle: resolved.mangle && !resolved.visualize
152 })
153 ]
154
155 if (resolved.visualizations.length > 0) {
156 for (const output of resolved.visualizations) {
157 const visualizerOptions: PluginVisualizerOptions = {
158 filename: output.filename,
159 gzipSize: true,
160 open: false,
161 template: output.template
162 }
163 if (output.title !== undefined) {
164 visualizerOptions.title = output.title
165 }
166 plugins.push(visualizer(visualizerOptions) as unknown as Plugin)
167 }
168 } else if (resolved.visualize) {
169 plugins.push(visualizer({
170 open: true,
171 gzipSize: true
172 }) as unknown as Plugin)
173 }
174
175 return plugins
176}