Skip to content

Commit 34ee308

Browse files
committed
chore: fix cli support
1 parent df4144a commit 34ee308

File tree

5 files changed

+132
-42
lines changed

5 files changed

+132
-42
lines changed

src/commands/build.ts

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import execa from "execa";
33
import * as fse from "fs-extra";
44
import globby from "globby";
55
import pLimit from "p-limit";
6-
import fs from "fs-extra";
76
import { resolve, join, dirname } from "path";
87
import { Consola } from "consola";
98
import get from "lodash.get";
@@ -124,9 +123,9 @@ export const buildCommand = createCommand<{}, {}>((api) => {
124123
const cwd = rootPackageJSONPath.replace("/package.json", "");
125124
const buildPath = join(cwd, ".bob");
126125

127-
await fs.remove(buildPath);
126+
await fse.remove(buildPath);
128127
await buildTypeScript(buildPath);
129-
const pkg = await fs.readJSON(resolve(cwd, "package.json"));
128+
const pkg = await fse.readJSON(resolve(cwd, "package.json"));
130129
const fullName: string = pkg.name;
131130

132131
const distPath = join(cwd, "dist");
@@ -160,15 +159,15 @@ export const buildCommand = createCommand<{}, {}>((api) => {
160159
packages.map((packagePath) =>
161160
limit(async () => {
162161
const cwd = packagePath.replace("/package.json", "");
163-
const pkg = await fs.readJSON(resolve(cwd, "package.json"));
162+
const pkg = await fse.readJSON(resolve(cwd, "package.json"));
164163
const fullName: string = pkg.name;
165164
return { packagePath, cwd, pkg, fullName };
166165
})
167166
)
168167
);
169168

170169
const bobBuildPath = join(cwd, ".bob");
171-
await fs.remove(bobBuildPath);
170+
await fse.remove(bobBuildPath);
172171
await buildTypeScript(bobBuildPath);
173172

174173
await Promise.all(
@@ -210,6 +209,7 @@ async function build({
210209
pkg: {
211210
name: string;
212211
buildOptions: BuildOptions;
212+
bin?: Record<string, string>;
213213
};
214214
fullName: string;
215215
config: BobConfig;
@@ -225,10 +225,10 @@ async function build({
225225
validatePackageJson(pkg);
226226

227227
// remove <project>/dist
228-
await fs.remove(distPath);
228+
await fse.remove(distPath);
229229

230230
// Copy type definitions
231-
await fs.ensureDir(join(distPath, "typings"));
231+
await fse.ensureDir(join(distPath, "typings"));
232232

233233
const declarations = await globby("**/*.d.ts", {
234234
cwd: getBuildPath("esm"),
@@ -239,7 +239,7 @@ async function build({
239239
await Promise.all(
240240
declarations.map((filePath) =>
241241
limit(() =>
242-
fs.copy(
242+
fse.copy(
243243
join(getBuildPath("esm"), filePath),
244244
join(distPath, "typings", filePath)
245245
)
@@ -248,7 +248,7 @@ async function build({
248248
);
249249

250250
// Move ESM to dist/esm
251-
await fs.ensureDir(join(distPath, "esm"));
251+
await fse.ensureDir(join(distPath, "esm"));
252252

253253
const esmFiles = await globby("**/*.js", {
254254
cwd: getBuildPath("esm"),
@@ -259,7 +259,7 @@ async function build({
259259
await Promise.all(
260260
esmFiles.map((filePath) =>
261261
limit(() =>
262-
fs.copy(
262+
fse.copy(
263263
join(getBuildPath("esm"), filePath),
264264
join(distPath, "esm", filePath)
265265
)
@@ -268,7 +268,7 @@ async function build({
268268
);
269269

270270
// Transpile ESM to CJS and move CJS to dist/cjs
271-
await fs.ensureDir(join(distPath, "cjs"));
271+
await fse.ensureDir(join(distPath, "cjs"));
272272

273273
const cjsFiles = await globby("**/*.js", {
274274
cwd: getBuildPath("cjs"),
@@ -279,7 +279,7 @@ async function build({
279279
await Promise.all(
280280
cjsFiles.map((filePath) =>
281281
limit(() =>
282-
fs.copy(
282+
fse.copy(
283283
join(getBuildPath("cjs"), filePath),
284284
join(distPath, "cjs", filePath)
285285
)
@@ -288,13 +288,13 @@ async function build({
288288
);
289289

290290
// Add package.json to dist/cjs to ensure files are interpreted as commonjs
291-
await fs.writeFile(
291+
await fse.writeFile(
292292
join(distPath, "cjs", "package.json"),
293293
JSON.stringify({ type: "commonjs" })
294294
);
295295

296296
// move the package.json to dist
297-
await fs.writeFile(
297+
await fse.writeFile(
298298
join(distPath, "package.json"),
299299
JSON.stringify(rewritePackageJson(pkg), null, 2)
300300
);
@@ -306,6 +306,21 @@ async function build({
306306
distPath
307307
);
308308

309+
if (pkg.bin) {
310+
if (globalThis.process.platform === "win32") {
311+
console.warn(
312+
"Package includes bin files, but cannot set the executable bit on Windows.\n" +
313+
"Please manually set the executable bit on the bin files before publishing."
314+
);
315+
} else {
316+
await Promise.all(
317+
Object.values(pkg.bin).map((filePath) =>
318+
execa("chmod", ["+x", join(cwd, filePath)])
319+
)
320+
);
321+
}
322+
}
323+
309324
reporter.success(`Built ${pkg.name}`);
310325
}
311326

@@ -378,29 +393,46 @@ export function validatePackageJson(pkg: any) {
378393
);
379394
}
380395

381-
expect("main", presetFields.main);
382-
expect("module", presetFields.module);
383-
expect("typings", presetFields.typings);
384-
expect("typescript.definition", presetFields.typescript.definition);
385-
386-
expect("exports['.'].require", presetFields.exports["."].require);
387-
expect("exports['.'].import", presetFields.exports["."].import);
388-
expect("exports['.'].default", presetFields.exports["."].default);
389-
expect("exports['./*'].require", presetFields.exports["./*"].require);
390-
expect("exports['./*'].import", presetFields.exports["./*"].import);
391-
expect("exports['./*'].default", presetFields.exports["./*"].default);
396+
// If the package has NO binary we need to check the exports map.
397+
// a package should either
398+
// 1. have a bin property
399+
// 2. have a exports property
400+
// 3. have an exports and bin property
401+
if (Object.keys(pkg.bin ?? {}).length === 0) {
402+
expect("main", presetFields.main);
403+
expect("module", presetFields.module);
404+
expect("typings", presetFields.typings);
405+
expect("typescript.definition", presetFields.typescript.definition);
406+
} else if (
407+
pkg.main !== undefined ||
408+
pkg.module !== undefined ||
409+
pkg.exports !== undefined ||
410+
pkg.typings !== undefined ||
411+
pkg.typescript !== undefined
412+
) {
413+
// if there is no bin property, we NEED to check the exports.
414+
expect("main", presetFields.main);
415+
expect("module", presetFields.module);
416+
expect("typings", presetFields.typings);
417+
expect("typescript.definition", presetFields.typescript.definition);
418+
419+
// For now we enforce a top level exports property
420+
expect("exports['.'].require", presetFields.exports["."].require);
421+
expect("exports['.'].import", presetFields.exports["."].import);
422+
expect("exports['.'].default", presetFields.exports["."].default);
423+
}
392424
}
393425

394426
async function copyToDist(cwd: string, files: string[], distDir: string) {
395427
const allFiles = await globby(files, { cwd });
396428

397429
return Promise.all(
398430
allFiles.map(async (file) => {
399-
if (await fs.pathExists(join(cwd, file))) {
431+
if (await fse.pathExists(join(cwd, file))) {
400432
const sourcePath = join(cwd, file);
401433
const destPath = join(cwd, distDir, file.replace("src/", ""));
402434
await mkdirp(dirname(destPath));
403-
await fs.copyFile(sourcePath, destPath);
435+
await fse.copyFile(sourcePath, destPath);
404436
}
405437
})
406438
);

src/commands/check.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const ExportsMapModel = zod.record(
2424
])
2525
);
2626

27+
const BinModel = zod.record(zod.string());
28+
2729
export const checkCommand = createCommand<{}, {}>((api) => {
2830
return {
2931
command: "check",
@@ -86,6 +88,7 @@ async function checkExportsMapIntegrity(args: {
8688
packageJSON: {
8789
name: string;
8890
exports: unknown;
91+
bin: unknown;
8992
};
9093
}) {
9194
const exportsMapResult = ExportsMapModel.safeParse(
@@ -210,6 +213,50 @@ async function checkExportsMapIntegrity(args: {
210213
legacyImportResult.all
211214
);
212215
}
216+
217+
if (args.packageJSON.bin) {
218+
const result = BinModel.safeParse(args.packageJSON.bin);
219+
if (result.success === false) {
220+
throw new Error(
221+
"Invalid format of bin field in package.json.\n" + result.error.message
222+
);
223+
}
224+
225+
const cache = new Set<string>();
226+
227+
for (const [_binary, filePath] of Object.entries(result.data)) {
228+
if (cache.has(filePath)) {
229+
continue;
230+
}
231+
cache.add(filePath);
232+
233+
const absoluteFilePath = path.join(args.cwd, filePath);
234+
await fse.stat(absoluteFilePath).catch(() => {
235+
throw new Error(
236+
"Could not find binary file '" + absoluteFilePath + "'."
237+
);
238+
});
239+
await fse
240+
.access(path.join(args.cwd, filePath), fse.constants.X_OK)
241+
.catch(() => {
242+
throw new Error(
243+
"Binary file '" +
244+
absoluteFilePath +
245+
"' is not executable.\n" +
246+
`Please set the executable bit e.g. by running 'chmod +x "${absoluteFilePath}"'.`
247+
);
248+
});
249+
250+
const contents = await fse.readFile(absoluteFilePath, "utf-8");
251+
if (contents.startsWith("#!/usr/bin/env node\n") === false) {
252+
throw new Error(
253+
"Binary file '" +
254+
absoluteFilePath +
255+
"' does not have a shebang.\n Please add '#!/usr/bin/env node' to the beginning of the file."
256+
);
257+
}
258+
}
259+
}
213260
}
214261

215262
const timeout = `;setTimeout(() => { throw new Error("The Node.js process hangs. There is probably some side-effects. All exports should be free of side effects.") }, 500).unref()`;

test/__fixtures__/simple-monorepo/packages/b/package.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,30 @@
2222
"default": "./dist/esm/index.js"
2323
}
2424
},
25-
"./*": {
25+
"./foo": {
2626
"require": {
27-
"types": "./dist/typings/*.d.ts",
28-
"default": "./dist/cjs/*.js"
27+
"types": "./dist/typings/foo.d.ts",
28+
"default": "./dist/cjs/foo.js"
2929
},
3030
"import": {
31-
"types": "./dist/typings/*.d.ts",
32-
"default": "./dist/esm/*.js"
31+
"types": "./dist/typings/foo.d.ts",
32+
"default": "./dist/esm/foo.js"
3333
},
3434
"default": {
35-
"types": "./dist/typings/*.d.ts",
36-
"default": "./dist/esm/*.js"
35+
"types": "./dist/typings/foo.d.ts",
36+
"default": "./dist/esm/foo.js"
3737
}
3838
},
3939
"./package.json": "./package.json"
4040
},
41+
"bin": {
42+
"bbb": "dist/cjs/log-the-world.js"
43+
},
4144
"buildOptions": {
4245
"input": "./src/index.ts"
4346
},
4447
"publishConfig": {
4548
"directory": "dist",
4649
"access": "public"
4750
}
48-
}
51+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
import * as foo from "./foo.js";
3+
import * as index from "./index.js";
4+
5+
console.log(foo, index);

test/integration.spec.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,21 +254,24 @@ it("can build a monorepo project", async () => {
254254
\\"default\\": \\"./esm/index.js\\"
255255
}
256256
},
257-
\\"./*\\": {
257+
\\"./foo\\": {
258258
\\"require\\": {
259-
\\"types\\": \\"./typings/*.d.ts\\",
260-
\\"default\\": \\"./cjs/*.js\\"
259+
\\"types\\": \\"./typings/foo.d.ts\\",
260+
\\"default\\": \\"./cjs/foo.js\\"
261261
},
262262
\\"import\\": {
263-
\\"types\\": \\"./typings/*.d.ts\\",
264-
\\"default\\": \\"./esm/*.js\\"
263+
\\"types\\": \\"./typings/foo.d.ts\\",
264+
\\"default\\": \\"./esm/foo.js\\"
265265
},
266266
\\"default\\": {
267-
\\"types\\": \\"./typings/*.d.ts\\",
268-
\\"default\\": \\"./esm/*.js\\"
267+
\\"types\\": \\"./typings/foo.d.ts\\",
268+
\\"default\\": \\"./esm/foo.js\\"
269269
}
270270
},
271271
\\"./package.json\\": \\"./package.json\\"
272+
},
273+
\\"bin\\": {
274+
\\"bbb\\": \\"cjs/log-the-world.js\\"
272275
}
273276
}"
274277
`);

0 commit comments

Comments
 (0)