components-differ 1.0.17 → 1.2.0
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 +16 -191
- package/dist/index.mjs +114 -0
- package/dist/src/components.mjs +98 -0
- package/dist/src/create-diff.mjs +96 -0
- package/dist/src/git.mjs +141 -0
- package/dist/src/parse-file-path.mjs +32 -0
- package/package.json +20 -16
- package/dist/components.d.ts +0 -6
- package/dist/components.d.ts.map +0 -1
- package/dist/components.js +0 -119
- package/dist/components.js.map +0 -1
- package/dist/create-diff.d.ts +0 -14
- package/dist/create-diff.d.ts.map +0 -1
- package/dist/create-diff.js +0 -246
- package/dist/create-diff.js.map +0 -1
- package/dist/git.d.ts +0 -5
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js +0 -120
- package/dist/git.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -166
- package/dist/index.js.map +0 -1
- package/dist/parse-file-path.d.ts +0 -3
- package/dist/parse-file-path.d.ts.map +0 -1
- package/dist/parse-file-path.js +0 -53
- package/dist/parse-file-path.js.map +0 -1
- package/dist/registry.d.ts +0 -3
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -112
- package/dist/registry.js.map +0 -1
- package/dist/shadcn-init.d.ts +0 -11
- package/dist/shadcn-init.d.ts.map +0 -1
- package/dist/shadcn-init.js +0 -208
- package/dist/shadcn-init.js.map +0 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,206 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ShadCN Project Differ
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This CLI tool figures out the difference between the initial commit of a ShadCN project and the current state of the project and creates a new ShadCN JSON output file with the changes. This ShadCN JSON file can then be used with the ShadCN CLI to generate a new project or add to an existing project.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Steps
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Supports environment-based configuration for registry metadata
|
|
13
|
-
- Ships with TypeScript types, tests, and build scripts for local development
|
|
7
|
+
1. Create a new NextJS app
|
|
8
|
+
2. Add ShadCN to the app
|
|
9
|
+
3. Create a new initial commit; `rm -fr .git && git init && git add . && git commit -m "Initial commit"` or `npx components-differ --init`
|
|
10
|
+
4. Make your updates to the project
|
|
11
|
+
5. Run the CLI tool; `npx components-differ`
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
The reason we are recreating the initial commit is so that the resulting JSON output is only the changes to the project after ShadCN was added, and not the entire project history.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
- `pnpm` (recommended) or `npm`
|
|
19
|
-
- A project that has already been initialised with the shadcn CLI
|
|
20
|
-
|
|
21
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
Install globally or run via `npx`:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pnpm add -g components-differ
|
|
27
|
-
# or
|
|
28
|
-
npx components-differ --help
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
During local development, clone the repo and install dependencies:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
git clone https://github.com/componentshost/components-differ.git
|
|
35
|
-
cd components-differ
|
|
36
|
-
pnpm install
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
1. Create a fresh Next.js app.
|
|
42
|
-
2. Initialise shadcn/ui (`npx shadcn@latest init`).
|
|
43
|
-
3. Commit the baseline scaffold:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
rm -fr .git && git init
|
|
47
|
-
git add .
|
|
48
|
-
git commit -m "Initial shadcn scaffold"
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
_Tip: use `npx components-differ --init` to automate the reset + commit._
|
|
52
|
-
|
|
53
|
-
4. Build out your custom components, hooks, utilities, and pages.
|
|
54
|
-
5. Run the differ:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npx components-differ > registry-item.json
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
6. Publish or host the JSON output, then use it with the shadcn CLI:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npx shadcn@latest add https://example.com/registry-item.json
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## CLI Usage
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npx components-differ [options]
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
| Option | Description |
|
|
73
|
-
| --- | --- |
|
|
74
|
-
| `-n, --name <name>` | Override the generated item name (defaults to the current directory name). |
|
|
75
|
-
| `--init` | Reset the git repository and recreate the initial commit (useful for fresh scaffolds). |
|
|
76
|
-
| `--registry [path]` | Create or update a `registry.json` file. If no path is provided, `./registry.json` is used. |
|
|
77
|
-
| `--initshadcn [type]` | Configure shadcn/ui for your project. Detects project type automatically; pass `next` or `vite` to force a specific scaffold. |
|
|
78
|
-
|
|
79
|
-
When `--registry` is omitted, the CLI prints a single registry item (JSON) to stdout. The item includes inferred dependencies, registry dependencies, file payloads, and metadata compatible with the shadcn registry tooling.
|
|
80
|
-
|
|
81
|
-
### Bootstrapping shadcn/ui
|
|
82
|
-
|
|
83
|
-
Use `--initshadcn` to apply the official shadcn setup steps for supported frameworks:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# Auto-detect (looks at package.json)
|
|
87
|
-
npx components-differ --initshadcn
|
|
88
|
-
|
|
89
|
-
# Explicit Vite support (adds Tailwind deps, config files, and runs the shadcn CLI)
|
|
90
|
-
npx components-differ --initshadcn vite
|
|
91
|
-
|
|
92
|
-
# Explicit Next.js support (runs the shadcn CLI in your project)
|
|
93
|
-
npx components-differ --initshadcn next
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
For Vite projects the command will:
|
|
97
|
-
|
|
98
|
-
- Add `tailwindcss`, `@tailwindcss/vite`, and `@types/node`
|
|
99
|
-
- Locate the first CSS file imported by the Vite entry file (`src/main.{ts,tsx,js,jsx}`) and replace its contents with the Tailwind entrypoint
|
|
100
|
-
- Inject the required `baseUrl`/`paths` aliases into `tsconfig.json` and `tsconfig.app.json`
|
|
101
|
-
- Overwrite `vite.config.ts` with the recommended configuration (includes Tailwind and path aliases)
|
|
102
|
-
- Run the interactive `shadcn@latest init` command
|
|
103
|
-
|
|
104
|
-
Next.js projects skip the file edits (Next provides the required Tailwind setup) and go straight to the `shadcn` CLI.
|
|
105
|
-
|
|
106
|
-
## Working with `registry.json`
|
|
107
|
-
|
|
108
|
-
Passing `--registry` switches the CLI into registry mode:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx components-differ --registry # writes ./registry.json
|
|
112
|
-
npx components-differ --registry configs/registry.json
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
- Existing files are parsed and updated; if the item already exists (matching `name`), it is replaced.
|
|
116
|
-
- New files are created with the official schema URL and project metadata.
|
|
117
|
-
- Parent directories are created automatically.
|
|
118
|
-
|
|
119
|
-
### Registry metadata
|
|
120
|
-
|
|
121
|
-
By default the generated file uses:
|
|
122
|
-
|
|
123
|
-
- `name`: `shadcn`
|
|
124
|
-
- `homepage`: `https://ui.shadcn.com`
|
|
125
|
-
|
|
126
|
-
Override these values by defining environment variables at runtime:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
REGISTRY_NAME=acme \
|
|
130
|
-
REGISTRY_HOMEPAGE=https://acme.dev \
|
|
131
|
-
npx components-differ --registry
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
If the variables are absent, the CLI also reads a `.env` file in the project root:
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
REGISTRY_NAME=acme
|
|
138
|
-
REGISTRY_HOMEPAGE=https://acme.dev
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Building registry artifacts
|
|
142
|
-
|
|
143
|
-
Add a script to your `package.json` so you can build the registry payload with the official shadcn toolchain:
|
|
144
|
-
|
|
145
|
-
```json
|
|
146
|
-
{
|
|
147
|
-
"scripts": {
|
|
148
|
-
"registry:build": "shadcn build"
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Then run:
|
|
15
|
+
You can then take the resulting JSON ouput and host it on a URL, then use that with the ShadCN CLI to generate a new project or add to an existing project.
|
|
154
16
|
|
|
155
17
|
```bash
|
|
156
|
-
|
|
18
|
+
npx shadcn@latest init http://your-json-output-url
|
|
157
19
|
```
|
|
158
20
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
## Recommended Workflow
|
|
21
|
+
You can use the `--src-dir` flag if you want to use the `src` directory in your project.
|
|
162
22
|
|
|
163
|
-
|
|
164
|
-
2. Execute `npx components-differ --registry` to keep `registry.json` current.
|
|
165
|
-
3. Publish the updated registry (via HTTP or a git-backed registry).
|
|
166
|
-
4. Consumers install the latest slice using `npx shadcn@latest add <url>`.
|
|
167
|
-
|
|
168
|
-
## Development Scripts
|
|
169
|
-
|
|
170
|
-
| Command | Description |
|
|
171
|
-
| --- | --- |
|
|
172
|
-
| `pnpm build` | Compile TypeScript sources to `dist/`. |
|
|
173
|
-
| `pnpm test` | Run the Vitest suite once. |
|
|
174
|
-
| `pnpm test:watch` | Run tests in watch mode. |
|
|
175
|
-
| `pnpm bump` | Increment the package version (patch) and update `package.json`. |
|
|
176
|
-
| `pnpm registry:build` | Run the shadcn CLI build pipeline (requires `shadcn` CLI). |
|
|
177
|
-
|
|
178
|
-
During development you can execute the CLI locally:
|
|
23
|
+
Or you can add the JSON output to an existing project:
|
|
179
24
|
|
|
180
25
|
```bash
|
|
181
|
-
|
|
182
|
-
node dist/index.js --registry
|
|
26
|
+
npx shadcn@latest add http://your-json-output-url
|
|
183
27
|
```
|
|
184
28
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
pnpm link --global
|
|
189
|
-
components-differ --help
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## Releasing
|
|
193
|
-
|
|
194
|
-
1. Bump the version: `pnpm bump` (or adjust `package.json` manually).
|
|
195
|
-
2. Rebuild and test: `pnpm build && pnpm test`.
|
|
196
|
-
3. Publish: `npm publish`.
|
|
197
|
-
|
|
198
|
-
Ensure you are authenticated with an npm account that has publish rights (`npm whoami`).
|
|
199
|
-
|
|
200
|
-
## Contributing
|
|
201
|
-
|
|
202
|
-
Contributions are welcome! Please open an issue or pull request with a clear description of the change. Run the full test suite before submitting (`pnpm test`), and include updates to the documentation where relevant.
|
|
203
|
-
|
|
204
|
-
## License
|
|
29
|
+
# Why is this useful?
|
|
205
30
|
|
|
206
|
-
|
|
31
|
+
This allows library maintainers or SaaS services to create a one step installer for their library or service into an existing project, or to bootstrap a new project with the library or service.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { program } from "commander";
|
|
6
|
+
|
|
7
|
+
import { scanForAlteredFiles, scanForFiles, hasSrcDir } from "./src/git.mjs";
|
|
8
|
+
import { readComponentsManifest } from "./src/components.mjs";
|
|
9
|
+
import { createDiff } from "./src/create-diff.mjs";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
program.option("-n, --name <name>").option('--init');
|
|
14
|
+
program.parse();
|
|
15
|
+
|
|
16
|
+
const options = program.opts();
|
|
17
|
+
|
|
18
|
+
const runCommand = (command) => {
|
|
19
|
+
try {
|
|
20
|
+
execSync(command, { stdio: "inherit" });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(`Failed to execute command: ${command}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ensureGitignore = () => {
|
|
28
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
29
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
30
|
+
console.log(".gitignore file is missing. Creating one...");
|
|
31
|
+
const content = `
|
|
32
|
+
/node_modules
|
|
33
|
+
/.pnp
|
|
34
|
+
.pnp.*
|
|
35
|
+
.yarn/*
|
|
36
|
+
!.yarn/patches
|
|
37
|
+
!.yarn/plugins
|
|
38
|
+
!.yarn/releases
|
|
39
|
+
!.yarn/versions
|
|
40
|
+
|
|
41
|
+
# testing
|
|
42
|
+
/coverage
|
|
43
|
+
|
|
44
|
+
# next.js
|
|
45
|
+
/.next/
|
|
46
|
+
/out/
|
|
47
|
+
|
|
48
|
+
# production
|
|
49
|
+
/build
|
|
50
|
+
|
|
51
|
+
# misc
|
|
52
|
+
.DS_Store
|
|
53
|
+
*.pem
|
|
54
|
+
|
|
55
|
+
# debug
|
|
56
|
+
npm-debug.log*
|
|
57
|
+
yarn-debug.log*
|
|
58
|
+
yarn-error.log*
|
|
59
|
+
|
|
60
|
+
# env files (can opt-in for committing if needed)
|
|
61
|
+
.env*
|
|
62
|
+
|
|
63
|
+
# vercel
|
|
64
|
+
.vercel
|
|
65
|
+
|
|
66
|
+
# typescript
|
|
67
|
+
*.tsbuildinfo
|
|
68
|
+
next-env.d.ts
|
|
69
|
+
`;
|
|
70
|
+
fs.writeFileSync(gitignorePath, content, "utf8");
|
|
71
|
+
console.log(".gitignore file created with default rules.");
|
|
72
|
+
} else {
|
|
73
|
+
console.log(".gitignore file already exists.");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const main = () => {
|
|
78
|
+
if (options.init) {
|
|
79
|
+
console.log("Initializing git repository for new component");
|
|
80
|
+
// Cross-platform logic
|
|
81
|
+
if (process.platform === "win32") {
|
|
82
|
+
runCommand("rmdir /s /q .git && git init && git add . && git commit -m \"Initial commit\"");
|
|
83
|
+
} else {
|
|
84
|
+
runCommand("rm -fr .git && git init && git add . && git commit -m \"Initial commit\"");
|
|
85
|
+
}
|
|
86
|
+
ensureGitignore()
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const name = options.name || path.basename(process.cwd());
|
|
91
|
+
|
|
92
|
+
const { alteredFiles, specificFiles } = scanForAlteredFiles([
|
|
93
|
+
"./package.json",
|
|
94
|
+
]);
|
|
95
|
+
const currentFiles = scanForFiles(process.cwd());
|
|
96
|
+
|
|
97
|
+
const currentPackageJson = fs.readFileSync("./package.json", "utf-8");
|
|
98
|
+
|
|
99
|
+
const config = readComponentsManifest(process.cwd());
|
|
100
|
+
config.isSrcDir = hasSrcDir(process.cwd());
|
|
101
|
+
|
|
102
|
+
const output = createDiff({
|
|
103
|
+
name,
|
|
104
|
+
config,
|
|
105
|
+
alteredFiles,
|
|
106
|
+
currentFiles,
|
|
107
|
+
specificFiles,
|
|
108
|
+
currentPackageJson,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(JSON.stringify(output, null, 2));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
main();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
const WHITELISTED_COMPONENTS = [
|
|
5
|
+
"accordion",
|
|
6
|
+
"alert",
|
|
7
|
+
"alert-dialog",
|
|
8
|
+
"aspect-ratio",
|
|
9
|
+
"avatar",
|
|
10
|
+
"badge",
|
|
11
|
+
"breadcrumb",
|
|
12
|
+
"button",
|
|
13
|
+
"calendar",
|
|
14
|
+
"card",
|
|
15
|
+
"carousel",
|
|
16
|
+
"chart",
|
|
17
|
+
"checkbox",
|
|
18
|
+
"collapsible",
|
|
19
|
+
"command",
|
|
20
|
+
"context-menu",
|
|
21
|
+
"table",
|
|
22
|
+
"dialog",
|
|
23
|
+
"drawer",
|
|
24
|
+
"dropdown-menu",
|
|
25
|
+
"form",
|
|
26
|
+
"hover-card",
|
|
27
|
+
"input",
|
|
28
|
+
"input-otp",
|
|
29
|
+
"label",
|
|
30
|
+
"menubar",
|
|
31
|
+
"navigation-menu",
|
|
32
|
+
"pagination",
|
|
33
|
+
"popover",
|
|
34
|
+
"progress",
|
|
35
|
+
"radio-group",
|
|
36
|
+
"resizable",
|
|
37
|
+
"scroll-area",
|
|
38
|
+
"select",
|
|
39
|
+
"separator",
|
|
40
|
+
"sheet",
|
|
41
|
+
"skeleton",
|
|
42
|
+
"slider",
|
|
43
|
+
"sonner",
|
|
44
|
+
"switch",
|
|
45
|
+
"tabs",
|
|
46
|
+
"textarea",
|
|
47
|
+
"toast",
|
|
48
|
+
"toggle",
|
|
49
|
+
"toggle-group",
|
|
50
|
+
"tooltip",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function findComponentFiles(config, originalFiles) {
|
|
54
|
+
const registryDependencies = [];
|
|
55
|
+
const compDir = config.ui.replace("@/", config.isSrcDir ? "src/" : "");
|
|
56
|
+
for (const { path: filePath } of originalFiles) {
|
|
57
|
+
if (filePath.startsWith(compDir)) {
|
|
58
|
+
const fileExtension = path.extname(filePath);
|
|
59
|
+
const fileName = path.basename(filePath, fileExtension);
|
|
60
|
+
if (
|
|
61
|
+
(fileExtension === ".tsx" || fileExtension === ".jsx") &&
|
|
62
|
+
WHITELISTED_COMPONENTS.includes(fileName)
|
|
63
|
+
) {
|
|
64
|
+
registryDependencies.push(path.basename(filePath, fileExtension));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return registryDependencies;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readComponentsManifest(dir) {
|
|
72
|
+
const manifestPath = path.join(dir, "./components.json");
|
|
73
|
+
if (fs.existsSync(manifestPath)) {
|
|
74
|
+
const config = JSON.parse(fs.readFileSync(manifestPath, "utf-8")).aliases;
|
|
75
|
+
return config;
|
|
76
|
+
} else {
|
|
77
|
+
console.error("Components manifest not found");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getAliasedPaths(config) {
|
|
83
|
+
return [
|
|
84
|
+
config.components.replace("@/", ""),
|
|
85
|
+
config.utils.replace("@/", ""),
|
|
86
|
+
config.ui.replace("@/", ""),
|
|
87
|
+
config.lib.replace("@/", ""),
|
|
88
|
+
config.hooks.replace("@/", ""),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isBuiltinComponent(config, filePath) {
|
|
93
|
+
if (filePath.startsWith(config.ui.replace("@/", ""))) {
|
|
94
|
+
const component = path.basename(filePath, path.extname(filePath));
|
|
95
|
+
return WHITELISTED_COMPONENTS.includes(component);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findComponentFiles,
|
|
3
|
+
getAliasedPaths,
|
|
4
|
+
isBuiltinComponent,
|
|
5
|
+
} from "./components.mjs";
|
|
6
|
+
import { parseFilePath } from "./parse-file-path.mjs";
|
|
7
|
+
|
|
8
|
+
function addFile(output, config, inSrcDir, relativeFilePath, content) {
|
|
9
|
+
if (!isBuiltinComponent(config, relativeFilePath)) {
|
|
10
|
+
output.files.push(
|
|
11
|
+
parseFilePath(inSrcDir, config, `./${relativeFilePath}`, content)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function addDependencies(
|
|
17
|
+
output,
|
|
18
|
+
initialPackageContents,
|
|
19
|
+
currentPackageContents
|
|
20
|
+
) {
|
|
21
|
+
const initialPackageJson = JSON.parse(initialPackageContents);
|
|
22
|
+
const currentPackageJson = JSON.parse(currentPackageContents);
|
|
23
|
+
|
|
24
|
+
const initialDependencies = initialPackageJson.dependencies ?? {};
|
|
25
|
+
const currentDependencies = currentPackageJson.dependencies ?? {};
|
|
26
|
+
const initialDevDependencies = initialPackageJson.devDependencies ?? {};
|
|
27
|
+
const currentDevDependencies = currentPackageJson.devDependencies ?? {};
|
|
28
|
+
|
|
29
|
+
output.dependencies = Object.keys(currentDependencies).filter(
|
|
30
|
+
(dep) => !Object.prototype.hasOwnProperty.call(initialDependencies, dep)
|
|
31
|
+
);
|
|
32
|
+
output.devDependencies = Object.keys(currentDevDependencies).filter(
|
|
33
|
+
(dep) => !Object.prototype.hasOwnProperty.call(initialDevDependencies, dep)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scanWithSrcDir(output, config, alteredFiles) {
|
|
38
|
+
for (const { path, content } of alteredFiles) {
|
|
39
|
+
if (path.startsWith("src/")) {
|
|
40
|
+
addFile(output, config, true, path.replace("src/", ""), content);
|
|
41
|
+
} else {
|
|
42
|
+
addFile(output, config, false, path, content);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isInAppDir(path) {
|
|
48
|
+
return path.startsWith("app/");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scanWithoutSrcDir(output, config, alteredFiles) {
|
|
52
|
+
const aliasedPaths = getAliasedPaths(config);
|
|
53
|
+
|
|
54
|
+
for (const { path, content } of alteredFiles) {
|
|
55
|
+
addFile(
|
|
56
|
+
output,
|
|
57
|
+
config,
|
|
58
|
+
aliasedPaths.includes(path) || isInAppDir(path),
|
|
59
|
+
path,
|
|
60
|
+
content
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDiff({
|
|
66
|
+
name,
|
|
67
|
+
config,
|
|
68
|
+
alteredFiles,
|
|
69
|
+
specificFiles,
|
|
70
|
+
currentFiles,
|
|
71
|
+
currentPackageJson,
|
|
72
|
+
}) {
|
|
73
|
+
const output = {
|
|
74
|
+
name,
|
|
75
|
+
type: "registry:block",
|
|
76
|
+
dependencies: [],
|
|
77
|
+
devDependencies: [],
|
|
78
|
+
registryDependencies: [],
|
|
79
|
+
files: [],
|
|
80
|
+
tailwind: {},
|
|
81
|
+
cssVars: {},
|
|
82
|
+
meta: {},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (config.isSrcDir) {
|
|
86
|
+
scanWithSrcDir(output, config, alteredFiles);
|
|
87
|
+
} else {
|
|
88
|
+
scanWithoutSrcDir(output, config, alteredFiles);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
output.registryDependencies = findComponentFiles(config, currentFiles);
|
|
92
|
+
|
|
93
|
+
addDependencies(output, specificFiles["./package.json"], currentPackageJson);
|
|
94
|
+
|
|
95
|
+
return output;
|
|
96
|
+
}
|
package/dist/src/git.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import ignore from "ignore";
|
|
5
|
+
import { start } from "node:repl";
|
|
6
|
+
|
|
7
|
+
const INITIAL_DIR = "_initial";
|
|
8
|
+
|
|
9
|
+
const EXCLUDE_DIRS = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"fonts",
|
|
13
|
+
"build",
|
|
14
|
+
"public",
|
|
15
|
+
"static",
|
|
16
|
+
".next",
|
|
17
|
+
".git",
|
|
18
|
+
INITIAL_DIR,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const EXCLUDE_FILES = [
|
|
22
|
+
".DS_Store",
|
|
23
|
+
"next-env.d.ts",
|
|
24
|
+
"package-lock.json",
|
|
25
|
+
"yarn.lock",
|
|
26
|
+
"pnpm-lock.yaml",
|
|
27
|
+
"bun.lockb",
|
|
28
|
+
"package.json",
|
|
29
|
+
"tailwind.config.ts",
|
|
30
|
+
"tailwind.config.js",
|
|
31
|
+
"components.json",
|
|
32
|
+
"favicon.ico",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function cloneInitialCommit() {
|
|
36
|
+
deleteInitialDir();
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Get the initial commit hash
|
|
40
|
+
const initialCommit = execSync("git rev-list --max-parents=0 HEAD")
|
|
41
|
+
.toString()
|
|
42
|
+
.trim();
|
|
43
|
+
|
|
44
|
+
// Clone the initial commit quietly
|
|
45
|
+
execSync(`git worktree add -f ${INITIAL_DIR} ${initialCommit}`, {
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("Error cloning initial commit:", error.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deleteInitialDir() {
|
|
55
|
+
if (fs.existsSync(INITIAL_DIR)) {
|
|
56
|
+
fs.rmSync(INITIAL_DIR, { recursive: true });
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
execSync("git worktree prune", { stdio: "ignore" });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error("Error pruning git worktree:", error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function checkIfFileIsChanged(relativeFilePath) {
|
|
67
|
+
const initialFilePath = path.join(INITIAL_DIR, relativeFilePath);
|
|
68
|
+
const fullPath = path.join(process.cwd(), relativeFilePath);
|
|
69
|
+
if (!fs.existsSync(initialFilePath)) {
|
|
70
|
+
return true; // New file
|
|
71
|
+
}
|
|
72
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
73
|
+
const initialContent = fs.readFileSync(initialFilePath, "utf-8");
|
|
74
|
+
return currentContent !== initialContent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function scanForFiles(startDir, checkFile = false) {
|
|
78
|
+
const foundFiles = [];
|
|
79
|
+
|
|
80
|
+
let ignorer = () => false;
|
|
81
|
+
if (fs.existsSync(path.join(startDir, ".gitignore"))) {
|
|
82
|
+
const gitIgnore = ignore().add(
|
|
83
|
+
fs.readFileSync(path.join(startDir, ".gitignore")).toString()
|
|
84
|
+
);
|
|
85
|
+
ignorer = (relativeFilePath) => {
|
|
86
|
+
return gitIgnore.ignores(relativeFilePath);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scanDirectory(dir, relativePath = "") {
|
|
91
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const fullPath = path.join(dir, entry.name);
|
|
95
|
+
const relativeFilePath = path.join(relativePath, entry.name);
|
|
96
|
+
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
if (!EXCLUDE_DIRS.includes(entry.name)) {
|
|
99
|
+
scanDirectory(path.join(dir, entry.name), relativeFilePath);
|
|
100
|
+
}
|
|
101
|
+
} else if (
|
|
102
|
+
!checkFile ||
|
|
103
|
+
(checkFile && checkIfFileIsChanged(relativeFilePath))
|
|
104
|
+
) {
|
|
105
|
+
if (!EXCLUDE_FILES.includes(entry.name) && !ignorer(relativeFilePath)) {
|
|
106
|
+
foundFiles.push({
|
|
107
|
+
path: relativeFilePath,
|
|
108
|
+
content: fs.readFileSync(fullPath, "utf-8"),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
scanDirectory(startDir);
|
|
116
|
+
|
|
117
|
+
return foundFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function scanForAlteredFiles(specificFilesToReturn = []) {
|
|
121
|
+
cloneInitialCommit();
|
|
122
|
+
|
|
123
|
+
const alteredFiles = scanForFiles(process.cwd(), true);
|
|
124
|
+
|
|
125
|
+
const specificFiles = specificFilesToReturn.reduce((out, file) => {
|
|
126
|
+
const fullPath = path.join(process.cwd(), INITIAL_DIR, file);
|
|
127
|
+
out[file] = fs.readFileSync(fullPath, "utf-8");
|
|
128
|
+
return out;
|
|
129
|
+
}, {});
|
|
130
|
+
|
|
131
|
+
deleteInitialDir();
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
alteredFiles,
|
|
135
|
+
specificFiles,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function hasSrcDir(dir) {
|
|
140
|
+
return fs.existsSync(path.join(dir, "src"));
|
|
141
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function fixAlias(alias) {
|
|
2
|
+
return alias.replace("@", ".");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function parseFilePath(wasInSrcDir, config, filePath, content) {
|
|
6
|
+
const out = {
|
|
7
|
+
path: filePath,
|
|
8
|
+
content,
|
|
9
|
+
type: "registry:example",
|
|
10
|
+
target: wasInSrcDir ? filePath : `~/${filePath.replace("./", "")}`,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (filePath.startsWith(fixAlias(config.ui))) {
|
|
14
|
+
out.type = "registry:ui";
|
|
15
|
+
out.target = undefined;
|
|
16
|
+
} else if (filePath.startsWith(fixAlias(config.components))) {
|
|
17
|
+
out.type = "registry:block";
|
|
18
|
+
out.target = undefined;
|
|
19
|
+
} else if (filePath.startsWith(fixAlias(config.hooks))) {
|
|
20
|
+
out.type = "registry:hook";
|
|
21
|
+
out.target = undefined;
|
|
22
|
+
} else if (filePath.startsWith(fixAlias(config.lib))) {
|
|
23
|
+
out.type = "registry:lib";
|
|
24
|
+
out.target = undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (out.type === "registry:example") {
|
|
28
|
+
out.path = filePath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return out;
|
|
32
|
+
}
|