UNPKG

skeleton

Version:

The CLI for Skeleton related tooling.

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