skeleton
Version:
The CLI for Skeleton related tooling.
1,165 lines (1,145 loc) • 31.7 kB
JavaScript
import { multiselect, isCancel, spinner, log, outro, intro } from '@clack/prompts';
import { glob } from 'tinyglobby';
import { coerce, lt } from 'semver';
import detectIndent from 'detect-indent';
import { red, dim, bgBlueBright, black, bgGreenBright } from 'colorette';
import { Command, Argument, Option } from 'commander';
import { fileURLToPath } from 'node:url';
import { extname, join, dirname } from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { parse as parse$1 } from 'svelte/compiler';
import { walk } from 'zimmerframe';
import MagicString from 'magic-string';
import { Project, Node } from 'ts-morph';
import { nanoid } from 'nanoid';
import { parse, atRule, comment } from 'postcss';
import { detect, resolveCommand } from 'package-manager-detector';
import child_process from 'node:child_process';
import { promisify } from 'node:util';
import getLatestVersion from 'latest-version';
import { parse as parse$2 } from 'node-html-parser';
function sortPropertiesAlphabetically(object) {
const orderedObject = {};
const sortedEntries = Object.entries(object).sort(([a], [b]) => a.localeCompare(b));
for (const [key, value] of sortedEntries) {
orderedObject[key] = value;
}
return orderedObject;
}
function transformPackageJson(code, skeletonVersion, skeletonSvelteVersion) {
let isUsingComponents = false;
const pkg = JSON.parse(code);
for (const field of ["dependencies", "devDependencies"]) {
if (!pkg[field]) {
continue;
}
const coerced = coerce(pkg[field]["@skeletonlabs/skeleton"]);
if (coerced && lt(coerced.version, "3.0.0")) {
isUsingComponents = true;
delete pkg[field]["@skeletonlabs/skeleton"];
pkg[field]["@skeletonlabs/skeleton-svelte"] = `^${skeletonSvelteVersion}`;
}
if (pkg[field]["@skeletonlabs/tw-plugin"]) {
delete pkg[field]["@skeletonlabs/tw-plugin"];
pkg[field]["@skeletonlabs/skeleton"] = `^${skeletonVersion}`;
}
pkg[field] = sortPropertiesAlphabetically(pkg[field]);
}
return {
code: JSON.stringify(pkg, null, detectIndent(code).indent || " "),
meta: {
isUsingComponents
}
};
}
const COLOR_PAIRING_REGEXES = [
{
find: /(\w+)-50-900-token\b/g,
replace: "$1-50-950"
},
{
find: /(\w+)-100-800-token\b/g,
replace: "$1-100-900"
},
{
find: /(\w+)-200-700-token\b/g,
replace: "$1-200-800"
},
{
find: /(\w+)-300-600-token\b/g,
replace: "$1-300-700"
},
{
find: /(\w+)-400-500-token\b/g,
replace: "$1-500"
},
{
find: /(\w+)-900-50-token\b/g,
replace: "$1-950-50"
},
{
find: /(\w+)-800-100-token\b/g,
replace: "$1-900-100"
},
{
find: /(\w+)-700-200-token\b/g,
replace: "$1-800-200"
},
{
find: /(\w+)-600-300-token\b/g,
replace: "$1-700-300"
},
{
find: /(\w+)-500-400-token\b/g,
replace: "$1-600-400"
}
];
const BACKGROUND_REGEXES = [
{
find: /bg-(\w+)-backdrop-token\b/g,
replace: "bg-$1-50/50 dark:bg-$1-950/50"
},
{
find: /bg-(\w+)-hover-token\b/g,
replace: "preset-tonal-$1"
},
{
find: /bg-(\w+)-active-token\b/g,
replace: "preset-filled-$1-500"
}
];
const BORDER_RADIUS_REGEXES = [
{
find: /rounded-token\b/g,
replace: "rounded-base"
},
{
find: /rounded-(tl|tr|bl|br)-token\b/g,
replace: "rounded-$1-base"
},
{
find: /rounded-container-token\b/g,
replace: "rounded-container"
},
{
find: /rounded-(tl|tr|bl|br)-container-token\b/g,
replace: "rounded-$1-container"
}
];
const BORDER_RING_REGEXES = [
{
find: /border-token\b/g,
replace: "border"
},
{
find: /border-(\w+)-(\d+)-(\d+)-token\b/g,
replace: "border-$1-$2-$3"
},
{
find: /ring-token\b/g,
replace: "ring"
},
{
find: /ring-(\w+)-(\d+)-(\d+)-token\b/g,
replace: "ring-$1-$2-$3"
}
];
const TEXT_REGEXES = [
{
find: /font-headings-token\b/g,
replace: "heading-font-family"
},
{
find: /font-token\b/g,
replace: "base-font-family"
},
{
find: /text-token\b/g,
replace: "base-font-color"
},
{
find: /text-on-(\w+)-token\b/g,
replace: "text-$1-contrast-500"
},
{
find: /text-(\w+)-([^-]+)-([^-]+)-token\b/g,
replace: "text-$1-$2-$3"
}
];
const DECORATION_ACCENT_REGEXES = [
{
find: /decoration-(\w+)-([^-]+)-([^-]+)-token\b/g,
replace: "decoration-$1-$2-$3"
},
{
find: /accent-(\w+)-token\b/g,
replace: "accent-$1-500"
}
];
const PRESET_REGEXES = [
{
find: /variant-filled-(\w+)\b/g,
replace: "preset-filled-$1-500"
},
{
find: /variant-filled\b/g,
replace: "preset-filled"
},
{
find: /variant-ghost-(\w+)\b/g,
replace: "preset-tonal-$1 border border-$1-500"
},
{
find: /variant-ghost\b/g,
replace: "preset-tonal border border-surface-500"
},
{
find: /variant-soft-(\w+)\b/g,
replace: "preset-tonal-$1"
},
{
find: /variant-soft\b/g,
replace: "preset-tonal"
},
{
find: /variant-ringed-(\w+)\b/g,
replace: "preset-outlined-$1-500"
},
{
find: /variant-ringed\b/g,
replace: "preset-outlined"
},
{
find: /variant-glass-(\w+)\b/g,
replace: "preset-tonal-$1"
},
{
find: /variant-glass\b/g,
replace: "preset-tonal"
},
{
find: /variant-gradient-(\w+)-(\w+)\b/g,
replace: "from-$1-500 to-$2-500"
}
];
const TAILWIND_COMPONENT_REGEXES = [
/**
* Disabled until further discussion
* @see https://github.com/skeletonlabs/skeleton/pull/2972#discussion_r1857260763
*/
// {
// find: /\bcard\b(?!.*bg-)/g,
// replace: 'card bg-surface-100-900-token'
// },
{
find: /btn-xl\b/g,
replace: "btn-lg"
},
{
find: /btn-icon-xl\b/g,
replace: "btn-icon-lg"
},
{
find: /btn-group\b/g,
replace: ""
},
{
find: /table-hover\b/g,
replace: ""
}
];
const CLASS_REGEXES = [
...COLOR_PAIRING_REGEXES,
...BACKGROUND_REGEXES,
...BORDER_RADIUS_REGEXES,
...BORDER_RING_REGEXES,
...TEXT_REGEXES,
...DECORATION_ACCENT_REGEXES,
...PRESET_REGEXES,
...TAILWIND_COMPONENT_REGEXES
];
function transformClasses(code) {
return {
code: CLASS_REGEXES.reduce((result, migration) => {
return result.replace(migration.find, migration.replace);
}, code)
};
}
function addNamedImport(file, specifier, name) {
const existingImportDeclaration = file.getImportDeclaration((importDeclaration) => {
const moduleSpecifier = importDeclaration.getModuleSpecifier().getLiteralText();
return moduleSpecifier === specifier;
});
if (existingImportDeclaration) {
if (!existingImportDeclaration.getNamedImports().some((namedImport) => namedImport.getName() === name)) {
existingImportDeclaration.addNamedImport(name);
}
} else {
file.addImportDeclaration({
moduleSpecifier: specifier,
namedImports: [name]
});
}
}
const project = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
skipFileDependencyResolution: true,
skipLoadingLibFiles: true
});
function parseSourceFile(code) {
return project.createSourceFile(`${nanoid()}.ts`, code);
}
const EXPORT_MAPPINGS = {
AccordionItem: {
namedImport: {
type: "renamed",
value: "Accordion"
},
identifier: {
type: "renamed",
value: "Accordion.Item"
}
},
AppShell: {
namedImport: {
type: "removed"
},
identifier: {
type: "removed"
}
},
Apollo: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
AppRailAnchor: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Autocomplete: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
BlueNight: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
CodeBlock: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
ConicGradient: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Drawer: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Emerald: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
GreenFall: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
LightSwitch: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
ListBox: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
ListBoxItem: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Modal: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Noir: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
NoirLight: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
RecursiveTreeView: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
RecursiveTreeViewItem: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Rustic: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Step: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Stepper: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Summer84: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
Table: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
TableOfContents: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
TreeView: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
TreeViewItem: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
XPro: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
autoModeWatcher: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
clipboard: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
filter: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
focusTrap: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getDrawerStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getModalStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getModeAutoPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getModeOsPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getModeUserPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
getToastStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
initializeStores: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
localStorageStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
modeCurrent: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
modeOsPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
modeUserPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
popup: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
prefersReducedMotionStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
setInitialClassState: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
setModeCurrent: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
setModeUserPrefers: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
storeHighlightJs: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
storePopup: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
tableMapperValues: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
tableSourceMapper: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
tableSourceValues: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
tocCrawler: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
tocStore: {
namedImport: { type: "removed" },
identifier: { type: "removed" }
},
AppRail: {
namedImport: {
type: "renamed",
value: "Navigation"
},
identifier: {
type: "renamed",
value: "Navigation"
}
},
AppRailTile: {
namedImport: {
type: "renamed",
value: "Navigation"
},
identifier: {
type: "renamed",
value: "Navigation.Tile"
}
},
FileButton: {
namedImport: {
type: "renamed",
value: "FileUpload"
},
identifier: {
type: "renamed",
value: "FileUpload"
}
},
FileDropzone: {
namedImport: {
type: "renamed",
value: "FileUpload"
},
identifier: {
type: "renamed",
value: "FileUpload"
}
},
InputChip: {
namedImport: {
type: "renamed",
value: "TagsInput"
},
identifier: {
type: "renamed",
value: "TagsInput"
}
},
Paginator: {
namedImport: {
type: "renamed",
value: "Pagination"
},
identifier: {
type: "renamed",
value: "Pagination"
}
},
ProgressBar: {
namedImport: {
type: "renamed",
value: "Progress"
},
identifier: {
type: "renamed",
value: "Progress"
}
},
ProgressRadial: {
namedImport: {
type: "renamed",
value: "ProgressRing"
},
identifier: {
type: "renamed",
value: "ProgressRing"
}
},
RadioGroup: {
namedImport: {
type: "renamed",
value: "Segment"
},
identifier: {
type: "renamed",
value: "Segment"
}
},
RadioItem: {
namedImport: {
type: "renamed",
value: "Segment"
},
identifier: {
type: "renamed",
value: "Segment.Item"
}
},
RangeSlider: {
namedImport: {
type: "renamed",
value: "Slider"
},
identifier: {
type: "renamed",
value: "Slider"
}
},
Ratings: {
namedImport: {
type: "renamed",
value: "Rating"
},
identifier: {
type: "renamed",
value: "Rating"
}
},
SlideToggle: {
namedImport: {
type: "renamed",
value: "Switch"
},
identifier: {
type: "renamed",
value: "Switch"
}
},
TabAnchor: {
namedImport: {
type: "renamed",
value: "Tabs"
},
identifier: {
type: "renamed",
value: "Tabs.Control"
}
},
TabGroup: {
namedImport: {
type: "renamed",
value: "Tabs"
},
identifier: {
type: "renamed",
value: "Tabs"
}
},
Toast: {
namedImport: {
type: "renamed",
value: "ToastProvider"
},
identifier: {
type: "renamed",
value: "ToastProvider"
}
}
};
function transformModule(code) {
const file = parseSourceFile(code);
const skeletonImports = [];
file.forEachDescendant((node) => {
if (Node.isImportDeclaration(node)) {
const moduleSpecifier = node.getModuleSpecifier();
if (moduleSpecifier.getLiteralText() === "@skeletonlabs/skeleton") {
node.setModuleSpecifier("@skeletonlabs/skeleton-svelte");
}
}
if (Node.isImportSpecifier(node) && node.getImportDeclaration().getModuleSpecifier().getLiteralText() === "@skeletonlabs/skeleton-svelte") {
const name = node.getName();
if (Object.hasOwn(EXPORT_MAPPINGS, name)) {
const exportMapping = EXPORT_MAPPINGS[name];
switch (exportMapping.namedImport.type) {
case "renamed": {
if (exportMapping.namedImport.value.match(/^[A-Za-z]+\.[A-Za-z]+$/)) {
break;
}
node.remove();
addNamedImport(file, "@skeletonlabs/skeleton-svelte", exportMapping.namedImport.value);
break;
}
case "removed": {
const parent = node.getImportDeclaration();
if (parent.getNamedImports().length === 1 && !parent.getDefaultImport() && !parent.getNamespaceImport()) {
parent.remove();
} else {
node.remove();
}
break;
}
}
skeletonImports.push(name);
}
}
});
file.forEachDescendant((node) => {
if (!node.wasForgotten() && Node.isIdentifier(node) && !Node.isImportSpecifier(node.getParent())) {
const name = node.getText();
if (Object.hasOwn(EXPORT_MAPPINGS, name) && skeletonImports.includes(name)) {
const exportMapping = EXPORT_MAPPINGS[name];
if (exportMapping.identifier.type === "renamed") {
node.replaceWithText(exportMapping.identifier.value);
}
}
}
if (!node.wasForgotten() && Node.isStringLiteral(node) && !Node.isImportDeclaration(node.getParent())) {
node.replaceWithText(transformClasses(node.getText()).code);
}
});
return {
code: file.getFullText(),
meta: {
skeletonImports
}
};
}
function transformStyleSheet(code) {
const parsed = parse(code);
parsed.walkAtRules("apply", (atRule) => {
atRule.params = transformClasses(atRule.params).code;
});
return {
code: parsed.toString()
};
}
function renameComponent(s, node, name) {
const adjustedStart = node.start + 1;
s.update(adjustedStart, adjustedStart + node.name.length, name);
const componentString = s.original.slice(node.start, node.end);
const indexOfNonSelfClosingTag = componentString.lastIndexOf("</");
if (indexOfNonSelfClosingTag === -1 || node.start + indexOfNonSelfClosingTag > node.end) {
return;
}
s.update(node.start + indexOfNonSelfClosingTag + 2, node.start + indexOfNonSelfClosingTag + 2 + node.name.length, name);
}
function transformScript(s, script) {
if (!script || !("start" in script.content && typeof script.content.start === "number" && "end" in script.content && typeof script.content.end === "number")) {
return {
meta: {
skeletonImports: []
}
};
}
const content = s.original.slice(script.content.start, script.content.end);
const transformed = transformModule(content);
s.overwrite(script.content.start, script.content.end, transformed.code);
return {
meta: transformed.meta
};
}
function transformCss(s, css) {
if (!css) {
return;
}
const transformed = transformStyleSheet(s.original.slice(css.content.start, css.content.end));
s.overwrite(css.content.start, css.content.end, transformed.code);
}
function hasRange(node) {
return "start" in node && "end" in node && typeof node.start === "number" && typeof node.end === "number";
}
function transformFragment(s, fragment, skeletonImports) {
walk(
fragment,
{},
{
Literal(node, ctx) {
const parent = ctx.path.at(-1);
if (typeof node.raw === "string" && node.raw !== "" && !(parent && parent.type === "ImportDeclaration") && hasRange(node)) {
s.update(node.start, node.end, transformClasses(node.raw).code);
}
ctx.next();
},
Text(node, ctx) {
if (node.data !== "" && hasRange(node)) {
s.update(node.start, node.end, transformClasses(node.data).code);
}
ctx.next();
},
ClassDirective(node, ctx) {
if (!(node.expression.type === "Identifier" && !("loc" in node.expression) && node.name === node.expression.name) && hasRange(node)) {
const adjustedStart = node.start + "class:".length;
s.update(adjustedStart, adjustedStart + node.name.length, transformClasses(node.name).code);
}
ctx.next();
},
Component(node, ctx) {
if (Object.hasOwn(EXPORT_MAPPINGS, node.name) && skeletonImports.includes(node.name)) {
const exportMapping = EXPORT_MAPPINGS[node.name];
if (exportMapping.identifier.type === "renamed" && hasRange(node)) {
renameComponent(s, node, exportMapping.identifier.value);
}
}
ctx.next();
}
}
);
return {
code: s.toString()
};
}
function transformSvelte(code) {
const s = new MagicString(code);
const root = parse$1(code, {
modern: true
});
const skeletonImports = [
...transformScript(s, root.module).meta.skeletonImports,
...transformScript(s, root.instance).meta.skeletonImports
];
transformFragment(s, root.fragment, skeletonImports);
transformCss(s, root.css);
return {
code: s.toString()
};
}
const exec = promisify(child_process.exec);
async function installDependencies(cwd = process.cwd()) {
const pm = await detect({
cwd
});
const resolvedCommand = resolveCommand(pm?.agent ?? "npm", "install", []);
if (!resolvedCommand) {
throw new Error("Could not resolve package manager command.");
}
return exec(`${resolvedCommand.command} ${resolvedCommand.args.join(" ")}`, {
cwd
});
}
function getTailwindImport(root) {
let tailwindImport;
root.walkAtRules("import", (atRule2) => {
if (atRule2.params.includes("tailwindcss")) {
tailwindImport = atRule2;
}
});
return tailwindImport;
}
function transformAppCss(code, theme, addAtSource) {
code = transformStyleSheet(code).code;
const root = parse(code);
const nodes = [];
nodes.push(
atRule({
name: "import",
params: '"@skeletonlabs/skeleton"'
})
);
nodes.push(
atRule({
name: "import",
params: '"@skeletonlabs/skeleton/optional/presets"'
})
);
switch (theme.type) {
case "preset":
nodes.push(
atRule({
name: "import",
params: `"@skeletonlabs/skeleton/themes/${theme.value}"`
})
);
break;
case "custom":
nodes.push(comment({ text: `Add your theme import for your theme: "${theme.value}" here` }));
break;
}
if (addAtSource) {
nodes.push(
atRule({
name: "source",
params: '"../node_modules/@skeletonlabs/skeleton-svelte/dist"'
})
);
}
const tailwindImport = getTailwindImport(root);
if (tailwindImport) {
root.insertAfter(tailwindImport, nodes);
} else {
root.prepend(nodes);
}
return {
code: root.toString()
};
}
const THEME_MAPPINGS = {
skeleton: { type: "preset", value: "legacy" },
"gold-nouveau": { type: "preset", value: "nouveau" },
wintry: { type: "preset", value: "wintry" },
modern: { type: "preset", value: "modern" },
rocket: { type: "preset", value: "rocket" },
seafoam: { type: "preset", value: "seafoam" },
vintage: { type: "preset", value: "vintage" },
sahara: { type: "preset", value: "sahara" },
hamlindigo: { type: "preset", value: "hamlindigo" },
crimson: { type: "preset", value: "crimson" }
};
function transformAppHtml(code) {
const parsed = parse$2(code);
const html = parsed.querySelector("html");
const body = parsed.querySelector("body");
if (!(html && body)) {
return {
code,
meta: {
theme: void 0
}
};
}
const theme = body.getAttribute("data-theme");
if (!theme) {
return {
code,
meta: {
theme: void 0
}
};
}
let type;
body.removeAttribute("data-theme");
if (Object.hasOwn(THEME_MAPPINGS, theme)) {
html.setAttribute("data-theme", THEME_MAPPINGS[theme].value);
type = "preset";
} else {
html.setAttribute("data-theme", theme);
type = "custom";
}
return {
code: parsed.toString(),
meta: {
theme: {
value: theme,
type
}
}
};
}
const FALLBACK_THEME = {
type: "preset",
value: "cerberus"
};
async function skeleton3(options) {
const cwd = options.cwd ?? process.cwd();
const migrations = [];
const packageJson = {
name: "package.json",
paths: await glob("package.json", { cwd })
};
const appHtml = {
name: "src/app.html",
paths: await glob("src/app.html", { cwd })
};
const appCss = {
name: "src/app.css",
paths: await glob("src/app.css", { cwd })
};
for (const file of [packageJson, appHtml, appCss]) {
if (file.paths.length === 0) {
cli.error(`"${file.name}" not found in directory "${cwd}".`);
}
if (file.paths.length > 1) {
cli.error(`Multiple "${file.name}" entries found in directory: "${cwd}", please ensure there is only one`);
}
}
const availableSourceFolders = await glob("*", {
cwd,
onlyDirectories: true,
ignore: ["node_modules"]
});
const sourceFolders = await multiselect({
message: "What folders make use of Skeleton? (classes, imports, etc.)",
options: availableSourceFolders.map((folder) => ({ label: folder, value: folder })),
initialValues: availableSourceFolders
});
if (isCancel(sourceFolders)) {
cli.error("Migration canceled, nothing written to disk");
return;
}
let isUsingComponents = false;
const packageSpinner = spinner();
packageSpinner.start(`Migrating ${packageJson.name}...`);
try {
const packageJsonCode = await readFile(packageJson.paths[0], "utf-8");
const skeletonVersion = await getLatestVersion("@skeletonlabs/skeleton", { version: ">=3.0.0-0 <4.0.0" });
const skeletonSvelteVersion = await getLatestVersion("@skeletonlabs/skeleton-svelte", { version: ">=1.0.0-0 <2.0.0" });
const transformedPackageJson = transformPackageJson(packageJsonCode, skeletonVersion, skeletonSvelteVersion);
isUsingComponents = transformedPackageJson.meta.isUsingComponents;
migrations.push({ path: packageJson.paths[0], content: transformedPackageJson.code });
packageSpinner.stop(`Successfully migrated ${packageJson.name}!`);
} catch (e) {
packageSpinner.stop(`Failed to migrate ${packageJson.name}: ${e instanceof Error ? e.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
let theme;
const appHtmlSpinner = spinner();
appHtmlSpinner.start(`Migrating ${appHtml.name}...`);
try {
const appHtmlCode = await readFile(appHtml.paths[0], "utf-8");
const transformedAppHtml = transformAppHtml(appHtmlCode);
if (transformedAppHtml.meta.theme && Object.hasOwn(THEME_MAPPINGS, transformedAppHtml.meta.theme.value)) {
theme = THEME_MAPPINGS[transformedAppHtml.meta.theme.value];
} else if (transformedAppHtml.meta.theme) {
theme = transformedAppHtml.meta.theme;
} else {
theme = FALLBACK_THEME;
}
migrations.push({ path: appHtml.paths[0], content: transformedAppHtml.code });
appHtmlSpinner.stop(`Successfully migrated ${appHtml.name}!`);
} catch (e) {
appHtmlSpinner.stop(`Failed to migrate ${appHtml.name}: ${e instanceof Error ? e.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
const appCssSpinner = spinner();
appCssSpinner.start(`Migrating ${appCss.name}...`);
try {
const appCssCode = await readFile(appCss.paths[0], "utf-8");
const transformedAppCss = transformAppCss(appCssCode, theme ?? FALLBACK_THEME, isUsingComponents);
migrations.push({ path: appCss.paths[0], content: transformedAppCss.code });
appCssSpinner.stop(`Successfully migrated ${appCss.name}!`);
} catch (e) {
appCssSpinner.stop(`Failed to migrate ${appCss.name}: ${e instanceof Error ? e.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
if (sourceFolders.length > 0) {
const sourceFiles = await glob(
sourceFolders.map((folder) => `${folder}**/*.{svelte,js,mjs,ts,mts,css,pcss,postcss}`),
{
cwd,
ignore: ["node_modules", "src/app.css"]
}
);
const sourceFilesSpinner = spinner();
sourceFilesSpinner.start(`Migrating source files...`);
for (const sourceFile of sourceFiles) {
sourceFilesSpinner.message(`Migrating ${sourceFile}...`);
const extension = extname(sourceFile);
try {
const code = await readFile(sourceFile, "utf-8");
switch (extension) {
case ".svelte": {
const transformedSvelte = transformSvelte(code);
migrations.push({ path: sourceFile, content: transformedSvelte.code });
break;
}
case ".js":
case ".mjs":
case ".ts":
case ".mts": {
const transformedModule = transformModule(code);
migrations.push({ path: sourceFile, content: transformedModule.code });
break;
}
case ".css":
case ".pcss":
case ".postcss": {
const transformedStyleSheet = transformStyleSheet(code);
migrations.push({ path: sourceFile, content: transformedStyleSheet.code });
break;
}
}
sourceFilesSpinner.message(`Successfully migrated ${sourceFile}!`);
} catch (e) {
sourceFilesSpinner.stop(`Failed to migrate ${sourceFile}: ${e instanceof Error ? e.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
}
sourceFilesSpinner.stop("Successfully migrated all source files!");
}
const writeSpinner = spinner();
writeSpinner.start("Applying all migrations...");
try {
await Promise.all(migrations.map(({ path, content }) => writeFile(path, content)));
writeSpinner.stop("Successfully applied all migrations!");
} catch (e) {
writeSpinner.stop(`Failed to apply migrations: ${e instanceof Error ? e.message.replace("\n", " ") : "Unknown error"}`, 1);
cli.error("Migration canceled");
}
const installDependenciesSpinner = spinner();
installDependenciesSpinner.start("Updating dependencies...");
try {
await installDependencies(cwd);
installDependenciesSpinner.stop("Successfully updated dependencies!");
} catch (e) {
installDependenciesSpinner.stop(`Failed to update dependencies: ${e instanceof Error ? e.message : "Unknown error"}`, 1);
cli.error("Migration canceled");
return;
}
log.info("Migration complete! Visit https://skeleton.dev for more information.");
}
const MIGRATIONS = {
"skeleton-3": skeleton3
};
const migrate = new Command("migrate").description("Run a migration").addArgument(new Argument("<migration>", "The migration to run").choices(Object.keys(MIGRATIONS))).addOption(new Option("--cwd <cwd>", "The directory to run the migration in")).action((migration, options) => MIGRATIONS[migration](options));
async function getOurPackageJson() {
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json");
const content = await readFile(packageJsonPath, "utf-8");
return JSON.parse(content);
}
const pkg = await getOurPackageJson();
const cli = new Command().name(pkg.name).description(pkg.description).version(pkg.version).addCommand(migrate).copyInheritedSettings(migrate).configureOutput({
writeOut: log.info,
writeErr(str) {
outro(red(str.replace("\n", " ")));
process.exit(1);
}
}).hook("preAction", (_, ctx) => {
const args = ctx.args.join(" ");
log.message(dim(`Running "${`${ctx.name()}${args ? ` ${args}` : ""}`}"...`));
});
async function main() {
intro(bgBlueBright(black(" Welcome to the Skeleton CLI \u{1F480} ")));
if (process.argv.length === 2) {
cli.error("error: no command provided");
}
await cli.parseAsync(process.argv);
outro(bgGreenBright(black(" All Done! ")));
}
await main();
export { cli };