components-differ 1.2.1 → 1.2.3
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 +48 -16
- package/dist/components.js +130 -0
- package/dist/create-diff.js +77 -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 +156 -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} +47 -14
- 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,31 +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
|
-
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`
|
|
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
|
-
|
|
12
|
+
From the `differ` folder:
|
|
16
13
|
|
|
17
14
|
```bash
|
|
18
|
-
|
|
15
|
+
npm install -g components-differ
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
After building, you can run the CLI with `node` or via the `components-differ` bin (if linked/installed).
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
### Default: folder mode (current directory)
|
|
21
|
+
|
|
22
|
+
Run with no arguments to generate a registry item from the **current folder** (and all files it imports):
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
|
-
|
|
25
|
+
# from your project root – includes whole project and resolves imports
|
|
26
|
+
components-differ > block.json
|
|
27
|
+
|
|
28
|
+
# optional name
|
|
29
|
+
components-differ -n my-block > block.json
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Or target a specific folder:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
components-differ --folder src/app/dashboard -n dashboard-block > block.json
|
|
36
|
+
components-differ --folder src/app/dashboard > block.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Git mode (changed files only)
|
|
40
|
+
|
|
41
|
+
Use `--git` when you want a registry item only from files changed since the initial commit:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# initialize a clean git history first
|
|
45
|
+
components-differ --init
|
|
46
|
+
|
|
47
|
+
# after making changes, generate from changed files only
|
|
48
|
+
components-differ --git -n my-block > block.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Add the block locally
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx shadcn add ./block.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Options summary
|
|
58
|
+
|
|
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).
|
|
30
63
|
|
|
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.
|
|
@@ -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,77 @@
|
|
|
1
|
+
import { findComponentFiles, getAliasedPaths, isBuiltinComponent, } 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
|
+
if (!isBuiltinComponent(config, relativeFilePath)) {
|
|
6
|
+
output.files.push(parseFilePath(inSrcDir, config, `./${relativeFilePath}`, content));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function addDependencies(output, _initialPackageContents, currentPackageContents, usedPackages) {
|
|
10
|
+
const currentPackageJson = JSON.parse(currentPackageContents);
|
|
11
|
+
const currentDependencies = currentPackageJson.dependencies ?? {};
|
|
12
|
+
const currentDevDependencies = currentPackageJson.devDependencies ?? {};
|
|
13
|
+
const shadcnNamespaces = new Set(output.registryDependencies
|
|
14
|
+
.map((dep) => dep.split("/")[0])
|
|
15
|
+
.filter((ns) => ns === "@shadcn"));
|
|
16
|
+
const shouldKeepDep = (dep) => {
|
|
17
|
+
if (!usedPackages.has(dep))
|
|
18
|
+
return false;
|
|
19
|
+
if (!shadcnNamespaces.size)
|
|
20
|
+
return true;
|
|
21
|
+
if (dep === "shadcn/ui")
|
|
22
|
+
return false;
|
|
23
|
+
for (const ns of shadcnNamespaces) {
|
|
24
|
+
if (dep === ns || dep === `${ns}/ui`) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
// Only include packages that are actually imported in the registry item files
|
|
31
|
+
// (and exist in package.json so we know dep vs devDep). No other deps.
|
|
32
|
+
output.dependencies = Object.keys(currentDependencies).filter(shouldKeepDep);
|
|
33
|
+
output.devDependencies = Object.keys(currentDevDependencies).filter(shouldKeepDep);
|
|
34
|
+
}
|
|
35
|
+
function scanWithSrcDir(output, config, alteredFiles) {
|
|
36
|
+
for (const { path, content } of alteredFiles) {
|
|
37
|
+
if (path.startsWith("src/")) {
|
|
38
|
+
addFile(output, config, true, path.replace("src/", ""), content);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
addFile(output, config, false, path, content);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isInAppDir(filePath) {
|
|
46
|
+
return filePath.startsWith("app/");
|
|
47
|
+
}
|
|
48
|
+
function scanWithoutSrcDir(output, config, alteredFiles) {
|
|
49
|
+
const aliasedPaths = getAliasedPaths(config);
|
|
50
|
+
for (const { path, content } of alteredFiles) {
|
|
51
|
+
const inSrcDir = aliasedPaths.includes(path) || isInAppDir(path);
|
|
52
|
+
addFile(output, config, inSrcDir, path, content);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function createDiff({ name, config, alteredFiles, specificFiles, currentFiles, currentPackageJson, }) {
|
|
56
|
+
const output = {
|
|
57
|
+
name,
|
|
58
|
+
type: "registry:block",
|
|
59
|
+
dependencies: [],
|
|
60
|
+
devDependencies: [],
|
|
61
|
+
registryDependencies: [],
|
|
62
|
+
files: [],
|
|
63
|
+
tailwind: {},
|
|
64
|
+
cssVars: {},
|
|
65
|
+
meta: {},
|
|
66
|
+
};
|
|
67
|
+
if (config.isSrcDir) {
|
|
68
|
+
scanWithSrcDir(output, config, alteredFiles);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
scanWithoutSrcDir(output, config, alteredFiles);
|
|
72
|
+
}
|
|
73
|
+
output.registryDependencies = findComponentFiles(config, currentFiles);
|
|
74
|
+
const usedPackages = extractImportedPackages(alteredFiles);
|
|
75
|
+
addDependencies(output, specificFiles["./package.json"], currentPackageJson, usedPackages);
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
@@ -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();
|