UNPKG

skeleton

Version:

The CLI for Skeleton related tooling.

1,358 lines (1,357 loc) 39.6 kB
#!/usr/bin/env node 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 };