components-differ 1.2.2 → 1.2.4
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 +33 -76
- package/dist/components.js +130 -0
- package/dist/create-diff.js +75 -0
- package/dist/extract-imports.js +90 -0
- package/dist/git.js +117 -0
- package/dist/index.js +136 -0
- package/dist/parse-file-path.js +73 -0
- package/dist/release.js +50 -0
- package/dist/resolve-imports.js +103 -0
- package/package.json +14 -43
- package/src/components.ts +170 -0
- package/src/create-diff.ts +154 -0
- package/src/extract-imports.ts +87 -0
- package/{dist/src/git.mjs → src/git.ts} +45 -27
- package/src/index.ts +173 -0
- package/{dist/src/parse-file-path.mjs → src/parse-file-path.ts} +45 -12
- package/src/release.ts +71 -0
- package/src/resolve-imports.ts +121 -0
- package/tsconfig.json +15 -0
- package/dist/index.mjs +0 -114
- package/dist/src/components.mjs +0 -111
- package/dist/src/create-diff.mjs +0 -96
package/README.md
CHANGED
|
@@ -1,106 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
## components-differ
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI to generate shadcn-compatible registry items (`registry:block`) from your project.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You can use it in two ways:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- 📦 **Full registry support** – Outputs files using the latest Shadcn registry types (`registry:ui`, `registry:block`, `registry:page`, `registry:file`, `registry:theme`, etc.).
|
|
10
|
-
- 🧩 **Automated dependency detection** – Detects new npm dependencies/devDependencies introduced since the initial commit.
|
|
11
|
-
- 🛠️ **Build+publish ready** – Includes a reproducible build step, release helpers, and Vitest coverage.
|
|
7
|
+
- **Folder mode** (default): create a registry item from every file in a folder (default: current directory).
|
|
8
|
+
- **Git mode** (`--git`): create a registry item from files changed since the initial git commit.
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
### Install & build
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
pnpm install components-differ
|
|
17
|
-
# or
|
|
18
|
-
npm install components-differ
|
|
19
|
-
# or
|
|
20
|
-
yarn add components-differ
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
For one-off usage you can run the CLI with `npx`:
|
|
12
|
+
From the `differ` folder:
|
|
24
13
|
|
|
25
14
|
```bash
|
|
26
|
-
|
|
15
|
+
npm install -g components-differ
|
|
27
16
|
```
|
|
28
17
|
|
|
29
|
-
|
|
18
|
+
After building, you can run the CLI with `node` or via the `components-differ` bin (if linked/installed).
|
|
30
19
|
|
|
31
|
-
###
|
|
20
|
+
### Default: folder mode (current directory)
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
2. Install [shadcn/ui](https://ui.shadcn.com/docs/components).
|
|
35
|
-
3. Commit the pristine template so that the first commit represents the baseline.
|
|
36
|
-
|
|
37
|
-
You can use the built-in init helper to automate step 3:
|
|
22
|
+
Run with no arguments to generate a registry item from the **current folder** (and all files it imports):
|
|
38
23
|
|
|
39
24
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
# from your project root – includes whole project and resolves imports
|
|
26
|
+
components-differ > block.json
|
|
42
27
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Add or modify components, hooks, utilities, env files, Tailwind styles, or any other project files.
|
|
28
|
+
# optional name
|
|
29
|
+
components-differ -n my-block > block.json
|
|
30
|
+
```
|
|
48
31
|
|
|
49
|
-
|
|
32
|
+
Or target a specific folder:
|
|
50
33
|
|
|
51
34
|
```bash
|
|
52
|
-
|
|
35
|
+
components-differ --folder src/app/dashboard -n dashboard-block > block.json
|
|
36
|
+
components-differ --folder src/app/dashboard > block.json
|
|
53
37
|
```
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- File list with appropriate registry types and targets.
|
|
58
|
-
- New registry dependencies detected in your component directory.
|
|
59
|
-
- Added npm dependencies and devDependencies.
|
|
39
|
+
### Git mode (changed files only)
|
|
60
40
|
|
|
61
|
-
|
|
41
|
+
Use `--git` when you want a registry item only from files changed since the initial commit:
|
|
62
42
|
|
|
63
43
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## CLI Options
|
|
44
|
+
# initialize a clean git history first
|
|
45
|
+
components-differ --init
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| `--init` | Re-initializes git and creates the baseline commit described above. |
|
|
47
|
+
# after making changes, generate from changed files only
|
|
48
|
+
components-differ --git -n my-block > block.json
|
|
49
|
+
```
|
|
74
50
|
|
|
75
|
-
|
|
51
|
+
### Add the block locally
|
|
76
52
|
|
|
77
53
|
```bash
|
|
78
|
-
|
|
79
|
-
pnpm test # run Vitest suite (snapshot + unit tests)
|
|
80
|
-
pnpm run build # build the distributable into dist/
|
|
81
|
-
pnpm run release # build, test, and publish (requires npm auth + clean git tree)
|
|
54
|
+
npx shadcn add ./block.json
|
|
82
55
|
```
|
|
83
56
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
## How It Works
|
|
87
|
-
|
|
88
|
-
1. **Worktree clone** – `scanForAlteredFiles` creates a temporary git worktree of the initial commit.
|
|
89
|
-
2. **File classification** – `parseFilePath` maps files to Shadcn registry types (page, ui, block, theme, style, lib, hook, file, item) while honoring `components.json` aliases and `src/` directory layouts.
|
|
90
|
-
3. **Manifest parsing** – `readComponentsManifest` reads alias definitions and determines whether the project uses `src/`.
|
|
91
|
-
4. **Dependency diff** – `createDiff` compares the current `package.json` with the baseline to capture dependency changes.
|
|
92
|
-
5. **Output assembly** – Files, registry dependencies, metadata, Tailwind placeholders, and CSS variables are assembled into a registry item schema.
|
|
93
|
-
|
|
94
|
-
## Releasing
|
|
95
|
-
|
|
96
|
-
1. Update `package.json` version (use `pnpm node ./scripts/bump-version.mjs` for a patch bump).
|
|
97
|
-
2. Ensure the working tree is clean.
|
|
98
|
-
3. Run `pnpm run release` to build, test, and publish to npm.
|
|
99
|
-
|
|
100
|
-
## Contributing
|
|
101
|
-
|
|
102
|
-
Pull requests are welcome! Please accompany feature changes with unit tests or snapshot updates as appropriate. If you’re unsure where files are classified or need new registry types, check `src/parse-file-path.mjs` and corresponding tests under `tests/tests`.
|
|
57
|
+
### Options summary
|
|
103
58
|
|
|
104
|
-
|
|
59
|
+
- **`--init`**: initialize a clean git repo and first commit (for use with `--git` later).
|
|
60
|
+
- **`-n, --name <name>`**: registry item name; defaults to current (or folder) name.
|
|
61
|
+
- **`-f, --folder <folder>`**: folder to include (default: current directory). Ignored if `--git` is set.
|
|
62
|
+
- **`--git`**: use git mode (only files changed since initial commit).
|
|
105
63
|
|
|
106
|
-
MIT © Components Host
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
const WHITELISTED_COMPONENTS = [
|
|
4
|
+
"accordion",
|
|
5
|
+
"alert",
|
|
6
|
+
"alert-dialog",
|
|
7
|
+
"aspect-ratio",
|
|
8
|
+
"avatar",
|
|
9
|
+
"badge",
|
|
10
|
+
"breadcrumb",
|
|
11
|
+
"button",
|
|
12
|
+
"button-group",
|
|
13
|
+
"calendar",
|
|
14
|
+
"card",
|
|
15
|
+
"carousel",
|
|
16
|
+
"chart",
|
|
17
|
+
"checkbox",
|
|
18
|
+
"collapsible",
|
|
19
|
+
"combobox",
|
|
20
|
+
"command",
|
|
21
|
+
"context-menu",
|
|
22
|
+
"data-table",
|
|
23
|
+
"date-picker",
|
|
24
|
+
"dialog",
|
|
25
|
+
"drawer",
|
|
26
|
+
"dropdown-menu",
|
|
27
|
+
"empty",
|
|
28
|
+
"field",
|
|
29
|
+
"form",
|
|
30
|
+
"hover-card",
|
|
31
|
+
"input",
|
|
32
|
+
"input-group",
|
|
33
|
+
"input-otp",
|
|
34
|
+
"item",
|
|
35
|
+
"kbd",
|
|
36
|
+
"label",
|
|
37
|
+
"menubar",
|
|
38
|
+
"native-select",
|
|
39
|
+
"navigation-menu",
|
|
40
|
+
"pagination",
|
|
41
|
+
"popover",
|
|
42
|
+
"progress",
|
|
43
|
+
"radio-group",
|
|
44
|
+
"resizable",
|
|
45
|
+
"scroll-area",
|
|
46
|
+
"select",
|
|
47
|
+
"separator",
|
|
48
|
+
"sheet",
|
|
49
|
+
"sidebar",
|
|
50
|
+
"skeleton",
|
|
51
|
+
"slider",
|
|
52
|
+
"sonner",
|
|
53
|
+
"spinner",
|
|
54
|
+
"switch",
|
|
55
|
+
"table",
|
|
56
|
+
"tabs",
|
|
57
|
+
"textarea",
|
|
58
|
+
"toast",
|
|
59
|
+
"toggle",
|
|
60
|
+
"toggle-group",
|
|
61
|
+
"tooltip",
|
|
62
|
+
"typography",
|
|
63
|
+
];
|
|
64
|
+
function findComponentsJson(startDir) {
|
|
65
|
+
let currentDir = startDir;
|
|
66
|
+
while (true) {
|
|
67
|
+
const manifestPath = path.join(currentDir, "components.json");
|
|
68
|
+
if (fs.existsSync(manifestPath)) {
|
|
69
|
+
return manifestPath;
|
|
70
|
+
}
|
|
71
|
+
const parentDir = path.dirname(currentDir);
|
|
72
|
+
if (parentDir === currentDir) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
currentDir = parentDir;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function findComponentFiles(config, originalFiles) {
|
|
80
|
+
const registryDependencies = [];
|
|
81
|
+
const compDir = config.ui.replace("@/", config.isSrcDir ? "src/" : "");
|
|
82
|
+
const registriesConfig = config.registries ?? {};
|
|
83
|
+
const registryNamespaces = Object.keys(registriesConfig);
|
|
84
|
+
const defaultNamespace = registryNamespaces.find((name) => name === "@shadcn") ??
|
|
85
|
+
registryNamespaces[0] ??
|
|
86
|
+
null;
|
|
87
|
+
for (const { path: filePath } of originalFiles) {
|
|
88
|
+
if (filePath.startsWith(compDir)) {
|
|
89
|
+
const fileExtension = path.extname(filePath);
|
|
90
|
+
const fileName = path.basename(filePath, fileExtension);
|
|
91
|
+
if ((fileExtension === ".tsx" || fileExtension === ".jsx") &&
|
|
92
|
+
WHITELISTED_COMPONENTS.includes(fileName)) {
|
|
93
|
+
const baseName = path.basename(filePath, fileExtension);
|
|
94
|
+
const dependencyName = defaultNamespace
|
|
95
|
+
? `${defaultNamespace}/${baseName}`
|
|
96
|
+
: baseName;
|
|
97
|
+
registryDependencies.push(dependencyName);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return registryDependencies;
|
|
102
|
+
}
|
|
103
|
+
export function readComponentsManifest(dir) {
|
|
104
|
+
const manifestPath = findComponentsJson(dir);
|
|
105
|
+
if (!manifestPath) {
|
|
106
|
+
console.error("Components manifest (components.json) not found");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const json = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
110
|
+
return {
|
|
111
|
+
...json.aliases,
|
|
112
|
+
registries: json.registries ?? {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function getAliasedPaths(config) {
|
|
116
|
+
return [
|
|
117
|
+
config.components.replace("@/", ""),
|
|
118
|
+
config.utils.replace("@/", ""),
|
|
119
|
+
config.ui.replace("@/", ""),
|
|
120
|
+
config.lib.replace("@/", ""),
|
|
121
|
+
config.hooks.replace("@/", ""),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
export function isBuiltinComponent(config, filePath) {
|
|
125
|
+
if (filePath.startsWith(config.ui.replace("@/", ""))) {
|
|
126
|
+
const component = path.basename(filePath, path.extname(filePath));
|
|
127
|
+
return WHITELISTED_COMPONENTS.includes(component);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { findComponentFiles, getAliasedPaths, } from "./components.js";
|
|
2
|
+
import { parseFilePath } from "./parse-file-path.js";
|
|
3
|
+
import { extractImportedPackages } from "./extract-imports.js";
|
|
4
|
+
function addFile(output, config, inSrcDir, relativeFilePath, content) {
|
|
5
|
+
output.files.push(parseFilePath(inSrcDir, config, `./${relativeFilePath}`, content));
|
|
6
|
+
}
|
|
7
|
+
function addDependencies(output, _initialPackageContents, currentPackageContents, usedPackages) {
|
|
8
|
+
const currentPackageJson = JSON.parse(currentPackageContents);
|
|
9
|
+
const currentDependencies = currentPackageJson.dependencies ?? {};
|
|
10
|
+
const currentDevDependencies = currentPackageJson.devDependencies ?? {};
|
|
11
|
+
const shadcnNamespaces = new Set(output.registryDependencies
|
|
12
|
+
.map((dep) => dep.split("/")[0])
|
|
13
|
+
.filter((ns) => ns === "@shadcn"));
|
|
14
|
+
const shouldKeepDep = (dep) => {
|
|
15
|
+
if (!usedPackages.has(dep))
|
|
16
|
+
return false;
|
|
17
|
+
if (!shadcnNamespaces.size)
|
|
18
|
+
return true;
|
|
19
|
+
if (dep === "shadcn/ui")
|
|
20
|
+
return false;
|
|
21
|
+
for (const ns of shadcnNamespaces) {
|
|
22
|
+
if (dep === ns || dep === `${ns}/ui`) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
// Only include packages that are actually imported in the registry item files
|
|
29
|
+
// (and exist in package.json so we know dep vs devDep). No other deps.
|
|
30
|
+
output.dependencies = Object.keys(currentDependencies).filter(shouldKeepDep);
|
|
31
|
+
output.devDependencies = Object.keys(currentDevDependencies).filter(shouldKeepDep);
|
|
32
|
+
}
|
|
33
|
+
function scanWithSrcDir(output, config, alteredFiles) {
|
|
34
|
+
for (const { path, content } of alteredFiles) {
|
|
35
|
+
if (path.startsWith("src/")) {
|
|
36
|
+
addFile(output, config, true, path.replace("src/", ""), content);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
addFile(output, config, false, path, content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isInAppDir(filePath) {
|
|
44
|
+
return filePath.startsWith("app/");
|
|
45
|
+
}
|
|
46
|
+
function scanWithoutSrcDir(output, config, alteredFiles) {
|
|
47
|
+
const aliasedPaths = getAliasedPaths(config);
|
|
48
|
+
for (const { path, content } of alteredFiles) {
|
|
49
|
+
const inSrcDir = aliasedPaths.includes(path) || isInAppDir(path);
|
|
50
|
+
addFile(output, config, inSrcDir, path, content);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function createDiff({ name, config, alteredFiles, specificFiles, currentFiles, currentPackageJson, }) {
|
|
54
|
+
const output = {
|
|
55
|
+
name,
|
|
56
|
+
type: "registry:block",
|
|
57
|
+
dependencies: [],
|
|
58
|
+
devDependencies: [],
|
|
59
|
+
registryDependencies: [],
|
|
60
|
+
files: [],
|
|
61
|
+
tailwind: {},
|
|
62
|
+
cssVars: {},
|
|
63
|
+
meta: {},
|
|
64
|
+
};
|
|
65
|
+
if (config.isSrcDir) {
|
|
66
|
+
scanWithSrcDir(output, config, alteredFiles);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
scanWithoutSrcDir(output, config, alteredFiles);
|
|
70
|
+
}
|
|
71
|
+
output.registryDependencies = findComponentFiles(config, currentFiles);
|
|
72
|
+
const usedPackages = extractImportedPackages(alteredFiles);
|
|
73
|
+
addDependencies(output, specificFiles["./package.json"], currentPackageJson, usedPackages);
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract npm package names from import/require statements in source code.
|
|
3
|
+
* Used to include only dependencies that are actually used by the registry item files.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Returns the root package name for a specifier. Handles scoped packages and subpaths.
|
|
7
|
+
* - "react" -> "react"
|
|
8
|
+
* - "lodash/merge" -> "lodash"
|
|
9
|
+
* - "@scope/pkg" -> "@scope/pkg"
|
|
10
|
+
* - "@scope/pkg/sub" -> "@scope/pkg"
|
|
11
|
+
*/
|
|
12
|
+
function specifierToPackageName(specifier) {
|
|
13
|
+
const s = specifier.trim();
|
|
14
|
+
if (!s)
|
|
15
|
+
return null;
|
|
16
|
+
// Node / bundler builtins
|
|
17
|
+
if (s.startsWith("node:") || s === "node")
|
|
18
|
+
return null;
|
|
19
|
+
// Relative imports – not packages
|
|
20
|
+
if (s.startsWith(".") || s.startsWith("/"))
|
|
21
|
+
return null;
|
|
22
|
+
// Common path aliases (not package names)
|
|
23
|
+
if (s.startsWith("@/") || s.startsWith("~/") || s.startsWith("#"))
|
|
24
|
+
return null;
|
|
25
|
+
// Scoped package: @scope/pkg or @scope/pkg/subpath
|
|
26
|
+
if (s.startsWith("@")) {
|
|
27
|
+
const firstSlash = s.indexOf("/");
|
|
28
|
+
if (firstSlash === -1)
|
|
29
|
+
return s;
|
|
30
|
+
const secondSlash = s.indexOf("/", firstSlash + 1);
|
|
31
|
+
return secondSlash === -1 ? s : s.slice(0, secondSlash);
|
|
32
|
+
}
|
|
33
|
+
// Normal package or subpath
|
|
34
|
+
const slash = s.indexOf("/");
|
|
35
|
+
return slash === -1 ? s : s.slice(0, slash);
|
|
36
|
+
}
|
|
37
|
+
const RE_IMPORT = /(?:import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?|import\s*)['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
38
|
+
/**
|
|
39
|
+
* Collect all package names that are imported or required in the given file content.
|
|
40
|
+
*/
|
|
41
|
+
function extractFromContent(content) {
|
|
42
|
+
const packages = new Set();
|
|
43
|
+
let m;
|
|
44
|
+
RE_IMPORT.lastIndex = 0;
|
|
45
|
+
while ((m = RE_IMPORT.exec(content)) !== null) {
|
|
46
|
+
const specifier = (m[1] ?? m[2] ?? m[3]) ?? "";
|
|
47
|
+
const pkg = specifierToPackageName(specifier);
|
|
48
|
+
if (pkg)
|
|
49
|
+
packages.add(pkg);
|
|
50
|
+
}
|
|
51
|
+
return packages;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Collect all package names imported by any of the given files (by their content).
|
|
55
|
+
*/
|
|
56
|
+
export function extractImportedPackages(files) {
|
|
57
|
+
const all = new Set();
|
|
58
|
+
for (const { content } of files) {
|
|
59
|
+
for (const pkg of extractFromContent(content)) {
|
|
60
|
+
all.add(pkg);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return all;
|
|
64
|
+
}
|
|
65
|
+
/** True if the import specifier refers to a project file (relative or path alias), not npm. */
|
|
66
|
+
export function isPathSpecifier(specifier) {
|
|
67
|
+
const s = specifier.trim();
|
|
68
|
+
if (!s || s.startsWith("node:"))
|
|
69
|
+
return false;
|
|
70
|
+
if (s.startsWith(".") || s.startsWith("/"))
|
|
71
|
+
return true;
|
|
72
|
+
if (s.startsWith("@/") || s.startsWith("~/") || s.startsWith("#"))
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract all import/require specifiers that are project paths (relative or alias)
|
|
78
|
+
* from file content. Used to pull in imported files into the registry.
|
|
79
|
+
*/
|
|
80
|
+
export function extractPathSpecifiers(content) {
|
|
81
|
+
const out = [];
|
|
82
|
+
let m;
|
|
83
|
+
RE_IMPORT.lastIndex = 0;
|
|
84
|
+
while ((m = RE_IMPORT.exec(content)) !== null) {
|
|
85
|
+
const specifier = (m[1] ?? m[2] ?? m[3]) ?? "";
|
|
86
|
+
if (isPathSpecifier(specifier))
|
|
87
|
+
out.push(specifier);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
const INITIAL_DIR = "_initial";
|
|
6
|
+
const EXCLUDE_DIRS = [
|
|
7
|
+
"node_modules",
|
|
8
|
+
"dist",
|
|
9
|
+
"fonts",
|
|
10
|
+
"build",
|
|
11
|
+
"public",
|
|
12
|
+
"static",
|
|
13
|
+
".next",
|
|
14
|
+
".git",
|
|
15
|
+
INITIAL_DIR,
|
|
16
|
+
];
|
|
17
|
+
const EXCLUDE_FILES = [
|
|
18
|
+
".DS_Store",
|
|
19
|
+
"next-env.d.ts",
|
|
20
|
+
"package-lock.json",
|
|
21
|
+
"yarn.lock",
|
|
22
|
+
"pnpm-lock.yaml",
|
|
23
|
+
"bun.lockb",
|
|
24
|
+
"package.json",
|
|
25
|
+
"tailwind.config.ts",
|
|
26
|
+
"tailwind.config.js",
|
|
27
|
+
"components.json",
|
|
28
|
+
"favicon.ico",
|
|
29
|
+
];
|
|
30
|
+
function cloneInitialCommit() {
|
|
31
|
+
deleteInitialDir();
|
|
32
|
+
try {
|
|
33
|
+
const initialCommit = execSync("git rev-list --max-parents=0 HEAD")
|
|
34
|
+
.toString()
|
|
35
|
+
.trim();
|
|
36
|
+
execSync(`git worktree add -f ${INITIAL_DIR} ${initialCommit}`, {
|
|
37
|
+
stdio: "ignore",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error("Error cloning initial commit:", error.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function deleteInitialDir() {
|
|
46
|
+
if (fs.existsSync(INITIAL_DIR)) {
|
|
47
|
+
fs.rmSync(INITIAL_DIR, { recursive: true, force: true });
|
|
48
|
+
try {
|
|
49
|
+
execSync("git worktree prune", { stdio: "ignore" });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error("Error pruning git worktree:", error.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function checkIfFileIsChanged(relativeFilePath) {
|
|
57
|
+
const initialFilePath = path.join(INITIAL_DIR, relativeFilePath);
|
|
58
|
+
const fullPath = path.join(process.cwd(), relativeFilePath);
|
|
59
|
+
if (!fs.existsSync(initialFilePath)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
63
|
+
const initialContent = fs.readFileSync(initialFilePath, "utf-8");
|
|
64
|
+
return currentContent !== initialContent;
|
|
65
|
+
}
|
|
66
|
+
export function scanForFiles(startDir, checkFile = false) {
|
|
67
|
+
const foundFiles = [];
|
|
68
|
+
let ignorer = () => false;
|
|
69
|
+
const gitignorePath = path.join(startDir, ".gitignore");
|
|
70
|
+
if (fs.existsSync(gitignorePath)) {
|
|
71
|
+
const gitIgnore = ignore().add(fs.readFileSync(gitignorePath).toString());
|
|
72
|
+
ignorer = (relativeFilePath) => gitIgnore.ignores(relativeFilePath);
|
|
73
|
+
}
|
|
74
|
+
function scanDirectory(dir, relativePath = "") {
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = path.join(dir, entry.name);
|
|
78
|
+
const relativeFilePath = path.join(relativePath, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (!EXCLUDE_DIRS.includes(entry.name)) {
|
|
81
|
+
scanDirectory(path.join(dir, entry.name), relativeFilePath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (!checkFile ||
|
|
85
|
+
(checkFile && checkIfFileIsChanged(relativeFilePath))) {
|
|
86
|
+
if (!EXCLUDE_FILES.includes(entry.name) &&
|
|
87
|
+
!ignorer(relativeFilePath)) {
|
|
88
|
+
foundFiles.push({
|
|
89
|
+
path: relativeFilePath,
|
|
90
|
+
content: fs.readFileSync(fullPath, "utf-8"),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
scanDirectory(startDir);
|
|
97
|
+
return foundFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
98
|
+
}
|
|
99
|
+
export function scanForAlteredFiles(specificFilesToReturn = []) {
|
|
100
|
+
cloneInitialCommit();
|
|
101
|
+
const alteredFiles = scanForFiles(process.cwd(), true);
|
|
102
|
+
const specificFiles = specificFilesToReturn.reduce((out, file) => {
|
|
103
|
+
const fullPath = path.join(process.cwd(), INITIAL_DIR, file);
|
|
104
|
+
out[file] = fs.existsSync(fullPath)
|
|
105
|
+
? fs.readFileSync(fullPath, "utf-8")
|
|
106
|
+
: "";
|
|
107
|
+
return out;
|
|
108
|
+
}, {});
|
|
109
|
+
deleteInitialDir();
|
|
110
|
+
return {
|
|
111
|
+
alteredFiles,
|
|
112
|
+
specificFiles,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function hasSrcDir(dir) {
|
|
116
|
+
return fs.existsSync(path.join(dir, "src"));
|
|
117
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { program } from "commander";
|
|
6
|
+
import { scanForAlteredFiles, scanForFiles, hasSrcDir, } from "./git.js";
|
|
7
|
+
import { readComponentsManifest } from "./components.js";
|
|
8
|
+
import { createDiff } from "./create-diff.js";
|
|
9
|
+
import { expandIncludedFiles } from "./resolve-imports.js";
|
|
10
|
+
function runCommand(command) {
|
|
11
|
+
try {
|
|
12
|
+
execSync(command, { stdio: "inherit" });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.error(`Failed to execute command: ${command}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function ensureGitignore() {
|
|
20
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
21
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
22
|
+
const content = `
|
|
23
|
+
/node_modules
|
|
24
|
+
/.pnp
|
|
25
|
+
.pnp.*
|
|
26
|
+
.yarn/*
|
|
27
|
+
!.yarn/patches
|
|
28
|
+
!.yarn/plugins
|
|
29
|
+
!.yarn/releases
|
|
30
|
+
!.yarn/versions
|
|
31
|
+
|
|
32
|
+
# testing
|
|
33
|
+
/coverage
|
|
34
|
+
|
|
35
|
+
# next.js
|
|
36
|
+
/.next/
|
|
37
|
+
/out/
|
|
38
|
+
|
|
39
|
+
# production
|
|
40
|
+
/build
|
|
41
|
+
|
|
42
|
+
# misc
|
|
43
|
+
.DS_Store
|
|
44
|
+
*.pem
|
|
45
|
+
|
|
46
|
+
# debug
|
|
47
|
+
npm-debug.log*
|
|
48
|
+
yarn-debug.log*
|
|
49
|
+
yarn-error.log*
|
|
50
|
+
|
|
51
|
+
# env files (can opt-in for committing if needed)
|
|
52
|
+
.env*
|
|
53
|
+
|
|
54
|
+
# vercel
|
|
55
|
+
.vercel
|
|
56
|
+
|
|
57
|
+
# typescript
|
|
58
|
+
*.tsbuildinfo
|
|
59
|
+
next-env.d.ts
|
|
60
|
+
`;
|
|
61
|
+
fs.writeFileSync(gitignorePath, content, "utf8");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function main() {
|
|
65
|
+
program
|
|
66
|
+
.option("-n, --name <name>")
|
|
67
|
+
.option("--init")
|
|
68
|
+
.option("-f, --folder <folder>", "folder to include (default: current directory)")
|
|
69
|
+
.option("--git", "use git mode: only files changed since initial commit");
|
|
70
|
+
program.parse();
|
|
71
|
+
const options = program.opts();
|
|
72
|
+
if (options.init) {
|
|
73
|
+
// Initialize a clean git repository for the current component
|
|
74
|
+
if (process.platform === "win32") {
|
|
75
|
+
runCommand('rmdir /s /q .git && git init && git add . && git commit -m "Initial commit"');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
runCommand('rm -fr .git && git init && git add . && git commit -m "Initial commit"');
|
|
79
|
+
}
|
|
80
|
+
ensureGitignore();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const useFolderMode = !options.git;
|
|
84
|
+
const folderOpt = options.folder ?? (useFolderMode ? "." : undefined);
|
|
85
|
+
const baseName = folderOpt != null
|
|
86
|
+
? path.basename(path.resolve(process.cwd(), folderOpt))
|
|
87
|
+
: path.basename(process.cwd());
|
|
88
|
+
const name = options.name || baseName;
|
|
89
|
+
let alteredFiles;
|
|
90
|
+
let specificFiles;
|
|
91
|
+
if (useFolderMode && folderOpt != null) {
|
|
92
|
+
const folderPath = path.resolve(process.cwd(), folderOpt);
|
|
93
|
+
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
|
|
94
|
+
console.error(`Folder path is not a directory: ${folderOpt}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const folderPrefix = path
|
|
98
|
+
.relative(process.cwd(), folderPath)
|
|
99
|
+
.replace(/\\/g, "/")
|
|
100
|
+
.replace(/^\.\//, "");
|
|
101
|
+
const allFiles = scanForFiles(process.cwd());
|
|
102
|
+
alteredFiles = allFiles.filter(({ path: filePath }) => {
|
|
103
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
104
|
+
return (normalized === folderPrefix ||
|
|
105
|
+
normalized.startsWith(`${folderPrefix}/`));
|
|
106
|
+
});
|
|
107
|
+
specificFiles = {
|
|
108
|
+
"./package.json": "{}",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const result = scanForAlteredFiles(["./package.json"]);
|
|
113
|
+
alteredFiles = result.alteredFiles;
|
|
114
|
+
specificFiles = result.specificFiles;
|
|
115
|
+
}
|
|
116
|
+
const currentFiles = scanForFiles(process.cwd());
|
|
117
|
+
const currentPackageJson = fs.readFileSync("./package.json", "utf-8");
|
|
118
|
+
const config = readComponentsManifest(process.cwd());
|
|
119
|
+
config.isSrcDir = hasSrcDir(process.cwd());
|
|
120
|
+
// Recursively add any project file imported by the included files so the
|
|
121
|
+
// registry item is self-contained (no "component not found").
|
|
122
|
+
alteredFiles = expandIncludedFiles(alteredFiles, currentFiles, config);
|
|
123
|
+
const output = createDiff({
|
|
124
|
+
name,
|
|
125
|
+
config,
|
|
126
|
+
alteredFiles,
|
|
127
|
+
currentFiles,
|
|
128
|
+
specificFiles,
|
|
129
|
+
currentPackageJson,
|
|
130
|
+
});
|
|
131
|
+
// Emit a complete registry:block JSON object suitable for local file support
|
|
132
|
+
// (e.g. `npx shadcn add ./block.json`)
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.log(JSON.stringify(output, null, 2));
|
|
135
|
+
}
|
|
136
|
+
main();
|