All files / src/plugin vitek-transform.ts

92% Statements 46/50
83.33% Branches 25/30
80% Functions 4/5
97.67% Lines 42/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86                            1x       27x                 11x 11x 11x 11x 11x 10x 10x 10x 10x   10x   10x 8x 8x 8x 7x 7x 6x 6x 6x 8x 8x     10x 10x 1x 1x   1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x         10x 7x                
/**
 * vitek:transform — rewrite relative and alias imports to root-relative paths
 */
 
import type { Plugin } from 'vite';
import * as path from 'path';
import MagicString from 'magic-string';
import {
  normalizeModuleIdPath,
  resolveWithExtension,
} from '../adapters/vite/path-utils.js';
import type { PluginContext } from './context.js';
 
function escapeRegex(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
 
export function createTransformPlugin(ctx: PluginContext): Plugin {
  return {
    name: 'vitek:transform',
    enforce: 'pre',
 
    transform: {
      filter: {
        id: { include: /\.(tsx?|jsx?|mjs)$/, exclude: /node_modules/ },
      },
      handler(code: string, id: string) {
        Iif (!ctx.root) return null;
        Iif (id.includes('node_modules') || !/\.(tsx?|jsx?|mjs)$/.test(id)) return null;
        const srcDir = path.resolve(ctx.root, ctx.options.srcDir ?? 'src');
        const normalizedId = normalizeModuleIdPath(id, ctx.root);
        if (!normalizedId.startsWith(srcDir)) return null;
        const dir = path.dirname(normalizedId);
        const rootSlash = path.resolve(ctx.root) + path.sep;
        const s = new MagicString(code);
        let hasChanges = false;
 
        const relImportRe = /from\s+['"](\.\.?[^'"]+)['"]/g;
        let match: RegExpExecArray | null;
        while ((match = relImportRe.exec(code)) !== null) {
          const specifier = match[1];
          const candidate = path.resolve(dir, specifier);
          if (!candidate.startsWith(rootSlash)) continue;
          const target = resolveWithExtension(candidate);
          if (!target) continue;
          const rootRelative = path.relative(ctx.root, target).replace(/\\/g, '/');
          const newSpecifier = `/${rootRelative}`;
          const quote = match[0].includes('"') ? '"' : "'";
          s.overwrite(match.index, match.index + match[0].length, `from ${quote}${newSpecifier}${quote}`);
          hasChanges = true;
        }
 
        const alias = ctx.options.alias;
        if (alias && Object.keys(alias).length > 0) {
          const entries = Object.entries(alias)
            .filter(([, v]) => v != null && v !== '')
            .sort(([a], [b]) => b.length - a.length);
          for (const [key, replacement] of entries) {
            const escapedKey = escapeRegex(key);
            const aliasRe = new RegExp(`from\\s+(['"])(${escapedKey})(/[^'"]*)?\\1`, 'g');
            while ((match = aliasRe.exec(code)) !== null) {
              const quote = match[1];
              const rest = (match[3] || '').replace(/^\//, '');
              const base = path.isAbsolute(replacement)
                ? path.join(replacement, rest)
                : path.join(ctx.root, replacement, rest);
              const target = resolveWithExtension(base);
              Iif (!target) continue;
              const rootRelative = path.relative(ctx.root, target).replace(/\\/g, '/');
              const newSpecifier = `/${rootRelative}`;
              s.overwrite(match.index, match.index + match[0].length, `from ${quote}${newSpecifier}${quote}`);
              hasChanges = true;
            }
          }
        }
 
        if (!hasChanges) return null;
        return {
          code: s.toString(),
          map: s.generateMap({ hires: 'boundary' }),
        };
      },
    } as unknown as NonNullable<Plugin['transform']>,
  };
}