unimport.0aitavbJ.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. import { basename } from 'node:path';
  2. import process from 'node:process';
  3. import { isAbsolute, relative, resolve } from 'pathe';
  4. import { camelCase, kebabCase } from 'scule';
  5. import MagicString from 'magic-string';
  6. import { resolvePathSync, findStaticImports, parseStaticImport } from 'mlly';
  7. import { stripLiteral } from 'strip-literal';
  8. const excludeRE = [
  9. // imported/exported from other module
  10. /\b(import|export)\b([\w$*{},\s]+?)\bfrom\s*["']/g,
  11. // defined as function
  12. /\bfunction\s*([\w$]+)\s*\(/g,
  13. // defined as class
  14. /\bclass\s*([\w$]+)\s*\{/g,
  15. // defined as local variable
  16. // eslint-disable-next-line regexp/no-super-linear-backtracking
  17. /\b(?:const|let|var)\s+?(\[.*?\]|\{.*?\}|.+?)\s*?[=;\n]/gs
  18. ];
  19. const importAsRE = /^.*\sas\s+/;
  20. const separatorRE = /[,[\]{}\n]|\b(?:import|export)\b/g;
  21. const matchRE = /(^|\.\.\.|(?:\bcase|\?)\s+|[^\w$/)]|\bextends\s+)([\w$]+)\s*(?=[.()[\]}:;?+\-*&|`<>,\n]|\b(?:instanceof|in)\b|$|(?<=extends\s+\w+)\s+\{)/g;
  22. const regexRE = /\/\S*?(?<!\\)(?<!\[[^\]]*)\/[gimsuy]*/g;
  23. function stripCommentsAndStrings(code, options) {
  24. return stripLiteral(code, options).replace(regexRE, 'new RegExp("")');
  25. }
  26. function defineUnimportPreset(preset) {
  27. return preset;
  28. }
  29. const safePropertyName = /^[a-z$_][\w$]*$/i;
  30. function stringifyWith(withValues) {
  31. let withDefs = "";
  32. for (let entries = Object.entries(withValues), l = entries.length, i = 0; i < l; i++) {
  33. const [prop, value] = entries[i];
  34. withDefs += safePropertyName.test(prop) ? prop : JSON.stringify(prop);
  35. withDefs += `: ${JSON.stringify(String(value))}`;
  36. if (i + 1 !== l)
  37. withDefs += ", ";
  38. }
  39. return `{ ${withDefs} }`;
  40. }
  41. function stringifyImports(imports, isCJS = false) {
  42. const map = toImportModuleMap(imports);
  43. return Object.entries(map).flatMap(([name, importSet]) => {
  44. const entries = [];
  45. const imports2 = Array.from(importSet).filter((i) => {
  46. if (!i.name || i.as === "") {
  47. let importStr;
  48. if (isCJS) {
  49. importStr = `require('${name}');`;
  50. } else {
  51. importStr = `import '${name}'`;
  52. if (i.with)
  53. importStr += ` with ${stringifyWith(i.with)}`;
  54. importStr += ";";
  55. }
  56. entries.push(importStr);
  57. return false;
  58. } else if (i.name === "default" || i.name === "=") {
  59. let importStr;
  60. if (isCJS) {
  61. importStr = i.name === "=" ? `const ${i.as} = require('${name}');` : `const { default: ${i.as} } = require('${name}');`;
  62. } else {
  63. importStr = `import ${i.as} from '${name}'`;
  64. if (i.with)
  65. importStr += ` with ${stringifyWith(i.with)}`;
  66. importStr += ";";
  67. }
  68. entries.push(importStr);
  69. return false;
  70. } else if (i.name === "*") {
  71. let importStr;
  72. if (isCJS) {
  73. importStr = `const ${i.as} = require('${name}');`;
  74. } else {
  75. importStr = `import * as ${i.as} from '${name}'`;
  76. if (i.with)
  77. importStr += ` with ${stringifyWith(i.with)}`;
  78. importStr += ";";
  79. }
  80. entries.push(importStr);
  81. return false;
  82. } else if (!isCJS && i.with) {
  83. entries.push(`import { ${stringifyImportAlias(i)} } from '${name}' with ${stringifyWith(i.with)};`);
  84. return false;
  85. }
  86. return true;
  87. });
  88. if (imports2.length) {
  89. const importsAs = imports2.map((i) => stringifyImportAlias(i, isCJS));
  90. entries.push(
  91. isCJS ? `const { ${importsAs.join(", ")} } = require('${name}');` : `import { ${importsAs.join(", ")} } from '${name}';`
  92. );
  93. }
  94. return entries;
  95. }).join("\n");
  96. }
  97. function dedupeImports(imports, warn) {
  98. const map = /* @__PURE__ */ new Map();
  99. const indexToRemove = /* @__PURE__ */ new Set();
  100. imports.filter((i) => !i.disabled).forEach((i, idx) => {
  101. if (i.declarationType === "enum" || i.declarationType === "class")
  102. return;
  103. const name = i.as ?? i.name;
  104. if (!map.has(name)) {
  105. map.set(name, idx);
  106. return;
  107. }
  108. const other = imports[map.get(name)];
  109. if (other.from === i.from) {
  110. indexToRemove.add(idx);
  111. return;
  112. }
  113. const diff = (other.priority || 1) - (i.priority || 1);
  114. if (diff === 0)
  115. warn(`Duplicated imports "${name}", the one from "${other.from}" has been ignored and "${i.from}" is used`);
  116. if (diff <= 0) {
  117. indexToRemove.add(map.get(name));
  118. map.set(name, idx);
  119. } else {
  120. indexToRemove.add(idx);
  121. }
  122. });
  123. return imports.filter((_, idx) => !indexToRemove.has(idx));
  124. }
  125. function toExports(imports, fileDir, includeType = false) {
  126. const map = toImportModuleMap(imports, includeType);
  127. return Object.entries(map).flatMap(([name, imports2]) => {
  128. if (isFilePath(name))
  129. name = name.replace(/\.[a-z]+$/i, "");
  130. if (fileDir && isAbsolute(name)) {
  131. name = relative(fileDir, name);
  132. if (!name.match(/^[./]/))
  133. name = `./${name}`;
  134. }
  135. const entries = [];
  136. const filtered = Array.from(imports2).filter((i) => {
  137. if (i.name === "*") {
  138. entries.push(`export * as ${i.as} from '${name}';`);
  139. return false;
  140. }
  141. return true;
  142. });
  143. if (filtered.length)
  144. entries.push(`export { ${filtered.map((i) => stringifyImportAlias(i, false)).join(", ")} } from '${name}';`);
  145. return entries;
  146. }).join("\n");
  147. }
  148. function stripFileExtension(path) {
  149. return path.replace(/\.[a-z]+$/i, "");
  150. }
  151. function toTypeDeclarationItems(imports, options) {
  152. return imports.map((i) => {
  153. const from = options?.resolvePath?.(i) || stripFileExtension(i.typeFrom || i.from);
  154. let typeDef = "";
  155. if (i.with)
  156. typeDef += `import('${from}', { with: ${stringifyWith(i.with)} })`;
  157. else
  158. typeDef += `import('${from}')`;
  159. if (i.name !== "*" && i.name !== "=")
  160. typeDef += `['${i.name}']`;
  161. return `const ${i.as}: typeof ${typeDef}`;
  162. }).sort();
  163. }
  164. function toTypeDeclarationFile(imports, options) {
  165. const items = toTypeDeclarationItems(imports, options);
  166. const {
  167. exportHelper = true
  168. } = options || {};
  169. let declaration = "";
  170. if (exportHelper)
  171. declaration += "export {}\n";
  172. declaration += `declare global {
  173. ${items.map((i) => ` ${i}`).join("\n")}
  174. }`;
  175. return declaration;
  176. }
  177. function makeTypeModulesMap(imports, resolvePath) {
  178. const modulesMap = /* @__PURE__ */ new Map();
  179. const resolveImportFrom = typeof resolvePath === "function" ? (i) => {
  180. return resolvePath(i) || stripFileExtension(i.typeFrom || i.from);
  181. } : (i) => stripFileExtension(i.typeFrom || i.from);
  182. for (const import_ of imports) {
  183. const from = resolveImportFrom(import_);
  184. let module = modulesMap.get(from);
  185. if (!module) {
  186. module = { typeImports: /* @__PURE__ */ new Set(), starTypeImport: undefined };
  187. modulesMap.set(from, module);
  188. }
  189. if (import_.name === "*") {
  190. if (import_.as)
  191. module.starTypeImport = import_;
  192. } else {
  193. module.typeImports.add(import_);
  194. }
  195. }
  196. return modulesMap;
  197. }
  198. function toTypeReExports(imports, options) {
  199. const importsMap = makeTypeModulesMap(imports, options?.resolvePath);
  200. const code = Array.from(importsMap).flatMap(([from, module]) => {
  201. from = from.replace(/\.d\.([cm]?)ts$/i, ".$1js");
  202. const { starTypeImport, typeImports } = module;
  203. const strings = [];
  204. if (typeImports.size) {
  205. const typeImportNames = Array.from(typeImports).map(({ name, as }) => {
  206. if (as && as !== name)
  207. return `${name} as ${as}`;
  208. return name;
  209. });
  210. strings.push(
  211. "// @ts-ignore",
  212. `export type { ${typeImportNames.join(", ")} } from '${from}'`
  213. );
  214. }
  215. if (starTypeImport) {
  216. strings.push(
  217. "// @ts-ignore",
  218. `export type * as ${starTypeImport.as} from '${from}'`
  219. );
  220. }
  221. if (strings.length) {
  222. strings.push(
  223. // This is a workaround for a TypeScript issue where type-only re-exports are not properly initialized.
  224. `import('${from}')`
  225. );
  226. }
  227. return strings;
  228. });
  229. return `// for type re-export
  230. declare global {
  231. ${code.map((i) => ` ${i}`).join("\n")}
  232. }`;
  233. }
  234. function stringifyImportAlias(item, isCJS = false) {
  235. return item.as === undefined || item.name === item.as ? item.name : isCJS ? `${item.name}: ${item.as}` : `${item.name} as ${item.as}`;
  236. }
  237. function toImportModuleMap(imports, includeType = false) {
  238. const map = {};
  239. for (const _import of imports) {
  240. if (_import.type && !includeType)
  241. continue;
  242. if (!map[_import.from])
  243. map[_import.from] = /* @__PURE__ */ new Set();
  244. map[_import.from].add(_import);
  245. }
  246. return map;
  247. }
  248. function getString(code) {
  249. if (typeof code === "string")
  250. return code;
  251. return code.toString();
  252. }
  253. function getMagicString(code) {
  254. if (typeof code === "string")
  255. return new MagicString(code);
  256. return code;
  257. }
  258. function addImportToCode(code, imports, isCJS = false, mergeExisting = false, injectAtLast = false, firstOccurrence = Number.POSITIVE_INFINITY, onResolved, onStringified) {
  259. let newImports = [];
  260. const s = getMagicString(code);
  261. let _staticImports;
  262. const strippedCode = stripCommentsAndStrings(s.original);
  263. function findStaticImportsLazy() {
  264. if (!_staticImports) {
  265. _staticImports = findStaticImports(s.original).filter((i) => Boolean(strippedCode.slice(i.start, i.end).trim())).map((i) => parseStaticImport(i));
  266. }
  267. return _staticImports;
  268. }
  269. function hasShebang() {
  270. const shebangRegex = /^#!.+/;
  271. return shebangRegex.test(s.original);
  272. }
  273. if (mergeExisting && !isCJS) {
  274. const existingImports = findStaticImportsLazy();
  275. const map = /* @__PURE__ */ new Map();
  276. imports.forEach((i) => {
  277. const target = existingImports.find((e) => e.specifier === i.from && e.imports.startsWith("{"));
  278. if (!target)
  279. return newImports.push(i);
  280. if (!map.has(target))
  281. map.set(target, []);
  282. map.get(target).push(i);
  283. });
  284. for (const [target, items] of map.entries()) {
  285. const strings = items.map((i) => `${stringifyImportAlias(i)}, `);
  286. const importLength = target.code.match(/^\s*import\s*\{/)?.[0]?.length;
  287. if (importLength)
  288. s.appendLeft(target.start + importLength, ` ${strings.join("").trim()}`);
  289. }
  290. } else {
  291. newImports = imports;
  292. }
  293. newImports = onResolved?.(newImports) ?? newImports;
  294. let newEntries = stringifyImports(newImports, isCJS);
  295. newEntries = onStringified?.(newEntries, newImports) ?? newEntries;
  296. if (newEntries) {
  297. const insertionIndex = injectAtLast ? findStaticImportsLazy().reverse().find((i) => i.end <= firstOccurrence)?.end ?? 0 : 0;
  298. if (insertionIndex > 0)
  299. s.appendRight(insertionIndex, `
  300. ${newEntries}
  301. `);
  302. else if (hasShebang())
  303. s.appendLeft(s.original.indexOf("\n") + 1, `
  304. ${newEntries}
  305. `);
  306. else
  307. s.prepend(`${newEntries}
  308. `);
  309. }
  310. return {
  311. s,
  312. get code() {
  313. return s.toString();
  314. }
  315. };
  316. }
  317. function normalizeImports(imports) {
  318. for (const _import of imports)
  319. _import.as = _import.as ?? _import.name;
  320. return imports;
  321. }
  322. function resolveIdAbsolute(id, parentId) {
  323. return resolvePathSync(id, {
  324. url: parentId
  325. });
  326. }
  327. function isFilePath(path) {
  328. return path.startsWith(".") || isAbsolute(path) || path.includes("://");
  329. }
  330. const toImports = stringifyImports;
  331. const contextRE$1 = /\b_ctx\.([$\w]+)\b/g;
  332. const UNREF_KEY = "__unimport_unref_";
  333. const VUE_TEMPLATE_NAME = "unimport:vue-template";
  334. function vueTemplateAddon() {
  335. const self = {
  336. name: VUE_TEMPLATE_NAME,
  337. async transform(s, id) {
  338. if (!s.original.includes("_ctx.") || s.original.includes(UNREF_KEY))
  339. return s;
  340. const matches = Array.from(s.original.matchAll(contextRE$1));
  341. const imports = await this.getImports();
  342. let targets = [];
  343. for (const match of matches) {
  344. const name = match[1];
  345. const item = imports.find((i) => i.as === name);
  346. if (!item)
  347. continue;
  348. const start = match.index;
  349. const end = start + match[0].length;
  350. const tempName = `__unimport_${name}`;
  351. s.overwrite(start, end, `(${JSON.stringify(name)} in _ctx ? _ctx.${name} : ${UNREF_KEY}(${tempName}))`);
  352. if (!targets.find((i) => i.as === tempName)) {
  353. targets.push({
  354. ...item,
  355. as: tempName
  356. });
  357. }
  358. }
  359. if (targets.length) {
  360. targets.push({
  361. name: "unref",
  362. from: "vue",
  363. as: UNREF_KEY
  364. });
  365. for (const addon of this.addons) {
  366. if (addon === self)
  367. continue;
  368. targets = await addon.injectImportsResolved?.call(this, targets, s, id) ?? targets;
  369. }
  370. let injection = stringifyImports(targets);
  371. for (const addon of this.addons) {
  372. if (addon === self)
  373. continue;
  374. injection = await addon.injectImportsStringified?.call(this, injection, targets, s, id) ?? injection;
  375. }
  376. s.prepend(injection);
  377. }
  378. return s;
  379. },
  380. async declaration(dts, options) {
  381. const imports = await this.getImports();
  382. const items = imports.map((i) => {
  383. if (i.type || i.dtsDisabled)
  384. return "";
  385. const from = options?.resolvePath?.(i) || i.from;
  386. return `readonly ${i.as}: UnwrapRef<typeof import('${from}')${i.name !== "*" ? `['${i.name}']` : ""}>`;
  387. }).filter(Boolean).sort();
  388. const extendItems = items.map((i) => ` ${i}`).join("\n");
  389. return `${dts}
  390. // for vue template auto import
  391. import { UnwrapRef } from 'vue'
  392. declare module 'vue' {
  393. interface ComponentCustomProperties {
  394. ${extendItems}
  395. }
  396. }`;
  397. }
  398. };
  399. return self;
  400. }
  401. const contextRE = /resolveDirective as _resolveDirective/;
  402. const contextText = `${contextRE.source}, `;
  403. const directiveRE = /(?:var|const) (\w+) = _resolveDirective\("([\w.-]+)"\);?\s*/g;
  404. const VUE_DIRECTIVES_NAME = "unimport:vue-directives";
  405. function vueDirectivesAddon(options = {}) {
  406. function isDirective(importEntry) {
  407. let isDirective2 = importEntry.meta?.vueDirective === true;
  408. if (isDirective2) {
  409. return true;
  410. }
  411. isDirective2 = options.isDirective?.(normalizePath(process.cwd(), importEntry.from), importEntry) ?? false;
  412. if (isDirective2) {
  413. importEntry.meta ??= {};
  414. importEntry.meta.vueDirective = true;
  415. }
  416. return isDirective2;
  417. }
  418. const self = {
  419. name: VUE_DIRECTIVES_NAME,
  420. async transform(s, id) {
  421. if (!s.original.match(contextRE))
  422. return s;
  423. const matches = Array.from(s.original.matchAll(directiveRE)).sort((a, b) => b.index - a.index);
  424. if (!matches.length)
  425. return s;
  426. let targets = [];
  427. for await (const [
  428. begin,
  429. end,
  430. importEntry
  431. ] of findDirectives(
  432. isDirective,
  433. matches,
  434. this.getImports()
  435. )) {
  436. s.overwrite(begin, end, "");
  437. targets.push(importEntry);
  438. }
  439. if (!targets.length)
  440. return s;
  441. s.replace(contextText, "");
  442. for (const addon of this.addons) {
  443. if (addon === self)
  444. continue;
  445. targets = await addon.injectImportsResolved?.call(this, targets, s, id) ?? targets;
  446. }
  447. let injection = stringifyImports(targets);
  448. for (const addon of this.addons) {
  449. if (addon === self)
  450. continue;
  451. injection = await addon.injectImportsStringified?.call(this, injection, targets, s, id) ?? injection;
  452. }
  453. s.prepend(injection);
  454. return s;
  455. },
  456. async declaration(dts, options2) {
  457. const directivesMap = await this.getImports().then((imports) => {
  458. return imports.filter(isDirective).reduce((acc, i) => {
  459. if (i.type || i.dtsDisabled)
  460. return acc;
  461. let name;
  462. if (i.name === "default" && (i.as === "default" || !i.as)) {
  463. const file = basename(i.from);
  464. const idx = file.indexOf(".");
  465. name = idx > -1 ? file.slice(0, idx) : file;
  466. } else {
  467. name = i.as ?? i.name;
  468. }
  469. name = name[0] === "v" ? camelCase(name) : camelCase(`v-${name}`);
  470. if (!acc.has(name)) {
  471. acc.set(name, i);
  472. }
  473. return acc;
  474. }, /* @__PURE__ */ new Map());
  475. });
  476. if (!directivesMap.size)
  477. return dts;
  478. const directives = Array.from(directivesMap.entries()).map(([name, i]) => ` ${name}: typeof import('${options2?.resolvePath?.(i) || i.from}')['${i.name}']`).sort().join("\n");
  479. return `${dts}
  480. // for vue directives auto import
  481. declare module 'vue' {
  482. interface ComponentCustomProperties {
  483. ${directives}
  484. }
  485. interface GlobalDirectives {
  486. ${directives}
  487. }
  488. }`;
  489. }
  490. };
  491. return self;
  492. }
  493. function resolvePath(cwd, path) {
  494. return path[0] === "." ? resolve(cwd, path) : path;
  495. }
  496. function normalizePath(cwd, path) {
  497. return resolvePath(cwd, path).replace(/\\/g, "/");
  498. }
  499. async function* findDirectives(isDirective, regexArray, importsPromise) {
  500. const imports = (await importsPromise).filter(isDirective);
  501. if (!imports.length)
  502. return;
  503. const symbols = regexArray.reduce((acc, regex) => {
  504. const [all, symbol, resolveDirectiveName] = regex;
  505. if (acc.has(symbol))
  506. return acc;
  507. acc.set(symbol, [
  508. regex.index,
  509. regex.index + all.length,
  510. kebabCase(resolveDirectiveName)
  511. ]);
  512. return acc;
  513. }, /* @__PURE__ */ new Map());
  514. for (const [symbol, data] of symbols.entries()) {
  515. yield* findDirective(imports, symbol, data);
  516. }
  517. }
  518. function* findDirective(imports, symbol, [begin, end, importName]) {
  519. let resolvedName;
  520. for (const i of imports) {
  521. if (i.name === "default" && (i.as === "default" || !i.as)) {
  522. const file = basename(i.from);
  523. const idx = file.indexOf(".");
  524. resolvedName = kebabCase(idx > -1 ? file.slice(0, idx) : file);
  525. } else {
  526. resolvedName = kebabCase(i.as ?? i.name);
  527. }
  528. if (resolvedName[0] === "v") {
  529. resolvedName = resolvedName.slice(resolvedName[1] === "-" ? 2 : 1);
  530. }
  531. if (resolvedName === importName) {
  532. yield [
  533. begin,
  534. end,
  535. { ...i, name: i.name, as: symbol }
  536. ];
  537. return;
  538. }
  539. }
  540. }
  541. export { VUE_TEMPLATE_NAME as V, vueTemplateAddon as a, stripCommentsAndStrings as b, stringifyImports as c, defineUnimportPreset as d, excludeRE as e, dedupeImports as f, stripFileExtension as g, toTypeDeclarationItems as h, importAsRE as i, toTypeDeclarationFile as j, toTypeReExports as k, getString as l, matchRE as m, getMagicString as n, addImportToCode as o, normalizeImports as p, toImports as q, resolveIdAbsolute as r, separatorRE as s, toExports as t, VUE_DIRECTIVES_NAME as u, vueDirectivesAddon as v };