@seed-design/cli 0.0.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bin/index.mjs +3 -0
- package/package.json +61 -0
- package/src/commands/add.ts +173 -0
- package/src/commands/init.ts +72 -0
- package/src/index.ts +23 -0
- package/src/schema.ts +34 -0
- package/src/test/add-relative-components.test.ts +67 -0
- package/src/utils/add-relative-components.ts +29 -0
- package/src/utils/get-config.ts +61 -0
- package/src/utils/get-metadata.ts +36 -0
- package/src/utils/get-package-info.ts +17 -0
- package/src/utils/get-package-manager.ts +13 -0
- package/src/utils/transformers/index.ts +51 -0
- package/src/utils/transformers/transform-css.ts +16 -0
- package/src/utils/transformers/transform-jsx.ts +93 -0
- package/src/utils/transformers/transform-rsc.ts +17 -0
package/README.md
ADDED
package/bin/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{cosmiconfig as Z}from"cosmiconfig";import T from"path";import{z as u}from"zod";var $="seed-design",ee=Z($,{searchPlaces:[`${$}.json`]}),k=u.object({$schema:u.string().optional(),rsc:u.coerce.boolean().default(!1),tsx:u.coerce.boolean().default(!0),css:u.coerce.boolean().default(!0),path:u.string()}).strict(),te=k.extend({resolvedUIPaths:u.string()});async function O(e){let t=await ne(e);return t?await oe(e,t):null}async function oe(e,t){let n=T.resolve(e,t.path);return te.parse({...t,resolvedUIPaths:T.join(n,"ui")})}async function ne(e){try{let t=await ee.search(e);return t?k.parse(t.config):null}catch(t){throw console.log(t),new Error(`Invalid configuration found in ${e}/seed-design.json.`)}}import{z as s}from"zod";var A=s.object({name:s.string(),description:s.string().optional(),dependencies:s.array(s.string()).optional(),devDependencies:s.array(s.string()).optional(),innerDependencies:s.array(s.string()).optional(),snippets:s.array(s.string()),type:s.enum(["component"])}),E=s.array(A),re=A.omit({snippets:!0}),se=re.extend({registries:s.array(s.object({name:s.string(),content:s.string()}))}),De=s.array(se);var I="https://component-seed.design.io";async function M(e){try{return await Promise.all(e.map(async n=>await(await fetch(`${I}/registry/component/${n}.json`)).json()))}catch(t){throw console.log(t),new Error(`Failed to fetch registry from ${I}.`)}}async function D(){try{let[e]=await M(["index"]);return E.parse(e)}catch(e){throw console.log(e),new Error(`Failed to fetch components from ${I}.`)}}import{detect as ae}from"@antfu/ni";async function F(e){let t=await ae({programmatic:!0,cwd:e});return t==="yarn@berry"?"yarn":t==="pnpm@6"?"pnpm":t==="bun"?"bun":t??"npm"}import{promises as le}from"fs";import{tmpdir as fe}from"os";import J from"path";import{transformFromAstSync as ie}from"@babel/core";import ce from"@babel/plugin-transform-typescript";import*as v from"recast";import{parse as pe}from"@babel/parser";var me={sourceType:"module",allowImportExportEverywhere:!0,allowReturnOutsideFunction:!0,startLine:1,tokens:!0,plugins:["asyncGenerators","bigInt","classPrivateMethods","classPrivateProperties","classProperties","classStaticBlock","decimal","decorators-legacy","doExpressions","dynamicImport","exportDefaultFrom","exportNamespaceFrom","functionBind","functionSent","importAssertions","importMeta","nullishCoalescingOperator","numericSeparator","objectRestSpread","optionalCatchBinding","optionalChaining",["pipelineOperator",{proposal:"minimal"}],["recordAndTuple",{syntaxType:"hash"}],"throwExpressions","topLevelAwait","v8intrinsic","typescript","jsx"]},z=async({sourceFile:e,config:t})=>{let n=e.getFullText();if(t.tsx)return n;let r=v.parse(n,{parser:{parse:c=>pe(c,me)}}),o=ie(r,n,{cloneInputAst:!1,code:!1,ast:!0,plugins:[ce],configFile:!1});if(!o||!o.ast)throw new Error("Failed to transform JSX");return v.print(o.ast).code};import{SyntaxKind as de}from"ts-morph";var N=async({sourceFile:e,config:t})=>{if(t.rsc)return e;let n=e.getFirstChildByKind(de.ExpressionStatement);return n?.getText()==='"use client";'&&n.remove(),e};var W=async({sourceFile:e,config:t})=>{if(t.css)return e;let r=e.getImportDeclarations().filter(o=>o.getModuleSpecifierValue().endsWith(".css"));for(let o of r)o.remove();return e};import{Project as ge,ScriptKind as ue}from"ts-morph";var he=[N,W],ye=new ge({compilerOptions:{}});async function xe(e){let t=await le.mkdtemp(J.join(fe(),"seed-deisgn-"));return J.join(t,e)}async function V(e){let t=await xe(e.filename),n=ye.createSourceFile(t,e.raw,{scriptKind:ue.TSX});for(let r of he)r({sourceFile:n,...e});return await z({sourceFile:n,...e})}import*as a from"@clack/prompts";import{execa as B}from"execa";import P from"fs-extra";import K from"path";import j from"picocolors";import{z as w}from"zod";function U(e,t){let n=new Set;function r(o){if(n.has(o))return;n.add(o);let c=t.find(d=>d.name===o);if(c&&c.innerDependencies)for(let d of c.innerDependencies)r(d)}for(let o of e)r(o);return Array.from(n)}var we=w.object({components:w.array(w.string()).optional(),cwd:w.string(),all:w.boolean()}),L=e=>{e.command("add [...components]","add component").option("-a, --all","Add all components",{default:!1}).option("-c, --cwd <cwd>","the working directory. defaults to the current directory.",{default:process.cwd()}).example("seed-design add box-button").example("seed-design add alert-dialog").action(async(t,n)=>{let r=we.parse({components:t,...n}),o=i=>j.cyan(i),c=r.cwd;P.existsSync(c)||(a.log.error(`The path ${c} does not exist. Please try again.`),process.exit(1));let d=await D(),f=r.all?d.map(i=>i.name):r.components;if(!r.components?.length&&!r.all){let i=await a.multiselect({message:"Select all components to add",options:d.map(l=>({label:l.name,value:l.name,hint:l.description}))});a.isCancel(i)&&(a.log.error("Aborted."),process.exit(0)),f=i}f?.length||(a.log.error("No components found."),process.exit(0));let y=U(f,d),S=y.filter(i=>!f.includes(i)),x=await O(c),H=await M(y);a.log.message(`Selection: ${o(f.join(", "))}`),S.length&&a.log.message(`Inner Dependencies: ${o(S.join(", "))} will be also added.`);for(let i of H){for(let m of i.registries){let g=x.resolvedUIPaths;P.existsSync(g)||await P.mkdir(g,{recursive:!0});let h=K.resolve(g,m.name),Q=await V({filename:m.name,config:x,raw:m.content});x.tsx||(h=h.replace(/\.tsx$/,".jsx"),h=h.replace(/\.ts$/,".js")),await P.writeFile(h,Q);let Y=K.relative(c,h);a.log.info(`Added ${o(m.name)} to ${o(Y)}`)}let l=await F(c),{start:b,stop:R}=a.spinner();if(i.dependencies?.length){b(j.gray("Installing dependencies"));let m=await B(l,[l==="npm"?"install":"add",...i.dependencies],{cwd:c});if(m.failed)console.error(m.all),process.exit(1);else{for(let g of i.dependencies)a.log.info(`- ${g}`);R("Dependencies installed.")}}if(i.devDependencies?.length){b(j.gray("Installing devDependencies"));let m=await B(l,[l==="npm"?"install":"add","-D",...i.devDependencies],{cwd:c});if(m.failed)console.error(m.all),process.exit(1);else{for(let g of i.devDependencies)a.log.info(`- ${g}`);R("Dependencies installed.")}}}a.outro("Components added.")})};import Ce from"findup-sync";import Se from"fs-extra";var ve="package.json";function Pe(){let e=Ce(ve);if(!e)throw new Error("No package.json file found in the project.");return e}function _(){return Se.readJSONSync(Pe())}import{cac as be}from"cac";import*as p from"@clack/prompts";import Ie from"fs-extra";import G from"path";import Me from"picocolors";import{z as X}from"zod";var je=X.object({cwd:X.string()}),q=e=>{e.command("init","initialize seed-design.json").option("-c, --cwd <cwd>","the working directory. defaults to the current directory.",{default:process.cwd()}).action(async t=>{let n=je.parse({...t}),r=x=>Me.cyan(x),o=await p.group({tsx:()=>p.confirm({message:`Would you like to use ${r("TypeScript")} (recommended)?`,initialValue:!0}),rsc:()=>p.confirm({message:`Are you using ${r("React Server Components")}?`,initialValue:!1}),css:()=>p.confirm({message:`Would you like to use ${r("CSS Modules")}? (If true, CSS import will be added in components)`,initialValue:!0}),path:()=>p.text({message:`Enter the path to your ${r("seed-design directory")}`,initialValue:"./seed-design",defaultValue:"./seed-design",placeholder:"./seed-design"})},{onCancel:()=>{p.cancel("Operation cancelled."),process.exit(0)}}),c={rsc:o.rsc,tsx:o.tsx,css:o.css,path:o.path},{start:d,stop:f}=p.spinner();d("Writing seed-design.json...");let y=G.resolve(n.cwd,"seed-design.json");await Ie.writeFile(y,`${JSON.stringify(c,null,2)}
|
|
3
|
+
`,"utf-8");let S=G.relative(process.cwd(),y);f(`seed-design.json written to ${r(S)}`)})};var Re="seed-design",C=be(Re);async function Te(){let e=_();L(C),q(C),C.version(e.version||"1.0.0","-v, --version"),C.help(),C.parse()}Te();
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seed-design/cli",
|
|
3
|
+
"version": "0.0.0-alpha",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/daangn/sprout",
|
|
8
|
+
"directory": "packages/cli"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bin": {
|
|
12
|
+
"seed-design": "./bin/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "ENV=prod node ./build.mjs",
|
|
23
|
+
"dev": "ENV=dev node ./dev.mjs",
|
|
24
|
+
"test": "yarn vitest"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@antfu/ni": "^0.22.0",
|
|
28
|
+
"@babel/core": "^7.24.9",
|
|
29
|
+
"@babel/parser": "^7.24.8",
|
|
30
|
+
"@babel/plugin-transform-typescript": "^7.24.8",
|
|
31
|
+
"@clack/prompts": "^0.7.0",
|
|
32
|
+
"cac": "^6.7.14",
|
|
33
|
+
"cosmiconfig": "^9.0.0",
|
|
34
|
+
"execa": "^9.3.0",
|
|
35
|
+
"findup-sync": "^5.0.0",
|
|
36
|
+
"fs-extra": "^11.2.0",
|
|
37
|
+
"mktemp": "^1.0.1",
|
|
38
|
+
"picocolors": "^1.0.1",
|
|
39
|
+
"recast": "^0.23.9",
|
|
40
|
+
"ts-morph": "^23.0.0",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/babel__core": "^7.20.5",
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"esbuild": "^0.19.3",
|
|
47
|
+
"type-fest": "^4.23.0",
|
|
48
|
+
"typescript": "^5.4.5",
|
|
49
|
+
"ultra-runner": "^3.10.5",
|
|
50
|
+
"vitest": "^2.0.5"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"ultra": {
|
|
56
|
+
"concurrent": [
|
|
57
|
+
"dev",
|
|
58
|
+
"build"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { getConfig } from "@/src/utils/get-config";
|
|
2
|
+
import { fetchComponentMetadatas, getMetadataIndex } from "@/src/utils/get-metadata";
|
|
3
|
+
import { getPackageManager } from "@/src/utils/get-package-manager";
|
|
4
|
+
import { transform } from "@/src/utils/transformers";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import color from "picocolors";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import type { CAC } from "cac";
|
|
13
|
+
import { addRelativeComponents } from "../utils/add-relative-components";
|
|
14
|
+
|
|
15
|
+
const addOptionsSchema = z.object({
|
|
16
|
+
components: z.array(z.string()).optional(),
|
|
17
|
+
cwd: z.string(),
|
|
18
|
+
all: z.boolean(),
|
|
19
|
+
// yes: z.boolean(),
|
|
20
|
+
// overwrite: z.boolean(),
|
|
21
|
+
// path: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const addCommand = (cli: CAC) => {
|
|
25
|
+
cli
|
|
26
|
+
.command("add [...components]", "add component")
|
|
27
|
+
.option("-a, --all", "Add all components", {
|
|
28
|
+
default: false,
|
|
29
|
+
})
|
|
30
|
+
.option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", {
|
|
31
|
+
default: process.cwd(),
|
|
32
|
+
})
|
|
33
|
+
.example("seed-design add box-button")
|
|
34
|
+
.example("seed-design add alert-dialog")
|
|
35
|
+
.action(async (components, opts) => {
|
|
36
|
+
const options = addOptionsSchema.parse({
|
|
37
|
+
components,
|
|
38
|
+
...opts,
|
|
39
|
+
});
|
|
40
|
+
const highlight = (text: string) => color.cyan(text);
|
|
41
|
+
const cwd = options.cwd;
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(cwd)) {
|
|
44
|
+
p.log.error(`The path ${cwd} does not exist. Please try again.`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const metadataIndex = await getMetadataIndex();
|
|
49
|
+
|
|
50
|
+
let selectedComponents: string[] = options.all
|
|
51
|
+
? metadataIndex.map((meatadata) => meatadata.name)
|
|
52
|
+
: options.components;
|
|
53
|
+
|
|
54
|
+
if (!options.components?.length && !options.all) {
|
|
55
|
+
const selects = await p.multiselect<
|
|
56
|
+
{ label: string; value: string; hint: string }[],
|
|
57
|
+
string
|
|
58
|
+
>({
|
|
59
|
+
message: "Select all components to add",
|
|
60
|
+
options: metadataIndex.map((metadata) => {
|
|
61
|
+
return {
|
|
62
|
+
label: metadata.name,
|
|
63
|
+
value: metadata.name,
|
|
64
|
+
hint: metadata.description,
|
|
65
|
+
};
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (p.isCancel(selects)) {
|
|
70
|
+
p.log.error("Aborted.");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
selectedComponents = selects as string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!selectedComponents?.length) {
|
|
78
|
+
p.log.error("No components found.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allComponents = addRelativeComponents(selectedComponents, metadataIndex);
|
|
83
|
+
const addedComponents = allComponents.filter((c) => !selectedComponents.includes(c));
|
|
84
|
+
const config = await getConfig(cwd);
|
|
85
|
+
const metadatas = await fetchComponentMetadatas(allComponents);
|
|
86
|
+
|
|
87
|
+
p.log.message(`Selection: ${highlight(selectedComponents.join(", "))}`);
|
|
88
|
+
if (addedComponents.length) {
|
|
89
|
+
p.log.message(
|
|
90
|
+
`Inner Dependencies: ${highlight(addedComponents.join(", "))} will be also added.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const metadata of metadatas) {
|
|
95
|
+
for (const registry of metadata.registries) {
|
|
96
|
+
const UIFolderPath = config.resolvedUIPaths;
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(UIFolderPath)) {
|
|
99
|
+
await fs.mkdir(UIFolderPath, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let filePath = path.resolve(UIFolderPath, registry.name);
|
|
103
|
+
|
|
104
|
+
const content = await transform({
|
|
105
|
+
filename: registry.name,
|
|
106
|
+
config,
|
|
107
|
+
raw: registry.content,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!config.tsx) {
|
|
111
|
+
filePath = filePath.replace(/\.tsx$/, ".jsx");
|
|
112
|
+
filePath = filePath.replace(/\.ts$/, ".js");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await fs.writeFile(filePath, content);
|
|
116
|
+
const relativePath = path.relative(cwd, filePath);
|
|
117
|
+
p.log.info(`Added ${highlight(registry.name)} to ${highlight(relativePath)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const packageManager = await getPackageManager(cwd);
|
|
121
|
+
|
|
122
|
+
const { start, stop } = p.spinner();
|
|
123
|
+
|
|
124
|
+
// Install dependencies.
|
|
125
|
+
if (metadata.dependencies?.length) {
|
|
126
|
+
start(color.gray("Installing dependencies"));
|
|
127
|
+
|
|
128
|
+
const result = await execa(
|
|
129
|
+
packageManager,
|
|
130
|
+
[packageManager === "npm" ? "install" : "add", ...metadata.dependencies],
|
|
131
|
+
{
|
|
132
|
+
cwd,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (result.failed) {
|
|
137
|
+
console.error(result.all);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
} else {
|
|
140
|
+
for (const deps of metadata.dependencies) {
|
|
141
|
+
p.log.info(`- ${deps}`);
|
|
142
|
+
}
|
|
143
|
+
stop("Dependencies installed.");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Install devDependencies.
|
|
148
|
+
if (metadata.devDependencies?.length) {
|
|
149
|
+
start(color.gray("Installing devDependencies"));
|
|
150
|
+
|
|
151
|
+
const result = await execa(
|
|
152
|
+
packageManager,
|
|
153
|
+
[packageManager === "npm" ? "install" : "add", "-D", ...metadata.devDependencies],
|
|
154
|
+
{
|
|
155
|
+
cwd,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (result.failed) {
|
|
160
|
+
console.error(result.all);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
} else {
|
|
163
|
+
for (const deps of metadata.devDependencies) {
|
|
164
|
+
p.log.info(`- ${deps}`);
|
|
165
|
+
}
|
|
166
|
+
stop("Dependencies installed.");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
p.outro("Components added.");
|
|
172
|
+
});
|
|
173
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import color from "picocolors";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import type { RawConfig } from "@/src/utils/get-config";
|
|
8
|
+
|
|
9
|
+
import type { CAC } from "cac";
|
|
10
|
+
|
|
11
|
+
const initOptionsSchema = z.object({
|
|
12
|
+
cwd: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const initCommand = (cli: CAC) => {
|
|
16
|
+
cli
|
|
17
|
+
.command("init", "initialize seed-design.json")
|
|
18
|
+
.option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", {
|
|
19
|
+
default: process.cwd(),
|
|
20
|
+
})
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
const options = initOptionsSchema.parse({ ...opts });
|
|
23
|
+
const highlight = (text: string) => color.cyan(text);
|
|
24
|
+
|
|
25
|
+
const group = await p.group(
|
|
26
|
+
{
|
|
27
|
+
tsx: () =>
|
|
28
|
+
p.confirm({
|
|
29
|
+
message: `Would you like to use ${highlight("TypeScript")} (recommended)?`,
|
|
30
|
+
initialValue: true,
|
|
31
|
+
}),
|
|
32
|
+
rsc: () =>
|
|
33
|
+
p.confirm({
|
|
34
|
+
message: `Are you using ${highlight("React Server Components")}?`,
|
|
35
|
+
initialValue: false,
|
|
36
|
+
}),
|
|
37
|
+
css: () =>
|
|
38
|
+
p.confirm({
|
|
39
|
+
message: `Would you like to use ${highlight("CSS Modules")}? (If true, CSS import will be added in components)`,
|
|
40
|
+
initialValue: true,
|
|
41
|
+
}),
|
|
42
|
+
path: () =>
|
|
43
|
+
p.text({
|
|
44
|
+
message: `Enter the path to your ${highlight("seed-design directory")}`,
|
|
45
|
+
initialValue: "./seed-design",
|
|
46
|
+
defaultValue: "./seed-design",
|
|
47
|
+
placeholder: "./seed-design",
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
onCancel: () => {
|
|
52
|
+
p.cancel("Operation cancelled.");
|
|
53
|
+
process.exit(0);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const config: RawConfig = {
|
|
59
|
+
rsc: group.rsc,
|
|
60
|
+
tsx: group.tsx,
|
|
61
|
+
css: group.css,
|
|
62
|
+
path: group.path,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const { start, stop } = p.spinner();
|
|
66
|
+
start("Writing seed-design.json...");
|
|
67
|
+
const targetPath = path.resolve(options.cwd, "seed-design.json");
|
|
68
|
+
await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
69
|
+
const relativePath = path.relative(process.cwd(), targetPath);
|
|
70
|
+
stop(`seed-design.json written to ${highlight(relativePath)}`);
|
|
71
|
+
});
|
|
72
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { addCommand } from "@/src/commands/add";
|
|
4
|
+
import { getPackageInfo } from "@/src/utils/get-package-info";
|
|
5
|
+
import { cac } from "cac";
|
|
6
|
+
import { initCommand } from "./commands/init";
|
|
7
|
+
|
|
8
|
+
const NAME = "seed-design";
|
|
9
|
+
const CLI = cac(NAME);
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const packageInfo = getPackageInfo();
|
|
13
|
+
|
|
14
|
+
/* Commands */
|
|
15
|
+
addCommand(CLI);
|
|
16
|
+
initCommand(CLI);
|
|
17
|
+
|
|
18
|
+
CLI.version(packageInfo.version || "1.0.0", "-v, --version");
|
|
19
|
+
CLI.help();
|
|
20
|
+
CLI.parse();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main();
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// TODO: Extract this to a shared package.
|
|
4
|
+
// INFO: also used in component-docs
|
|
5
|
+
export const componentMetadataSchema = z.object({
|
|
6
|
+
name: z.string(),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
dependencies: z.array(z.string()).optional(),
|
|
9
|
+
devDependencies: z.array(z.string()).optional(),
|
|
10
|
+
innerDependencies: z.array(z.string()).optional(),
|
|
11
|
+
snippets: z.array(z.string()),
|
|
12
|
+
type: z.enum(["component"]),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const componentMetadataIndexSchema = z.array(componentMetadataSchema);
|
|
16
|
+
|
|
17
|
+
const omittedComponentMetadataIndexSchema = componentMetadataSchema.omit({ snippets: true });
|
|
18
|
+
|
|
19
|
+
export const componentMetadataSchemaWithRegistry = omittedComponentMetadataIndexSchema.extend({
|
|
20
|
+
registries: z.array(
|
|
21
|
+
z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
content: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const componentMetadataWithRegistrySchema = z.array(componentMetadataSchemaWithRegistry);
|
|
29
|
+
|
|
30
|
+
export type ComponentMetadata = z.infer<typeof componentMetadataSchema>;
|
|
31
|
+
export type ComponentMetadataIndex = z.infer<typeof componentMetadataIndexSchema>;
|
|
32
|
+
export type ComponentMetadataWithRegistrySchema = z.infer<
|
|
33
|
+
typeof componentMetadataSchemaWithRegistry
|
|
34
|
+
>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { addRelativeComponents } from "../utils/add-relative-components";
|
|
3
|
+
import type { ComponentMetadataIndex } from "@/src/schema";
|
|
4
|
+
|
|
5
|
+
const config: ComponentMetadataIndex = [
|
|
6
|
+
{
|
|
7
|
+
name: "a",
|
|
8
|
+
snippets: ["a.tsx"],
|
|
9
|
+
type: "component",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "b",
|
|
13
|
+
innerDependencies: ["a"],
|
|
14
|
+
snippets: ["b.tsx"],
|
|
15
|
+
type: "component",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "c",
|
|
19
|
+
innerDependencies: ["b"],
|
|
20
|
+
snippets: ["c.tsx"],
|
|
21
|
+
type: "component",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "d",
|
|
25
|
+
innerDependencies: ["a", "b"],
|
|
26
|
+
snippets: ["d.tsx"],
|
|
27
|
+
type: "component",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "e",
|
|
31
|
+
innerDependencies: ["d"],
|
|
32
|
+
snippets: ["d.tsx"],
|
|
33
|
+
type: "component",
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
describe("addRelativeComponents", () => {
|
|
38
|
+
test("4 deps test", () => {
|
|
39
|
+
const userSelects = ["e"];
|
|
40
|
+
const result = addRelativeComponents(userSelects, config);
|
|
41
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b", "d", "e"]));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("3 deps test", () => {
|
|
45
|
+
const userSelects = ["d"];
|
|
46
|
+
const result = addRelativeComponents(userSelects, config);
|
|
47
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b", "d"]));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("3 deps test", () => {
|
|
51
|
+
const userSelects = ["c"];
|
|
52
|
+
const result = addRelativeComponents(userSelects, config);
|
|
53
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b", "c"]));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("2 deps test", () => {
|
|
57
|
+
const userSelects = ["b"];
|
|
58
|
+
const result = addRelativeComponents(userSelects, config);
|
|
59
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b"]));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("1 deps test", () => {
|
|
63
|
+
const userSelects = ["a"];
|
|
64
|
+
const result = addRelativeComponents(userSelects, config);
|
|
65
|
+
expect(result).toEqual(expect.arrayContaining(["a"]));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentMetadataIndex } from "@/src/schema";
|
|
2
|
+
|
|
3
|
+
export function addRelativeComponents(
|
|
4
|
+
userSelects: string[],
|
|
5
|
+
metadataIndex: ComponentMetadataIndex,
|
|
6
|
+
) {
|
|
7
|
+
const selectedComponents = new Set<string>();
|
|
8
|
+
|
|
9
|
+
function addSeedDependencies(componentName: string) {
|
|
10
|
+
if (selectedComponents.has(componentName)) return;
|
|
11
|
+
|
|
12
|
+
selectedComponents.add(componentName);
|
|
13
|
+
|
|
14
|
+
const component = metadataIndex.find((c) => c.name === componentName);
|
|
15
|
+
if (!component) return;
|
|
16
|
+
|
|
17
|
+
if (component.innerDependencies) {
|
|
18
|
+
for (const dep of component.innerDependencies) {
|
|
19
|
+
addSeedDependencies(dep);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const componentName of userSelects) {
|
|
25
|
+
addSeedDependencies(componentName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Array.from(selectedComponents);
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const MODULE_NAME = "seed-design";
|
|
6
|
+
|
|
7
|
+
const explorer = cosmiconfig(MODULE_NAME, {
|
|
8
|
+
searchPlaces: [`${MODULE_NAME}.json`],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const rawConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
$schema: z.string().optional(),
|
|
14
|
+
rsc: z.coerce.boolean().default(false),
|
|
15
|
+
tsx: z.coerce.boolean().default(true),
|
|
16
|
+
css: z.coerce.boolean().default(true),
|
|
17
|
+
path: z.string(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
|
|
21
|
+
export type RawConfig = z.infer<typeof rawConfigSchema>;
|
|
22
|
+
|
|
23
|
+
export const configSchema = rawConfigSchema.extend({
|
|
24
|
+
resolvedUIPaths: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export async function getConfig(cwd: string) {
|
|
28
|
+
const config = await getRawConfig(cwd);
|
|
29
|
+
|
|
30
|
+
if (!config) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return await resolveConfigPaths(cwd, config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Config = z.infer<typeof configSchema>;
|
|
38
|
+
|
|
39
|
+
export async function resolveConfigPaths(cwd: string, config: RawConfig) {
|
|
40
|
+
const seedComponentRootPath = path.resolve(cwd, config.path);
|
|
41
|
+
|
|
42
|
+
return configSchema.parse({
|
|
43
|
+
...config,
|
|
44
|
+
resolvedUIPaths: path.join(seedComponentRootPath, "ui"),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
|
|
49
|
+
try {
|
|
50
|
+
const configResult = await explorer.search(cwd);
|
|
51
|
+
|
|
52
|
+
if (!configResult) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return rawConfigSchema.parse(configResult.config);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.log(error);
|
|
59
|
+
throw new Error(`Invalid configuration found in ${cwd}/seed-design.json.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
componentMetadataIndexSchema,
|
|
3
|
+
type ComponentMetadataWithRegistrySchema,
|
|
4
|
+
} from "@/src/schema";
|
|
5
|
+
|
|
6
|
+
const BASE_URL =
|
|
7
|
+
process.env.NODE_ENV === "prod" ? "https://component-seed.design.io" : "http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function fetchComponentMetadatas(
|
|
10
|
+
fileNames?: string[],
|
|
11
|
+
): Promise<ComponentMetadataWithRegistrySchema[]> {
|
|
12
|
+
try {
|
|
13
|
+
const results = await Promise.all(
|
|
14
|
+
fileNames.map(async (fileName) => {
|
|
15
|
+
const response = await fetch(`${BASE_URL}/registry/component/${fileName}.json`);
|
|
16
|
+
return await response.json();
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return results;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.log(error);
|
|
23
|
+
throw new Error(`Failed to fetch registry from ${BASE_URL}.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getMetadataIndex() {
|
|
28
|
+
try {
|
|
29
|
+
const [result] = await fetchComponentMetadatas(["index"]);
|
|
30
|
+
|
|
31
|
+
return componentMetadataIndexSchema.parse(result);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.log(error);
|
|
34
|
+
throw new Error(`Failed to fetch components from ${BASE_URL}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import findup from "findup-sync";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import type { PackageJson } from "type-fest";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_JSON = "package.json";
|
|
6
|
+
|
|
7
|
+
function getPackagePath() {
|
|
8
|
+
const packageJsonPath = findup(PACKAGE_JSON);
|
|
9
|
+
if (!packageJsonPath) {
|
|
10
|
+
throw new Error("No package.json file found in the project.");
|
|
11
|
+
}
|
|
12
|
+
return packageJsonPath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getPackageInfo() {
|
|
16
|
+
return fs.readJSONSync(getPackagePath()) as PackageJson;
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { detect } from "@antfu/ni";
|
|
2
|
+
|
|
3
|
+
export async function getPackageManager(
|
|
4
|
+
targetDir: string,
|
|
5
|
+
): Promise<"yarn" | "pnpm" | "bun" | "npm"> {
|
|
6
|
+
const packageManager = await detect({ programmatic: true, cwd: targetDir });
|
|
7
|
+
|
|
8
|
+
if (packageManager === "yarn@berry") return "yarn";
|
|
9
|
+
if (packageManager === "pnpm@6") return "pnpm";
|
|
10
|
+
if (packageManager === "bun") return "bun";
|
|
11
|
+
|
|
12
|
+
return packageManager ?? "npm";
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
import { transformJsx } from "@/src/utils/transformers/transform-jsx";
|
|
7
|
+
import { transformRsc } from "@/src/utils/transformers/transform-rsc";
|
|
8
|
+
import { transformCSS } from "./transform-css";
|
|
9
|
+
|
|
10
|
+
import { Project, ScriptKind, type SourceFile } from "ts-morph";
|
|
11
|
+
|
|
12
|
+
import type { Config } from "@/src/utils/get-config";
|
|
13
|
+
|
|
14
|
+
export type TransformOpts = {
|
|
15
|
+
filename: string;
|
|
16
|
+
raw: string;
|
|
17
|
+
config: Config;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type Transformer<Output = SourceFile> = (
|
|
21
|
+
opts: TransformOpts & {
|
|
22
|
+
sourceFile: SourceFile;
|
|
23
|
+
},
|
|
24
|
+
) => Promise<Output>;
|
|
25
|
+
|
|
26
|
+
const transformers: Transformer[] = [transformRsc, transformCSS];
|
|
27
|
+
|
|
28
|
+
const project = new Project({
|
|
29
|
+
compilerOptions: {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function createTempSourceFile(filename: string) {
|
|
33
|
+
const dir = await fs.mkdtemp(path.join(tmpdir(), "seed-deisgn-"));
|
|
34
|
+
return path.join(dir, filename);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function transform(opts: TransformOpts) {
|
|
38
|
+
const tempFile = await createTempSourceFile(opts.filename);
|
|
39
|
+
const sourceFile = project.createSourceFile(tempFile, opts.raw, {
|
|
40
|
+
scriptKind: ScriptKind.TSX,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
for (const transformer of transformers) {
|
|
44
|
+
transformer({ sourceFile, ...opts });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return await transformJsx({
|
|
48
|
+
sourceFile,
|
|
49
|
+
...opts,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Transformer } from "@/src/utils/transformers";
|
|
2
|
+
|
|
3
|
+
export const transformCSS: Transformer = async ({ sourceFile, config }) => {
|
|
4
|
+
if (config.css) {
|
|
5
|
+
return sourceFile;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const imports = sourceFile.getImportDeclarations();
|
|
9
|
+
const cssImports = imports.filter((i) => i.getModuleSpecifierValue().endsWith(".css"));
|
|
10
|
+
|
|
11
|
+
for (const cssImport of cssImports) {
|
|
12
|
+
cssImport.remove();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return sourceFile;
|
|
16
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import { transformFromAstSync } from "@babel/core";
|
|
3
|
+
import transformTypescript from "@babel/plugin-transform-typescript";
|
|
4
|
+
import * as recast from "recast";
|
|
5
|
+
|
|
6
|
+
import { type ParserOptions, parse } from "@babel/parser";
|
|
7
|
+
import type { Transformer } from "@/src/utils/transformers";
|
|
8
|
+
|
|
9
|
+
// TODO.
|
|
10
|
+
// I'm using recast for the AST here.
|
|
11
|
+
// Figure out if ts-morph AST is compatible with Babel.
|
|
12
|
+
|
|
13
|
+
// This is a copy of the babel options from recast/parser.
|
|
14
|
+
// The goal here is to tolerate as much syntax as possible.
|
|
15
|
+
// We want to be able to parse any valid tsx code.
|
|
16
|
+
// See https://github.com/benjamn/recast/blob/master/parsers/_babel_options.ts.
|
|
17
|
+
const PARSE_OPTIONS: ParserOptions = {
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
allowImportExportEverywhere: true,
|
|
20
|
+
allowReturnOutsideFunction: true,
|
|
21
|
+
startLine: 1,
|
|
22
|
+
tokens: true,
|
|
23
|
+
plugins: [
|
|
24
|
+
"asyncGenerators",
|
|
25
|
+
"bigInt",
|
|
26
|
+
"classPrivateMethods",
|
|
27
|
+
"classPrivateProperties",
|
|
28
|
+
"classProperties",
|
|
29
|
+
"classStaticBlock",
|
|
30
|
+
"decimal",
|
|
31
|
+
"decorators-legacy",
|
|
32
|
+
"doExpressions",
|
|
33
|
+
"dynamicImport",
|
|
34
|
+
"exportDefaultFrom",
|
|
35
|
+
"exportNamespaceFrom",
|
|
36
|
+
"functionBind",
|
|
37
|
+
"functionSent",
|
|
38
|
+
"importAssertions",
|
|
39
|
+
"importMeta",
|
|
40
|
+
"nullishCoalescingOperator",
|
|
41
|
+
"numericSeparator",
|
|
42
|
+
"objectRestSpread",
|
|
43
|
+
"optionalCatchBinding",
|
|
44
|
+
"optionalChaining",
|
|
45
|
+
[
|
|
46
|
+
"pipelineOperator",
|
|
47
|
+
{
|
|
48
|
+
proposal: "minimal",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
"recordAndTuple",
|
|
53
|
+
{
|
|
54
|
+
syntaxType: "hash",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
"throwExpressions",
|
|
58
|
+
"topLevelAwait",
|
|
59
|
+
"v8intrinsic",
|
|
60
|
+
"typescript",
|
|
61
|
+
"jsx",
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const transformJsx: Transformer<string> = async ({ sourceFile, config }) => {
|
|
66
|
+
const output = sourceFile.getFullText();
|
|
67
|
+
|
|
68
|
+
if (config.tsx) {
|
|
69
|
+
return output;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ast = recast.parse(output, {
|
|
73
|
+
parser: {
|
|
74
|
+
parse: (code: string) => {
|
|
75
|
+
return parse(code, PARSE_OPTIONS);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = transformFromAstSync(ast, output, {
|
|
81
|
+
cloneInputAst: false,
|
|
82
|
+
code: false,
|
|
83
|
+
ast: true,
|
|
84
|
+
plugins: [transformTypescript],
|
|
85
|
+
configFile: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!result || !result.ast) {
|
|
89
|
+
throw new Error("Failed to transform JSX");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return recast.print(result.ast).code;
|
|
93
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
import type { Transformer } from "@/src/utils/transformers";
|
|
4
|
+
|
|
5
|
+
export const transformRsc: Transformer = async ({ sourceFile, config }) => {
|
|
6
|
+
if (config.rsc) {
|
|
7
|
+
return sourceFile;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Remove "use client" from the top of the file.
|
|
11
|
+
const first = sourceFile.getFirstChildByKind(SyntaxKind.ExpressionStatement);
|
|
12
|
+
if (first?.getText() === `"use client";`) {
|
|
13
|
+
first.remove();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return sourceFile;
|
|
17
|
+
};
|