From 219561bc90d29d4babd331588ce16ca6d9b9bdd2 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 25 Aug 2022 15:12:03 +0200 Subject: [PATCH] feat: added --ecosystem-preset to init --- .releaserc.json | 5 +- .../ecosystem/.github/workflows/main.yml | 85 +++++++++++++++++++ assets/splat/ecosystem/.husky/commit-msg | 4 + assets/splat/ecosystem/.husky/pre-commit | 4 + assets/splat/ecosystem/.releaserc.json | 4 + assets/splat/ecosystem/commitlint.config.js | 3 + assets/splat/ecosystem/renovate.json | 7 ++ src/actions/init.ts | 6 ++ src/actions/splat.ts | 19 +++-- src/cmds/init.ts | 2 + src/ecosystem/ecosystem-preset.ts | 32 +++++++ src/util/files.ts | 11 +++ test/init.test.ts | 76 +++++++++++++---- 13 files changed, 235 insertions(+), 23 deletions(-) create mode 100644 assets/splat/ecosystem/.github/workflows/main.yml create mode 100755 assets/splat/ecosystem/.husky/commit-msg create mode 100755 assets/splat/ecosystem/.husky/pre-commit create mode 100644 assets/splat/ecosystem/.releaserc.json create mode 100644 assets/splat/ecosystem/commitlint.config.js create mode 100644 assets/splat/ecosystem/renovate.json create mode 100644 src/ecosystem/ecosystem-preset.ts diff --git a/.releaserc.json b/.releaserc.json index fd9fad14..d29b7c56 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,4 +1,7 @@ { "extends": "@sanity/semantic-release-preset", - "branches": ["main"] + "branches": [ + "main", + {"name": "ecosystem-preset", "channel": "ecosystem-preset", "prerelease": true} + ] } diff --git a/assets/splat/ecosystem/.github/workflows/main.yml b/assets/splat/ecosystem/.github/workflows/main.yml new file mode 100644 index 00000000..c346f68d --- /dev/null +++ b/assets/splat/ecosystem/.github/workflows/main.yml @@ -0,0 +1,85 @@ +name: CI & Release +on: + # Build on pushes to release branches + push: + branches: [main] + # Build on pull requests targeting release branches + pull_request: + branches: [main] + workflow_dispatch: + inputs: + release: + description: Release new version + required: true + default: false + type: boolean + +jobs: + log-the-inputs: + name: Log inputs + runs-on: ubuntu-latest + steps: + - run: | + echo "Inputs: $INPUTS" + env: + INPUTS: ${{ toJSON(inputs) }} + + build: + name: Lint & Build + runs-on: ubuntu-latest + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + cache: npm + - run: npm ci + - run: npm run lint --if-present + - run: npm run prepublishOnly + + test: + name: Test + needs: build + strategy: + matrix: + os: [ macos-latest, ubuntu-latest ] + node: [ lts/*, current ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npm test --if-present + + release: + name: Semantic release + needs: test + runs-on: ubuntu-latest + # only run if opt-in during workflow_dispatch + if: inputs.release == true + steps: + - uses: actions/checkout@v3 + with: + # Need to fetch entire commit history to + # analyze every commit since last release + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + cache: npm + - run: npm ci + # Branches that will release new versions are defined in .releaserc.json + - run: npx semantic-release + # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state + # e.g. git tags were pushed but it exited before `npm publish` + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/assets/splat/ecosystem/.husky/commit-msg b/assets/splat/ecosystem/.husky/commit-msg new file mode 100755 index 00000000..fa859b0d --- /dev/null +++ b/assets/splat/ecosystem/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no -- commitlint --edit "" diff --git a/assets/splat/ecosystem/.husky/pre-commit b/assets/splat/ecosystem/.husky/pre-commit new file mode 100755 index 00000000..36af2198 --- /dev/null +++ b/assets/splat/ecosystem/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/assets/splat/ecosystem/.releaserc.json b/assets/splat/ecosystem/.releaserc.json new file mode 100644 index 00000000..fd9fad14 --- /dev/null +++ b/assets/splat/ecosystem/.releaserc.json @@ -0,0 +1,4 @@ +{ + "extends": "@sanity/semantic-release-preset", + "branches": ["main"] +} diff --git a/assets/splat/ecosystem/commitlint.config.js b/assets/splat/ecosystem/commitlint.config.js new file mode 100644 index 00000000..98ee7dfc --- /dev/null +++ b/assets/splat/ecosystem/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +} diff --git a/assets/splat/ecosystem/renovate.json b/assets/splat/ecosystem/renovate.json new file mode 100644 index 00000000..1195e4ea --- /dev/null +++ b/assets/splat/ecosystem/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>sanity-io/renovate-presets//ecosystem/auto", + "github>sanity-io/renovate-presets//ecosystem/studio-v3" + ] +} diff --git a/src/actions/init.ts b/src/actions/init.ts index 2b189869..2903ce54 100644 --- a/src/actions/init.ts +++ b/src/actions/init.ts @@ -7,6 +7,7 @@ import {TypedFlags} from 'meow' import {getPackage} from '../npm/package' import {defaultSourceJs, defaultSourceTs} from '../configs/default-source' import {incompatiblePluginPackage} from '../constants' +import {ecosystemDevDependencies} from '../ecosystem/ecosystem-preset' export const initFlags = { ...sharedFlags, @@ -33,6 +34,10 @@ export const initFlags = { type: 'boolean', default: true, }, + ecosystemPreset: { + type: 'boolean', + default: false, + }, gitignore: { type: 'boolean', default: true, @@ -77,6 +82,7 @@ export async function init(options: InitOptions) { devDependencies = { ...devDependencies, ...defaultDevDependencies, + ...(options.flags.ecosystemPreset ? await ecosystemDevDependencies() : []), ...(await resolveLatestVersions(['rimraf'])), } peerDependencies = { diff --git a/src/actions/splat.ts b/src/actions/splat.ts index 9d13a138..c5c80448 100644 --- a/src/actions/splat.ts +++ b/src/actions/splat.ts @@ -18,6 +18,7 @@ import { } from '../util/files' import {InitFlags} from './init' import {PackageJson} from './verify/types' +import {ecosystemPresetFiles} from '../ecosystem/ecosystem-preset' const bannedFields = ['login', 'description', 'projecturl', 'email'] const preferredLicenses = ['MIT', 'ISC', 'BSD-3-Clause'] @@ -29,6 +30,8 @@ const otherLicenses = Object.keys(licenses.list).filter((id) => { ) }) +export type FromTo = {from: string | string[]; to: string | string[]} + export interface SplatOptions { basePath: string requireUserConfirmation?: boolean @@ -284,8 +287,6 @@ async function resolveProjectDescription(basePath: string, pkg: PackageJson | un } } -type FromTo = {from: string; to: string} - async function writeStaticAssets({basePath, flags}: SplatOptions) { const assetsDir = await findAssetsDir() @@ -300,18 +301,26 @@ async function writeStaticAssets({basePath, flags}: SplatOptions) { flags.gitignore && {from: 'gitignore', to: '.gitignore'}, flags.typescript && {from: 'template-tsconfig.json', to: 'tsconfig.json'}, flags.prettier && {from: 'prettierrc.js', to: '.prettierrc.js'}, - ].filter((f): f is FromTo => !!f) + + ...(flags.ecosystemPreset ? ecosystemPresetFiles() : []), + ].filter((f: false | FromTo): f is FromTo => !!f) const writes: string[] = [] for (const file of files) { - if (await copyFileWithOverwritePrompt(from(file.from), to(file.to), flags)) { - writes.push(file.to) + const fromPath = asArray(file.from) + const toPath = asArray(file.to) + if (await copyFileWithOverwritePrompt(from(...fromPath), to(...toPath), flags)) { + writes.push(path.join(...toPath)) } } return writes } +function asArray(input: string | string[]): string[] { + return typeof input === 'string' ? [input] : input +} + /** * assets dir might be in higher or lower in the dir hierarchy depending on * if we run from lib or src diff --git a/src/cmds/init.ts b/src/cmds/init.ts index 4d5844a6..e9c3cc28 100644 --- a/src/cmds/init.ts +++ b/src/cmds/init.ts @@ -24,6 +24,8 @@ Options --no-scripts Disables scripts from being added to package.json --no-install Disables automatically running package manager install + --ecosystem-preset [beta]: Adds opinionated files and dependencies for conventional-commits, githubworkflow, reonvatebot and semantic-release + --name [package-name] Use the provided package-name --author [name] Use the provided author --repo [url] Use the provided repo url diff --git a/src/ecosystem/ecosystem-preset.ts b/src/ecosystem/ecosystem-preset.ts new file mode 100644 index 00000000..da8117f0 --- /dev/null +++ b/src/ecosystem/ecosystem-preset.ts @@ -0,0 +1,32 @@ +import {FromTo} from '../actions/splat' +import {resolveLatestVersions} from '../npm/resolveLatestVersions' + +export function ecosystemPresetFiles(): FromTo[] { + return [ + { + from: ['ecosystem', '.github', 'workflows', 'main.yml'], + to: ['.github', 'workflows', 'main.yml'], + }, + { + from: ['ecosystem', '.husky', 'commit-msg'], + to: ['.husky', 'commit-msg'], + }, + { + from: ['ecosystem', '.husky', 'pre-commit'], + to: ['.husky', 'pre-commit'], + }, + {from: ['ecosystem', '.releaserc.json'], to: '.releaserc.json'}, + {from: ['ecosystem', 'commitlint.config.js'], to: 'commitlint.config.js'}, + {from: ['ecosystem', 'renovate.json'], to: 'renovate.json'}, + ] +} + +export async function ecosystemDevDependencies(): Promise> { + return resolveLatestVersions([ + '@commitlint/cli', + '@commitlint/config-conventional', + '@sanity/semantic-release-preset', + 'husky', + 'lint-staged', + ]) +} diff --git a/src/util/files.ts b/src/util/files.ts index bd236862..99c059a3 100644 --- a/src/util/files.ts +++ b/src/util/files.ts @@ -146,6 +146,8 @@ export async function copyFileWithOverwritePrompt(from: string, to: string, flag return false } + await ensureDirectoryExists(to) + if ( !flags.force && (await fileExists(to)) && @@ -161,6 +163,15 @@ export async function copyFileWithOverwritePrompt(from: string, to: string, flag return true } +async function ensureDirectoryExists(filePath: string) { + const dirname = path.dirname(filePath) + if (await fileExists(dirname)) { + return true + } + await ensureDirectoryExists(dirname) + await mkdir(dirname) +} + export async function fileEqualsData(filePath: string, content: string) { const contentHash = crypto.createHash('sha1').update(content).digest('hex') const remoteHash = await getFileHash(filePath) diff --git a/test/init.test.ts b/test/init.test.ts index 73f13b6b..9c02a054 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -13,6 +13,24 @@ import {fileExists} from '../src/util/files' import {incompatiblePluginPackage} from '../src/constants' import {PackageJson} from '../src/actions/verify/types' +const defaultDevDependencies = [ + '@sanity/plugin-kit', + '@typescript-eslint/eslint-plugin', + '@typescript-eslint/parser', + 'eslint', + 'eslint-config-prettier', + 'eslint-config-sanity', + 'eslint-plugin-prettier', + 'eslint-plugin-react', + 'eslint-plugin-react-hooks', + 'parcel', + 'prettier', + 'react', + 'rimraf', + 'sanity', + 'typescript', +] + tap.test('plugin-kit init --force in empty directory', async (t) => { await testFixture({ fixturePath: 'init/empty', @@ -98,25 +116,10 @@ tap.test('plugin-kit init --force in empty directory', async (t) => { ['react', 'sanity'], 'should have expected peerDependencies' ) + t.strictSame( Object.keys(pkg.devDependencies ?? {}), - [ - '@sanity/plugin-kit', - '@typescript-eslint/eslint-plugin', - '@typescript-eslint/parser', - 'eslint', - 'eslint-config-prettier', - 'eslint-config-sanity', - 'eslint-plugin-prettier', - 'eslint-plugin-react', - 'eslint-plugin-react-hooks', - 'parcel', - 'prettier', - 'react', - 'rimraf', - 'sanity', - 'typescript', - ], + defaultDevDependencies, 'should have expected devDependencies' ) }, @@ -178,3 +181,42 @@ tap.test('plugin-kit init --force with all the opt-outs in empty directory', asy }, }) }) + +tap.test('plugin-kit init --force --ecosystem-preset in empty directory', async (t) => { + await testFixture({ + fixturePath: 'init/empty', + relativeOutPath: 'defaults-ecosystem', + command: ({outputDir}) => + runCliCommand('init', [outputDir, ...initTestArgs, '--ecosystem-preset']), + assert: async ({result: {stdout, stderr}, outputDir}) => { + t.equal(stderr, '', 'should have empty stderr') + + const fileContains = fileContainsValidator(t, outputDir) + + await fileContains(path.join('.github', 'workflows', 'main.yml'), 'CI & Release') + await fileContains(path.join('.husky', 'commit-msg'), 'npx --no -- commitlint') + await fileContains(path.join('.husky', 'pre-commit'), 'npx lint-staged') + await fileContains(path.join('.releaserc.json'), '@sanity/semantic-release-preset') + await fileContains(path.join('commitlint.config.js'), '@commitlint/config-conventional') + await fileContains( + path.join('renovate.json'), + 'github>sanity-io/renovate-presets//ecosystem/auto' + ) + + const pkg: PackageJson = JSON.parse(await readFile(path.join(outputDir, 'package.json'))) + + t.strictSame( + Object.keys(pkg.devDependencies ?? {}), + [ + ...defaultDevDependencies, + '@commitlint/cli', + '@commitlint/config-conventional', + '@sanity/semantic-release-preset', + 'husky', + 'lint-staged', + ].sort(), + 'should have expected devDependencies' + ) + }, + }) +})