Skip to content

Commit

Permalink
chore: fix cli support
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l committed Jun 24, 2022
1 parent df4144a commit 34ee308
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 42 deletions.
86 changes: 59 additions & 27 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import execa from "execa";
import * as fse from "fs-extra";
import globby from "globby";
import pLimit from "p-limit";
import fs from "fs-extra";
import { resolve, join, dirname } from "path";
import { Consola } from "consola";
import get from "lodash.get";
Expand Down Expand Up @@ -124,9 +123,9 @@ export const buildCommand = createCommand<{}, {}>((api) => {
const cwd = rootPackageJSONPath.replace("/package.json", "");
const buildPath = join(cwd, ".bob");

await fs.remove(buildPath);
await fse.remove(buildPath);
await buildTypeScript(buildPath);
const pkg = await fs.readJSON(resolve(cwd, "package.json"));
const pkg = await fse.readJSON(resolve(cwd, "package.json"));
const fullName: string = pkg.name;

const distPath = join(cwd, "dist");
Expand Down Expand Up @@ -160,15 +159,15 @@ export const buildCommand = createCommand<{}, {}>((api) => {
packages.map((packagePath) =>
limit(async () => {
const cwd = packagePath.replace("/package.json", "");
const pkg = await fs.readJSON(resolve(cwd, "package.json"));
const pkg = await fse.readJSON(resolve(cwd, "package.json"));
const fullName: string = pkg.name;
return { packagePath, cwd, pkg, fullName };
})
)
);

const bobBuildPath = join(cwd, ".bob");
await fs.remove(bobBuildPath);
await fse.remove(bobBuildPath);
await buildTypeScript(bobBuildPath);

await Promise.all(
Expand Down Expand Up @@ -210,6 +209,7 @@ async function build({
pkg: {
name: string;
buildOptions: BuildOptions;
bin?: Record<string, string>;
};
fullName: string;
config: BobConfig;
Expand All @@ -225,10 +225,10 @@ async function build({
validatePackageJson(pkg);

// remove <project>/dist
await fs.remove(distPath);
await fse.remove(distPath);

// Copy type definitions
await fs.ensureDir(join(distPath, "typings"));
await fse.ensureDir(join(distPath, "typings"));

const declarations = await globby("**/*.d.ts", {
cwd: getBuildPath("esm"),
Expand All @@ -239,7 +239,7 @@ async function build({
await Promise.all(
declarations.map((filePath) =>
limit(() =>
fs.copy(
fse.copy(
join(getBuildPath("esm"), filePath),
join(distPath, "typings", filePath)
)
Expand All @@ -248,7 +248,7 @@ async function build({
);

// Move ESM to dist/esm
await fs.ensureDir(join(distPath, "esm"));
await fse.ensureDir(join(distPath, "esm"));

const esmFiles = await globby("**/*.js", {
cwd: getBuildPath("esm"),
Expand All @@ -259,7 +259,7 @@ async function build({
await Promise.all(
esmFiles.map((filePath) =>
limit(() =>
fs.copy(
fse.copy(
join(getBuildPath("esm"), filePath),
join(distPath, "esm", filePath)
)
Expand All @@ -268,7 +268,7 @@ async function build({
);

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

const cjsFiles = await globby("**/*.js", {
cwd: getBuildPath("cjs"),
Expand All @@ -279,7 +279,7 @@ async function build({
await Promise.all(
cjsFiles.map((filePath) =>
limit(() =>
fs.copy(
fse.copy(
join(getBuildPath("cjs"), filePath),
join(distPath, "cjs", filePath)
)
Expand All @@ -288,13 +288,13 @@ async function build({
);

// Add package.json to dist/cjs to ensure files are interpreted as commonjs
await fs.writeFile(
await fse.writeFile(
join(distPath, "cjs", "package.json"),
JSON.stringify({ type: "commonjs" })
);

// move the package.json to dist
await fs.writeFile(
await fse.writeFile(
join(distPath, "package.json"),
JSON.stringify(rewritePackageJson(pkg), null, 2)
);
Expand All @@ -306,6 +306,21 @@ async function build({
distPath
);

if (pkg.bin) {
if (globalThis.process.platform === "win32") {
console.warn(
"Package includes bin files, but cannot set the executable bit on Windows.\n" +
"Please manually set the executable bit on the bin files before publishing."
);
} else {
await Promise.all(
Object.values(pkg.bin).map((filePath) =>
execa("chmod", ["+x", join(cwd, filePath)])
)
);
}
}

reporter.success(`Built ${pkg.name}`);
}

Expand Down Expand Up @@ -378,29 +393,46 @@ export function validatePackageJson(pkg: any) {
);
}

expect("main", presetFields.main);
expect("module", presetFields.module);
expect("typings", presetFields.typings);
expect("typescript.definition", presetFields.typescript.definition);

expect("exports['.'].require", presetFields.exports["."].require);
expect("exports['.'].import", presetFields.exports["."].import);
expect("exports['.'].default", presetFields.exports["."].default);
expect("exports['./*'].require", presetFields.exports["./*"].require);
expect("exports['./*'].import", presetFields.exports["./*"].import);
expect("exports['./*'].default", presetFields.exports["./*"].default);
// If the package has NO binary we need to check the exports map.
// a package should either
// 1. have a bin property
// 2. have a exports property
// 3. have an exports and bin property
if (Object.keys(pkg.bin ?? {}).length === 0) {
expect("main", presetFields.main);
expect("module", presetFields.module);
expect("typings", presetFields.typings);
expect("typescript.definition", presetFields.typescript.definition);
} else if (
pkg.main !== undefined ||
pkg.module !== undefined ||
pkg.exports !== undefined ||
pkg.typings !== undefined ||
pkg.typescript !== undefined
) {
// if there is no bin property, we NEED to check the exports.
expect("main", presetFields.main);
expect("module", presetFields.module);
expect("typings", presetFields.typings);
expect("typescript.definition", presetFields.typescript.definition);

// For now we enforce a top level exports property
expect("exports['.'].require", presetFields.exports["."].require);
expect("exports['.'].import", presetFields.exports["."].import);
expect("exports['.'].default", presetFields.exports["."].default);
}
}

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

return Promise.all(
allFiles.map(async (file) => {
if (await fs.pathExists(join(cwd, file))) {
if (await fse.pathExists(join(cwd, file))) {
const sourcePath = join(cwd, file);
const destPath = join(cwd, distDir, file.replace("src/", ""));
await mkdirp(dirname(destPath));
await fs.copyFile(sourcePath, destPath);
await fse.copyFile(sourcePath, destPath);
}
})
);
Expand Down
47 changes: 47 additions & 0 deletions src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const ExportsMapModel = zod.record(
])
);

const BinModel = zod.record(zod.string());

export const checkCommand = createCommand<{}, {}>((api) => {
return {
command: "check",
Expand Down Expand Up @@ -86,6 +88,7 @@ async function checkExportsMapIntegrity(args: {
packageJSON: {
name: string;
exports: unknown;
bin: unknown;
};
}) {
const exportsMapResult = ExportsMapModel.safeParse(
Expand Down Expand Up @@ -210,6 +213,50 @@ async function checkExportsMapIntegrity(args: {
legacyImportResult.all
);
}

if (args.packageJSON.bin) {
const result = BinModel.safeParse(args.packageJSON.bin);
if (result.success === false) {
throw new Error(
"Invalid format of bin field in package.json.\n" + result.error.message
);
}

const cache = new Set<string>();

for (const [_binary, filePath] of Object.entries(result.data)) {
if (cache.has(filePath)) {
continue;
}
cache.add(filePath);

const absoluteFilePath = path.join(args.cwd, filePath);
await fse.stat(absoluteFilePath).catch(() => {
throw new Error(
"Could not find binary file '" + absoluteFilePath + "'."
);
});
await fse
.access(path.join(args.cwd, filePath), fse.constants.X_OK)
.catch(() => {
throw new Error(
"Binary file '" +
absoluteFilePath +
"' is not executable.\n" +
`Please set the executable bit e.g. by running 'chmod +x "${absoluteFilePath}"'.`
);
});

const contents = await fse.readFile(absoluteFilePath, "utf-8");
if (contents.startsWith("#!/usr/bin/env node\n") === false) {
throw new Error(
"Binary file '" +
absoluteFilePath +
"' does not have a shebang.\n Please add '#!/usr/bin/env node' to the beginning of the file."
);
}
}
}
}

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()`;
Expand Down
19 changes: 11 additions & 8 deletions test/__fixtures__/simple-monorepo/packages/b/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,30 @@
"default": "./dist/esm/index.js"
}
},
"./*": {
"./foo": {
"require": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/cjs/*.js"
"types": "./dist/typings/foo.d.ts",
"default": "./dist/cjs/foo.js"
},
"import": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
"types": "./dist/typings/foo.d.ts",
"default": "./dist/esm/foo.js"
},
"default": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
"types": "./dist/typings/foo.d.ts",
"default": "./dist/esm/foo.js"
}
},
"./package.json": "./package.json"
},
"bin": {
"bbb": "dist/cjs/log-the-world.js"
},
"buildOptions": {
"input": "./src/index.ts"
},
"publishConfig": {
"directory": "dist",
"access": "public"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import * as foo from "./foo.js";
import * as index from "./index.js";

console.log(foo, index);
17 changes: 10 additions & 7 deletions test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,21 +254,24 @@ it("can build a monorepo project", async () => {
\\"default\\": \\"./esm/index.js\\"
}
},
\\"./*\\": {
\\"./foo\\": {
\\"require\\": {
\\"types\\": \\"./typings/*.d.ts\\",
\\"default\\": \\"./cjs/*.js\\"
\\"types\\": \\"./typings/foo.d.ts\\",
\\"default\\": \\"./cjs/foo.js\\"
},
\\"import\\": {
\\"types\\": \\"./typings/*.d.ts\\",
\\"default\\": \\"./esm/*.js\\"
\\"types\\": \\"./typings/foo.d.ts\\",
\\"default\\": \\"./esm/foo.js\\"
},
\\"default\\": {
\\"types\\": \\"./typings/*.d.ts\\",
\\"default\\": \\"./esm/*.js\\"
\\"types\\": \\"./typings/foo.d.ts\\",
\\"default\\": \\"./esm/foo.js\\"
}
},
\\"./package.json\\": \\"./package.json\\"
},
\\"bin\\": {
\\"bbb\\": \\"cjs/log-the-world.js\\"
}
}"
`);
Expand Down

0 comments on commit 34ee308

Please sign in to comment.