skeleton
Version:
The CLI for Skeleton related tooling.
1,358 lines (1,357 loc) • 39.6 kB
JavaScript
import child_process from "node:child_process";
import { promisify } from "node:util";
import { detect, resolveCommand } from "package-manager-detector";
import { intro, isCancel, log, multiselect, outro, spinner } from "@clack/prompts";
import { AtRule, atRule, comment, parse } from "postcss";
import { parse as parse$1 } from "node-html-parser";
import { nanoid } from "nanoid";
import { Node, Project } from "ts-morph";
import detectIndent from "detect-indent";
import { coerce, lt } from "semver";
import MagicString from "magic-string";
import { parse as parse$2 } from "svelte/compiler";
import { walk } from "zimmerframe";
import getLatestVersion from "latest-version";
import { readFile, writeFile } from "node:fs/promises";
import { dirname, extname, join } from "node:path";
import { glob } from "tinyglobby";
import { readFile as readFile$1, writeFile as writeFile$1 } from "fs/promises";
import { extname as extname$1 } from "path";
import { Argument, Command, Option } from "commander";
import { fileURLToPath } from "node:url";
import { bgBlueBright, bgGreenBright, black, dim, red } from "colorette";
//#region src/utility/install-dependencies.ts
const exec = promisify(child_process.exec);
async function installDependencies(cwd = process.cwd()) {
const resolvedCommand = resolveCommand((await detect({ cwd }))?.agent ?? "npm", "install", []);
if (!resolvedCommand) throw new Error("Could not resolve package manager command.");
return exec(`${resolvedCommand.command} ${resolvedCommand.args.join(" ")}`, { cwd });
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-classes.ts
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: /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) };
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-stylesheet.ts
function transformStylesheet$1(code) {
try {
const parsed = parse(code);
parsed.walkAtRules("apply", (atRule) => {
atRule.params = transformClasses(atRule.params).code;
});
return { code: parsed.toString() };
} catch (error) {
log.warn(`Failed to parse CSS, skipping transformation: ${error instanceof Error ? error.message : "Unknown error"}`);
return { code };
}
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-app.css.ts
function getTailwindImport(root) {
let tailwindImport;
root.walkAtRules("import", (atRule) => {
if (atRule.params.includes("tailwindcss")) tailwindImport = atRule;
});
return tailwindImport;
}
function transformAppCss(code, theme) {
code = transformStylesheet$1(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;
}
const tailwindImport = getTailwindImport(root);
if (tailwindImport) root.insertAfter(tailwindImport, nodes);
else root.prepend(nodes);
return { code: root.toString() };
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/utility/theme-mappings.ts
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"
}
};
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-app.html.ts
function transformAppHtml(code) {
const parsed = parse$1(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
} }
};
}
//#endregion
//#region src/utility/ts-morph/add-named-import.ts
function addNamedImport(file, specifier, name) {
const existingImportDeclaration = file.getImportDeclaration((importDeclaration) => {
return importDeclaration.getModuleSpecifier().getLiteralText() === specifier;
});
if (existingImportDeclaration) {
if (!existingImportDeclaration.getNamedImports().some((namedImport) => namedImport.getName() === name)) existingImportDeclaration.addNamedImport(name);
} else file.addImportDeclaration({
moduleSpecifier: specifier,
namedImports: [name]
});
}
//#endregion
//#region src/utility/ts-morph/parse-source-file.ts
const project = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
skipFileDependencyResolution: true,
skipLoadingLibFiles: true
});
function parseSourceFile(code) {
return project.createSourceFile(`${nanoid()}.ts`, code);
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/utility/export-mappings.ts
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"
}
}
};
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-module.ts
function transformModule$1(code) {
const file = parseSourceFile(code);
const skeletonImports = [];
file.forEachDescendant((node) => {
if (Node.isImportDeclaration(node)) {
if (node.getModuleSpecifier().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") {
const updated = exportMapping.identifier.value;
if (updated !== node.getText()) node.replaceWithText(updated);
}
}
}
if (!node.wasForgotten() && Node.isStringLiteral(node) && !Node.isImportDeclaration(node.getParent())) {
const transformed = transformClasses(node.getText()).code;
if (transformed !== node.getText()) node.replaceWithText(transformed);
}
});
return {
code: file.getFullText(),
meta: { skeletonImports }
};
}
//#endregion
//#region src/utility/sort-properties-alphabetically.ts
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;
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-package.json.ts
function transformPackageJson$1(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, void 0, detectIndent(code).indent || " "),
meta: { isUsingComponents }
};
}
//#endregion
//#region src/utility/svelte/has-range.ts
function hasRange(node) {
return "start" in node && "end" in node && typeof node.start === "number" && typeof node.end === "number";
}
//#endregion
//#region src/utility/svelte/rename-component.ts
function renameComponent(s, node, name) {
const adjustedStart = node.start + 1;
s.update(adjustedStart, adjustedStart + node.name.length, name);
const indexOfNonSelfClosingTag = s.original.slice(node.start, node.end).lastIndexOf("</");
if (indexOfNonSelfClosingTag === -1 || node.start + indexOfNonSelfClosingTag > node.end) return;
s.update(node.start + indexOfNonSelfClosingTag + 2, node.start + indexOfNonSelfClosingTag + 2 + node.name.length, name);
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/transformers/transform-svelte.ts
function transformScript$1(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 transformed = transformModule$1(s.original.slice(script.content.start, script.content.end));
s.overwrite(script.content.start, script.content.end, transformed.code);
return { meta: transformed.meta };
}
function transformCss$1(s, css) {
if (!css) return;
try {
const transformed = transformStylesheet$1(s.original.slice(css.content.start, css.content.end));
s.overwrite(css.content.start, css.content.end, transformed.code);
} catch (error) {
log.warn(`Failed to transform CSS in Svelte component, skipping: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
function transformFragment$1(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 + 6;
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$1(code) {
const s = new MagicString(code);
const root = parse$2(code, { modern: true });
const skeletonImports = [...transformScript$1(s, root.module).meta.skeletonImports, ...transformScript$1(s, root.instance).meta.skeletonImports];
transformFragment$1(s, root.fragment, skeletonImports);
transformCss$1(s, root.css);
return { code: s.toString() };
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/utility/constants.ts
const FALLBACK_THEME = {
type: "preset",
value: "cerberus"
};
//#endregion
//#region src/commands/migrate/migrations/skeleton-3/index.ts
async function skeleton_3_default(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;
}
const packageSpinner = spinner();
packageSpinner.start(`Migrating ${packageJson.name}...`);
try {
const transformedPackageJson = transformPackageJson$1(await readFile(packageJson.paths.at(0), "utf-8"), await getLatestVersion("@skeletonlabs/skeleton", { version: ">=3.0.0-0 <4.0.0" }), await getLatestVersion("@skeletonlabs/skeleton-svelte", { version: ">=1.0.0-0 <2.0.0" }));
migrations.push({
path: packageJson.paths.at(0),
content: transformedPackageJson.code
});
packageSpinner.stop(`Successfully migrated ${packageJson.name}!`);
} catch (error) {
packageSpinner.stop(`Failed to migrate ${packageJson.name}: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
let theme;
const appHtmlSpinner = spinner();
appHtmlSpinner.start(`Migrating ${appHtml.name}...`);
try {
const transformedAppHtml = transformAppHtml(await readFile(appHtml.paths.at(0), "utf-8"));
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.at(0),
content: transformedAppHtml.code
});
appHtmlSpinner.stop(`Successfully migrated ${appHtml.name}!`);
} catch (error) {
appHtmlSpinner.stop(`Failed to migrate ${appHtml.name}: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
const appCssSpinner = spinner();
appCssSpinner.start(`Migrating ${appCss.name}...`);
try {
const transformedAppCss = transformAppCss(await readFile(appCss.paths.at(0), "utf-8"), theme ?? FALLBACK_THEME);
migrations.push({
path: appCss.paths.at(0),
content: transformedAppCss.code
});
appCssSpinner.stop(`Successfully migrated ${appCss.name}!`);
} catch (error) {
appCssSpinner.stop(`Failed to migrate ${appCss.name}: ${error instanceof Error ? error.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$1(code);
migrations.push({
path: sourceFile,
content: transformedSvelte.code
});
break;
}
case ".js":
case ".mjs":
case ".ts":
case ".mts": {
const transformedModule = transformModule$1(code);
migrations.push({
path: sourceFile,
content: transformedModule.code
});
break;
}
case ".css":
case ".pcss":
case ".postcss": {
const transformedStyleSheet = transformStylesheet$1(code);
migrations.push({
path: sourceFile,
content: transformedStyleSheet.code
});
break;
}
}
sourceFilesSpinner.message(`Successfully migrated ${sourceFile}!`);
} catch (error) {
sourceFilesSpinner.stop(`Failed to migrate ${sourceFile}: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
}
sourceFilesSpinner.stop(`Successfully migrated ${sourceFiles.length} 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 (error) {
writeSpinner.stop(`Failed to apply migrations: ${error instanceof Error ? error.message.replace("\n", " ") : "Unknown error"}`, 1);
cli.error("Migration canceled");
}
const installDependenciesSpinner = spinner();
installDependenciesSpinner.start("Installing dependencies...");
try {
await installDependencies(cwd);
installDependenciesSpinner.stop("Successfully installed dependencies!");
} catch (error) {
installDependenciesSpinner.stop(`Failed to install dependencies: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled");
return;
}
log.info("Migration complete! Visit https://skeleton.dev for more information.");
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/utility/identifier-mappings.ts
const IDENTIFIER_MAPPINGS = {
Modal: "Dialog",
"Navigation.Bar": "Navigation",
"Navigation.Rail": "Navigation",
ProgressRing: "Progress",
Ratings: "RatingGroup",
Segment: "SegmentedControl",
Toaster: "Toast.Group"
};
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/utility/import-mappings.ts
const IMPORT_MAPPINGS = {
Modal: "Dialog",
Navigation: "Navigation",
ProgressRing: "Progress",
Ratings: "RatingGroup",
Segment: "SegmentedControl",
Toaster: "Toast"
};
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/transformers/transform-module.ts
function transformModule(code) {
const file = parseSourceFile(code);
const skeletonImports = [];
file.forEachDescendant((node) => {
if (Node.isImportSpecifier(node) && ["@skeletonlabs/skeleton-svelte", "@skeletonlabs/skeleton-react"].includes(node.getImportDeclaration().getModuleSpecifier().getLiteralText())) {
const name = node.getName();
if (Object.hasOwn(IMPORT_MAPPINGS, name)) {
skeletonImports.push(name);
node.setName(IMPORT_MAPPINGS[name]);
}
}
});
file.forEachDescendant((node) => {
if (Node.isPropertyAccessExpression(node)) {
const fullName = `${node.getExpression().getText()}.${node.getName()}`;
if (Object.hasOwn(IDENTIFIER_MAPPINGS, fullName)) node.replaceWithText(IDENTIFIER_MAPPINGS[fullName]);
} else if (Node.isIdentifier(node) && !Node.isImportSpecifier(node.getParent())) {
if (Object.hasOwn(IDENTIFIER_MAPPINGS, node.getText())) node.replaceWithText(IDENTIFIER_MAPPINGS[node.getText()]);
}
});
return {
code: file.getFullText(),
meta: { skeletonImports }
};
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/transformers/transform-package.json.ts
function transformPackageJson(code) {
const pkg = JSON.parse(code);
for (const field of ["dependencies", "devDependencies"]) {
if (!pkg[field]) continue;
for (const dep of [
"@skeletonlabs/skeleton",
"@skeletonlabs/skeleton-svelte",
"@skeletonlabs/skeleton-react"
]) {
if (!pkg[field][dep]) continue;
const coerced = coerce(pkg[field][dep]);
if (coerced && lt(coerced.version, "4.0.0")) {
delete pkg[field][dep];
pkg[field][dep] = "^4.0.0";
}
}
pkg[field] = sortPropertiesAlphabetically(pkg[field]);
}
return { code: JSON.stringify(pkg, void 0, detectIndent(code).indent || " ") };
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/transformers/transform-stylesheet.ts
function transformStylesheet(content) {
try {
const ast = parse(content);
ast.walkAtRules((atRule) => {
if (atRule.name === "import" && atRule.params.includes("@skeletonlabs/skeleton/optional/presets")) atRule.remove();
if (atRule.name === "source") {
if (atRule.params.includes("@skeletonlabs/skeleton-svelte")) atRule.replaceWith(new AtRule({
name: "import",
params: `'@skeletonlabs/skeleton-svelte'`
}));
if (atRule.params.includes("@skeletonlabs/skeleton-react")) atRule.replaceWith(new AtRule({
name: "import",
params: `'@skeletonlabs/skeleton-react'`
}));
}
});
return { code: ast.toString() };
} catch (error) {
log.warn(`Failed to parse CSS, skipping transformation: ${error instanceof Error ? error.message : "Unknown error"}`);
return { code: content };
}
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/transformers/transform-svelte.ts
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 transformed = transformModule(s.original.slice(script.content.start, script.content.end));
s.overwrite(script.content.start, script.content.end, transformed.code);
return transformed;
}
function transformCss(s, css) {
if (!css) return;
try {
const transformed = transformStylesheet(s.original.slice(css.content.start, css.content.end));
s.overwrite(css.content.start, css.content.end, transformed.code);
} catch (error) {
log.warn(`Failed to transform CSS in Svelte component, skipping: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
function transformFragment(s, fragment, skeletonImports) {
walk(fragment, {}, { Component(node, ctx) {
if (Object.hasOwn(IDENTIFIER_MAPPINGS, node.name) && hasRange(node) && skeletonImports.includes(node.name.split(".").at(0) || "")) renameComponent(s, node, IDENTIFIER_MAPPINGS[node.name]);
ctx.next();
} });
}
function transformSvelte(code) {
const s = new MagicString(code);
const root = parse$2(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() };
}
//#endregion
//#region src/commands/migrate/migrations/skeleton-4/index.ts
async function skeleton_4_default(options) {
const cwd = options.cwd ?? process.cwd();
const migrations = [];
const packageJson = {
name: "package.json",
paths: await glob("package.json", { cwd })
};
for (const file of [packageJson]) {
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;
}
const packageSpinner = spinner();
packageSpinner.start(`Migrating ${packageJson.name}...`);
try {
const transformedPackageJson = transformPackageJson(await readFile$1(packageJson.paths.at(0), "utf-8"));
migrations.push({
path: packageJson.paths.at(0),
content: transformedPackageJson.code
});
packageSpinner.stop(`Successfully migrated ${packageJson.name}!`);
} catch (error) {
packageSpinner.stop(`Failed to migrate ${packageJson.name}: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled, nothing written to disk");
}
const sourceFiles = await glob(sourceFolders.map((folder) => `${folder}**/*.{svelte,js,mjs,ts,mts,css,pcss,postcss}`), {
cwd,
ignore: ["node_modules"]
});
const sourceFilesSpinner = spinner();
sourceFilesSpinner.start(`Migrating source files...`);
for (const path of sourceFiles) {
const content = await readFile$1(path, "utf-8");
switch (extname$1(path)) {
case ".svelte":
const transformedSvelte = transformSvelte(content);
migrations.push({
path,
content: transformedSvelte.code
});
break;
case ".css":
case ".pcss":
case ".postcss":
const transformedStylesheet = transformStylesheet(content);
migrations.push({
path,
content: transformedStylesheet.code
});
break;
case ".js":
case ".mjs":
case ".cjs":
case ".ts":
case ".mts":
case ".jsx":
case ".tsx":
const transformedModule = transformModule(content);
migrations.push({
path,
content: transformedModule.code
});
break;
}
sourceFilesSpinner.message(`Migrated source file: ${path}!`);
}
sourceFilesSpinner.stop(`Successfully migrated ${sourceFiles.length} source files!`);
const writeSpinner = spinner();
writeSpinner.start("Applying all migrations...");
try {
await Promise.all(migrations.map(({ path, content }) => writeFile$1(path, content)));
writeSpinner.stop("Successfully applied all migrations!");
} catch (error) {
writeSpinner.stop(`Failed to apply migrations: ${error instanceof Error ? error.message.replace("\n", " ") : "Unknown error"}`, 1);
cli.error("Migration canceled");
}
const installDependenciesSpinner = spinner();
installDependenciesSpinner.start("Installing dependencies...");
try {
await installDependencies(cwd);
installDependenciesSpinner.stop("Successfully installed dependencies!");
} catch (error) {
installDependenciesSpinner.stop(`Failed to install dependencies: ${error instanceof Error ? error.message : "Unknown error"}`, 1);
cli.error("Migration canceled");
return;
}
log.info("Migration complete! Visit https://skeleton.dev for more information.");
}
//#endregion
//#region src/commands/migrate/index.ts
const MIGRATIONS = {
"skeleton-3": skeleton_3_default,
"skeleton-4": skeleton_4_default
};
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));
//#endregion
//#region src/utility/get-our-package-json.ts
async function getOurPackageJson() {
const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf-8");
return JSON.parse(content);
}
//#endregion
//#region src/index.ts
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 skeleton() {
intro(bgBlueBright(black(" Welcome to the Skeleton CLI 💀 ")));
if (process.argv.length === 2) cli.error("error: no command provided");
await cli.parseAsync(process.argv);
outro(bgGreenBright(black(" All Done! ")));
}
await skeleton();
//#endregion
export { cli };