vite-plugin-conditional-imports 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # vite-plugin-conditional-imports
2
+
3
+ [![npm version](https://img.shields.io/npm/v/vite-plugin-conditional-imports.svg)](https://www.npmjs.com/package/vite-plugin-conditional-imports)
4
+
5
+ > Vite plugin to strip imports conditionally.
6
+
7
+ Typical use case:
8
+
9
+ ```js
10
+ import debug from './debug' with { only: 'dev' }
11
+
12
+ if (import.meta.env.DEV) {
13
+ debug()
14
+ }
15
+ ```
16
+
17
+ This plugin can be configured to strip the `./debug` import when
18
+ building for production. Then we rely on Rollup's tree shaking to remove
19
+ the dead code during a production build.
20
+
21
+ If we forget to guard dev code, the plugin notices missing references in
22
+ the chunk output and **fails the build**.
23
+
24
+ You can also perform stripping based on the import path itself.
25
+
26
+ ## Usage
27
+
28
+ Strip based on `import with` metadata:
29
+
30
+ ```js
31
+ import { defineConfig } from 'vite'
32
+ import { conditionalImports } from 'vite-plugin-conditional-imports'
33
+
34
+ export default defineConfig({
35
+ plugins: [
36
+ conditionalImports(ctx => {
37
+ // Strip in production when import has `with { only: 'dev' }`
38
+ return ctx.config.mode === 'production' && ctx.withObject?.only === 'dev'
39
+ }),
40
+ ],
41
+ })
42
+ ```
43
+
44
+ Strip based on path:
45
+
46
+ ```js
47
+ export default defineConfig({
48
+ plugins: [
49
+ conditionalImports(async ctx => {
50
+ // Strip in production when import paths contains `/dev/`
51
+ return (
52
+ ctx.config.mode === 'production' &&
53
+ (await ctx.resolvedTarget).includes('/dev/')
54
+ )
55
+ }),
56
+ ],
57
+ })
58
+ ```
59
+
60
+ ## Context object
61
+
62
+ | Property | Description |
63
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
64
+ | `target` | Import specifier as written in source (e.g. `./dev-only.js`, `@lib/custom`). |
65
+ | `resolvedTarget` | `Promise<string>`, resolved path relative to Vite's `config.root`. Lazy: resolution runs only when you access it (e.g. `await ctx.resolvedTarget`). |
66
+ | `source` | Path of the file that contains the import, relative to `config.root`. |
67
+ | `withObject` | Import attributes from `with { ... }` (e.g. `{ only: 'dev' }`). |
68
+ | `config` | Resolved Vite config (`config.mode`, `config.build.ssr`, etc.). |
69
+ | `env` | Vite config env (`env.mode`, `env.ssrBuild`, etc.). |
70
+
71
+ ## Extra options
72
+
73
+ You can pass an options object after the callback functon with the following properties:
74
+
75
+ | Option | Type | Default | Description |
76
+ | ---------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
77
+ | `autoSourceMaps` | `boolean` | `true` | When `true`, the plugin enables Vite sourcemaps (if not set) and uses them to resolve error locations to the original file. When `false`, it does not touch the config, and if source maps are not already enabled, it falls back to reporting all files where a given import was stripped, which is less useful, but builds faster. |
78
+ | `applyInServe` | `boolean` | `false` | Set to `true` to also use the plugin with `vite serve`. |
package/dist/index.cjs ADDED
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ conditionalImports: () => conditionalImports
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_promises = require("fs/promises");
27
+ var import_node_path2 = require("path");
28
+
29
+ // src/options.ts
30
+ var DEFAULT_STRIP_IN_MODES = ["production"];
31
+ function normalizeOptions(options = {}) {
32
+ return {
33
+ stripInModes: options.stripInModes ?? [...DEFAULT_STRIP_IN_MODES],
34
+ onlyActiveInModes: options.onlyActiveInModes ?? {},
35
+ pathPatterns: options.pathPatterns ?? [],
36
+ shouldStrip: options.shouldStrip
37
+ };
38
+ }
39
+
40
+ // src/transform.ts
41
+ var import_core = require("@swc/core");
42
+
43
+ // src/importAttributes.ts
44
+ function getOnlyAttribute(node) {
45
+ const list = "attributes" in node && Array.isArray(node.attributes) ? node.attributes : "assertions" in node && Array.isArray(node.assertions) ? node.assertions : void 0;
46
+ if (!list) return void 0;
47
+ for (const attr of list) {
48
+ const key = attr.key && typeof attr.key === "object" && "value" in attr.key ? attr.key.value : void 0;
49
+ if (key === "only" && attr.value) {
50
+ const val = typeof attr.value === "object" && "value" in attr.value ? attr.value.value : void 0;
51
+ return val;
52
+ }
53
+ }
54
+ return void 0;
55
+ }
56
+
57
+ // src/resolve.ts
58
+ var import_node_module = require("module");
59
+ var import_node_path = require("path");
60
+ var import_meta = {};
61
+ var require2 = (0, import_node_module.createRequire)(import_meta.url);
62
+ var resolveSync = require2("resolve/sync");
63
+ function resolveSpecifier(specifier, importerId) {
64
+ if (specifier.startsWith("\0")) return specifier;
65
+ const basedir = (0, import_node_path.dirname)(importerId);
66
+ try {
67
+ return resolveSync(specifier, {
68
+ basedir,
69
+ preserveSymlinks: false,
70
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"]
71
+ });
72
+ } catch {
73
+ return specifier;
74
+ }
75
+ }
76
+
77
+ // src/shouldStrip.ts
78
+ var import_minimatch = require("minimatch");
79
+ function shouldStripImport(specifier, importerId, resolvedSpecifier, options, currentMode, stripInModes, onlyValueFromImport) {
80
+ if (!stripInModes.includes(currentMode)) return false;
81
+ if (onlyValueFromImport != null) {
82
+ const activeModes = options.onlyActiveInModes[onlyValueFromImport];
83
+ if (activeModes != null && !activeModes.includes(currentMode)) return true;
84
+ }
85
+ for (const pattern of options.pathPatterns) {
86
+ if ((0, import_minimatch.minimatch)(resolvedSpecifier, pattern, { matchBase: true })) return true;
87
+ }
88
+ if (options.shouldStrip?.(resolvedSpecifier, importerId)) return true;
89
+ return false;
90
+ }
91
+
92
+ // src/transform.ts
93
+ function getLocalName(spec) {
94
+ const local = spec.local;
95
+ if (!local) return void 0;
96
+ return "value" in local ? local.value : void 0;
97
+ }
98
+ async function transformModule(code, id, options, currentMode) {
99
+ const mod = await (0, import_core.parse)(code, {
100
+ syntax: "typescript",
101
+ tsx: /\.tsx$/.test(id)
102
+ });
103
+ const strippedBindings = [];
104
+ const bindingToSourceFiles = /* @__PURE__ */ new Map();
105
+ const resolvedSpecifierCache = /* @__PURE__ */ new Map();
106
+ const body = mod.body.filter((item) => {
107
+ if (item.type !== "ImportDeclaration") return true;
108
+ const src = item.source.value;
109
+ const resolved = resolvedSpecifierCache.get(src) ?? resolveSpecifier(src, id);
110
+ resolvedSpecifierCache.set(src, resolved);
111
+ const onlyValue = getOnlyAttribute(
112
+ item
113
+ );
114
+ const strip = shouldStripImport(
115
+ src,
116
+ id,
117
+ resolved,
118
+ {
119
+ pathPatterns: options.pathPatterns,
120
+ onlyActiveInModes: options.onlyActiveInModes,
121
+ shouldStrip: options.shouldStrip
122
+ },
123
+ currentMode,
124
+ options.stripInModes,
125
+ onlyValue
126
+ );
127
+ if (!strip) return true;
128
+ const typeOnly = "typeOnly" in item && item.typeOnly === true;
129
+ if (!typeOnly && item.specifiers) {
130
+ for (const spec of item.specifiers) {
131
+ const name = getLocalName(
132
+ spec
133
+ );
134
+ if (name) {
135
+ strippedBindings.push(name);
136
+ const list = bindingToSourceFiles.get(name) ?? [];
137
+ list.push(id);
138
+ bindingToSourceFiles.set(name, list);
139
+ }
140
+ }
141
+ }
142
+ return false;
143
+ });
144
+ mod.body = body;
145
+ const out = await (0, import_core.print)(mod);
146
+ if (out.code === code) return null;
147
+ return {
148
+ code: out.code,
149
+ map: out.map ?? null,
150
+ strippedBindings,
151
+ bindingToSourceFiles
152
+ };
153
+ }
154
+
155
+ // src/check.ts
156
+ var import_eslint = require("eslint");
157
+ var import_trace_mapping = require("@jridgewell/trace-mapping");
158
+ var NO_UNDEF_MSG_RE = /^'([^']+)' is not defined\.$/;
159
+ function findUndefinedStrippedRefs(code, strippedBindings, bindingToSourceFiles) {
160
+ if (strippedBindings.size === 0) return [];
161
+ const linter = new import_eslint.Linter({ configType: "flat" });
162
+ const config = [
163
+ {
164
+ languageOptions: {
165
+ ecmaVersion: "latest",
166
+ sourceType: "module"
167
+ },
168
+ rules: { "no-undef": ["error"] }
169
+ }
170
+ ];
171
+ let messages;
172
+ try {
173
+ messages = linter.verify(code, config, "chunk.js");
174
+ } catch {
175
+ return [];
176
+ }
177
+ const seen = /* @__PURE__ */ new Set();
178
+ const errors = [];
179
+ for (const msg of messages) {
180
+ if (msg.ruleId !== "no-undef") continue;
181
+ const m = NO_UNDEF_MSG_RE.exec(msg.message);
182
+ const name = m?.[1];
183
+ if (!name || !strippedBindings.has(name) || seen.has(name)) continue;
184
+ seen.add(name);
185
+ const sourceIds = bindingToSourceFiles.get(name) ?? [];
186
+ errors.push({
187
+ name,
188
+ sourceIds,
189
+ line: msg.line,
190
+ column: msg.column
191
+ });
192
+ }
193
+ return errors;
194
+ }
195
+ function toShortPath(id) {
196
+ const srcMatch = id.match(/(?:^|\/)src\/(.+)$/);
197
+ if (srcMatch) return srcMatch[1];
198
+ const lastSlash = id.lastIndexOf("/");
199
+ return lastSlash >= 0 ? id.slice(lastSlash + 1) : id;
200
+ }
201
+ function traceToSource(chunkMap, line, column) {
202
+ try {
203
+ const traced = (0, import_trace_mapping.originalPositionFor)(
204
+ new import_trace_mapping.TraceMap(chunkMap),
205
+ { line, column }
206
+ );
207
+ return traced.source ?? null;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ // src/index.ts
214
+ function conditionalImports(options = {}) {
215
+ const opts = normalizeOptions(options);
216
+ let currentMode = "development";
217
+ let pluginRequestedSourcemap = false;
218
+ const strippedBindings = /* @__PURE__ */ new Set();
219
+ const bindingToSourceFiles = /* @__PURE__ */ new Map();
220
+ return {
221
+ name: "vite-plugin-conditional-imports",
222
+ enforce: "pre",
223
+ config(userConfig, env) {
224
+ if (env.command === "build" && userConfig.build?.sourcemap == null) {
225
+ pluginRequestedSourcemap = true;
226
+ return { build: { sourcemap: "hidden" } };
227
+ }
228
+ },
229
+ configResolved(config) {
230
+ currentMode = config.mode;
231
+ },
232
+ buildStart() {
233
+ strippedBindings.clear();
234
+ bindingToSourceFiles.clear();
235
+ },
236
+ async transform(code, id) {
237
+ if (!opts.stripInModes.includes(currentMode)) return null;
238
+ if (!/\.(tsx?|jsx?|mts|mjs)$/.test(id)) return null;
239
+ const result = await transformModule(code, id, opts, currentMode);
240
+ if (!result) return null;
241
+ for (const name of result.strippedBindings) {
242
+ strippedBindings.add(name);
243
+ const list = bindingToSourceFiles.get(name) ?? [];
244
+ const fromResult = result.bindingToSourceFiles.get(name) ?? [];
245
+ for (const fid of fromResult) {
246
+ if (!list.includes(fid)) list.push(fid);
247
+ }
248
+ bindingToSourceFiles.set(name, list);
249
+ }
250
+ return { code: result.code, map: result.map };
251
+ },
252
+ generateBundle(_options, bundle) {
253
+ if (!opts.stripInModes.includes(currentMode) || strippedBindings.size === 0)
254
+ return;
255
+ for (const item of Object.values(bundle)) {
256
+ if (item.type !== "chunk") continue;
257
+ const chunk = item;
258
+ const moduleIdsInChunk = new Set(
259
+ chunk.moduleIds ?? Object.keys(chunk.modules ?? {})
260
+ );
261
+ const undefinedRefs = findUndefinedStrippedRefs(
262
+ chunk.code,
263
+ strippedBindings,
264
+ bindingToSourceFiles
265
+ );
266
+ const chunkMap = chunk.map;
267
+ for (const { name, sourceIds, line, column } of undefinedRefs) {
268
+ let toShow = sourceIds.filter(
269
+ (id) => moduleIdsInChunk.has(id)
270
+ );
271
+ if (toShow.length === 0) toShow = sourceIds;
272
+ let resolvedSingle = null;
273
+ if (chunkMap && line != null && column != null) {
274
+ resolvedSingle = traceToSource(chunkMap, line, column);
275
+ if (resolvedSingle != null) resolvedSingle = toShortPath(resolvedSingle);
276
+ }
277
+ const origin = resolvedSingle ? ` (in: ${resolvedSingle})` : toShow.length > 0 ? ` (in: ${toShow.map(toShortPath).join(", ")})` : "";
278
+ this.error(
279
+ `Stripped conditional import binding "${name}" still in output${origin}. Guard or remove the usage so it's tree-shaken.`
280
+ );
281
+ }
282
+ }
283
+ },
284
+ async writeBundle(options2, bundle) {
285
+ if (!pluginRequestedSourcemap || !options2.dir) return;
286
+ for (const name of Object.keys(bundle)) {
287
+ if (name.endsWith(".map")) {
288
+ await (0, import_promises.unlink)((0, import_node_path2.join)(options2.dir, name)).catch(() => {
289
+ });
290
+ }
291
+ }
292
+ }
293
+ };
294
+ }
295
+ // Annotate the CommonJS export names for ESM import in node:
296
+ 0 && (module.exports = {
297
+ conditionalImports
298
+ });
@@ -0,0 +1,40 @@
1
+ import { ConfigEnv, Plugin, ResolvedConfig } from 'vite';
2
+
3
+ interface ShouldStripContext {
4
+ /** Raw import target as is from source code. */
5
+ target: string;
6
+ /**
7
+ * Resolved import path (relative to project root). Lazy getter — resolution
8
+ * runs only when this is accessed. Await it when you need the path.
9
+ */
10
+ get resolvedTarget(): Promise<string>;
11
+ /** The file where the import is found, relative to project root. */
12
+ source: string;
13
+ /** Import attributes from `with { ... }` clause (e.g. `{ only: 'dev' }`). */
14
+ withObject: Record<string, string>;
15
+ /** Vite config. */
16
+ config: ResolvedConfig;
17
+ /** Vite env. */
18
+ env: ConfigEnv;
19
+ }
20
+ type ShouldStripFn = (ctx: ShouldStripContext) => boolean | Promise<boolean>;
21
+ interface Options {
22
+ /**
23
+ * When `true` (default), the plugin enables Vite source maps (`hidden` mode)
24
+ * and uses them to resolve error locations and provide more useful error
25
+ * messages. If source maps were initially off, the source maps are removed
26
+ * after the build.
27
+ *
28
+ * If you don't want this plugin to touch your source maps configuration and
29
+ * output folder, set this to `false`.
30
+ */
31
+ autoSourceMaps?: boolean;
32
+ /**
33
+ * When `false` (default), the plugin only runs in `vite build`. If you set it
34
+ * to `true`, it also runs in `vite serve`.
35
+ */
36
+ applyInServe?: boolean;
37
+ }
38
+ declare function conditionalImports(shouldStrip: ShouldStripFn, options?: Options): Plugin;
39
+
40
+ export { conditionalImports,type Options, type ShouldStripContext, type ShouldStripFn };
package/dist/index.js ADDED
@@ -0,0 +1,273 @@
1
+ // src/index.ts
2
+ import { parse, print } from "@swc/core";
3
+ import createDebug from "debug";
4
+ import { unlink } from "fs/promises";
5
+ import { join } from "path";
6
+ // src/transform.ts
7
+ import path from "path";
8
+ import { normalizePath } from "vite";
9
+
10
+ // src/importAttributes.ts
11
+ function getImportAttributes(node) {
12
+ const list = node.with && typeof node.with === "object" && Array.isArray(node.with.properties) ? node.with.properties : Array.isArray(node.attributes) ? node.attributes : Array.isArray(node.assertions) ? node.assertions : void 0;
13
+ const out = {};
14
+ if (!list) return out;
15
+ for (const attr of list) {
16
+ const key = attr.key && typeof attr.key === "object" && "value" in attr.key ? attr.key.value : void 0;
17
+ const val = attr.value && typeof attr.value === "object" && "value" in attr.value ? attr.value.value : void 0;
18
+ if (key != null && val != null) out[key] = val;
19
+ }
20
+ return out;
21
+ }
22
+
23
+ // src/transform.ts
24
+ function toRootRelative(filePath, root) {
25
+ if (!path.isAbsolute(filePath)) return filePath;
26
+ const rel = path.relative(root, filePath);
27
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return filePath;
28
+ return normalizePath(rel);
29
+ }
30
+ function getLocalName(spec) {
31
+ const local = spec.local;
32
+ if (!local) return void 0;
33
+ return "value" in local ? local.value : void 0;
34
+ }
35
+ async function transform(code, id, shouldStrip, context) {
36
+ const mod = await parse(code, {
37
+ syntax: "typescript",
38
+ tsx: id.endsWith(".tsx")
39
+ });
40
+ const strippedImports = [];
41
+ const bindingToSourceFiles = /* @__PURE__ */ new Map();
42
+ const resolveCache = /* @__PURE__ */ new Map();
43
+ const body = [];
44
+ for (const item of mod.body) {
45
+ if (item.type !== "ImportDeclaration") {
46
+ body.push(item);
47
+ continue;
48
+ }
49
+ const target = item.source.value;
50
+ const withObject = getImportAttributes(
51
+ item
52
+ );
53
+ const ctx = {
54
+ target,
55
+ get resolvedTarget() {
56
+ let p = resolveCache.get(target);
57
+ if (!p) {
58
+ p = context.resolve(target, id).then(
59
+ (r) => toRootRelative(r?.id ?? target, context.config.root)
60
+ );
61
+ resolveCache.set(target, p);
62
+ }
63
+ return p;
64
+ },
65
+ source: toRootRelative(id, context.config.root),
66
+ withObject,
67
+ config: context.config,
68
+ env: context.env
69
+ };
70
+ if (!await shouldStrip(ctx)) {
71
+ body.push(item);
72
+ continue;
73
+ }
74
+ const typeOnly = "typeOnly" in item && item.typeOnly === true;
75
+ if (!typeOnly && item.specifiers) {
76
+ for (const spec of item.specifiers) {
77
+ const name = getLocalName(
78
+ spec
79
+ );
80
+ if (name) {
81
+ strippedImports.push(name);
82
+ const list = bindingToSourceFiles.get(name) ?? [];
83
+ list.push(id);
84
+ bindingToSourceFiles.set(name, list);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ mod.body = body;
90
+ const out = await print(mod);
91
+ if (out.code === code) return null;
92
+ return {
93
+ code: out.code,
94
+ map: out.map ?? null,
95
+ strippedImports,
96
+ bindingToSourceFiles
97
+ };
98
+ }
99
+
100
+ // src/check.ts
101
+ import { originalPositionFor,TraceMap } from "@jridgewell/trace-mapping";
102
+ import { Linter } from "eslint";
103
+ var NO_UNDEF_MSG_RE = /^'([^']+)' is not defined\.$/;
104
+ function findUndefinedStrippedRefs(code, strippedImports, bindingToSourceFiles) {
105
+ if (strippedImports.size === 0) return [];
106
+ const linter = new Linter({ configType: "flat" });
107
+ const config = [
108
+ {
109
+ languageOptions: {
110
+ ecmaVersion: "latest",
111
+ sourceType: "module"
112
+ },
113
+ rules: { "no-undef": ["error"] }
114
+ }
115
+ ];
116
+ let messages;
117
+ try {
118
+ messages = linter.verify(code, config, "chunk.js");
119
+ } catch {
120
+ return [];
121
+ }
122
+ const seen = /* @__PURE__ */ new Set();
123
+ const errors = [];
124
+ for (const msg of messages) {
125
+ if (msg.ruleId !== "no-undef") continue;
126
+ const m = NO_UNDEF_MSG_RE.exec(msg.message);
127
+ const name = m?.[1];
128
+ if (!name || !strippedImports.has(name) || seen.has(name)) continue;
129
+ seen.add(name);
130
+ const sourceIds = bindingToSourceFiles.get(name) ?? [];
131
+ errors.push({
132
+ name,
133
+ sourceIds,
134
+ line: msg.line,
135
+ column: msg.column
136
+ });
137
+ }
138
+ return errors;
139
+ }
140
+ function toShortPath(id) {
141
+ const srcMatch = id.match(/(?:^|\/)src\/(.+)$/);
142
+ if (srcMatch) return srcMatch[1];
143
+ const lastSlash = id.lastIndexOf("/");
144
+ return lastSlash >= 0 ? id.slice(lastSlash + 1) : id;
145
+ }
146
+ function traceToSource(chunkMap, line, column) {
147
+ try {
148
+ const traced = originalPositionFor(
149
+ new TraceMap(chunkMap),
150
+ { line, column }
151
+ );
152
+ return traced.source ?? null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ // src/index.ts
159
+ var log = createDebug("vite-plugin-conditional-imports");
160
+ function conditionalImports(shouldStrip, options) {
161
+ const autoSourceMaps = options?.autoSourceMaps !== false;
162
+ const applyInServe = options?.applyInServe === true;
163
+ let pluginRequestedSourcemap = false;
164
+ const strippedImports = /* @__PURE__ */ new Set();
165
+ const bindingToSourceFiles = /* @__PURE__ */ new Map();
166
+ let cachedConfig;
167
+ let cachedEnv;
168
+ return {
169
+ name: "vite-plugin-conditional-imports",
170
+ enforce: "pre",
171
+ apply: applyInServe ? void 0 : "build",
172
+ config(userConfig, env) {
173
+ cachedEnv = env;
174
+ if (!autoSourceMaps) {
175
+ return;
176
+ }
177
+ if (env.command === "build" && (userConfig.build?.sourcemap == null || userConfig.build?.sourcemap === false)) {
178
+ pluginRequestedSourcemap = true;
179
+ return {
180
+ build: {
181
+ sourcemap: "hidden"
182
+ }
183
+ };
184
+ }
185
+ },
186
+ configResolved(config) {
187
+ cachedConfig = config;
188
+ },
189
+ buildStart() {
190
+ strippedImports.clear();
191
+ bindingToSourceFiles.clear();
192
+ },
193
+ async transform(code, id) {
194
+ if (!cachedConfig || !cachedEnv) {
195
+ this.error(
196
+ "vite-plugin-conditional-imports: config or env not resolved"
197
+ );
198
+ }
199
+ if (!/\.(tsx?|jsx?|mts|mjs)$/.test(id)) {
200
+ return null;
201
+ }
202
+ const result = await transform(code, id, shouldStrip, {
203
+ resolve: this.resolve,
204
+ config: cachedConfig,
205
+ env: cachedEnv
206
+ });
207
+ if (!result) {
208
+ return null;
209
+ }
210
+ if (result.strippedImports.length > 0) {
211
+ log(
212
+ "stripped import in %s: %s",
213
+ toRootRelative(id, cachedConfig.root),
214
+ result.strippedImports.join(", ")
215
+ );
216
+ }
217
+ for (const name of result.strippedImports) {
218
+ strippedImports.add(name);
219
+ const list = bindingToSourceFiles.get(name) ?? [];
220
+ const fromResult = result.bindingToSourceFiles.get(name) ?? [];
221
+ for (const fid of fromResult) {
222
+ if (!list.includes(fid)) list.push(fid);
223
+ }
224
+ bindingToSourceFiles.set(name, list);
225
+ }
226
+ return { code: result.code, map: result.map };
227
+ },
228
+ generateBundle(_options, bundle) {
229
+ if (strippedImports.size === 0) return;
230
+ for (const item of Object.values(bundle)) {
231
+ if (item.type !== "chunk") continue;
232
+ const chunk = item;
233
+ const moduleIdsInChunk = new Set(
234
+ chunk.moduleIds ?? Object.keys(chunk.modules ?? {})
235
+ );
236
+ const undefinedRefs = findUndefinedStrippedRefs(
237
+ chunk.code,
238
+ strippedImports,
239
+ bindingToSourceFiles
240
+ );
241
+ const chunkMap = chunk.map;
242
+ for (const { name, sourceIds, line, column } of undefinedRefs) {
243
+ let toShow = sourceIds.filter(
244
+ (id) => moduleIdsInChunk.has(id)
245
+ );
246
+ if (toShow.length === 0) toShow = sourceIds;
247
+ let resolvedSingle = null;
248
+ if (chunkMap && line != null && column != null) {
249
+ resolvedSingle = traceToSource(chunkMap, line, column);
250
+ if (resolvedSingle != null)
251
+ resolvedSingle = toShortPath(resolvedSingle);
252
+ }
253
+ const origin = resolvedSingle ? ` (in: ${resolvedSingle})` : toShow.length > 0 ? ` (in: ${toShow.map(toShortPath).join(", ")})` : "";
254
+ this.error(
255
+ `Stripped conditional import binding "${name}" still in output${origin}. Guard or remove the usage so it's tree-shaken.`
256
+ );
257
+ }
258
+ }
259
+ },
260
+ async writeBundle(options2, bundle) {
261
+ if (!pluginRequestedSourcemap || !options2.dir) return;
262
+ for (const name of Object.keys(bundle)) {
263
+ if (name.endsWith(".map")) {
264
+ await unlink(join(options2.dir, name)).catch(() => {
265
+ });
266
+ }
267
+ }
268
+ }
269
+ };
270
+ }
271
+ export {
272
+ conditionalImports
273
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "vite-plugin-conditional-imports",
3
+ "version": "0.1.0",
4
+ "description": "Strip conditional imports (only, path patterns, or custom predicate) in production builds and warn on leftover references",
5
+ "keywords": [
6
+ "vite",
7
+ "vite-plugin",
8
+ "conditional-imports",
9
+ "tree-shake"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Val (https://val.codejam.info)",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "types": "./dist/index.d.ts",
26
+ "repository": "EveToolsHQ/vite-plugin-conditional-imports",
27
+ "dependencies": {
28
+ "@jridgewell/trace-mapping": "^0.3.31",
29
+ "@swc/core": "^1.15.11",
30
+ "debug": "^4.4.3",
31
+ "eslint": "^9.39.2"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/js": "^9.39.2",
35
+ "@types/debug": "^4.1.12",
36
+ "@types/node": "^25.1.0",
37
+ "eslint-config-prettier": "^10.1.8",
38
+ "eslint-plugin-import-x": "^4.16.0",
39
+ "eslint-plugin-prettier": "^5.5.5",
40
+ "eslint-plugin-unused-imports": "^4.3.0",
41
+ "minimatch": "^10.1.1",
42
+ "prettier": "^3.8.1",
43
+ "rollup": "^4.57.1",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^5.9.3",
46
+ "typescript-eslint": "^8.54.0",
47
+ "vite": "^7.3.1",
48
+ "vitest": "^4.0.18"
49
+ },
50
+ "peerDependencies": {
51
+ "vite": ">=5.0.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "vite": {
55
+ "optional": false
56
+ }
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup src/index.ts --format esm --dts --tsconfig tsconfig.build.json",
63
+ "lint": "eslint .",
64
+ "lint:fix": "eslint . --fix",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest"
67
+ }
68
+ }