Skip to content

Commit dceabc3

Browse files
feat: postinstall scaffolds config and npm run runly
Add scripts/postinstall.mjs to create runly.config.js with defaults, inject scripts.runly when missing, and ship the script in the published package. Made-with: Cursor
1 parent 9411657 commit dceabc3

5 files changed

Lines changed: 130 additions & 4 deletions

File tree

dist/resolve-node.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/resolve-node.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/resolve-node.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hamdymohamedak/runly",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"publishConfig": {
55
"access": "public"
66
},
@@ -19,9 +19,11 @@
1919
}
2020
},
2121
"files": [
22-
"dist"
22+
"dist",
23+
"scripts/postinstall.mjs"
2324
],
2425
"scripts": {
26+
"postinstall": "node ./scripts/postinstall.mjs",
2527
"test": "node --test test/smoke.test.js",
2628
"ci:runly-smoke": "node dist/cli.js -c examples/all-versions-pass/runly.config.mjs",
2729
"build": "tsc",

scripts/postinstall.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
2+
import { dirname, join, sep } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
const PKG = "@hamdymohamedak/runly";
6+
const CONFIG_CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"];
7+
8+
function isInsideNodeModules(dir) {
9+
return dir.split(sep).includes("node_modules");
10+
}
11+
12+
function installContextRoot() {
13+
const init = process.env.INIT_CWD;
14+
if (init) return init;
15+
const cwd = process.cwd();
16+
if (isInsideNodeModules(cwd)) return null;
17+
return cwd;
18+
}
19+
20+
function readPkg(dir) {
21+
const p = join(dir, "package.json");
22+
if (!existsSync(p)) return null;
23+
try {
24+
return JSON.parse(readFileSync(p, "utf8"));
25+
} catch {
26+
return null;
27+
}
28+
}
29+
30+
function declaresRunly(pkg) {
31+
if (!pkg || typeof pkg !== "object") return false;
32+
const blocks = [
33+
pkg.dependencies,
34+
pkg.devDependencies,
35+
pkg.optionalDependencies,
36+
pkg.peerDependencies,
37+
];
38+
for (const b of blocks) {
39+
if (b && typeof b === "object" && PKG in b) return true;
40+
}
41+
return false;
42+
}
43+
44+
/** First directory from `start` upward whose package.json lists this package. */
45+
function findConsumerRoot(start) {
46+
let dir = start;
47+
for (;;) {
48+
const pkg = readPkg(dir);
49+
if (pkg && declaresRunly(pkg)) return { dir, pkg };
50+
const parent = dirname(dir);
51+
if (parent === dir) break;
52+
dir = parent;
53+
}
54+
return null;
55+
}
56+
57+
function hasRunlyConfig(projectRoot) {
58+
return CONFIG_CANDIDATES.some((name) => existsSync(join(projectRoot, name)));
59+
}
60+
61+
function defaultConfigBody() {
62+
// One-liner so first `npm run runly` succeeds without a test/ tree; users can switch to `node --test` etc.
63+
return `{
64+
versions: ["18", "20", "22"],
65+
bail: false,
66+
run: {
67+
argv: ["node", "-e", "console.log('runly ok', process.version)"],
68+
shell: false,
69+
},
70+
}`;
71+
}
72+
73+
function writePackageJson(projectRoot, pkg) {
74+
const pkgPath = join(projectRoot, "package.json");
75+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
76+
}
77+
78+
/** Ensures `scripts.runly` so `npm run runly` works. Returns true if package.json was written. */
79+
function ensureRunlyNpmScript(pkg) {
80+
const prev = pkg.scripts;
81+
const scripts =
82+
prev != null && typeof prev === "object" && !Array.isArray(prev)
83+
? { ...prev }
84+
: {};
85+
if (Object.hasOwn(scripts, "runly")) return false;
86+
scripts.runly = "runly";
87+
pkg.scripts = scripts;
88+
return true;
89+
}
90+
91+
function main() {
92+
if (process.env.RUNLY_SKIP_POSTINSTALL === "1" || process.env.RUNLY_SKIP_POSTINSTALL === "true") {
93+
return;
94+
}
95+
if (process.env.npm_config_global === "true") {
96+
return;
97+
}
98+
99+
const start = installContextRoot();
100+
if (!start) return;
101+
102+
const found = findConsumerRoot(start);
103+
if (!found) return;
104+
105+
const { dir: projectRoot, pkg } = found;
106+
107+
if (!hasRunlyConfig(projectRoot)) {
108+
const isEsm = pkg.type === "module";
109+
const body = defaultConfigBody();
110+
const content = isEsm
111+
? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n`
112+
: `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`;
113+
114+
writeFileSync(join(projectRoot, "runly.config.js"), content, "utf8");
115+
}
116+
117+
if (ensureRunlyNpmScript(pkg)) {
118+
writePackageJson(projectRoot, pkg);
119+
}
120+
}
121+
122+
main();

0 commit comments

Comments
 (0)