Ctrl K
ComunidadGuías…

front/vite-plugins/server-commonjs-interop.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { readFileSync } from 'fs';
import { build } from 'esbuild';
import { resolve } from 'path';

const FRONT_ROOT = resolve(import.meta.dirname, '..');
const SERVER_ROOT = resolve(FRONT_ROOT, '../server');
const REPO_ROOT = resolve(FRONT_ROOT, '..');
const NODE_MODULE_PATHS = [resolve(FRONT_ROOT, 'node_modules'), resolve(SERVER_ROOT, 'node_modules')];

function normalizeId(id) {
  return id.split('?')[0];
}

function isServerModule(id) {
  const filePath = normalizeId(id);

  if (!filePath.endsWith('.js') || filePath.includes('node_modules')) {
    return false;
  }

  return filePath.startsWith(`${SERVER_ROOT}/`) || filePath.includes('/server/');
}

function parseExportEntries(body) {
  const names = [];
  let depth = 0;
  let current = '';

  const addName = trimmed => {
    if (!trimmed || trimmed.startsWith('//')) {
      return;
    }

    const shorthand = trimmed.match(/^(\w+)$/);
    if (shorthand) {
      names.push(shorthand[1]);
      return;
    }

    const explicit = trimmed.match(/^(\w+)\s*:/);
    if (explicit) {
      names.push(explicit[1]);
      return;
    }

    // Quoted keys: only those that are valid identifiers can become named exports.
    const quoted = trimmed.match(/^['"](\w+)['"]\s*:/);
    if (quoted) {
      names.push(quoted[1]);
    }
  };

  for (const char of body) {
    if (char === '{' || char === '[' || char === '(') {
      depth += 1;
    } else if (char === '}' || char === ']' || char === ')') {
      depth -= 1;
    } else if (char === ',' && depth === 0) {
      addName(current.trim());
      current = '';
      continue;
    }

    current += char;
  }

  addName(current.trim());
  return names;
}

function getModuleExportsObjectBody(code) {
  const assignment = code.match(/module\.exports\s*=\s*\{/);
  if (!assignment) {
    return null;
  }

  let depth = 1;
  let index = assignment.index + assignment[0].length;

  while (index < code.length && depth > 0) {
    const char = code[index];
    if (char === '{') {
      depth += 1;
    } else if (char === '}') {
      depth -= 1;
    }
    index += 1;
  }

  if (depth !== 0) {
    return null;
  }

  return code.slice(assignment.index + assignment[0].length, index - 1);
}

function getCjsExportNames(code) {
  const names = new Set();

  for (const match of code.matchAll(/module\.exports\.(\w+)\s*=/g)) {
    names.add(match[1]);
  }

  for (const match of code.matchAll(/exports\.(\w+)\s*=/g)) {
    names.add(match[1]);
  }

  // Computed keys (module.exports['x'] = ...): only valid identifiers can
  // become named exports, hyphenated keys stay on the default export object.
  for (const match of code.matchAll(/(?:module\.)?exports\[['"](\w+)['"]\]\s*=/g)) {
    names.add(match[1]);
  }

  const objectExportBody = getModuleExportsObjectBody(code);
  if (objectExportBody) {
    for (const name of parseExportEntries(objectExportBody)) {
      names.add(name);
    }
  }

  return [...names];
}

function appendNamedExports(code, exportNames) {
  if (exportNames.length === 0) {
    return code;
  }

  const replaced = code.replace(/export default (require_\w+\(\));/, (_, initializer) => {
    const namedExports = exportNames.map(name => `export const ${name} = __cjsModule.${name};`).join('\n');
    return `const __cjsModule = ${initializer};\nexport default __cjsModule;\n${namedExports}`;
  });

  return replaced === code ? code : replaced;
}

/**
 * Vite dev serves files as native ESM, but the front imports many CommonJS modules
 * from ../server with named exports. Rollup's commonjs plugin alone leaves bare npm
 * imports (e.g. dayjs) in production bundles when server/node_modules is absent.
 * This plugin pre-bundles each server module with esbuild for dev and production.
 */
export function serverCommonjsInterop() {
  const cache = new Map();

  return {
    name: 'server-commonjs-interop',
    enforce: 'pre',
    configureServer(server) {
      // The bundled output inlines the whole require() graph: drop the whole
      // cache when a server file changes so edits show up without a restart.
      server.watcher.add(SERVER_ROOT);
      server.watcher.on('change', filePath => {
        if (isServerModule(filePath)) {
          cache.clear();
          const mod = server.moduleGraph.getModuleById(filePath);
          if (mod) {
            server.moduleGraph.invalidateModule(mod);
          }
        }
      });
    },
    async load(id) {
      if (!isServerModule(id)) {
        return null;
      }

      const filePath = normalizeId(id);
      if (cache.has(filePath)) {
        return cache.get(filePath);
      }

      const source = readFileSync(filePath, 'utf-8');
      if (!source.includes('module.exports') && !source.includes('exports.')) {
        return null;
      }

      const exportNames = getCjsExportNames(source);
      const result = await build({
        entryPoints: [filePath],
        write: false,
        bundle: true,
        format: 'esm',
        platform: 'node',
        absWorkingDir: REPO_ROOT,
        nodePaths: NODE_MODULE_PATHS,
        logLevel: 'silent'
      });

      const transformed = appendNamedExports(result.outputFiles[0].text, exportNames);
      cache.set(filePath, transformed);
      return transformed;
    }
  };
}