vite-plugin-conditional-imports 0.1.1 → 0.1.2

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/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { ConfigEnv, Plugin, ResolvedConfig } from 'vite';
1
+ import { ResolvedConfig, ConfigEnv, Plugin } from 'vite';
2
2
 
3
3
  interface ShouldStripContext {
4
4
  /** Raw import target as is from source code. */
5
5
  target: string;
6
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.
7
+ * Resolved import path (relative to project root). Lazy, resolution runs only
8
+ * when you access it.
9
9
  */
10
- get resolvedTarget(): Promise<string>;
10
+ resolvedTarget: Promise<string>;
11
11
  /** The file where the import is found, relative to project root. */
12
12
  source: string;
13
13
  /** Import attributes from `with { ... }` clause (e.g. `{ only: 'dev' }`). */
@@ -37,4 +37,4 @@ interface Options {
37
37
  }
38
38
  declare function conditionalImports(shouldStrip: ShouldStripFn, options?: Options): Plugin;
39
39
 
40
- export { conditionalImports,type Options, type ShouldStripContext, type ShouldStripFn };
40
+ export { type Options, type ShouldStripContext, type ShouldStripFn, conditionalImports };
package/dist/index.js CHANGED
@@ -1,158 +1,196 @@
1
1
  // src/index.ts
2
- import { parse, print } from "@swc/core";
3
- import createDebug from "debug";
4
2
  import { unlink } from "fs/promises";
3
+ import { join as join2 } from "path";
4
+ import createDebug from "debug";
5
+
6
+ // src/check.ts
5
7
  import { join } from "path";
8
+ import { originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
9
+ import { Linter } from "eslint";
10
+
6
11
  // src/transform.ts
7
12
  import path from "path";
13
+ import { parse, print } from "@swc/core";
8
14
  import { normalizePath } from "vite";
9
15
 
10
16
  // src/importAttributes.ts
17
+ function getWithObject(node) {
18
+ return node.with ?? node.asserts;
19
+ }
20
+ function getAttrList(node) {
21
+ const withObj = getWithObject(node);
22
+ return withObj?.properties ?? [];
23
+ }
24
+ function getKey(attr) {
25
+ const k = "key" in attr ? attr.key : void 0;
26
+ return k && typeof k === "object" && "value" in k ? k.value : void 0;
27
+ }
28
+ function getVal(attr) {
29
+ const v = "value" in attr ? attr.value : void 0;
30
+ return v && typeof v === "object" && "value" in v ? v.value : void 0;
31
+ }
11
32
  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;
33
+ const list = getAttrList(node);
13
34
  const out = {};
14
- if (!list) return out;
15
35
  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;
36
+ const key = getKey(attr);
37
+ const val = getVal(attr);
38
+ if (key != null && val != null) {
39
+ out[key] = val;
40
+ }
19
41
  }
20
42
  return out;
21
43
  }
22
44
 
23
45
  // src/transform.ts
24
- function toRootRelative(filePath, root) {
25
- if (!path.isAbsolute(filePath)) return filePath;
46
+ function relativeToRoot(filePath, root) {
26
47
  const rel = path.relative(root, filePath);
27
48
  if (rel.startsWith("..") || path.isAbsolute(rel)) return filePath;
28
49
  return normalizePath(rel);
29
50
  }
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
51
  async function transform(code, id, shouldStrip, context) {
36
52
  const mod = await parse(code, {
37
53
  syntax: "typescript",
38
54
  tsx: id.endsWith(".tsx")
39
55
  });
40
- const strippedImports = [];
41
- const bindingToSourceFiles = /* @__PURE__ */ new Map();
42
56
  const resolveCache = /* @__PURE__ */ new Map();
57
+ const strippedImportTargets = /* @__PURE__ */ new Set();
58
+ const strippedImportSymbols = /* @__PURE__ */ new Set();
59
+ const source = relativeToRoot(id, context.config.root);
43
60
  const body = [];
44
61
  for (const item of mod.body) {
45
- if (item.type !== "ImportDeclaration") {
62
+ if (item.type !== "ImportDeclaration" || item.typeOnly) {
46
63
  body.push(item);
47
64
  continue;
48
65
  }
49
66
  const target = item.source.value;
50
- const withObject = getImportAttributes(
51
- item
52
- );
67
+ const withObject = getImportAttributes(item);
53
68
  const ctx = {
54
69
  target,
55
70
  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);
71
+ let promise2 = resolveCache.get(target);
72
+ if (promise2) {
73
+ return promise2;
62
74
  }
63
- return p;
75
+ promise2 = context.resolve(target, id).then((r) => relativeToRoot(r?.id ?? target, context.config.root));
76
+ resolveCache.set(target, promise2);
77
+ return promise2;
64
78
  },
65
- source: toRootRelative(id, context.config.root),
79
+ source,
66
80
  withObject,
67
81
  config: context.config,
68
82
  env: context.env
69
83
  };
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) {
84
+ const promise = Promise.resolve(shouldStrip(ctx)).then((strip) => {
85
+ if (!strip) {
86
+ return item;
87
+ }
88
+ strippedImportTargets.add(target);
76
89
  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
- }
90
+ strippedImportSymbols.add(spec.local.value);
86
91
  }
87
- }
92
+ return null;
93
+ });
94
+ body.push(promise);
95
+ }
96
+ const results = await Promise.all(body);
97
+ mod.body = results.filter((item) => item != null);
98
+ if (strippedImportSymbols.size === 0) {
99
+ return null;
88
100
  }
