alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
125 lines (113 loc) • 3.63 kB
JavaScript
// Rewrites a project's source for an alouette migration. Parsing uses the
// project's own `typescript` package; nothing else is needed.
//
// Usage: npx alouette-codemod <transform> [--dry-run] [--only=<group>] <paths...>
//
// Transforms:
// surface-and-stacks Surface → <Box className="surface …">,
// HStack / VStack / Stack → <View className="…">,
// EditableSurface / FormEditableSurface →
// EditableSection / FormEditableSection
// (--only=surface, --only=stacks or --only=editable to
// run one part)
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { extname, join, relative } from "node:path";
import {
GROUPS,
transformSurfaceAndStacks,
} from "./codemods/surface-and-stacks.mjs";
const TRANSFORMS = {
"surface-and-stacks": {
run: transformSurfaceAndStacks,
groups: GROUPS,
},
};
const EXTENSIONS = new Set([
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
]);
const IGNORED_DIRECTORIES = new Set([
"node_modules",
"dist",
"build",
".git",
".expo",
]);
function usage(message) {
if (message) console.error(`${message}\n`);
console.error(
`Usage: alouette-codemod <transform> [--dry-run] [--only=<group>] <paths...>\n\nTransforms: ${Object.keys(TRANSFORMS).join(", ")}`,
);
process.exit(1);
}
function loadTypescript() {
for (const base of [join(process.cwd(), "package.json"), import.meta.url]) {
try {
return createRequire(base)("typescript");
} catch {
// try the next resolution base
}
}
console.error(
"alouette-codemod parses with the `typescript` package: add it to the project's devDependencies.",
);
process.exit(1);
}
function* sourceFiles(path) {
const stats = statSync(path);
if (stats.isFile()) {
if (EXTENSIONS.has(extname(path)) && !path.endsWith(".d.ts")) yield path;
return;
}
for (const entry of readdirSync(path)) {
if (IGNORED_DIRECTORIES.has(entry)) continue;
yield* sourceFiles(join(path, entry));
}
}
const args = process.argv.slice(2);
const transformName = args.shift();
const transform = TRANSFORMS[transformName];
if (!transform) {
usage(transformName ? `Unknown transform: ${transformName}` : undefined);
}
const dryRun = args.includes("--dry-run");
const only = args
.find((arg) => arg.startsWith("--only="))
?.slice("--only=".length);
if (only && !transform.groups.includes(only)) {
usage(`--only must be one of: ${transform.groups.join(", ")}`);
}
const paths = args.filter((arg) => !arg.startsWith("--"));
if (paths.length === 0) usage("No paths given.");
const ts = loadTypescript();
const groups = only ? [only] : transform.groups;
let changedCount = 0;
let warningCount = 0;
for (const path of paths) {
for (const file of sourceFiles(path)) {
const text = readFileSync(file, "utf8");
const { output, changed, warnings } = transform.run(text, file, ts, {
groups,
});
const displayPath = relative(process.cwd(), file);
for (const { line, message } of warnings) {
console.warn(`${displayPath}:${line}: ${message}`);
}
warningCount += warnings.length;
if (!changed) continue;
changedCount += 1;
if (dryRun) console.log(`would rewrite ${displayPath}`);
else writeFileSync(file, output);
}
}
console.log(
`${dryRun ? "Would rewrite" : "Rewrote"} ${changedCount} file(s), ${warningCount} warning(s). Run your formatter and linter to reorder imports.`,
);