Ctrl K
ComunidadGuías…

scripts/check-composables.mjs

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Composable convention check.
 *
 * A "composable" is a `use*` function that uses Vue reactivity
 * (ref/computed/watch/lifecycle hooks/...). See CONTRIBUTING.md.
 *
 *  - ERROR: a file in src/composable/ that is not a composable (must move to
 *    src/utils/ or src/lib/), or a composable file with no `use*` export.
 *  - WARN:  a `use*` function using Vue reactivity defined outside
 *    src/composable/ (should live there).
 */
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';

const SRC = fileURLToPath(new URL('../src/', import.meta.url));
const COMPOSABLE_DIR = join(SRC, 'composable');
// Pinia store factories are named `use*` but are stores, not composables.
const STORE_DIR = join(SRC, 'store');

const REACTIVITY_NAMES = [
  'ref', 'shallowRef', 'computed', 'watch', 'watchEffect', 'reactive',
  'shallowReactive', 'readonly', 'toRef', 'toRefs', 'isRef', 'unref',
  'markRaw', 'toRaw', 'defineModel', 'nextTick', 'inject', 'provide',
  'onMounted', 'onBeforeMount', 'onUpdated', 'onBeforeUpdate',
  'onUnmounted', 'onBeforeUnmount', 'onActivated', 'onDeactivated',
];
const REACTIVITY_RE = new RegExp(
  `\\b(${REACTIVITY_NAMES.join('|')})\\s*\\(`
);

const VUE_NAMED_IMPORT_RE = /from\s*['"]vue['"]/;

const USE_EXPORT_RES = [
  /export\s+(?:async\s+)?function\s+(use[A-Z]\w*)/g,
  /export\s+default\s+(?:async\s+)?function\s+(use[A-Z]\w*)/g,
  /export\s+\{\s*([^}]*\bus[A-Z]\w*[^}]*)\s*\}/g,
  /const\s+(use[A-Z]\w*)\s*=/g, // const useX = ... (exported later)
];

function usesReactivity(source) {
  if (!VUE_NAMED_IMPORT_RE.test(source)) return false;
  return REACTIVITY_RE.test(source);
}

function exportedUseNames(source) {
  const names = new Set();
  for (const res of USE_EXPORT_RES) {
    for (const match of source.matchAll(res)) {
      const captured = match[1] ?? '';
      const found = captured.match(/\buse[A-Z]\w*/g) ?? [];
      for (const name of found) names.add(name);
    }
  }
  return names;
}

function isComposableSource(source) {
  return usesReactivity(source) && exportedUseNames(source).size > 0;
}

function collectFiles(dir) {
  const out = [];
  for (const entry of readdirSync(dir)) {
    const full = join(dir, entry);
    if (statSync(full).isDirectory()) {
      out.push(...collectFiles(full));
    } else if (/\.(js|ts)$/.test(entry) && !entry.endsWith('.spec.js') && !entry.endsWith('.spec.ts')) {
      out.push(full);
    }
  }
  return out;
}

function walkSrc() {
  return collectFiles(SRC).filter(
    (f) => !f.startsWith(COMPOSABLE_DIR) && !f.startsWith(STORE_DIR)
  );
}

const errors = [];
const warnings = [];

for (const file of collectFiles(COMPOSABLE_DIR)) {
  const source = readFileSync(file, 'utf8');
  const rel = relative(SRC, file);
  const usesReact = usesReactivity(source);
  const useNames = exportedUseNames(source);

  if (useNames.size === 0) {
    errors.push(`${rel}: file in composable/ exports no \`use*\` function`);
  } else if (!usesReact) {
    errors.push(
      `${rel}: \`${[...useNames].join(', ')}\` uses no Vue reactivity — not a composable (move to src/utils/ or src/lib/)`
    );
  }
}

for (const file of walkSrc()) {
  const source = readFileSync(file, 'utf8');
  if (!usesReactivity(source)) continue;
  const useNames = exportedUseNames(source);
  if (useNames.size === 0) continue;
  const rel = relative(SRC, file);
  warnings.push(
    `${rel}: \`${[...useNames].join(', ')}\` uses Vue reactivity outside composable/`
  );
}

if (warnings.length > 0) {
  console.error(`[check-composables] ${warnings.length} composable(s) outside composable/:`);
  for (const w of warnings) console.error(`  warn  ${w}`);
}
if (errors.length > 0) {
  console.error(`[check-composables] ${errors.length} file(s) in composable/ that are not composables:`);
  for (const e of errors) console.error(`  error ${e}`);
}

process.exitCode = errors.length > 0 ? 1 : 0;