89
- mod.body = body;
90
- const out = await print(mod);
91
- if (out.code === code) return null;
101
+ const out = await print(mod, {
102
+ sourceMaps: true,
103
+ filename: source
104
+ });
92
105
  return {
93
106
  code: out.code,
94
107
  map: out.map ?? null,
95
- strippedImports,
96
- bindingToSourceFiles
108
+ strippedImportTargets,
109
+ strippedImportSymbols,
110
+ relativeSource: source
97
111
  };
98
112
  }
99
113
 
100
114
  // 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 [];
115
+ var eslintConfig = [
116
+ {
117
+ languageOptions: {
118
+ ecmaVersion: "latest",
119
+ sourceType: "module"
120
+ },
121
+ rules: { "no-undef": ["error"] }
121
122
  }
123
+ ];
124
+ function findUndefinedRefs(code) {
125
+ const linter = new Linter({ configType: "flat" });
126
+ const messages = linter.verify(code, eslintConfig, "chunk.js");
122
127
  const seen = /* @__PURE__ */ new Set();
123
- const errors = [];
128
+ const refs = [];
124
129
  for (const msg of messages) {
125
130
  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;
131
+ const name = msg.message.split("'")[1] ?? null;
132
+ if (!name || seen.has(name)) {
133
+ continue;
134
+ }
129
135
  seen.add(name);
130
- const sourceIds = bindingToSourceFiles.get(name) ?? [];
131
- errors.push({
136
+ refs.push({
132
137
  name,
133
- sourceIds,
134
138
  line: msg.line,
135
139
  column: msg.column
136
140
  });
137
141
  }
138
- return errors;
142
+ return refs;
139
143
  }
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;
144
+ function checkChunk(chunk, importSymbolsToSource, root, outputDir) {
145
+ const errors = [];
146
+ const undefinedRefs = findUndefinedRefs(chunk.code);
147
+ if (undefinedRefs.length === 0) {
148
+ return errors;
149
+ }
150
+ const moduleIdsInChunk = new Set(
151
+ chunk.moduleIds ?? Object.keys(chunk.modules ?? {})
152
+ );
153
+ let traceMap = null;
154
+ for (const undefinedRef of undefinedRefs) {
155
+ const sourceIds = importSymbolsToSource.get(undefinedRef.name);
156
+ if (!sourceIds) {
157
+ continue;
158
+ }
159
+ const relevantSourceIds = sourceIds.filter((id) => moduleIdsInChunk.has(id));
160
+ if (relevantSourceIds.length === 0) {
161
+ continue;
162
+ }
163
+ let sourceOfError = null;
164
+ if (chunk.map && undefinedRef.line != null && undefinedRef.column != null) {
165
+ const mapUrl = outputDir ? join(outputDir, chunk.sourcemapFileName ?? `${chunk.fileName}.map`) : null;
166
+ traceMap ??= new TraceMap(chunk.map, mapUrl);
167
+ sourceOfError = originalPositionFor(traceMap, {
168
+ line: undefinedRef.line,
169
+ column: undefinedRef.column
170
+ }).source;
171
+ }
172
+ const errorSourceIds = sourceOfError ? [sourceOfError] : relevantSourceIds;
173
+ const relativeSourceIds = outputDir ? errorSourceIds.map((id) => relativeToRoot(id, root)) : errorSourceIds;
174
+ errors.push(
175
+ `Stripped conditional import binding '${undefinedRef.name}' still in output (${relativeSourceIds.sort().join(", ")})`
176
+ );
177
+ }
178
+ return errors;
145
179
  }
146
- function traceToSource(chunkMap, line, column) {
147
- try {
148
- const traced = originalPositionFor(
149
- new TraceMap(chunkMap),
150
- { line, column }
180
+ function checkBundle(bundle, importSymbolsToSource, root, outputDir) {
181
+ if (importSymbolsToSource.size === 0) {
182
+ return [];
183
+ }
184
+ const errors = [];
185
+ for (const chunkOrAsset of Object.values(bundle)) {
186
+ if (chunkOrAsset.type !== "chunk") {
187
+ continue;
188
+ }
189
+ errors.push(
190
+ ...checkChunk(chunkOrAsset, importSymbolsToSource, root, outputDir)
151
191
  );
152
- return traced.source ?? null;
153
- } catch {
154
- return null;
155
192
  }
193
+ return errors;
156
194
  }
157
195
 
158
196
  // src/index.ts
@@ -161,8 +199,7 @@ function conditionalImports(shouldStrip, options) {
161
199
  const autoSourceMaps = options?.autoSourceMaps !== false;
162
200
  const applyInServe = options?.applyInServe === true;
163
201
  let pluginRequestedSourcemap = false;
164
- const strippedImports = /* @__PURE__ */ new Set();
165
- const bindingToSourceFiles = /* @__PURE__ */ new Map();
202
+ const importSymbolsToSource = /* @__PURE__ */ new Map();
166
203
  let cachedConfig;
167
204
  let cachedEnv;
168
205
  return {
@@ -187,12 +224,14 @@ function conditionalImports(shouldStrip, options) {
187
224
  cachedConfig = config;
188
225
  },
189
226
  buildStart() {
190
- strippedImports.clear();
191
- bindingToSourceFiles.clear();
227
+ importSymbolsToSource.clear();
192
228
  },
193
229
  async transform(code, id) {
230
+ if (id.includes("/node_modules/")) {
231
+ return null;
232
+ }
194
233
  if (!cachedConfig || !cachedEnv) {
195
- this.error(
234
+ return this.error(
196
235
  "vite-plugin-conditional-imports: config or env not resolved"
197
236
  );
198
237
  }
@@ -207,61 +246,35 @@ function conditionalImports(shouldStrip, options) {
207
246
  if (!result) {
208
247
  return null;
209
248
  }
210
- if (result.strippedImports.length > 0) {
211
- log(
212
- "stripped import in %s: %s",
213
- toRootRelative(id, cachedConfig.root),
214
- result.strippedImports.join(", ")
215
- );
249
+ for (const target of result.strippedImportTargets) {
250
+ log("stripped import in %s: %s", result.relativeSource, target);
216
251
  }
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);
252
+ for (const name of result.strippedImportSymbols) {
253
+ const list = importSymbolsToSource.get(name) ?? [];
254
+ list.push(id);
255
+ importSymbolsToSource.set(name, list);
225
256
  }
226
257
  return { code: result.code, map: result.map };
227
258
  },
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
- }
259
+ generateBundle(options2, bundle) {
260
+ if (!cachedConfig) {
261
+ return;
262
+ }
263
+ const errors = checkBundle(
264
+ bundle,
265
+ importSymbolsToSource,
266
+ cachedConfig.root,
267
+ options2.dir
268
+ );
269
+ if (errors.length > 0) {
270
+ this.error(errors.join("\n"));
258
271
  }
259
272
  },
260
273
  async writeBundle(options2, bundle) {
261
274
  if (!pluginRequestedSourcemap || !options2.dir) return;
262
275
  for (const name of Object.keys(bundle)) {
263
276
  if (name.endsWith(".map")) {
264
- await unlink(join(options2.dir, name)).catch(() => {
277
+ await unlink(join2(options2.dir, name)).catch(() => {
265
278
  });
266
279
  }
267
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-conditional-imports",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Strip conditional imports (only, path patterns, or custom predicate) in production builds and warn on leftover references",
5
5
  "keywords": [
6
6
  "vite",