@navneet_25/tempjs 1.1.0 → 3.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 +800 -35
- package/cli/brand-manager.js +277 -78
- package/cli/config.js +22 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +170 -49
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -0
- package/cli/fetch.js +47 -29
- package/cli/file-tree.js +113 -0
- package/cli/fs-ignore.js +60 -0
- package/cli/index.js +274 -112
- package/cli/info.js +115 -0
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +177 -0
- package/cli/progress.js +33 -0
- package/cli/project-stamp.js +69 -0
- package/cli/prompt.js +19 -0
- package/cli/template-resolver.js +48 -0
- package/cli/theme-manager.js +28 -2
- package/cli/update.js +227 -0
- package/cli/version-manager.js +518 -0
- package/package.json +31 -4
- package/packages/core/modules/README.md +72 -0
- package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
- package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
- package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
- package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
- package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
- package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
- package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
- package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
- package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
- package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
- package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
- package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
- package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
- package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
- package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
- package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
- package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
- package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
- package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
- package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
- package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
- package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
- package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
- package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
- package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
- package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
- package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
- package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
- package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
- package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
- package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
- package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
- package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
- package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
- package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
- package/packages/core/modules/seo/app/robots.ts +30 -0
- package/packages/core/modules/seo/app/sitemap.ts +9 -0
- package/packages/core/modules/seo/lib/seo/index.ts +5 -0
- package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
- package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
- package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
- package/packages/core/modules/seo/lib/seo/types.ts +30 -0
- package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
- package/packages/core/modules.json +140 -0
- package/packages/core/prisma/schema.prisma +33 -0
- package/scripts/module-installer-core.mjs +657 -0
- package/scripts/template-modules-admin.mjs +366 -0
- package/scripts/template-modules-home.mjs +120 -0
- package/templates.json +188 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
|
|
9
|
+
const CODELOAD_BASE = "https://codeload.github.com";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{ coreDir: string, cleanupDir: string }} CoreModulesFetchResult
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
17
|
+
* @returns {Promise<CoreModulesFetchResult>}
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchCoreModulesFromGitHub(repo) {
|
|
20
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "tempjs-core-"));
|
|
21
|
+
const tarballPath = join(tempRoot, "archive.tar.gz");
|
|
22
|
+
const coreDir = join(tempRoot, "core");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await downloadTarball(repo, tarballPath);
|
|
26
|
+
await extractCoreFromTarball(tarballPath, coreDir, repo);
|
|
27
|
+
await readFile(join(coreDir, "modules.json"));
|
|
28
|
+
|
|
29
|
+
return { coreDir, cleanupDir: tempRoot };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} packageRoot
|
|
38
|
+
* @returns {string | null}
|
|
39
|
+
*/
|
|
40
|
+
export function resolveLocalCoreDir(packageRoot) {
|
|
41
|
+
const localPath = join(packageRoot, "packages", "core");
|
|
42
|
+
if (existsSync(join(localPath, "modules.json"))) {
|
|
43
|
+
return localPath;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
50
|
+
* @param {string} destPath
|
|
51
|
+
*/
|
|
52
|
+
async function downloadTarball(repo, destPath) {
|
|
53
|
+
const branch = encodeURIComponent(repo.branch);
|
|
54
|
+
const url = `${CODELOAD_BASE}/${repo.owner}/${repo.repo}/tar.gz/${branch}`;
|
|
55
|
+
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
headers: buildHeaders(),
|
|
58
|
+
redirect: "follow",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let detail = `Could not download ${repo.owner}/${repo.repo}@${repo.branch}`;
|
|
63
|
+
if (response.status === 404) {
|
|
64
|
+
detail += "\nVerify the repository, branch, and templates.json settings.";
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 401 || response.status === 403) {
|
|
67
|
+
detail += "\nFor private repositories, set GITHUB_TOKEN or GH_TOKEN.";
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`${detail} (HTTP ${response.status})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!response.body) {
|
|
73
|
+
throw new Error("Empty response while downloading repository archive");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
|
|
77
|
+
return statSync(destPath).size;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} tarballPath
|
|
82
|
+
* @param {string} destDir
|
|
83
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
84
|
+
*/
|
|
85
|
+
async function extractCoreFromTarball(tarballPath, destDir, repo) {
|
|
86
|
+
const listing = execFileSync("tar", ["-tzf", tarballPath], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const lines = listing.split("\n").filter(Boolean);
|
|
92
|
+
if (lines.length === 0) {
|
|
93
|
+
throw new Error("Repository archive is empty");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const archiveRoot = lines[0].replace(/\/$/, "").split("/")[0];
|
|
97
|
+
const coreArchivePath = `${archiveRoot}/packages/core`;
|
|
98
|
+
const corePrefix = `${coreArchivePath}/`;
|
|
99
|
+
|
|
100
|
+
const hasCore = lines.some((line) => line === corePrefix || line.startsWith(corePrefix));
|
|
101
|
+
|
|
102
|
+
if (!hasCore) {
|
|
103
|
+
throw new Error(`Core package not found in repository: packages/core`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stripComponents = coreArchivePath.split("/").length;
|
|
107
|
+
|
|
108
|
+
await mkdir(destDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
execFileSync(
|
|
111
|
+
"tar",
|
|
112
|
+
[
|
|
113
|
+
"-xzf",
|
|
114
|
+
tarballPath,
|
|
115
|
+
"-C",
|
|
116
|
+
destDir,
|
|
117
|
+
`--strip-components=${stripComponents}`,
|
|
118
|
+
coreArchivePath,
|
|
119
|
+
],
|
|
120
|
+
{ stdio: "pipe", maxBuffer: 64 * 1024 * 1024 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildHeaders() {
|
|
125
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
126
|
+
if (token) {
|
|
127
|
+
return { Authorization: `token ${token}` };
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
package/cli/fetch.js
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createWriteStream, existsSync } from "node:fs";
|
|
2
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
|
+
import { shouldSkipFileName } from "./fs-ignore.js";
|
|
9
|
+
import { countFiles } from "./file-tree.js";
|
|
8
10
|
|
|
9
11
|
const CODELOAD_BASE = "https://codeload.github.com";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
14
|
+
* @typedef {{ bytes: number, files: number, durationMs: number, source: string }} FetchStats
|
|
15
|
+
* @typedef {{ templateRoot: string, cleanupDir: string, stats: FetchStats }} FetchResult
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
18
19
|
* @param {import('./config.js').RepositoryConfig} repo
|
|
19
20
|
* @param {string} templateDirectory
|
|
20
|
-
* @returns {Promise<
|
|
21
|
+
* @returns {Promise<FetchResult>}
|
|
21
22
|
*/
|
|
22
23
|
export async function fetchTemplateFromGitHub(repo, templateDirectory) {
|
|
24
|
+
const startMs = performance.now();
|
|
23
25
|
const tempRoot = await mkdtemp(join(tmpdir(), "template-cli-"));
|
|
24
26
|
const tarballPath = join(tempRoot, "archive.tar.gz");
|
|
25
27
|
const templateRoot = join(tempRoot, "template");
|
|
26
28
|
|
|
27
29
|
try {
|
|
28
|
-
await downloadTarball(repo, tarballPath);
|
|
29
|
-
await extractTemplateFromTarball(
|
|
30
|
-
tarballPath,
|
|
31
|
-
templateRoot,
|
|
32
|
-
repo,
|
|
33
|
-
templateDirectory
|
|
34
|
-
);
|
|
30
|
+
const bytes = await downloadTarball(repo, tarballPath);
|
|
31
|
+
await extractTemplateFromTarball(tarballPath, templateRoot, repo, templateDirectory);
|
|
35
32
|
await readFile(join(templateRoot, "package.json"));
|
|
36
|
-
|
|
33
|
+
|
|
34
|
+
const durationMs = performance.now() - startMs;
|
|
35
|
+
const files = countFiles(templateRoot);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
templateRoot,
|
|
39
|
+
cleanupDir: tempRoot,
|
|
40
|
+
stats: { bytes, files, durationMs, source: "remote" },
|
|
41
|
+
};
|
|
37
42
|
} catch (error) {
|
|
38
43
|
await rm(tempRoot, { recursive: true, force: true });
|
|
39
44
|
throw error;
|
|
@@ -43,6 +48,7 @@ export async function fetchTemplateFromGitHub(repo, templateDirectory) {
|
|
|
43
48
|
/**
|
|
44
49
|
* @param {import('./config.js').RepositoryConfig} repo
|
|
45
50
|
* @param {string} destPath
|
|
51
|
+
* @returns {Promise<number>} Downloaded bytes
|
|
46
52
|
*/
|
|
47
53
|
async function downloadTarball(repo, destPath) {
|
|
48
54
|
const branch = encodeURIComponent(repo.branch);
|
|
@@ -69,6 +75,7 @@ async function downloadTarball(repo, destPath) {
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
|
|
78
|
+
return statSync(destPath).size;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -128,13 +135,12 @@ async function extractTemplateFromTarball(
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
/**
|
|
131
|
-
* Remove files that must never appear in generated projects.
|
|
132
138
|
* @param {string} dir
|
|
133
139
|
*/
|
|
134
140
|
async function removeSkippedFiles(dir) {
|
|
135
141
|
if (!existsSync(dir)) return;
|
|
136
142
|
|
|
137
|
-
const entries = await
|
|
143
|
+
const entries = await readDirEntries(dir);
|
|
138
144
|
for (const entry of entries) {
|
|
139
145
|
const fullPath = join(dir, entry.name);
|
|
140
146
|
if (entry.isDirectory) {
|
|
@@ -155,7 +161,7 @@ async function removeSkippedFiles(dir) {
|
|
|
155
161
|
/**
|
|
156
162
|
* @param {string} dir
|
|
157
163
|
*/
|
|
158
|
-
async function
|
|
164
|
+
async function readDirEntries(dir) {
|
|
159
165
|
const names = await readdir(dir);
|
|
160
166
|
const result = [];
|
|
161
167
|
for (const name of names) {
|
|
@@ -166,16 +172,6 @@ async function readDirSafe(dir) {
|
|
|
166
172
|
return result;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
/**
|
|
170
|
-
* @param {string} name
|
|
171
|
-
*/
|
|
172
|
-
function shouldSkipFileName(name) {
|
|
173
|
-
if (name === ".env" || (name.startsWith(".env.") && name !== ".env.example")) {
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
175
|
function buildHeaders() {
|
|
180
176
|
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
181
177
|
if (token) {
|
|
@@ -199,3 +195,25 @@ export async function resolveLocalTemplate(packageRoot, templatesPath, templateD
|
|
|
199
195
|
return null;
|
|
200
196
|
}
|
|
201
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} packageRoot
|
|
201
|
+
* @param {string} templatesPath
|
|
202
|
+
* @param {string} templateDirectory
|
|
203
|
+
* @returns {Promise<FetchResult | null>}
|
|
204
|
+
*/
|
|
205
|
+
export async function loadLocalTemplate(packageRoot, templatesPath, templateDirectory) {
|
|
206
|
+
const localPath = await resolveLocalTemplate(packageRoot, templatesPath, templateDirectory);
|
|
207
|
+
if (!localPath) return null;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
templateRoot: localPath,
|
|
211
|
+
cleanupDir: "",
|
|
212
|
+
stats: {
|
|
213
|
+
bytes: 0,
|
|
214
|
+
files: countFiles(localPath),
|
|
215
|
+
durationMs: 0,
|
|
216
|
+
source: "local",
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/cli/file-tree.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { shouldSkipPath } from "./fs-ignore.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} filePath
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function hashFileContents(filePath) {
|
|
11
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} rootDir
|
|
16
|
+
* @returns {Record<string, string>}
|
|
17
|
+
*/
|
|
18
|
+
export function collectFileHashes(rootDir) {
|
|
19
|
+
const hashes = {};
|
|
20
|
+
collectRecursive(rootDir, rootDir, hashes);
|
|
21
|
+
return hashes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} rootDir
|
|
26
|
+
* @param {string} currentDir
|
|
27
|
+
* @param {Record<string, string>} hashes
|
|
28
|
+
*/
|
|
29
|
+
function collectRecursive(rootDir, currentDir, hashes) {
|
|
30
|
+
if (!existsSync(currentDir)) return;
|
|
31
|
+
|
|
32
|
+
for (const name of readdirSync(currentDir)) {
|
|
33
|
+
const fullPath = join(currentDir, name);
|
|
34
|
+
const relPath = relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
35
|
+
|
|
36
|
+
if (shouldSkipPath(relPath, name)) continue;
|
|
37
|
+
|
|
38
|
+
const stat = statSync(fullPath);
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
collectRecursive(rootDir, fullPath, hashes);
|
|
41
|
+
} else {
|
|
42
|
+
hashes[relPath] = hashFileContents(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} rootDir
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
export function countFiles(rootDir) {
|
|
52
|
+
return Object.keys(collectFileHashes(rootDir)).length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {{
|
|
57
|
+
* newFiles: string[],
|
|
58
|
+
* upToDate: string[],
|
|
59
|
+
* safeUpdates: string[],
|
|
60
|
+
* conflicts: string[],
|
|
61
|
+
* protectedSkips: string[],
|
|
62
|
+
* removedFromTemplate: string[]
|
|
63
|
+
* }} UpdateDiff
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compare project against baseline (at generation) and latest template hashes.
|
|
68
|
+
* @param {Record<string, string>} baselineHashes
|
|
69
|
+
* @param {Record<string, string>} currentHashes
|
|
70
|
+
* @param {Record<string, string>} latestHashes
|
|
71
|
+
* @returns {UpdateDiff}
|
|
72
|
+
*/
|
|
73
|
+
export function diffTemplateTrees(baselineHashes, currentHashes, latestHashes) {
|
|
74
|
+
const diff = {
|
|
75
|
+
newFiles: [],
|
|
76
|
+
upToDate: [],
|
|
77
|
+
safeUpdates: [],
|
|
78
|
+
conflicts: [],
|
|
79
|
+
protectedSkips: [],
|
|
80
|
+
removedFromTemplate: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
for (const path of Object.keys(latestHashes).sort()) {
|
|
84
|
+
const latest = latestHashes[path];
|
|
85
|
+
const current = currentHashes[path];
|
|
86
|
+
const baseline = baselineHashes[path];
|
|
87
|
+
|
|
88
|
+
if (current === undefined) {
|
|
89
|
+
diff.newFiles.push(path);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (current === latest) {
|
|
94
|
+
diff.upToDate.push(path);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (baseline !== undefined && current === baseline && latest !== baseline) {
|
|
99
|
+
diff.safeUpdates.push(path);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
diff.conflicts.push(path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const path of Object.keys(baselineHashes).sort()) {
|
|
107
|
+
if (!latestHashes[path]) {
|
|
108
|
+
diff.removedFromTemplate.push(path);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return diff;
|
|
113
|
+
}
|
package/cli/fs-ignore.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Files and directories never copied from templates or merged on update. */
|
|
2
|
+
export const NEVER_COPY_NAMES = new Set([".git", ".gitignore.bak"]);
|
|
3
|
+
|
|
4
|
+
/** Paths relative to project root that must not be overwritten by template update. */
|
|
5
|
+
export const UPDATE_PROTECTED_PATHS = new Set([
|
|
6
|
+
".env",
|
|
7
|
+
".tempjsrc",
|
|
8
|
+
".tempjs.json",
|
|
9
|
+
"constants/site.ts",
|
|
10
|
+
"app/tempjs-theme.css",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/** Directory names skipped when walking trees. */
|
|
14
|
+
export const SKIP_DIR_NAMES = new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
".next",
|
|
17
|
+
".git",
|
|
18
|
+
"dist",
|
|
19
|
+
"build",
|
|
20
|
+
"coverage",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function shouldSkipFileName(name) {
|
|
28
|
+
if (NEVER_COPY_NAMES.has(name)) return true;
|
|
29
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
30
|
+
return name !== ".env.example";
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} relativePath
|
|
37
|
+
* @param {string} [name]
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function shouldSkipPath(relativePath, name) {
|
|
41
|
+
const base = name ?? relativePath.split("/").pop() ?? "";
|
|
42
|
+
if (shouldSkipFileName(base)) return true;
|
|
43
|
+
|
|
44
|
+
const parts = relativePath.split("/");
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
if (SKIP_DIR_NAMES.has(part)) return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} relativePath
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isUpdateProtected(relativePath) {
|
|
56
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
57
|
+
if (UPDATE_PROTECTED_PATHS.has(normalized)) return true;
|
|
58
|
+
if (normalized.startsWith(".env.") && normalized !== ".env.example") return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|