181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
|
import { join, Node, parse, Project, resolve } from "./src/dev/deps.ts";
|
||
|
import { error } from "./src/dev/error.ts";
|
||
|
import { freshImports, twindImports } from "./src/dev/imports.ts";
|
||
|
import { collect, ensureMinDenoVersion, generate } from "./src/dev/mod.ts";
|
||
|
|
||
|
ensureMinDenoVersion();
|
||
|
|
||
|
const help = `fresh-update
|
||
|
|
||
|
Update a Fresh project. This updates dependencies and optionally performs code
|
||
|
mods to update a project's source code to the latest recommended patterns.
|
||
|
|
||
|
To upgrade a projecct in the current directory, run:
|
||
|
fresh-update .
|
||
|
|
||
|
USAGE:
|
||
|
fresh-update <DIRECTORY>
|
||
|
`;
|
||
|
|
||
|
const flags = parse(Deno.args, {});
|
||
|
|
||
|
if (flags._.length !== 1) {
|
||
|
error(help);
|
||
|
}
|
||
|
|
||
|
const unresolvedDirectory = Deno.args[0];
|
||
|
const resolvedDirectory = resolve(unresolvedDirectory);
|
||
|
|
||
|
// Update dependencies in the import map.
|
||
|
const IMPORT_MAP_PATH = join(resolvedDirectory, "import_map.json");
|
||
|
let importMapText = await Deno.readTextFile(IMPORT_MAP_PATH);
|
||
|
const importMap = JSON.parse(importMapText);
|
||
|
freshImports(importMap.imports);
|
||
|
if (importMap.imports["twind"]) {
|
||
|
twindImports(importMap.imports);
|
||
|
}
|
||
|
importMapText = JSON.stringify(importMap, null, 2);
|
||
|
await Deno.writeTextFile(IMPORT_MAP_PATH, importMapText);
|
||
|
|
||
|
// Code mod for classic JSX -> automatic JSX.
|
||
|
const JSX_CODEMOD =
|
||
|
`This project is using the classic JSX transform. Would you like to update to the
|
||
|
automatic JSX transform? This will remove the /** @jsx h */ pragma from your
|
||
|
source code and add the jsx: "react-jsx" compiler option to your deno.json file.`;
|
||
|
const DENO_JSON_PATH = join(resolvedDirectory, "deno.json");
|
||
|
let denoJsonText = await Deno.readTextFile(DENO_JSON_PATH);
|
||
|
const denoJson = JSON.parse(denoJsonText);
|
||
|
if (denoJson.compilerOptions?.jsx !== "react-jsx" && confirm(JSX_CODEMOD)) {
|
||
|
console.log("Updating config file...");
|
||
|
denoJson.compilerOptions = denoJson.compilerOptions || {};
|
||
|
denoJson.compilerOptions.jsx = "react-jsx";
|
||
|
denoJson.compilerOptions.jsxImportSource = "preact";
|
||
|
denoJsonText = JSON.stringify(denoJson, null, 2);
|
||
|
await Deno.writeTextFile(DENO_JSON_PATH, denoJsonText);
|
||
|
|
||
|
const project = new Project();
|
||
|
const sfs = project.addSourceFilesAtPaths(
|
||
|
join(resolvedDirectory, "**", "*.{js,jsx,ts,tsx}"),
|
||
|
);
|
||
|
|
||
|
for (const sf of sfs) {
|
||
|
for (const d of sf.getImportDeclarations()) {
|
||
|
if (d.getModuleSpecifierValue() !== "preact") continue;
|
||
|
for (const n of d.getNamedImports()) {
|
||
|
const name = n.getName();
|
||
|
if (name === "h" || name === "Fragment") n.remove();
|
||
|
}
|
||
|
if (
|
||
|
d.getNamedImports().length === 0 &&
|
||
|
d.getNamespaceImport() === undefined &&
|
||
|
d.getDefaultImport() === undefined
|
||
|
) {
|
||
|
d.remove();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let text = sf.getFullText();
|
||
|
text = text.replaceAll("/** @jsx h */\n", "");
|
||
|
text = text.replaceAll("/** @jsxFrag Fragment */\n", "");
|
||
|
sf.replaceWithText(text);
|
||
|
|
||
|
await sf.save();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Code mod for class={tw`border`} to class="border".
|
||
|
const TWIND_CODEMOD =
|
||
|
`This project is using an old version of the twind integration. Would you like to
|
||
|
update to the new twind plugin? This will remove the 'class={tw\`border\`}'
|
||
|
boilerplate from your source code replace it with the simpler 'class="border"'.`;
|
||
|
if (importMap.imports["@twind"] && confirm(TWIND_CODEMOD)) {
|
||
|
await Deno.remove(join(resolvedDirectory, importMap.imports["@twind"]));
|
||
|
|
||
|
delete importMap.imports["@twind"];
|
||
|
importMapText = JSON.stringify(importMap, null, 2);
|
||
|
await Deno.writeTextFile(IMPORT_MAP_PATH, importMapText);
|
||
|
|
||
|
const MAIN_TS = `/// <reference no-default-lib="true" />
|
||
|
/// <reference lib="dom" />
|
||
|
/// <reference lib="dom.iterable" />
|
||
|
/// <reference lib="dom.asynciterable" />
|
||
|
/// <reference lib="deno.ns" />
|
||
|
|
||
|
import { start } from "$fresh/server.ts";
|
||
|
import manifest from "./fresh.gen.ts";
|
||
|
|
||
|
import twindPlugin from "$fresh/plugins/twind.ts";
|
||
|
import twindConfig from "./twind.config.ts";
|
||
|
|
||
|
await start(manifest, { plugins: [twindPlugin(twindConfig)] });\n`;
|
||
|
const MAIN_TS_PATH = join(resolvedDirectory, "main.ts");
|
||
|
await Deno.writeTextFile(MAIN_TS_PATH, MAIN_TS);
|
||
|
|
||
|
const TWIND_CONFIG_TS = `import { Options } from "$fresh/plugins/twind.ts";
|
||
|
|
||
|
export default {
|
||
|
selfURL: import.meta.url,
|
||
|
} as Options;
|
||
|
`;
|
||
|
await Deno.writeTextFile(
|
||
|
join(resolvedDirectory, "twind.config.ts"),
|
||
|
TWIND_CONFIG_TS,
|
||
|
);
|
||
|
|
||
|
const project = new Project();
|
||
|
const sfs = project.addSourceFilesAtPaths(
|
||
|
join(resolvedDirectory, "**", "*.{js,jsx,ts,tsx}"),
|
||
|
);
|
||
|
|
||
|
for (const sf of sfs) {
|
||
|
const nodes = sf.forEachDescendantAsArray();
|
||
|
for (const n of nodes) {
|
||
|
if (!n.wasForgotten() && Node.isJsxAttribute(n)) {
|
||
|
const init = n.getInitializer();
|
||
|
const name = n.getName();
|
||
|
if (
|
||
|
Node.isJsxExpression(init) &&
|
||
|
(name === "class" || name === "className")
|
||
|
) {
|
||
|
const expr = init.getExpression();
|
||
|
if (Node.isTaggedTemplateExpression(expr)) {
|
||
|
const tag = expr.getTag();
|
||
|
if (Node.isIdentifier(tag) && tag.getText() === "tw") {
|
||
|
const template = expr.getTemplate();
|
||
|
if (Node.isNoSubstitutionTemplateLiteral(template)) {
|
||
|
n.setInitializer(`"${template.getLiteralValue()}"`);
|
||
|
}
|
||
|
}
|
||
|
} else if (expr?.getFullText() === `tw(props.class ?? "")`) {
|
||
|
n.setInitializer(`{props.class}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const text = sf.getFullText();
|
||
|
const removeTw = [...text.matchAll(/tw[,\s`(]/g)].length === 1;
|
||
|
|
||
|
for (const d of sf.getImportDeclarations()) {
|
||
|
if (d.getModuleSpecifierValue() !== "@twind") continue;
|
||
|
for (const n of d.getNamedImports()) {
|
||
|
const name = n.getName();
|
||
|
if (name === "tw" && removeTw) n.remove();
|
||
|
}
|
||
|
d.setModuleSpecifier("twind");
|
||
|
if (
|
||
|
d.getNamedImports().length === 0 &&
|
||
|
d.getNamespaceImport() === undefined &&
|
||
|
d.getDefaultImport() === undefined
|
||
|
) {
|
||
|
d.remove();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await sf.save();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const manifest = await collect(resolvedDirectory);
|
||
|
await generate(resolvedDirectory, manifest);
|