Files
netto/scripts/generate-manifest.mjs
Maofeng b3a7a2a8f5 feat: add Base UI Tailwind component library
Add composable wrappers for 37 Base UI component families.

Expose primitive and convenience APIs with shared styling utilities.

Add the registry manifest, installer CLI, generation and export checks.

Include a smoke example covering primitive and shadcn-style imports.
2026-07-25 06:04:19 +08:00

167 lines
5.1 KiB
JavaScript

import { readdir, readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import process from "node:process"
const root = process.cwd()
const sourceDir = path.join(root, "src")
const componentsDir = path.join(sourceDir, "components", "ui")
const libDir = path.join(sourceDir, "lib")
const outputPath = path.join(root, "manifest.json")
const packageJson = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"))
const components = await listDirectories(componentsDir)
const utils = await listFiles(libDir)
const items = {}
for (const file of utils) {
const name = path.basename(file, path.extname(file))
const source = await readFile(path.join(root, file), "utf8")
const target = stripSourcePrefix(file)
items[`utils/${name}`] = {
name: `utils/${name}`,
type: "util",
description: `Utility source for ${name}.`,
dependencies: collectPackageDependencies(source),
files: [
{
path: file,
target,
},
],
}
}
for (const component of components) {
const componentFiles = await listFiles(path.join("src", "components", "ui", component))
const sources = await Promise.all(
componentFiles.map(async (file) => readFile(path.join(root, file), "utf8")),
)
const source = sources.join("\n")
items[component] = {
name: component,
type: "component",
description: `${toTitle(component)} component wrapper for Base UI and Tailwind CSS.`,
dependencies: collectPackageDependencies(source),
localDependencies: collectLocalDependencies(source, component, components, utils),
files: componentFiles.map((file) => ({
path: file,
target: stripSourcePrefix(file),
})),
}
}
const manifest = {
name: "base-ui-tailwind",
version: packageJson.version ?? "0.0.0",
generatedAt: new Date().toISOString(),
command: "base-ui-tailwind",
configFile: "base-ui-tailwind.json",
dependencies: {
"@base-ui/react": packageJson.dependencies?.["@base-ui/react"],
"clsx": packageJson.dependencies?.clsx,
"lucide-react": packageJson.dependencies?.["lucide-react"],
"tailwind-merge": packageJson.dependencies?.["tailwind-merge"],
},
items,
}
await writeFile(outputPath, `${JSON.stringify(manifest, null, 2)}\n`)
console.log(`Generated manifest.json with ${Object.keys(items).length} items.`)
async function listDirectories(directory) {
const entries = await readdir(directory, { withFileTypes: true })
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((a, b) => a.localeCompare(b))
}
async function listFiles(relativeOrAbsoluteDirectory) {
const absoluteDirectory = path.isAbsolute(relativeOrAbsoluteDirectory)
? relativeOrAbsoluteDirectory
: path.join(root, relativeOrAbsoluteDirectory)
const relativeDirectory = path.relative(root, absoluteDirectory)
const entries = await readdir(absoluteDirectory, { withFileTypes: true })
const files = []
for (const entry of entries) {
const relativePath = path.join(relativeDirectory, entry.name)
const absolutePath = path.join(absoluteDirectory, entry.name)
if (entry.isDirectory()) {
files.push(...(await listFiles(absolutePath)))
} else if (entry.isFile()) {
files.push(relativePath.split(path.sep).join("/"))
}
}
return files.sort((a, b) => a.localeCompare(b))
}
function collectPackageDependencies(source) {
const dependencies = new Set()
const importPattern = /from\s+["']([^"']+)["']|import\s+["']([^"']+)["']/g
for (const match of source.matchAll(importPattern)) {
const specifier = match[1] ?? match[2]
if (!specifier || specifier.startsWith(".") || specifier.startsWith("@/")) {
continue
}
dependencies.add(getPackageName(specifier))
}
return [...dependencies].sort((a, b) => a.localeCompare(b))
}
function collectLocalDependencies(source, component, components, utils) {
const dependencies = new Set()
const localPattern = /from\s+["']@\/([^"']+)["']|import\s+["']@\/([^"']+)["']/g
for (const match of source.matchAll(localPattern)) {
const specifier = match[1] ?? match[2]
if (!specifier) {
continue
}
const componentMatch = specifier.match(/^components\/ui\/([^/]+)/)
if (componentMatch && componentMatch[1] !== component && components.includes(componentMatch[1])) {
dependencies.add(componentMatch[1])
continue
}
const utilMatch = specifier.match(/^lib\/([^/]+)/)
if (utilMatch) {
const utilPath = utils.find(
(file) => file === `src/lib/${utilMatch[1]}.ts` || file === `src/lib/${utilMatch[1]}.tsx`,
)
if (utilPath) {
dependencies.add(`utils/${path.basename(utilPath, path.extname(utilPath))}`)
}
}
}
return [...dependencies].sort((a, b) => a.localeCompare(b))
}
function stripSourcePrefix(file) {
return file.startsWith("src/") ? file.slice("src/".length) : file
}
function getPackageName(specifier) {
if (specifier.startsWith("@")) {
const [scope, name] = specifier.split("/")
return `${scope}/${name}`
}
return specifier.split("/")[0]
}
function toTitle(value) {
return value
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ")
}