UNPKG

@sanity/pkg-utils

Version:

Simple utilities for modern npm packages.

530 lines (524 loc) 17.3 kB
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