diff --git a/apps/v4/scripts/build-registry.mts b/apps/v4/scripts/build-registry.mts index bc48161021..d8ed7e2683 100644 --- a/apps/v4/scripts/build-registry.mts +++ b/apps/v4/scripts/build-registry.mts @@ -1,8 +1,10 @@ import { spawn } from "child_process" import { createHash } from "crypto" import { promises as fs } from "fs" +import { createRequire } from "module" import { availableParallelism } from "os" import path from "path" +import { fileURLToPath } from "url" import prettier from "prettier" import { rimraf } from "rimraf" import { registrySchema, type RegistryItem } from "shadcn/schema" @@ -72,7 +74,7 @@ const CLI_BUILD_CONCURRENCY = Math.max( 1, Math.min(Math.floor(CPU_COUNT / 2), 4) ) -const TRANSFORM_CACHE_VERSION = "2" +const TRANSFORM_CACHE_VERSION = "3" const CACHE_ROOT = path.join( process.cwd(), "node_modules/.cache/build-registry" @@ -82,10 +84,21 @@ const TRANSFORM_CACHE_MANIFEST_PATH = path.join( CACHE_ROOT, "transform-manifest.json" ) +const GENERATED_REGISTRY_CACHE_PATHS = new Set([ + "registry/__blocks__.json", + "registry/__index__.tsx", + "registry/bases/__index__.tsx", +]) -const transformCacheManifest = new Map() +type TransformCacheManifestEntry = { + inputHash: string + outputHash: string +} + +const transformCacheManifest = new Map() let transformCacheDirty = false let prettierConfigPromise: Promise | null = null +const resolveFromScript = createRequire(import.meta.url).resolve const iconProject = new Project({ compilerOptions: {}, @@ -153,6 +166,88 @@ function hashContent(...parts: string[]) { return hash.digest("hex") } +async function getTransformCacheHash() { + const [implementationHash, registryHash] = await Promise.all([ + getTransformImplementationHash(), + getAuthoredRegistryHash(), + ]) + + return hashContent(implementationHash, registryHash) +} + +async function getTransformImplementationHash() { + const dependencyFiles = [ + fileURLToPath(import.meta.url), + resolveFromScript("shadcn/utils"), + path.resolve(process.cwd(), "../../pnpm-lock.yaml"), + ] + const dependencyContent = await Promise.all( + dependencyFiles.map(async (filePath) => { + const content = await readFileIfExists(filePath) + const relativePath = toPosixPath(path.relative(process.cwd(), filePath)) + + return `${relativePath}\0${content ?? "missing"}` + }) + ) + + return hashContent(...dependencyContent) +} + +async function getAuthoredRegistryHash() { + const registryRoot = path.join(process.cwd(), "registry") + const filePaths = await getCacheableRegistryFiles(registryRoot) + const fileContent = await Promise.all( + filePaths.map(async (filePath) => { + const relativePath = toPosixPath(path.relative(process.cwd(), filePath)) + const content = await fs.readFile(filePath, "utf8") + + return `${relativePath}\0${content}` + }) + ) + + return hashContent(...fileContent) +} + +async function getCacheableRegistryFiles(dirPath: string): Promise { + const entries = await readDirectoryEntries(dirPath) + const files = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(dirPath, entry.name) + const relativePath = toPosixPath(path.relative(process.cwd(), entryPath)) + + if (shouldSkipRegistryCachePath(relativePath)) { + return [] + } + + if (entry.isDirectory()) { + return getCacheableRegistryFiles(entryPath) + } + + if (!entry.isFile()) { + return [] + } + + return [entryPath] + }) + ) + + return files.flat().sort((a, b) => a.localeCompare(b)) +} + +function shouldSkipRegistryCachePath(relativePath: string) { + if (GENERATED_REGISTRY_CACHE_PATHS.has(relativePath)) { + return true + } + + return STYLE_COMBINATIONS.some((style) => + relativePath.startsWith(`registry/${style.name}/`) + ) +} + +function toPosixPath(filePath: string) { + return filePath.split(path.sep).join("/") +} + async function readFileIfExists(filePath: string) { try { return await fs.readFile(filePath, "utf8") @@ -200,13 +295,28 @@ async function loadTransformCache() { return } - const payload = JSON.parse(existingManifest) as Record + const payload = JSON.parse(existingManifest) as Record for (const [key, value] of Object.entries(payload)) { - transformCacheManifest.set(key, value) + if (isTransformCacheManifestEntry(value)) { + transformCacheManifest.set(key, value) + } } } +function isTransformCacheManifestEntry( + value: unknown +): value is TransformCacheManifestEntry { + return ( + typeof value === "object" && + value !== null && + "inputHash" in value && + "outputHash" in value && + typeof value.inputHash === "string" && + typeof value.outputHash === "string" + ) +} + async function saveTransformCache() { if (!transformCacheDirty) { return @@ -234,6 +344,7 @@ async function getCachedStyledContent({ filePath, source, styleHash, + transformCacheHash, styleMap, }: { styleName: string @@ -241,6 +352,7 @@ async function getCachedStyledContent({ filePath: string source: string styleHash: string + transformCacheHash: string styleMap: Record }) { const cacheKey = `${styleName}:${filePath}` @@ -250,13 +362,18 @@ async function getCachedStyledContent({ styleName, baseName, filePath, + transformCacheHash, styleHash, source ) - if (transformCacheManifest.get(cacheKey) === inputHash) { + const cachedEntry = transformCacheManifest.get(cacheKey) + if (cachedEntry?.inputHash === inputHash) { const cachedContent = await readFileIfExists(cachePath) - if (cachedContent !== null) { + if ( + cachedContent !== null && + hashContent(cachedContent) === cachedEntry.outputHash + ) { return cachedContent } } @@ -274,8 +391,13 @@ async function getCachedStyledContent({ await fs.mkdir(path.dirname(cachePath), { recursive: true }) await fs.writeFile(cachePath, transformedContent) - if (transformCacheManifest.get(cacheKey) !== inputHash) { - transformCacheManifest.set(cacheKey, inputHash) + const outputHash = hashContent(transformedContent) + const nextEntry = { inputHash, outputHash } + if ( + cachedEntry?.inputHash !== nextEntry.inputHash || + cachedEntry?.outputHash !== nextEntry.outputHash + ) { + transformCacheManifest.set(cacheKey, nextEntry) transformCacheDirty = true } @@ -457,7 +579,7 @@ export const Index: Record> = {` } async function buildBases(bases: Base[]) { - const [baseImports, styleMaps] = await Promise.all([ + const [baseImports, styleMaps, transformCacheHash] = await Promise.all([ Promise.all( bases.map(async (base) => { const { registry: baseRegistry } = await import( @@ -516,6 +638,7 @@ async function buildBases(bases: Base[]) { } }) ), + getTransformCacheHash(), ]) const combinations: Array<{ @@ -525,6 +648,7 @@ async function buildBases(bases: Base[]) { registryItems: (typeof baseImports)[number]["registryItems"] sourceFiles: (typeof baseImports)[number]["sourceFiles"] styleHash: string + transformCacheHash: string styleMap: Record }> = [] @@ -542,6 +666,7 @@ async function buildBases(bases: Base[]) { registryItems, sourceFiles, styleHash, + transformCacheHash, styleMap, }) } @@ -557,6 +682,7 @@ async function buildBases(bases: Base[]) { registryItems, sourceFiles, styleHash, + transformCacheHash, styleMap, }) => { const styleName = `${base.name}-${style.name}` @@ -597,6 +723,7 @@ async function buildBases(bases: Base[]) { filePath: file.path, source, styleHash, + transformCacheHash, styleMap, }) : source