@sanity/pkg-utils
Version:
Simple utilities for modern npm packages.
530 lines (524 loc) • 17.3 kB
JavaScript
import { writeFile, readdir, lstat } from "node:fs/promises";
import { relative, resolve, dirname } from "node:path";
import { mkdirp } from "mkdirp";
import prompts from "prompts";
import { fileExists } from "./fileExists.js";
import { isRecord, createLogger, handleError } from "./handleError.js";
import { execSync } from "node:child_process";
import { getLatestVersion } from "get-latest-version";
import gitUrlParse from "git-url-parse";
import { outdent } from "outdent";
import { format } from "prettier";
import { defineTemplateOption } from "./define.js";
const promptsTypes = {
string: "text"
};
async function createFromTemplate(options) {
const { cwd, logger, packagePath, template: templateOrResolver } = options, template = typeof templateOrResolver == "function" ? await templateOrResolver({ cwd, logger, packagePath }) : templateOrResolver;
logger.log("create new package at", relative(cwd, packagePath));
const templateOptions = {};
for (const templateOption of template.options) {
const templateValidate = templateOption.validate, res = await prompts(
{
type: promptsTypes[templateOption.type],
name: templateOption.name,
message: templateOption.description,
validate: templateValidate ? (prev) => templateValidate(prev) : void 0,
initial: typeof templateOption.initial == "function" ? templateOption.initial(templateOptions) : templateOption.initial
},
{ onCancel: () => process.exit(0) }
);
templateOptions[templateOption.name] = templateOption.parse ? templateOption.parse(res[templateOption.name]) : res[templateOption.name];
}
const features = {};
for (const templateFeature of template.features) {
const res = templateFeature.optional ? await prompts(
{
type: "confirm",
name: "confirm",
message: `use ${templateFeature.name}?`,
initial: templateFeature.initial
},
{ onCancel: () => process.exit(0) }
) : void 0;
features[templateFeature.name] = res?.confirm || !templateFeature.optional;
}
const files = await template.getFiles(templateOptions, features);
files.sort((a, b) => a.name.localeCompare(b.name));
for (const file of files) {
const filePath = resolve(packagePath, file.name);
await mkdirp(dirname(filePath)), await writeFile(filePath, file.contents.trim() + `
`), logger.success(`wrote ${relative(cwd, filePath)}`);
}
}
async function isEmptyDirectory(dirPath) {
return (await readdir(dirPath)).length === 0;
}
const overridableDefaults = {
endOfLine: "lf",
tabWidth: 2,
useTabs: !1
}, json5 = {
files: ["*.json5"],
options: {
quoteProps: "preserve",
singleQuote: !1
}
}, yaml = {
files: ["*.yml"],
options: {
singleQuote: !1
}
}, config = {
...overridableDefaults,
printWidth: 100,
semi: !1,
singleQuote: !0,
quoteProps: "consistent",
bracketSpacing: !1,
plugins: ["prettier-plugin-packagejson"],
overrides: [json5, yaml]
}, RE_NAME = /^(?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)\/)?[a-z0-9-~][a-z0-9-._~]*$/i, defaultTemplate = async ({ cwd, logger, packagePath }) => {
const gitConfig = getGitUserConfig(cwd);
return {
options: [
defineTemplateOption({
name: "repo",
type: "string",
description: "git url",
validate: (v) => {
if (!v) return !0;
try {
return gitUrlParse(v), !0;
} catch {
return "invalid git url";
}
},
parse: (v) => {
if (!v) return null;
const result = gitUrlParse(v);
return { source: result.source, owner: result.owner, name: result.name };
}
}),
defineTemplateOption({
name: "pkgName",
type: "string",
description: "package name",
initial: (options) => options.repo?.name || void 0,
validate: (v) => v ? RE_NAME.exec(v) ? !0 : "invalid package name" : "package name is required",
parse: (v) => {
if (!v)
throw new Error("package name is required");
if (!RE_NAME.exec(v))
throw new Error("invalid package name");
const [scope, name] = v.split("/");
return { scope, name, fullName: v };
}
}),
defineTemplateOption({
name: "description",
type: "string",
description: "package description"
}),
defineTemplateOption({
name: "authorName",
type: "string",
description: "package author name",
initial: gitConfig.user
}),
defineTemplateOption({
name: "authorEmail",
type: "string",
description: "package author email",
initial: gitConfig.email
}),
defineTemplateOption({
name: "license",
type: "string",
description: "package license",
initial: "MIT",
validate: (v) => v ? !0 : "license is required"
})
],
features: [
{
name: "eslint",
optional: !0,
initial: !0
},
{
name: "prettier",
optional: !0,
initial: !0
},
{
name: "typescript",
optional: !0,
initial: !0
}
],
async getFiles(options, features) {
const { pkgName, repo } = options, { fullName: name } = pkgName, author = [options.authorName, options.authorEmail && `<${options.authorEmail}>`].filter(Boolean).join(" ") ?? void 0, pkgJson = {
name,
version: "0.0.0",
description: options.description ?? void 0,
keywords: [],
homepage: void 0,
bugs: void 0,
repository: void 0,
license: options.license,
author,
sideEffects: !1,
type: "module",
exports: {
".": {
source: features.typescript ? "./src/index.ts" : "./src/index.js",
require: "./dist/index.cjs",
default: "./dist/index.js"
},
"./package.json": "./package.json"
},
main: "./dist/index.cjs",
module: "./dist/index.js",
types: void 0,
files: ["dist", "src"],
scripts: {
build: "pkg build --strict --clean --check",
format: features.prettier ? "prettier --write --cache --ignore-unknown ." : void 0
},
"lint-staged": features.prettier ? {
"*": ["prettier --write --cache --ignore-unknown"]
} : void 0,
browserslist: "extends @sanity/browserslist-config",
prettier: features.prettier ? "@sanity/prettier-config" : void 0,
dependencies: {},
devDependencies: {
"@sanity/tsconfig": features.typescript ? "^1" : void 0,
"@sanity/pkg-utils": "^9",
"@sanity/prettier-config": features.prettier ? "^1" : void 0,
"@typescript-eslint/eslint-plugin": void 0,
"@typescript-eslint/parser": void 0,
eslint: void 0,
"eslint-config-prettier": void 0,
"eslint-plugin-import": void 0,
"eslint-plugin-prettier": void 0,
"eslint-plugin-simple-import-sort": void 0,
"lint-staged": "^15",
prettier: features.prettier ? "^3" : void 0,
typescript: void 0
},
engines: {
node: ">=20.19 <22 || >=22.12"
}
}, files = [];
if (files.push({
name: ".editorconfig",
contents: outdent`
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
`
}), files.push({
name: ".gitignore",
contents: outdent`
*.local
*.log
*.tgz
.DS_Store
dist
etc
node_modules
`
}), features.prettier && files.push({
name: ".prettierignore",
contents: outdent`
dist
pnpm-lock.yaml
`
}), repo && (pkgJson.repository = {
type: "git",
url: `git+ssh://git@${repo.source}/${repo.owner}/${repo.name}.git`
}, pkgJson.bugs = {
url: `https://${repo.source}/${repo.owner}/${repo.name}/issues`
}, pkgJson.homepage = `https://${repo.source}/${repo.owner}/${repo.name}#readme`), features.typescript) {
pkgJson.types = "./dist/index.d.ts", pkgJson.scripts = {
...pkgJson.scripts,
"ts:check": "tsc --noEmit"
};
const devDependencies = pkgJson.devDependencies;
isRecord(devDependencies) && (devDependencies.typescript = "^5.9");
}
if (features.eslint) {
const eslintConfig = {
root: !0,
env: {
browser: !0,
es6: !0,
node: !0
},
extends: [
"eslint:recommended",
features.prettier ? "plugin:prettier/recommended" : void 0
].filter(Boolean),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module"
},
plugins: [
"import",
"simple-import-sort",
features.prettier ? "prettier" : void 0
].filter(Boolean),
rules: {
"no-console": "error",
"no-shadow": "error",
"no-warning-comments": ["warn", { location: "start", terms: ["todo", "fixme"] }],
"quote-props": ["warn", "consistent-as-needed"],
"simple-import-sort/exports": "warn",
"simple-import-sort/imports": "warn",
strict: ["warn", "global"]
}
};
if (files.push({
name: ".eslintignore",
contents: outdent`
dist
`
}), pkgJson.scripts = {
...pkgJson.scripts,
lint: features.typescript ? "eslint . --ext .cjs,.js,.ts,.tsx" : "eslint . --ext .cjs,.js"
}, pkgJson.devDependencies = {
...pkgJson.devDependencies,
eslint: "^8",
"eslint-config-prettier": features.prettier ? "^9" : void 0,
"eslint-plugin-import": "^2",
"eslint-plugin-prettier": features.prettier ? "^5" : void 0,
"eslint-plugin-simple-import-sort": "^12"
}, features.typescript) {
pkgJson.devDependencies = {
...pkgJson.devDependencies,
"@typescript-eslint/eslint-plugin": "^7",
"@typescript-eslint/parser": "^7"
};
const eslintConfigOverride = {
files: ["**/*.ts", "**/*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: ["./tsconfig.json"]
},
extends: [
"eslint:recommended",
features.prettier ? "plugin:prettier/recommended" : void 0,
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
].filter(Boolean),
plugins: [
"import",
"@typescript-eslint",
"simple-import-sort",
features.prettier ? "prettier" : void 0
].filter(Boolean),
rules: {
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-empty-interface": "off"
}
};
eslintConfig.overrides = [eslintConfigOverride];
}
files.push({
name: ".eslintrc.cjs",
contents: await prettierFormat(
resolve(packagePath, ".eslintrc.cjs"),
outdent`
'use strict'
/** @type import('eslint').Linter.Config */
module.exports = ${JSON.stringify(eslintConfig, null, 2)}
`,
config
)
});
}
features.typescript && (files.push({
name: "tsconfig.settings.json",
contents: await prettierFormat(
resolve(packagePath, "tsconfig.settings.json"),
outdent`
{
"extends": "@sanity/tsconfig/strictest",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist"
}
}
`,
config
)
}), files.push({
name: "tsconfig.dist.json",
contents: await prettierFormat(
resolve(packagePath, "tsconfig.dist.json"),
outdent`
{
"extends": "./tsconfig.settings",
"include": ["./src"],
"exclude": ["./src/**/*.test.ts"]
}
`,
config
)
}), files.push({
name: "tsconfig.json",
contents: await prettierFormat(
resolve(packagePath, "tsconfig.json"),
outdent`
{
"extends": "./tsconfig.settings",
"include": ["./**/*.cjs", "./**/*.ts", "./**/*.tsx"],
"exclude": ["./node_modules"]
}
`,
config
)
})), features.typescript ? (files.push({
name: "package.config.ts",
contents: await prettierFormat(
resolve(packagePath, "package.config.ts"),
outdent`
import {defineConfig} from '@sanity/pkg-utils'
// https://github.com/sanity-io/pkg-utils#configuration
export default defineConfig({
// the path to the tsconfig file for distributed builds
tsconfig: 'tsconfig.dist.json',
})
`,
config
)
}), files.push({
name: "src/index.ts",
contents: await prettierFormat(
resolve(packagePath, "src/index.ts"),
outdent`
/** @public */
export function main(): void {
//
}
`,
config
)
})) : (files.push({
name: "package.config.js",
contents: await prettierFormat(
resolve(packagePath, "package.config.js"),
outdent`
import {defineConfig} from '@sanity/pkg-utils'
export default defineConfig({
extract: {
rules: {
// do not require internal members to be prefixed with \`_\`
'ae-internal-missing-underscore': 'off',
},
},
})
`,
config
)
}), files.push({
name: "src/index.js",
contents: await prettierFormat(
resolve(packagePath, "src/index.js"),
outdent`
/** @public */
export function main() {
//
}
`,
config
)
}));
try {
pkgJson.dependencies = await resolveLatestDeps(pkgJson.dependencies ?? {});
} catch (error) {
logger.warn(error instanceof Error ? error.message : error);
}
try {
pkgJson.devDependencies = await resolveLatestDeps(pkgJson.devDependencies ?? {});
} catch (error) {
logger.warn(error instanceof Error ? error.message : error);
}
return files.push({
name: "package.json",
contents: await prettierFormat(
resolve(packagePath, "package.json"),
JSON.stringify(pkgJson, null, 2),
config
)
}), files;
}
};
};
function prettierFormat(filepath, input, prettierOptions) {
return format(input, { ...prettierOptions, plugins: [], filepath });
}
async function resolveLatestDeps(deps) {
const depsEntries = Object.entries(deps), latestDeps = {};
for (const entry of depsEntries) {
const [name, version] = entry;
if (version) {
const latestVersion = await getLatestVersion(name, { range: version });
latestDeps[name] = latestVersion ? `^${latestVersion}` : version;
}
}
return latestDeps;
}
function getGitUserConfig(cwd) {
let user, email;
try {
user = execSync("git config user.name", { encoding: "utf8", cwd }).trim() || void 0, email = execSync("git config user.email", { encoding: "utf8", cwd }).trim() || void 0;
} catch {
}
return { user, email };
}
async function init(options) {
if (!options.cwd)
throw new Error("Missing required option: cwd");
if (!options.path)
throw new Error("Missing required option: path");
const logger = createLogger(), packagePath = resolve(options.cwd, options.path);
await ensurePackagePath(packagePath), await createFromTemplate({
cwd: options.cwd,
logger,
template: defaultTemplate,
packagePath
});
}
async function ensurePackagePath(packagePath) {
if (!fileExists(packagePath)) {
await mkdirp(packagePath);
return;
}
if (!(await lstat(packagePath)).isDirectory())
throw new Error("the package path is a file, not a directory");
if (!await isEmptyDirectory(packagePath))
throw new Error("the package directory is not empty");
}
async function initAction(options) {
try {
await init({
cwd: process.cwd(),
path: options.path
});
} catch (err) {
handleError(err);
}
}
export {
initAction
};
//# sourceMappingURL=initAction.js.map