mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat: update registry build commands (#10880)
* feat: update registry build commands * fix
This commit is contained in:
@@ -141,6 +141,11 @@ When adding or modifying components, please ensure that:
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
See [`apps/v4/registry/README.md`](apps/v4/registry/README.md) for how the
|
||||
registry pipeline is structured and for the faster targeted build modes
|
||||
(`--style`, `--registry`, `--examples`, `--indexes`) you can use while
|
||||
iterating locally. Always run the full `pnpm registry:build` before committing.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Before you create a Pull Request, please check whether your commits comply with
|
||||
|
||||
100
apps/v4/registry/README.md
Normal file
100
apps/v4/registry/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Registry
|
||||
|
||||
This directory is the source of truth for the v4 component registry. The build
|
||||
pipeline (`../scripts/build-registry.mts`) reads the authored source here and
|
||||
generates the runtime indexes, the local `styles/` consumed by the docs app, and
|
||||
the installable output under `public/r/`.
|
||||
|
||||
## Source of truth (authored by hand)
|
||||
|
||||
- **`bases/base/`, `bases/radix/`** — the two authored base registries (Base UI
|
||||
and Radix). Each holds a `registry.ts` plus `ui/`, `lib/`, `hooks/`, `blocks/`,
|
||||
`examples/`, and `internal/`. Shared surfaces should stay in sync across both
|
||||
bases — see [`bases/README.md`](./bases/README.md).
|
||||
- **`styles/style-*.css`** — the style token files (`nova`, `sera`, `vega`, …).
|
||||
Each defines the design tokens for one style.
|
||||
- **`new-york-v4/`** — the legacy source registry. Unlike the generated
|
||||
combinations below, its `registry.ts` and component files are authored
|
||||
directly and committed.
|
||||
- **`../examples/base`, `../examples/radix`** — authored component demos. See
|
||||
[`../examples/README.md`](../examples/README.md).
|
||||
|
||||
## Generated output (do not edit by hand)
|
||||
|
||||
Persistent (committed):
|
||||
|
||||
- `bases/__index__.tsx` — runtime lookup for the authored bases.
|
||||
- `__index__.tsx` — runtime lookup across legacy styles and every base/style
|
||||
combination.
|
||||
- `__blocks__.json` — block metadata index.
|
||||
- `../examples/__index__.tsx` — runtime lookup for demos.
|
||||
- `../styles/<style>/ui/*` — compiled components for each base/style
|
||||
combination, imported by the docs app.
|
||||
- `../styles/<style>/ui-rtl/*` — RTL variants, generated for `base-nova` and
|
||||
`radix-nova` only.
|
||||
- `../public/r/*` — installable registry JSON served by the website and the CLI.
|
||||
|
||||
Temporary (created during the build, then cleaned up):
|
||||
|
||||
- `<style>/*` — per-combination registries (e.g. `base-nova/`).
|
||||
- `../registry-<style>.json`
|
||||
|
||||
## The style model
|
||||
|
||||
There are two kinds of "styles", and the distinction drives the build flags:
|
||||
|
||||
- **Generated combinations** — every base (`base`, `radix`) crossed with every
|
||||
style token (`nova`, `sera`, …) produces a combination like `base-nova` or
|
||||
`radix-sera`. These are generated from the authored bases plus the style CSS;
|
||||
nothing under `registry/<combination>/` is committed.
|
||||
- **Legacy source registry** — `new-york-v4` is authored directly and committed.
|
||||
It is not generated from a base/style combination.
|
||||
|
||||
## Building
|
||||
|
||||
Run from `apps/v4`:
|
||||
|
||||
```bash
|
||||
pnpm registry:build
|
||||
```
|
||||
|
||||
This runs the full pipeline: build the bases, generate every combination, write
|
||||
the runtime indexes, export `public/r/` for every style, copy the compiled UI
|
||||
into `styles/`, and build the RTL styles. It is the canonical build — generated
|
||||
output is prettier-formatted. **Run this before committing or for production.**
|
||||
|
||||
### Fast targeted builds
|
||||
|
||||
The targeted flags below are for quick local iteration. To stay fast they
|
||||
**skip formatting** the generated output, so they can leave generated files
|
||||
unformatted (and produce large but harmless `git diff` churn). The full
|
||||
`pnpm registry:build` above re-canonicalizes everything, so run it before you
|
||||
commit.
|
||||
|
||||
For local iteration you can rebuild only the artifact you changed:
|
||||
|
||||
```bash
|
||||
pnpm registry:build --examples # examples/__index__.tsx
|
||||
pnpm registry:build --indexes # runtime registry indexes
|
||||
pnpm registry:build --style base-nova # styles/base-nova/ui (+ ui-rtl)
|
||||
pnpm registry:build --style all # every generated combination
|
||||
pnpm registry:build --registry base-nova # public/r/styles/base-nova
|
||||
pnpm registry:build --registry all # every style, incl. new-york-v4
|
||||
```
|
||||
|
||||
| Flag | Rebuilds | Run after |
|
||||
| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `--examples` | `../examples/__index__.tsx` | adding, removing, or renaming a demo |
|
||||
| `--indexes` | `bases/__index__.tsx`, `__index__.tsx`, `__blocks__.json`, `public/r/index.json` | changing registry or block metadata |
|
||||
| `--style <style\|all>` | `../styles/<style>/ui` and `ui-rtl` | editing authored base UI/components |
|
||||
| `--registry <style\|all>` | `../public/r/styles/<style>` | changing what the CLI installs |
|
||||
|
||||
Notes:
|
||||
|
||||
- Flags can be combined, e.g. `--style base-nova --registry base-nova`.
|
||||
- `all` targets every supported style.
|
||||
- Editing an existing example file usually does **not** need a rebuild — only
|
||||
adding, removing, or renaming one (which changes the index) does.
|
||||
- `--style new-york-v4` is rejected because it is a legacy source registry, not a
|
||||
generated combination. Use `--registry new-york-v4` instead.
|
||||
- Unknown targets fail with the list of valid style ids.
|
||||
@@ -5,6 +5,7 @@ import { createRequire } from "module"
|
||||
import { availableParallelism } from "os"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { parseArgs } from "util"
|
||||
import prettier from "prettier"
|
||||
import { rimraf } from "rimraf"
|
||||
import { registrySchema, type RegistryItem } from "shadcn/schema"
|
||||
@@ -55,6 +56,13 @@ import { STYLES } from "@/registry/styles"
|
||||
* 7. Build styles/<style>/ui-rtl for base-nova and radix-nova only.
|
||||
* 8. Format the generated persistent outputs.
|
||||
* 9. Clean up the temporary registry/<base-style> trees and registry-*.json.
|
||||
*
|
||||
* Targeted modes (see parseBuildOptions):
|
||||
* - --examples rebuilds examples/__index__.tsx only.
|
||||
* - --indexes rebuilds the runtime registry indexes only.
|
||||
* - --style <style|all> rebuilds local styles/<style>/ui (+ ui-rtl).
|
||||
* - --registry <style|all> rebuilds installable public/r/styles/<style>.
|
||||
* Running with no options performs the full build described above.
|
||||
*/
|
||||
|
||||
const STYLE_COMBINATIONS = Array.from(BASES).flatMap((base) =>
|
||||
@@ -98,6 +106,12 @@ type TransformCacheManifestEntry = {
|
||||
const transformCacheManifest = new Map<string, TransformCacheManifestEntry>()
|
||||
let transformCacheDirty = false
|
||||
let prettierConfigPromise: Promise<prettier.Options | null> | null = null
|
||||
|
||||
// Generated output is prettier-formatted in the full (prod) build. Targeted dev
|
||||
// builds skip formatting for speed; the next full build re-canonicalizes
|
||||
// everything. The transform cache always stores formatted content (see
|
||||
// getCachedStyledContent), so a full build never reads an unformatted entry.
|
||||
let shouldFormatOutput = true
|
||||
const resolveFromScript = createRequire(import.meta.url).resolve
|
||||
|
||||
const iconProject = new Project({
|
||||
@@ -125,6 +139,101 @@ function getStyleCombination(styleName: string) {
|
||||
return STYLE_COMBINATIONS.find((style) => style.name === styleName) ?? null
|
||||
}
|
||||
|
||||
type BuildOptions = {
|
||||
examples: boolean
|
||||
indexes: boolean
|
||||
style: "all" | string | null
|
||||
registry: "all" | string | null
|
||||
}
|
||||
|
||||
const USAGE = `Usage: registry:build [options]
|
||||
|
||||
Run with no options for a full registry build, or target a single artifact:
|
||||
|
||||
--examples Rebuild examples/__index__.tsx only.
|
||||
--indexes Rebuild the runtime registry indexes only.
|
||||
--style <style|all> Rebuild local generated style files under styles/<style>/ui.
|
||||
--registry <style|all> Rebuild installable registry JSON under public/r/styles/<style>.
|
||||
|
||||
<style> must be "all" or a known final style id (e.g. base-nova, radix-nova, base-sera, new-york-v4).
|
||||
Flags can be combined, e.g. --style base-nova --registry base-nova.`
|
||||
|
||||
function getKnownStyleNames() {
|
||||
return new Set(getStylesToBuild().map((style) => style.name))
|
||||
}
|
||||
|
||||
function assertKnownTarget(flag: "--style" | "--registry", target: string) {
|
||||
if (target === "all") {
|
||||
return
|
||||
}
|
||||
|
||||
const knownStyleNames = getKnownStyleNames()
|
||||
if (!knownStyleNames.has(target)) {
|
||||
const valid = ["all", ...Array.from(knownStyleNames)].join(", ")
|
||||
throw new Error(
|
||||
`Unknown ${flag} target "${target}". Valid targets: ${valid}.\n\n${USAGE}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function parseBuildOptions(argv: string[]): BuildOptions {
|
||||
let values: {
|
||||
examples?: boolean
|
||||
indexes?: boolean
|
||||
style?: string
|
||||
registry?: string
|
||||
}
|
||||
|
||||
try {
|
||||
;({ values } = parseArgs({
|
||||
args: argv,
|
||||
options: {
|
||||
examples: { type: "boolean" },
|
||||
indexes: { type: "boolean" },
|
||||
style: { type: "string" },
|
||||
registry: { type: "string" },
|
||||
},
|
||||
allowPositionals: false,
|
||||
strict: true,
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new Error(`${(error as Error).message}\n\n${USAGE}`)
|
||||
}
|
||||
|
||||
if (values.style !== undefined) {
|
||||
assertKnownTarget("--style", values.style)
|
||||
}
|
||||
if (values.registry !== undefined) {
|
||||
assertKnownTarget("--registry", values.registry)
|
||||
}
|
||||
|
||||
return {
|
||||
examples: values.examples ?? false,
|
||||
indexes: values.indexes ?? false,
|
||||
style: values.style ?? null,
|
||||
registry: values.registry ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function isFullBuild(options: BuildOptions) {
|
||||
return (
|
||||
!options.examples &&
|
||||
!options.indexes &&
|
||||
options.style === null &&
|
||||
options.registry === null
|
||||
)
|
||||
}
|
||||
|
||||
function getTargetStyles(target: "all" | string | null) {
|
||||
const stylesToBuild = getStylesToBuild()
|
||||
|
||||
if (target === "all") {
|
||||
return stylesToBuild
|
||||
}
|
||||
|
||||
return stylesToBuild.filter((style) => style.name === target)
|
||||
}
|
||||
|
||||
function stripFileExtension(filePath: string) {
|
||||
return filePath.replace(/\.(tsx|ts|json|mdx)$/, "")
|
||||
}
|
||||
@@ -272,7 +381,7 @@ async function writeIfChanged(filePath: string, content: string) {
|
||||
return true
|
||||
}
|
||||
|
||||
async function formatGeneratedSource(content: string, filePath: string) {
|
||||
async function formatSource(content: string, filePath: string) {
|
||||
prettierConfigPromise ??= prettier.resolveConfig(
|
||||
path.join(process.cwd(), "package.json")
|
||||
)
|
||||
@@ -285,6 +394,14 @@ async function formatGeneratedSource(content: string, filePath: string) {
|
||||
})
|
||||
}
|
||||
|
||||
async function formatGeneratedSource(content: string, filePath: string) {
|
||||
if (!shouldFormatOutput) {
|
||||
return content
|
||||
}
|
||||
|
||||
return formatSource(content, filePath)
|
||||
}
|
||||
|
||||
async function formatGeneratedJson(value: unknown, filePath: string) {
|
||||
return formatGeneratedSource(JSON.stringify(value, null, 2), filePath)
|
||||
}
|
||||
@@ -383,7 +500,9 @@ async function getCachedStyledContent({
|
||||
new RegExp(`@/registry/bases/${baseName}/`, "g"),
|
||||
`@/registry/${styleName}/`
|
||||
)
|
||||
transformedContent = await formatGeneratedSource(
|
||||
// Always format cached content so a later full build never reads an
|
||||
// unformatted entry produced by a targeted dev build.
|
||||
transformedContent = await formatSource(
|
||||
transformedContent,
|
||||
path.join(getTemporaryRegistryRoot(styleName), filePath)
|
||||
)
|
||||
@@ -430,10 +549,26 @@ async function runWithConcurrency<T, R>(
|
||||
|
||||
try {
|
||||
const totalStart = performance.now()
|
||||
const options = parseBuildOptions(process.argv.slice(2))
|
||||
|
||||
if (isFullBuild(options)) {
|
||||
await runFullBuild()
|
||||
} else {
|
||||
await runTargetedBuild(options)
|
||||
}
|
||||
|
||||
const elapsed = ((performance.now() - totalStart) / 1000).toFixed(2)
|
||||
console.log(`\n✅ Build complete in ${elapsed}s!`)
|
||||
} catch (error) {
|
||||
await saveTransformCache().catch(console.error)
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function runFullBuild() {
|
||||
await loadTransformCache()
|
||||
|
||||
console.log("🏗️ Building bases...")
|
||||
console.log("\n🏗️ Building bases...")
|
||||
await buildBasesIndex(Array.from(BASES))
|
||||
await buildBases(Array.from(BASES))
|
||||
|
||||
@@ -475,15 +610,120 @@ try {
|
||||
await buildRtlStyles()
|
||||
|
||||
console.log("\n🧹 Cleaning up...")
|
||||
await cleanUp(stylesToBuild)
|
||||
await cleanUpTemporaryFiles(stylesToBuild.map((style) => style.name))
|
||||
await saveTransformCache()
|
||||
}
|
||||
|
||||
const elapsed = ((performance.now() - totalStart) / 1000).toFixed(2)
|
||||
console.log(`\n✅ Build complete in ${elapsed}s!`)
|
||||
} catch (error) {
|
||||
await saveTransformCache().catch(console.error)
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
async function runTargetedBuild(options: BuildOptions) {
|
||||
// Targeted builds are for quick dev iteration: skip prettier on generated
|
||||
// output. The full (prod) build re-formats everything to its canonical state.
|
||||
shouldFormatOutput = false
|
||||
|
||||
await loadTransformCache()
|
||||
|
||||
// Phases run in dependency-safe order: indexes and examples write the runtime
|
||||
// lookup files first, the targeted style build copies compiled ui into
|
||||
// styles/<style>, and the targeted registry build exports public/r last.
|
||||
if (options.indexes) {
|
||||
await runIndexesBuild()
|
||||
}
|
||||
|
||||
if (options.examples) {
|
||||
await runExamplesBuild()
|
||||
}
|
||||
|
||||
if (options.style !== null) {
|
||||
await runTargetedStyleBuild(options.style)
|
||||
}
|
||||
|
||||
if (options.registry !== null) {
|
||||
await runTargetedRegistryBuild(options.registry)
|
||||
}
|
||||
|
||||
await saveTransformCache()
|
||||
}
|
||||
|
||||
async function runIndexesBuild() {
|
||||
console.log("🏗️ Building registry/bases/__index__.tsx...")
|
||||
await buildBasesIndex(Array.from(BASES))
|
||||
|
||||
// buildBlocksIndex imports @/registry/__index__, so the registry index must
|
||||
// be regenerated before the blocks index.
|
||||
console.log("\n📦 Building registry/__index__.tsx...")
|
||||
await buildRegistryIndex(getStylesToBuild())
|
||||
|
||||
console.log("\n🗂️ Building registry/__blocks__.json...")
|
||||
await buildBlocksIndex()
|
||||
|
||||
console.log("\n📦 Building public/r/index.json...")
|
||||
await buildIndex()
|
||||
}
|
||||
|
||||
async function runExamplesBuild() {
|
||||
console.log("📋 Building examples/__index__.tsx...")
|
||||
await buildExamplesIndex()
|
||||
}
|
||||
|
||||
async function runTargetedStyleBuild(target: "all" | string) {
|
||||
if (target !== "all" && !getStyleCombination(target)) {
|
||||
throw new Error(
|
||||
`--style ${target} is not supported because it is a legacy source registry. Use --registry ${target}.`
|
||||
)
|
||||
}
|
||||
|
||||
// styles/<style>/ui only exists for generated base/style combinations, so we
|
||||
// skip legacy source styles (e.g. new-york-v4) when targeting "all".
|
||||
const targetStyles = getTargetStyles(target).filter((style) =>
|
||||
getStyleCombination(style.name)
|
||||
)
|
||||
const targetStyleNames = new Set(targetStyles.map((style) => style.name))
|
||||
|
||||
if (targetStyleNames.size === 0) {
|
||||
console.log(" No generated styles to build.")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("💅 Building styles...")
|
||||
await buildBases(Array.from(BASES), targetStyleNames)
|
||||
|
||||
console.log("\n📋 Copying compiled ui to styles...")
|
||||
await copyUIToStyles(targetStyleNames)
|
||||
|
||||
console.log("\n🔄 Building RTL styles...")
|
||||
await buildRtlStyles(targetStyleNames)
|
||||
|
||||
console.log("\n🧹 Cleaning up...")
|
||||
await cleanUpTemporaryFiles(Array.from(targetStyleNames))
|
||||
}
|
||||
|
||||
async function runTargetedRegistryBuild(target: "all" | string) {
|
||||
const targetStyles = getTargetStyles(target)
|
||||
const comboStyleNames = new Set(
|
||||
targetStyles
|
||||
.filter((style) => getStyleCombination(style.name))
|
||||
.map((style) => style.name)
|
||||
)
|
||||
|
||||
// Only generated base/style combinations need a temporary registry/<style>
|
||||
// tree. Legacy source styles (e.g. new-york-v4) already ship registry.ts.
|
||||
if (comboStyleNames.size > 0) {
|
||||
console.log("🏗️ Building bases...")
|
||||
await buildBases(Array.from(BASES), comboStyleNames)
|
||||
}
|
||||
|
||||
console.log("\n💅 Building registry...")
|
||||
await runWithConcurrency(
|
||||
targetStyles,
|
||||
CLI_BUILD_CONCURRENCY,
|
||||
async (style) => {
|
||||
await buildRegistryJsonFile(style.name)
|
||||
await buildRegistry(style.name)
|
||||
console.log(` ✅ ${style.name}`)
|
||||
}
|
||||
)
|
||||
|
||||
console.log("\n🧹 Cleaning up...")
|
||||
await cleanUpTemporaryFiles(targetStyles.map((style) => style.name))
|
||||
}
|
||||
|
||||
async function buildBasesIndex(bases: Base[]) {
|
||||
@@ -575,10 +815,21 @@ export const Index: Record<string, Record<string, any>> = {`
|
||||
)
|
||||
}
|
||||
|
||||
async function buildBases(bases: Base[]) {
|
||||
async function buildBases(bases: Base[], targetStyleNames?: Set<string>) {
|
||||
// For targeted builds, only load bases that contribute a requested
|
||||
// combination. Otherwise a single-base target (e.g. --style base-nova) would
|
||||
// still import and read every source file for the other base.
|
||||
const basesToBuild = targetStyleNames
|
||||
? bases.filter((base) =>
|
||||
STYLES.some((style) =>
|
||||
targetStyleNames.has(`${base.name}-${style.name}`)
|
||||
)
|
||||
)
|
||||
: bases
|
||||
|
||||
const [baseImports, styleMaps, transformCacheHash] = await Promise.all([
|
||||
Promise.all(
|
||||
bases.map(async (base) => {
|
||||
basesToBuild.map(async (base) => {
|
||||
const { registry: baseRegistry } = await import(
|
||||
`../registry/bases/${base.name}/registry.ts`
|
||||
)
|
||||
@@ -656,6 +907,11 @@ async function buildBases(bases: Base[]) {
|
||||
sourceFiles,
|
||||
} of baseImports) {
|
||||
for (const { style, styleHash, styleMap } of styleMaps) {
|
||||
const styleName = `${base.name}-${style.name}`
|
||||
if (targetStyleNames && !targetStyleNames.has(styleName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
combinations.push({
|
||||
base,
|
||||
style,
|
||||
@@ -995,15 +1251,21 @@ async function buildBlocksIndex() {
|
||||
)
|
||||
}
|
||||
|
||||
async function cleanUp(stylesToBuild: { name: string; title: string }[]) {
|
||||
const cleanupTasks: Promise<boolean>[] = stylesToBuild.map((style) =>
|
||||
rimraf(path.join(process.cwd(), `registry-${style.name}.json`))
|
||||
)
|
||||
async function cleanUpTemporaryFiles(styleNames: string[]) {
|
||||
const cleanupTasks: Promise<boolean>[] = []
|
||||
|
||||
for (const style of STYLE_COMBINATIONS) {
|
||||
const tempRegistryRoot = getTemporaryRegistryRoot(style.name)
|
||||
console.log(` 🗑️ registry/${style.name}`)
|
||||
cleanupTasks.push(rimraf(tempRegistryRoot))
|
||||
for (const styleName of styleNames) {
|
||||
cleanupTasks.push(
|
||||
rimraf(path.join(process.cwd(), `registry-${styleName}.json`))
|
||||
)
|
||||
|
||||
// Only generated combinations have a temporary registry/<style> tree.
|
||||
// Legacy source styles (e.g. new-york-v4) own registry/<style> and must
|
||||
// never be removed.
|
||||
if (getStyleCombination(styleName)) {
|
||||
console.log(` 🗑️ registry/${styleName}`)
|
||||
cleanupTasks.push(rimraf(getTemporaryRegistryRoot(styleName)))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(cleanupTasks)
|
||||
@@ -1047,9 +1309,13 @@ async function applyIconTransform(content: string, filename: string) {
|
||||
return sourceFile.getText()
|
||||
}
|
||||
|
||||
async function copyUIToStyles() {
|
||||
async function copyUIToStyles(targetStyleNames?: Set<string>) {
|
||||
const styleCombinations = targetStyleNames
|
||||
? STYLE_COMBINATIONS.filter((style) => targetStyleNames.has(style.name))
|
||||
: STYLE_COMBINATIONS
|
||||
|
||||
await runWithConcurrency(
|
||||
STYLE_COMBINATIONS,
|
||||
styleCombinations,
|
||||
COPY_CONCURRENCY,
|
||||
async ({ name: styleName }) => {
|
||||
const sourceDir = path.join(getTemporaryRegistryRoot(styleName), "ui")
|
||||
@@ -1093,9 +1359,13 @@ async function copyUIToStyles() {
|
||||
)
|
||||
}
|
||||
|
||||
async function buildRtlStyles() {
|
||||
async function buildRtlStyles(targetStyleNames?: Set<string>) {
|
||||
await runWithConcurrency(
|
||||
STYLE_COMBINATIONS.filter((style) => shouldGenerateRtlStyles(style.name)),
|
||||
STYLE_COMBINATIONS.filter(
|
||||
(style) =>
|
||||
shouldGenerateRtlStyles(style.name) &&
|
||||
(!targetStyleNames || targetStyleNames.has(style.name))
|
||||
),
|
||||
COPY_CONCURRENCY,
|
||||
async ({ name: styleName }) => {
|
||||
const sourceDir = path.join(getPersistentStyleRoot(styleName), "ui")
|
||||
|
||||
Reference in New Issue
Block a user