components-differ 1.0.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 ADDED
@@ -0,0 +1,31 @@
1
+ # ShadCN Project Differ
2
+
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
+
5
+ # Steps
6
+
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`
12
+
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.
14
+
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.
16
+
17
+ ```bash
18
+ npx shadcn@latest init http://your-json-output-url
19
+ ```
20
+
21
+ You can use the `--src-dir` flag if you want to use the `src` directory in your project.
22
+
23
+ Or you can add the JSON output to an existing project:
24
+
25
+ ```bash
26
+ npx shadcn@latest add http://your-json-output-url
27
+ ```
28
+
29
+ # Why is this useful?
30
+
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/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 dockerignorePath = path.join(process.cwd(), ".dockerignore");
29
+ if (!fs.existsSync(dockerignorePath)) {
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(dockerignorePath, content, "utf8");
71
+ console.log(".dockerignore file created with default rules.");
72
+ } else {
73
+ console.log(".dockerignore 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();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "components-differ",
3
+ "version": "1.0.0",
4
+ "description": "Simple CLI to create Shadcn components from project",
5
+ "main": "index.mjs",
6
+ "bin": "index.mjs",
7
+ "directories": {
8
+ "test": "tests"
9
+ },
10
+ "dependencies": {
11
+ "commander": "^12.1.0",
12
+ "ignore": "^6.0.2",
13
+ "components-differ": "^1.0.3"
14
+ },
15
+ "devDependencies": {
16
+ "vitest": "^2.1.1"
17
+ },
18
+ "scripts": {
19
+ "test": "npx vitest"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/componentshost/components-differ.git"
24
+ },
25
+ "keywords": [
26
+ "components",
27
+ "shadcn",
28
+ "differ",
29
+ "react",
30
+ "vite"
31
+ ],
32
+ "author": "Izet Molla",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/componentshost/components-differ/issues"
36
+ },
37
+ "homepage": "https://github.com/componentshost/components-differ#readme"
38
+ }
@@ -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,91 @@
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
+ output.dependencies = Object.keys(
25
+ currentPackageJson.dependencies || {}
26
+ ).filter((dep) => !initialPackageJson.dependencies.hasOwnProperty(dep));
27
+ output.devDependencies = Object.keys(
28
+ currentPackageJson.devDependencies || {}
29
+ ).filter((dep) => !initialPackageJson.devDependencies.hasOwnProperty(dep));
30
+ }
31
+
32
+ function scanWithSrcDir(output, config, alteredFiles) {
33
+ for (const { path, content } of alteredFiles) {
34
+ if (path.startsWith("src/")) {
35
+ addFile(output, config, true, path.replace("src/", ""), content);
36
+ } else {
37
+ addFile(output, config, false, path, content);
38
+ }
39
+ }
40
+ }
41
+
42
+ function isInAppDir(path) {
43
+ return path.startsWith("app/");
44
+ }
45
+
46
+ function scanWithoutSrcDir(output, config, alteredFiles) {
47
+ const aliasedPaths = getAliasedPaths(config);
48
+
49
+ for (const { path, content } of alteredFiles) {
50
+ addFile(
51
+ output,
52
+ config,
53
+ aliasedPaths.includes(path) || isInAppDir(path),
54
+ path,
55
+ content
56
+ );
57
+ }
58
+ }
59
+
60
+ export function createDiff({
61
+ name,
62
+ config,
63
+ alteredFiles,
64
+ specificFiles,
65
+ currentFiles,
66
+ currentPackageJson,
67
+ }) {
68
+ const output = {
69
+ name,
70
+ type: "registry:block",
71
+ dependencies: [],
72
+ devDependencies: [],
73
+ registryDependencies: [],
74
+ files: [],
75
+ tailwind: {},
76
+ cssVars: {},
77
+ meta: {},
78
+ };
79
+
80
+ if (config.isSrcDir) {
81
+ scanWithSrcDir(output, config, alteredFiles);
82
+ } else {
83
+ scanWithoutSrcDir(output, config, alteredFiles);
84
+ }
85
+
86
+ output.registryDependencies = findComponentFiles(config, currentFiles);
87
+
88
+ addDependencies(output, specificFiles["./package.json"], currentPackageJson);
89
+
90
+ return output;
91
+ }
package/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;
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
+ }
@@ -0,0 +1 @@
1
+ page;
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ hook;
@@ -0,0 +1 @@
1
+ lib;
@@ -0,0 +1 @@
1
+ non - src - sub;
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "template",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.0",
13
+ "@radix-ui/react-collapsible": "^1.1.0",
14
+ "@radix-ui/react-dialog": "^1.1.1",
15
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
16
+ "@radix-ui/react-icons": "^1.3.0",
17
+ "@radix-ui/react-popover": "^1.1.1",
18
+ "@radix-ui/react-progress": "^1.1.0",
19
+ "@radix-ui/react-separator": "^1.1.0",
20
+ "@radix-ui/react-slot": "^1.1.0",
21
+ "@workos-inc/authkit-nextjs": "^0.10.1",
22
+ "class-variance-authority": "^0.7.0",
23
+ "clsx": "^2.1.1",
24
+ "lucide-react": "^0.441.0",
25
+ "next": "14.2.11",
26
+ "react": "^18",
27
+ "react-dom": "^18",
28
+ "tailwind-merge": "^2.5.2",
29
+ "tailwindcss-animate": "^1.0.7",
30
+ "vaul": "^0.9.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20",
34
+ "@types/react": "^18",
35
+ "@types/react-dom": "^18",
36
+ "eslint": "^8",
37
+ "eslint-config-next": "14.2.11",
38
+ "postcss": "^8",
39
+ "tailwindcss": "^3.4.1",
40
+ "typescript": "^5"
41
+ }
42
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ non - src - sub;
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "template",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.0",
13
+ "@radix-ui/react-collapsible": "^1.1.0",
14
+ "@radix-ui/react-dialog": "^1.1.1",
15
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
16
+ "@radix-ui/react-icons": "^1.3.0",
17
+ "@radix-ui/react-popover": "^1.1.1",
18
+ "@radix-ui/react-progress": "^1.1.0",
19
+ "@radix-ui/react-separator": "^1.1.0",
20
+ "@radix-ui/react-slot": "^1.1.0",
21
+ "@workos-inc/authkit-nextjs": "^0.10.1",
22
+ "class-variance-authority": "^0.7.0",
23
+ "clsx": "^2.1.1",
24
+ "lucide-react": "^0.441.0",
25
+ "next": "14.2.11",
26
+ "react": "^18",
27
+ "react-dom": "^18",
28
+ "tailwind-merge": "^2.5.2",
29
+ "tailwindcss-animate": "^1.0.7",
30
+ "vaul": "^0.9.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20",
34
+ "@types/react": "^18",
35
+ "@types/react-dom": "^18",
36
+ "eslint": "^8",
37
+ "eslint-config-next": "14.2.11",
38
+ "postcss": "^8",
39
+ "tailwindcss": "^3.4.1",
40
+ "typescript": "^5"
41
+ }
42
+ }
@@ -0,0 +1 @@
1
+ page;
@@ -0,0 +1 @@
1
+ hook;
@@ -0,0 +1 @@
1
+ lib;
@@ -0,0 +1,129 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`fixture tests > should diff a non-src directory project 1`] = `
4
+ {
5
+ "cssVars": {},
6
+ "dependencies": [],
7
+ "devDependencies": [],
8
+ "files": [
9
+ {
10
+ "content": "example env;",
11
+ "path": "./.env.example",
12
+ "target": "~/.env.example",
13
+ "type": "registry:example",
14
+ },
15
+ {
16
+ "content": "page;
17
+ ",
18
+ "path": "./app/page.tsx",
19
+ "target": "./app/page.tsx",
20
+ "type": "registry:example",
21
+ },
22
+ {
23
+ "content": "comp;
24
+ ",
25
+ "path": "./components/comp.tsx",
26
+ "target": undefined,
27
+ "type": "registry:block",
28
+ },
29
+ {
30
+ "content": "new-ui-comp;",
31
+ "path": "./components/ui/new-ui-comp.tsx",
32
+ "target": undefined,
33
+ "type": "registry:ui",
34
+ },
35
+ {
36
+ "content": "hook;
37
+ ",
38
+ "path": "./hooks/hook.ts",
39
+ "target": undefined,
40
+ "type": "registry:hook",
41
+ },
42
+ {
43
+ "content": "lib;
44
+ ",
45
+ "path": "./lib/lib.ts",
46
+ "target": undefined,
47
+ "type": "registry:lib",
48
+ },
49
+ {
50
+ "content": "non - src - sub;
51
+ ",
52
+ "path": "./non-src-sub/test.ts",
53
+ "target": "~/non-src-sub/test.ts",
54
+ "type": "registry:example",
55
+ },
56
+ ],
57
+ "meta": {},
58
+ "name": "example",
59
+ "registryDependencies": [
60
+ "select",
61
+ ],
62
+ "tailwind": {},
63
+ "type": "registry:block",
64
+ }
65
+ `;
66
+
67
+ exports[`fixture tests > should diff a src directory project 1`] = `
68
+ {
69
+ "cssVars": {},
70
+ "dependencies": [],
71
+ "devDependencies": [],
72
+ "files": [
73
+ {
74
+ "content": "example env;",
75
+ "path": "./.env.example",
76
+ "target": "~/.env.example",
77
+ "type": "registry:example",
78
+ },
79
+ {
80
+ "content": "non - src - sub;
81
+ ",
82
+ "path": "./non-src-sub/test.ts",
83
+ "target": "~/non-src-sub/test.ts",
84
+ "type": "registry:example",
85
+ },
86
+ {
87
+ "content": "page;
88
+ ",
89
+ "path": "./app/page.tsx",
90
+ "target": "./app/page.tsx",
91
+ "type": "registry:example",
92
+ },
93
+ {
94
+ "content": "comp;
95
+ ",
96
+ "path": "./components/comp.tsx",
97
+ "target": undefined,
98
+ "type": "registry:block",
99
+ },
100
+ {
101
+ "content": "new-ui-comp;",
102
+ "path": "./components/ui/new-ui-comp.tsx",
103
+ "target": undefined,
104
+ "type": "registry:ui",
105
+ },
106
+ {
107
+ "content": "hook;
108
+ ",
109
+ "path": "./hooks/hook.ts",
110
+ "target": undefined,
111
+ "type": "registry:hook",
112
+ },
113
+ {
114
+ "content": "lib;
115
+ ",
116
+ "path": "./lib/lib.ts",
117
+ "target": undefined,
118
+ "type": "registry:lib",
119
+ },
120
+ ],
121
+ "meta": {},
122
+ "name": "example",
123
+ "registryDependencies": [
124
+ "select",
125
+ ],
126
+ "tailwind": {},
127
+ "type": "registry:block",
128
+ }
129
+ `;
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { parseFilePath } from "../../src/parse-file-path.mjs";
3
+
4
+ const CONFIG_IN_SRC = {
5
+ components: "@/components",
6
+ utils: "@/lib/utils",
7
+ ui: "@/components/ui",
8
+ lib: "@/lib",
9
+ hooks: "@/hooks",
10
+ isSrcDir: true,
11
+ };
12
+ const CONFIG = {
13
+ ...CONFIG_IN_SRC,
14
+ isSrcDir: false,
15
+ };
16
+
17
+ const trim = ({ path, type, target }) => ({
18
+ path,
19
+ type,
20
+ target,
21
+ });
22
+
23
+ describe("parseFilePath", () => {
24
+ test("should handle a new component in a src directory", () => {
25
+ expect(
26
+ trim(
27
+ parseFilePath(
28
+ true,
29
+ CONFIG_IN_SRC,
30
+ "./components/spinning-credit-card.tsx"
31
+ )
32
+ )
33
+ ).toEqual({
34
+ type: "registry:block",
35
+ path: "./components/spinning-credit-card.tsx",
36
+ });
37
+ });
38
+
39
+ test("should handle a new component", () => {
40
+ expect(
41
+ trim(
42
+ parseFilePath(false, CONFIG, "./components/spinning-credit-card.tsx")
43
+ )
44
+ ).toEqual({
45
+ type: "registry:block",
46
+ path: "./components/spinning-credit-card.tsx",
47
+ });
48
+ });
49
+
50
+ test("should handle a new page in a src directory", () => {
51
+ expect(trim(parseFilePath(true, CONFIG_IN_SRC, "./app/page.tsx"))).toEqual({
52
+ type: "registry:example",
53
+ target: "./app/page.tsx",
54
+ path: "./app/page.tsx",
55
+ });
56
+ });
57
+
58
+ test("should handle a new page in an app directory", () => {
59
+ expect(trim(parseFilePath(true, CONFIG, "./app/page.tsx"))).toEqual({
60
+ type: "registry:example",
61
+ target: "./app/page.tsx",
62
+ path: "./app/page.tsx",
63
+ });
64
+ });
65
+
66
+ test("should handle an environment file with a src directory", () => {
67
+ expect(trim(parseFilePath(false, CONFIG_IN_SRC, "./.env.example"))).toEqual(
68
+ {
69
+ type: "registry:example",
70
+ target: "~/.env.example",
71
+ path: "./.env.example",
72
+ }
73
+ );
74
+ });
75
+
76
+ test("should handle an environment file", () => {
77
+ expect(trim(parseFilePath(false, CONFIG, "./.env.example"))).toEqual({
78
+ type: "registry:example",
79
+ target: "~/.env.example",
80
+ path: "./.env.example",
81
+ });
82
+ });
83
+
84
+ test("should handle a new hook", () => {
85
+ expect(
86
+ trim(
87
+ parseFilePath(
88
+ false,
89
+ CONFIG_IN_SRC,
90
+ "./hooks/use-spinning-credit-card.ts"
91
+ )
92
+ )
93
+ ).toEqual({
94
+ type: "registry:hook",
95
+ path: "./hooks/use-spinning-credit-card.ts",
96
+ });
97
+ });
98
+
99
+ test("should handle a new library file", () => {
100
+ expect(trim(parseFilePath(true, CONFIG_IN_SRC, "./lib/utils.ts"))).toEqual({
101
+ type: "registry:lib",
102
+ path: "./lib/utils.ts",
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { scanForFiles, hasSrcDir } from "../../src/git.mjs";
6
+ import { readComponentsManifest } from "../../src/components.mjs";
7
+ import { createDiff } from "../../src/create-diff.mjs";
8
+
9
+ const EXPECTED_FILES = [
10
+ {
11
+ path: "./.env.example",
12
+ content: "example env;",
13
+ type: "registry:example",
14
+ target: "~/.env.example",
15
+ },
16
+ {
17
+ path: "./non-src-sub/test.ts",
18
+ content: "non - src - sub;\n",
19
+ type: "registry:example",
20
+ target: "~/non-src-sub/test.ts",
21
+ },
22
+ {
23
+ path: "./app/page.tsx",
24
+ content: "page;\n",
25
+ type: "registry:example",
26
+ target: "./app/page.tsx",
27
+ },
28
+ {
29
+ path: "./components/comp.tsx",
30
+ content: "comp;\n",
31
+ type: "registry:block",
32
+ target: undefined,
33
+ },
34
+ {
35
+ path: "./components/ui/new-ui-comp.tsx",
36
+ content: "new-ui-comp;",
37
+ type: "registry:ui",
38
+ target: undefined,
39
+ },
40
+ {
41
+ path: "./hooks/hook.ts",
42
+ content: "hook;\n",
43
+ type: "registry:hook",
44
+ target: undefined,
45
+ },
46
+ {
47
+ path: "./lib/lib.ts",
48
+ content: "lib;\n",
49
+ type: "registry:lib",
50
+ target: undefined,
51
+ },
52
+ ];
53
+
54
+ function createDiffRequest(dir) {
55
+ const currentFiles = scanForFiles(dir);
56
+
57
+ const config = readComponentsManifest(dir);
58
+ config.isSrcDir = hasSrcDir(dir);
59
+
60
+ const specificFiles = {
61
+ "./package.json": fs.readFileSync(
62
+ path.join(dir, "./package.json"),
63
+ "utf-8"
64
+ ),
65
+ };
66
+
67
+ return {
68
+ name: "example",
69
+ config,
70
+ alteredFiles: currentFiles,
71
+ specificFiles,
72
+ currentFiles,
73
+ currentPackageJson: specificFiles["./package.json"],
74
+ };
75
+ }
76
+
77
+ describe("fixture tests", () => {
78
+ test("should diff a src directory project", () => {
79
+ const diff = createDiff(createDiffRequest("./tests/fixtures/src-dir"));
80
+ expect(diff).toMatchSnapshot();
81
+ });
82
+ test("should diff a non-src directory project", () => {
83
+ const diff = createDiff(createDiffRequest("./tests/fixtures/non-src-dir"));
84
+ expect(diff).toMatchSnapshot();
85
+ });
86
+ });