@simpill/utils 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/CONTRIBUTING.md +787 -0
- package/README.md +186 -0
- package/__tests__/README.md +32 -0
- package/__tests__/e2e/all-packages-resolve.e2e.test.ts +40 -0
- package/__tests__/integration/env-and-async.integration.test.ts +12 -0
- package/__tests__/integration/errors-and-uuid.integration.test.ts +14 -0
- package/__tests__/integration/object-and-array.integration.test.ts +15 -0
- package/__tests__/unit/@simpill/_resolver/resolve-packages.unit.test.ts +47 -0
- package/__tests__/unit/@simpill/array.utils/array.utils.unit.test.ts +11 -0
- package/__tests__/unit/@simpill/async.utils/async.utils.unit.test.ts +12 -0
- package/__tests__/unit/@simpill/cache.utils/cache.utils.unit.test.ts +21 -0
- package/__tests__/unit/@simpill/env.utils/env.utils.unit.test.ts +13 -0
- package/__tests__/unit/@simpill/errors.utils/errors.utils.unit.test.ts +13 -0
- package/__tests__/unit/@simpill/object.utils/object.utils.unit.test.ts +11 -0
- package/__tests__/unit/@simpill/patterns.utils/patterns.utils.unit.test.ts +23 -0
- package/__tests__/unit/@simpill/string.utils/string.utils.unit.test.ts +11 -0
- package/__tests__/unit/@simpill/time.utils/time.utils.unit.test.ts +12 -0
- package/__tests__/unit/@simpill/uuid.utils/uuid.utils.unit.test.ts +12 -0
- package/docs/PUBLISHING_AND_PACKAGES.md +258 -0
- package/docs/template/.env.sample +0 -0
- package/docs/template/README.md +0 -0
- package/docs/template/TEMPLATE.md +1040 -0
- package/docs/template/assets/logo-banner.svg +20 -0
- package/docs/template/package.json +14 -0
- package/index.ts +89 -0
- package/package.json +87 -0
- package/scripts/README.md +57 -0
- package/scripts/github/github-set-all-topics.js +120 -0
- package/scripts/github/github-set-repo-topics.sh +33 -0
- package/scripts/github/github-set-repos-public.sh +71 -0
- package/scripts/lib/package-topics.js +57 -0
- package/scripts/lib/publish-order.js +140 -0
- package/scripts/lib/sync-repo-links.js +75 -0
- package/scripts/monorepo/install-hooks.sh +64 -0
- package/scripts/monorepo/monorepo-clean.sh +7 -0
- package/scripts/monorepo/monorepo-sync-deps.js +81 -0
- package/scripts/monorepo/monorepo-verify-deps.js +37 -0
- package/scripts/monorepo/use-local-utils-at-root.js +49 -0
- package/scripts/publish/publish-all.sh +152 -0
- package/scripts/utils/utils-fix-repo-metadata.js +61 -0
- package/scripts/utils/utils-prepare-all.sh +107 -0
- package/scripts/utils/utils-set-npm-keywords.js +132 -0
- package/scripts/utils/utils-update-readme-badges.js +83 -0
- package/scripts/utils/utils-use-local-deps.js +43 -0
- package/scripts/utils/utils-verify-all.sh +45 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Set GitHub repository topics for this monorepo and all @simpill package repos
|
|
4
|
+
* listed in package.json dependencies. Uses gh API (requires gh CLI authenticated).
|
|
5
|
+
* Run from repo root.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
13
|
+
const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
|
|
14
|
+
const TOPICS_FILE = path.join(REPO_ROOT, ".github", "TOPICS.md");
|
|
15
|
+
const MAX_TOPICS = 20;
|
|
16
|
+
const ACCEPT_HEADER = "Accept: application/vnd.github.mercy-preview+json";
|
|
17
|
+
|
|
18
|
+
const { BASE_TOPICS, PACKAGE_TOPICS } = require("../lib/package-topics.js");
|
|
19
|
+
|
|
20
|
+
function run(cmd, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
return execSync(cmd, { encoding: "utf8", ...options });
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (options.ignoreStderr) return e.stdout || "";
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getTopicsFromTopicsFile() {
|
|
30
|
+
const content = fs.readFileSync(TOPICS_FILE, "utf8");
|
|
31
|
+
const names = [];
|
|
32
|
+
const re = /^\s*-\s*`([^`]+)`/gm;
|
|
33
|
+
let m;
|
|
34
|
+
while ((m = re.exec(content)) !== null) names.push(m[1]);
|
|
35
|
+
return names.slice(0, MAX_TOPICS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setRepoTopics(repo, names) {
|
|
39
|
+
const payload = JSON.stringify({ names });
|
|
40
|
+
run(
|
|
41
|
+
`gh api -X PUT -H "${ACCEPT_HEADER}" "repos/${repo}/topics" --input -`,
|
|
42
|
+
{ input: payload, cwd: REPO_ROOT }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRepoTopics(repo) {
|
|
47
|
+
const out = run(
|
|
48
|
+
`gh api "repos/${repo}/topics" -H "${ACCEPT_HEADER}" -q '.names[]'`,
|
|
49
|
+
{ cwd: REPO_ROOT, ignoreStderr: true }
|
|
50
|
+
);
|
|
51
|
+
return out
|
|
52
|
+
.trim()
|
|
53
|
+
? out.trim().split("\n").filter(Boolean)
|
|
54
|
+
: [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getPackageRepos() {
|
|
58
|
+
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
|
|
59
|
+
const repos = [];
|
|
60
|
+
const deps = { ...pkg.dependencies, ...(pkg.devDependencies || {}) };
|
|
61
|
+
for (const spec of Object.values(deps)) {
|
|
62
|
+
const match = spec && spec.match(/github:([^#]+)/);
|
|
63
|
+
if (match) repos.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(repos)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function topicsForPackageRepo(repoName) {
|
|
69
|
+
// repoName is like "SkinnnyJay/env.utils" -> package "env.utils"
|
|
70
|
+
const packageName = repoName.split("/").pop();
|
|
71
|
+
const extra = PACKAGE_TOPICS[packageName] || [packageName.replace(".utils", "")];
|
|
72
|
+
const combined = [...BASE_TOPICS, ...extra];
|
|
73
|
+
return [...new Set(combined)].slice(0, MAX_TOPICS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function main() {
|
|
77
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
78
|
+
const onlyPackages = process.argv.includes("--packages-only");
|
|
79
|
+
|
|
80
|
+
console.log("Setting GitHub topics for @simpill repos...\n");
|
|
81
|
+
|
|
82
|
+
// 1) This monorepo (e.g. SkinnnyJay/simpill-utils)
|
|
83
|
+
if (!onlyPackages) {
|
|
84
|
+
const repo = run("gh repo view --json nameWithOwner -q .nameWithOwner", {
|
|
85
|
+
cwd: REPO_ROOT,
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
}).trim();
|
|
88
|
+
const topics = getTopicsFromTopicsFile();
|
|
89
|
+
console.log(`${repo}: ${topics.length} topics`);
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
console.log(" (dry run)", topics.join(", "));
|
|
92
|
+
} else {
|
|
93
|
+
setRepoTopics(repo, topics);
|
|
94
|
+
console.log(" Set:", getRepoTopics(repo).join(", "));
|
|
95
|
+
}
|
|
96
|
+
console.log("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2) Package repos from package.json
|
|
100
|
+
const packageRepos = getPackageRepos();
|
|
101
|
+
console.log(`Found ${packageRepos.length} package repos in package.json\n`);
|
|
102
|
+
|
|
103
|
+
for (const repo of packageRepos) {
|
|
104
|
+
const topics = topicsForPackageRepo(repo);
|
|
105
|
+
process.stdout.write(`${repo}: ${topics.length} topics ... `);
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
console.log("(dry run)", topics.join(", "));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
setRepoTopics(repo, topics);
|
|
112
|
+
const current = getRepoTopics(repo);
|
|
113
|
+
console.log("OK:", current.slice(0, 8).join(", ") + (current.length > 8 ? "..." : ""));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.log("FAIL:", e.message.split("\n")[0]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Set this repository's GitHub topics from .github/TOPICS.md using the GitHub API.
|
|
3
|
+
# Requires: gh CLI (authenticated), jq. GitHub allows at most 20 topics.
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
8
|
+
TOPICS_FILE="${REPO_ROOT}/.github/TOPICS.md"
|
|
9
|
+
MAX_TOPICS=20
|
|
10
|
+
|
|
11
|
+
if [[ ! -f "$TOPICS_FILE" ]]; then
|
|
12
|
+
echo "Missing $TOPICS_FILE" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Extract topic names from lines like: - `topic-name`
|
|
17
|
+
topics_raw=$(grep -E '^\s*-\s*`[^`]+`' "$TOPICS_FILE" | sed -n 's/.*`\([^`]*\)`.*/\1/p')
|
|
18
|
+
# Take first MAX_TOPICS (GitHub limit)
|
|
19
|
+
topics_list=$(echo "$topics_raw" | head -n "$MAX_TOPICS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
|
20
|
+
|
|
21
|
+
payload=$(jq -n --argjson names "$topics_list" '{ names: $names }')
|
|
22
|
+
|
|
23
|
+
cd "$REPO_ROOT"
|
|
24
|
+
repo=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
25
|
+
|
|
26
|
+
echo "Setting ${repo} topics (from .github/TOPICS.md, max ${MAX_TOPICS})..."
|
|
27
|
+
gh api -X PUT \
|
|
28
|
+
-H "Accept: application/vnd.github.mercy-preview+json" \
|
|
29
|
+
"repos/${repo}/topics" \
|
|
30
|
+
--input - <<< "$payload"
|
|
31
|
+
echo "Done. Current topics:"
|
|
32
|
+
gh api "repos/${repo}/topics" -H "Accept: application/vnd.github.mercy-preview+json" -q '.names[]' | paste -sd ' ' -
|
|
33
|
+
echo
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Set each @simpill package GitHub repo from private to public.
|
|
3
|
+
# Usage: run from repo root. Requires: gh CLI. Set GITHUB_OWNER if not using your user.
|
|
4
|
+
# DRY_RUN=1 ./scripts/github/github-set-repos-public.sh # list only, no changes
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
GITHUB_OWNER="${GITHUB_OWNER:-$(gh api user -q .login 2>/dev/null || echo '')}"
|
|
8
|
+
if [[ -z "$GITHUB_OWNER" ]]; then
|
|
9
|
+
echo "Error: Could not get GitHub user. Run: gh auth login or set GITHUB_OWNER"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
REPOS=(
|
|
14
|
+
adapters.utils
|
|
15
|
+
algorithms.utils
|
|
16
|
+
annotations.utils
|
|
17
|
+
api.utils
|
|
18
|
+
array.utils
|
|
19
|
+
async.utils
|
|
20
|
+
cache.utils
|
|
21
|
+
collections.utils
|
|
22
|
+
crypto.utils
|
|
23
|
+
data.utils
|
|
24
|
+
env.utils
|
|
25
|
+
enum.utils
|
|
26
|
+
errors.utils
|
|
27
|
+
events.utils
|
|
28
|
+
factories.utils
|
|
29
|
+
file.utils
|
|
30
|
+
function.utils
|
|
31
|
+
http.utils
|
|
32
|
+
logger.utils
|
|
33
|
+
middleware.utils
|
|
34
|
+
misc.utils
|
|
35
|
+
nextjs.utils
|
|
36
|
+
number.utils
|
|
37
|
+
object.utils
|
|
38
|
+
observability.utils
|
|
39
|
+
patterns.utils
|
|
40
|
+
protocols.utils
|
|
41
|
+
react.utils
|
|
42
|
+
request-context.utils
|
|
43
|
+
resilience.utils
|
|
44
|
+
socket.utils
|
|
45
|
+
string.utils
|
|
46
|
+
test.utils
|
|
47
|
+
time.utils
|
|
48
|
+
token-optimizer.utils
|
|
49
|
+
uuid.utils
|
|
50
|
+
zod.utils
|
|
51
|
+
zustand.utils
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
echo "Owner: $GITHUB_OWNER"
|
|
55
|
+
[[ -n "${DRY_RUN:-}" ]] && echo "DRY RUN (no visibility changes)"
|
|
56
|
+
|
|
57
|
+
for repo in "${REPOS[@]}"; do
|
|
58
|
+
full="$GITHUB_OWNER/$repo"
|
|
59
|
+
if ! gh repo view "$full" &>/dev/null; then
|
|
60
|
+
echo "Skip $full (repo not found)"
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
if [[ -n "${DRY_RUN:-}" ]]; then
|
|
64
|
+
echo "Would set $full -> public"
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
67
|
+
gh repo edit "$full" --visibility public --accept-visibility-change-consequences
|
|
68
|
+
echo "Set $full to public"
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
echo "Done."
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared package → topics/keywords for GitHub topics and npm package.json keywords.
|
|
3
|
+
* Used by github-set-all-topics.js and utils-set-npm-keywords.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const BASE_TOPICS = [
|
|
7
|
+
"simpill",
|
|
8
|
+
"typescript",
|
|
9
|
+
"utilities",
|
|
10
|
+
"type-safe",
|
|
11
|
+
"esm",
|
|
12
|
+
"tree-shakeable",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** Package short name (e.g. env.utils) → extra topics for that package */
|
|
16
|
+
const PACKAGE_TOPICS = {
|
|
17
|
+
"adapters.utils": ["adapters", "nodejs"],
|
|
18
|
+
"algorithms.utils": ["algorithms", "nodejs"],
|
|
19
|
+
"annotations.utils": ["annotations", "metadata", "nodejs"],
|
|
20
|
+
"api.utils": ["api", "http", "nodejs"],
|
|
21
|
+
"array.utils": ["array", "nodejs"],
|
|
22
|
+
"async.utils": ["async", "promises", "nodejs"],
|
|
23
|
+
"cache.utils": ["cache", "nodejs"],
|
|
24
|
+
"collections.utils": ["collections", "data-structures", "nodejs"],
|
|
25
|
+
"crypto.utils": ["crypto", "security", "nodejs"],
|
|
26
|
+
"data.utils": ["data", "validation", "nodejs"],
|
|
27
|
+
"env.utils": ["env", "dotenv", "nodejs", "edge-runtime"],
|
|
28
|
+
"enum.utils": ["enum", "nodejs"],
|
|
29
|
+
"errors.utils": ["errors", "nodejs"],
|
|
30
|
+
"events.utils": ["events", "nodejs"],
|
|
31
|
+
"factories.utils": ["factories", "nodejs"],
|
|
32
|
+
"file.utils": ["file", "nodejs"],
|
|
33
|
+
"function.utils": ["function", "nodejs"],
|
|
34
|
+
"http.utils": ["http", "nodejs"],
|
|
35
|
+
"logger.utils": ["logging", "nodejs"],
|
|
36
|
+
"middleware.utils": ["middleware", "nodejs"],
|
|
37
|
+
"misc.utils": ["nodejs"],
|
|
38
|
+
"nextjs.utils": ["nextjs", "react", "nodejs"],
|
|
39
|
+
"number.utils": ["number", "nodejs"],
|
|
40
|
+
"object.utils": ["object", "nodejs"],
|
|
41
|
+
"observability.utils": ["observability", "logging", "nodejs"],
|
|
42
|
+
"patterns.utils": ["patterns", "nodejs"],
|
|
43
|
+
"protocols.utils": ["protocols", "nodejs"],
|
|
44
|
+
"react.utils": ["react", "nodejs"],
|
|
45
|
+
"request-context.utils": ["request-context", "nodejs"],
|
|
46
|
+
"resilience.utils": ["resilience", "nodejs"],
|
|
47
|
+
"socket.utils": ["socket", "websocket", "nodejs"],
|
|
48
|
+
"string.utils": ["string", "nodejs"],
|
|
49
|
+
"test.utils": ["testing", "nodejs"],
|
|
50
|
+
"time.utils": ["time", "date", "nodejs"],
|
|
51
|
+
"token-optimizer.utils": ["tokens", "nodejs"],
|
|
52
|
+
"uuid.utils": ["uuid", "nodejs"],
|
|
53
|
+
"zod.utils": ["zod", "validation", "nodejs"],
|
|
54
|
+
"zustand.utils": ["zustand", "react", "state", "nodejs"],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = { BASE_TOPICS, PACKAGE_TOPICS };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Publish order and package.json rewrite for @simpill monorepo.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node publish-order.js order [REPO_ROOT]
|
|
7
|
+
* Prints package directory names in topological publish order (one per line).
|
|
8
|
+
*
|
|
9
|
+
* node publish-order.js rewrite <package-dir> [REPO_ROOT]
|
|
10
|
+
* Reads package-dir/package.json, replaces file:../<x> with ^<version> for
|
|
11
|
+
* @simpill deps, prints result to stdout. Use with backup/restore when publishing.
|
|
12
|
+
*
|
|
13
|
+
* REPO_ROOT defaults to parent of scripts/lib (repo root).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
|
|
19
|
+
const DEFAULT_REPO_ROOT = path.resolve(__dirname, "../..");
|
|
20
|
+
|
|
21
|
+
function getPackageDirs(utilsDir) {
|
|
22
|
+
const dirs = [];
|
|
23
|
+
try {
|
|
24
|
+
const entries = fs.readdirSync(utilsDir, { withFileTypes: true });
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
if (e.isDirectory() && e.name.endsWith(".utils")) {
|
|
27
|
+
const pkgPath = path.join(utilsDir, e.name, "package.json");
|
|
28
|
+
if (fs.existsSync(pkgPath)) dirs.push(e.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error("Failed to read utils:", err.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return dirs.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPackageJson(utilsDir, dir) {
|
|
39
|
+
const p = path.join(utilsDir, dir, "package.json");
|
|
40
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
41
|
+
return { obj: JSON.parse(raw), raw };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectDeps(obj) {
|
|
45
|
+
const deps = [];
|
|
46
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
47
|
+
const section = obj[key];
|
|
48
|
+
if (!section || typeof section !== "object") continue;
|
|
49
|
+
for (const [name, value] of Object.entries(section)) {
|
|
50
|
+
if (name.startsWith("@simpill/") && typeof value === "string" && value.startsWith("file:../")) {
|
|
51
|
+
const depDir = value.replace(/^file:\.\.\//, "").replace(/\/$/, "");
|
|
52
|
+
if (depDir.endsWith(".utils")) deps.push(depDir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...new Set(deps)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function topologicalOrder(utilsDir) {
|
|
60
|
+
const dirs = getPackageDirs(utilsDir);
|
|
61
|
+
const dirToDeps = new Map();
|
|
62
|
+
for (const dir of dirs) {
|
|
63
|
+
const { obj } = readPackageJson(utilsDir, dir);
|
|
64
|
+
const depDirs = collectDeps(obj).filter((d) => dirs.includes(d));
|
|
65
|
+
dirToDeps.set(dir, depDirs);
|
|
66
|
+
}
|
|
67
|
+
// inDegree[dir] = number of @simpill deps (must publish deps before dir)
|
|
68
|
+
const inDegree = new Map();
|
|
69
|
+
for (const dir of dirs) inDegree.set(dir, dirToDeps.get(dir).length);
|
|
70
|
+
const order = [];
|
|
71
|
+
let queue = dirs.filter((d) => inDegree.get(d) === 0);
|
|
72
|
+
while (queue.length) {
|
|
73
|
+
const d = queue.shift();
|
|
74
|
+
order.push(d);
|
|
75
|
+
for (const [dir, deps] of dirToDeps) {
|
|
76
|
+
if (deps.includes(d)) {
|
|
77
|
+
const newDeg = inDegree.get(dir) - 1;
|
|
78
|
+
inDegree.set(dir, newDeg);
|
|
79
|
+
if (newDeg === 0) queue.push(dir);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const remaining = dirs.filter((d) => !order.includes(d));
|
|
84
|
+
if (remaining.length) {
|
|
85
|
+
console.error("Circular dependency among:", remaining.join(", "));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return order;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rewritePackageJsonForPublish(utilsDir, packageDir) {
|
|
92
|
+
const packagePath = path.join(utilsDir, packageDir, "package.json");
|
|
93
|
+
if (!fs.existsSync(packagePath)) {
|
|
94
|
+
console.error("Not found:", packagePath);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
98
|
+
const dirs = getPackageDirs(utilsDir);
|
|
99
|
+
const versions = new Map();
|
|
100
|
+
for (const d of dirs) {
|
|
101
|
+
const obj = JSON.parse(fs.readFileSync(path.join(utilsDir, d, "package.json"), "utf8"));
|
|
102
|
+
if (obj.name && obj.version) versions.set(obj.name, obj.version);
|
|
103
|
+
}
|
|
104
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
105
|
+
const section = pkg[key];
|
|
106
|
+
if (!section || typeof section !== "object") continue;
|
|
107
|
+
for (const [name, value] of Object.entries(section)) {
|
|
108
|
+
if (name.startsWith("@simpill/") && typeof value === "string" && value.startsWith("file:../")) {
|
|
109
|
+
const ver = versions.get(name);
|
|
110
|
+
if (ver) section[name] = `^${ver}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return JSON.stringify(pkg, null, 2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cmd = process.argv[2];
|
|
118
|
+
const repoRoot = cmd === "rewrite" ? (process.argv[4] || DEFAULT_REPO_ROOT) : (process.argv[3] || DEFAULT_REPO_ROOT);
|
|
119
|
+
const utilsDir = path.join(repoRoot, "utils");
|
|
120
|
+
|
|
121
|
+
if (cmd === "order") {
|
|
122
|
+
topologicalOrder(utilsDir).forEach((d) => console.log(d));
|
|
123
|
+
} else if (cmd === "rewrite") {
|
|
124
|
+
const packageDir = process.argv[3];
|
|
125
|
+
if (!packageDir || !packageDir.endsWith(".utils")) {
|
|
126
|
+
console.error("Usage: node publish-order.js rewrite <package-dir> [REPO_ROOT]");
|
|
127
|
+
console.error(" package-dir must end with .utils (e.g. async.utils)");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const dirName = path.basename(packageDir);
|
|
131
|
+
if (!fs.existsSync(path.join(utilsDir, dirName, "package.json"))) {
|
|
132
|
+
console.error("Not found:", path.join(utilsDir, dirName, "package.json"));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
console.log(rewritePackageJsonForPublish(utilsDir, dirName));
|
|
136
|
+
} else {
|
|
137
|
+
console.error("Usage: node publish-order.js order [REPO_ROOT]");
|
|
138
|
+
console.error(" node publish-order.js rewrite <package-dir> [REPO_ROOT]");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Set repository, homepage, and bugs to a single monorepo base for all utils packages.
|
|
4
|
+
* Run from repo root or with REPO_ROOT; writes in place. Commit after running.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* REPO_BASE="https://github.com/owner/repo" [BRANCH=main] node scripts/lib/sync-repo-links.js
|
|
8
|
+
*
|
|
9
|
+
* REPO_BASE Base URL without trailing slash (e.g. https://github.com/SkinnnyJay/simpill-utils).
|
|
10
|
+
* BRANCH Default branch for homepage links (default: main).
|
|
11
|
+
*
|
|
12
|
+
* If REPO_BASE is not set, exits without changing anything.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REPO_ROOT = path.resolve(__dirname, "../..");
|
|
19
|
+
const REPO_ROOT = process.env.REPO_ROOT || DEFAULT_REPO_ROOT;
|
|
20
|
+
const UTILS = path.join(REPO_ROOT, "utils");
|
|
21
|
+
const REPO_BASE = process.env.REPO_BASE || "";
|
|
22
|
+
const BRANCH = process.env.BRANCH || "main";
|
|
23
|
+
|
|
24
|
+
if (!REPO_BASE) {
|
|
25
|
+
console.error("REPO_BASE is not set. Example:");
|
|
26
|
+
console.error(' REPO_BASE="https://github.com/SkinnnyJay/simpill-utils" BRANCH=main node scripts/lib/sync-repo-links.js');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const base = REPO_BASE.replace(/\/$/, "");
|
|
31
|
+
const repoGit = base + ".git";
|
|
32
|
+
const repoIssues = base + "/issues";
|
|
33
|
+
|
|
34
|
+
let dirs;
|
|
35
|
+
try {
|
|
36
|
+
dirs = fs.readdirSync(UTILS, { withFileTypes: true })
|
|
37
|
+
.filter((e) => e.isDirectory() && e.name.endsWith(".utils"))
|
|
38
|
+
.map((e) => e.name);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error("Failed to read utils:", err.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let updated = 0;
|
|
45
|
+
for (const dir of dirs) {
|
|
46
|
+
const pkgPath = path.join(UTILS, dir, "package.json");
|
|
47
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
49
|
+
const repo = {
|
|
50
|
+
type: "git",
|
|
51
|
+
url: repoGit,
|
|
52
|
+
directory: dir,
|
|
53
|
+
};
|
|
54
|
+
const homepage = `${base}/tree/${BRANCH}/utils/${dir}#readme`;
|
|
55
|
+
const bugs = { url: repoIssues };
|
|
56
|
+
let changed = false;
|
|
57
|
+
if (JSON.stringify(pkg.repository) !== JSON.stringify(repo)) {
|
|
58
|
+
pkg.repository = repo;
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
if (pkg.homepage !== homepage) {
|
|
62
|
+
pkg.homepage = homepage;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
if (JSON.stringify(pkg.bugs) !== JSON.stringify(bugs)) {
|
|
66
|
+
pkg.bugs = bugs;
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
if (changed) {
|
|
70
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
71
|
+
console.log("Updated:", dir);
|
|
72
|
+
updated++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log("Done. Updated", updated, "packages.");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Install git hooks at repo root. Pre-commit runs per-package checks when
|
|
4
|
+
# utils/ is present. This script is the source of truth; generated hook
|
|
5
|
+
# always uses a valid "then exit 1" block to avoid bash syntax errors.
|
|
6
|
+
# =============================================================================
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
10
|
+
HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
|
11
|
+
|
|
12
|
+
if [ ! -d "$REPO_ROOT/.git" ] || [ ! -d "$HOOKS_DIR" ]; then
|
|
13
|
+
echo "Not a git repository or .git/hooks missing. Skipping hook installation."
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Package dir names from root package.json (@simpill/name.utils -> @simpill-name.utils)
|
|
18
|
+
PKG_NAMES=""
|
|
19
|
+
if [ -f "$REPO_ROOT/package.json" ]; then
|
|
20
|
+
PKG_NAMES=$(node -e "
|
|
21
|
+
const p = require('$REPO_ROOT/package.json');
|
|
22
|
+
const deps = p.dependencies || {};
|
|
23
|
+
const names = Object.keys(deps)
|
|
24
|
+
.filter(k => k.startsWith('@simpill/') && k.endsWith('.utils'))
|
|
25
|
+
.map(k => '@simpill-' + k.replace('@simpill/', '').replace('.utils', '') + '.utils');
|
|
26
|
+
console.log(names.sort().join('\n'));
|
|
27
|
+
" 2>/dev/null || true)
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Build pre-commit hook. Each block must have "exit 1" in the then clause (no empty then).
|
|
31
|
+
PRE_COMMIT="$HOOKS_DIR/pre-commit"
|
|
32
|
+
cat > "$PRE_COMMIT" << 'HEADER'
|
|
33
|
+
#!/usr/bin/env bash
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
# Package Pre-Commit Hook (generated by scripts/monorepo/install-hooks.sh)
|
|
37
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
38
|
+
|
|
39
|
+
HEADER
|
|
40
|
+
|
|
41
|
+
while IFS= read -r dir; do
|
|
42
|
+
[ -z "$dir" ] && continue
|
|
43
|
+
# Use a single, valid block template: then clause always contains exit 1
|
|
44
|
+
name="${dir#@simpill-}"
|
|
45
|
+
name="${name%.utils}"
|
|
46
|
+
cat >> "$PRE_COMMIT" << BLOCK
|
|
47
|
+
|
|
48
|
+
# --- BEGIN ${name}.utils ---
|
|
49
|
+
if [ -f "\$REPO_ROOT/utils/${dir}/scripts/pre-commit.sh" ]; then
|
|
50
|
+
if git diff --cached --name-only | grep -q "^utils/${dir}/"; then
|
|
51
|
+
if ! "\$REPO_ROOT/utils/${dir}/scripts/pre-commit.sh"; then
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
# --- END ${name}.utils ---
|
|
57
|
+
BLOCK
|
|
58
|
+
done << EOF
|
|
59
|
+
$PKG_NAMES
|
|
60
|
+
EOF
|
|
61
|
+
|
|
62
|
+
echo "exit 0" >> "$PRE_COMMIT"
|
|
63
|
+
chmod +x "$PRE_COMMIT"
|
|
64
|
+
echo "Git hooks installed (pre-commit)."
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clean monorepo root: node_modules, lockfile, and build artifacts.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
REPO_ROOT="${1:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
5
|
+
cd "$REPO_ROOT"
|
|
6
|
+
rm -rf node_modules package-lock.json dist coverage .next .jest-cache 2>/dev/null || true
|
|
7
|
+
echo "Monorepo root cleaned (node_modules, package-lock.json, dist, coverage, .next, .jest-cache)."
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync monorepo root package.json @simpill dependencies to npm versions.
|
|
4
|
+
* If utils/@simpill-*.utils exists, uses each package's version (^version); otherwise uses ^1.0.0.
|
|
5
|
+
* Run from repo root. Use npm run use:local to switch to file:./utils/ for local development.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = path.join(__dirname, "..", "..");
|
|
11
|
+
const UTILS = path.join(REPO_ROOT, "utils");
|
|
12
|
+
const ROOT_PKG = path.join(REPO_ROOT, "package.json");
|
|
13
|
+
|
|
14
|
+
function getPackageDirs() {
|
|
15
|
+
if (!fs.existsSync(UTILS)) return [];
|
|
16
|
+
return fs
|
|
17
|
+
.readdirSync(UTILS, { withFileTypes: true })
|
|
18
|
+
.filter(
|
|
19
|
+
(d) =>
|
|
20
|
+
d.isDirectory() &&
|
|
21
|
+
d.name.startsWith("@simpill-") &&
|
|
22
|
+
d.name.endsWith(".utils")
|
|
23
|
+
)
|
|
24
|
+
.map((d) => d.name)
|
|
25
|
+
.sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dirs = getPackageDirs();
|
|
29
|
+
const deps = {};
|
|
30
|
+
|
|
31
|
+
for (const dir of dirs) {
|
|
32
|
+
const pkgPath = path.join(UTILS, dir, "package.json");
|
|
33
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
35
|
+
const name = pkg.name;
|
|
36
|
+
const version = (pkg.version && String(pkg.version).trim()) || "1.0.0";
|
|
37
|
+
if (name && name.startsWith("@simpill/")) {
|
|
38
|
+
deps[name] = `^${version}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If no utils/ or empty, preserve existing @simpill names with ^1.0.0
|
|
43
|
+
const rootPkg = JSON.parse(fs.readFileSync(ROOT_PKG, "utf8"));
|
|
44
|
+
const existing = rootPkg.dependencies || {};
|
|
45
|
+
if (Object.keys(deps).length === 0) {
|
|
46
|
+
for (const [name, spec] of Object.entries(existing)) {
|
|
47
|
+
if (name.startsWith("@simpill/")) {
|
|
48
|
+
const v = spec.match(/^\^?([0-9.]+)/);
|
|
49
|
+
deps[name] = v ? `^${v[1]}` : "^1.0.0";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rootDeps = rootPkg.dependencies || {};
|
|
55
|
+
let changed = false;
|
|
56
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
57
|
+
if (rootDeps[name] !== spec) {
|
|
58
|
+
rootDeps[name] = spec;
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const utilNames = new Set(Object.keys(deps));
|
|
63
|
+
for (const name of Object.keys(rootDeps)) {
|
|
64
|
+
if (name.startsWith("@simpill/") && !utilNames.has(name)) {
|
|
65
|
+
delete rootDeps[name];
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
rootPkg.dependencies = rootDeps;
|
|
70
|
+
|
|
71
|
+
if (changed) {
|
|
72
|
+
const keys = Object.keys(rootPkg.dependencies).sort();
|
|
73
|
+
const sorted = {};
|
|
74
|
+
for (const k of keys) sorted[k] = rootPkg.dependencies[k];
|
|
75
|
+
rootPkg.dependencies = sorted;
|
|
76
|
+
fs.writeFileSync(ROOT_PKG, JSON.stringify(rootPkg, null, 2) + "\n", "utf8");
|
|
77
|
+
console.log("Updated root package.json dependencies to npm versions.");
|
|
78
|
+
} else {
|
|
79
|
+
console.log("Root package.json already in sync.");
|
|
80
|
+
}
|
|
81
|
+
console.log("Monorepo deps:", Object.keys(deps).length, "packages (npm ^version).");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify all @simpill dependencies: resolve and load each package.
|
|
4
|
+
* Run from repo root after npm install.
|
|
5
|
+
*/
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const pkg = require(path.join(process.cwd(), "package.json"));
|
|
8
|
+
const deps = Object.keys(pkg.dependencies || {}).filter((d) => d.startsWith("@simpill/"));
|
|
9
|
+
|
|
10
|
+
let resolved = 0;
|
|
11
|
+
let loaded = 0;
|
|
12
|
+
const loadErrors = [];
|
|
13
|
+
|
|
14
|
+
for (const d of deps) {
|
|
15
|
+
try {
|
|
16
|
+
require.resolve(d);
|
|
17
|
+
resolved++;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error("Resolve failed:", d, e.message);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
require(d);
|
|
24
|
+
loaded++;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
loadErrors.push({ name: d, message: e.message });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log("Resolvable:", resolved + "/" + deps.length);
|
|
31
|
+
console.log("Loadable (require):", loaded + "/" + deps.length);
|
|
32
|
+
if (loadErrors.length > 0) {
|
|
33
|
+
console.error("Packages that did not load (may be ESM-only or need build):");
|
|
34
|
+
loadErrors.forEach(({ name, message }) => console.error(" ", name, message));
|
|
35
|
+
}
|
|
36
|
+
const ok = resolved === deps.length;
|
|
37
|
+
process.exit(ok ? 0 : 1);
|