diff --git a/.git-cliff/changelog.toml b/.git-cliff/changelog.toml new file mode 100644 index 0000000..e2ec3d9 --- /dev/null +++ b/.git-cliff/changelog.toml @@ -0,0 +1,124 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[remote.github] +owner = "biomejs" +repo = "create-biome-reproduction" + +[bump] +features_always_bump_minor = true +breaking_always_bump_major = true +initial_tag = "0.1.0" + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% if version -%} + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} + ## [Unreleased] +{% endif -%} + +### Details\ + +{% for group, commits in commits | group_by(attribute="group") %} + #### {{ group | upper_first }} + {%- for commit in commits %} + - {{ commit.message | upper_first | trim }}\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%} + {% if commit.github.pr_number %} in \ + [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \ + {%- endif -%} + {% endfor %} +{% endfor %} + +{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ## New Contributors +{%- endif -%} + +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + * @{{ contributor.username }} made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}](({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %} +{%- endfor %}\n +""" +# template for the changelog footer +footer = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% for release in releases -%} + {% if release.version -%} + {% if release.previous.version -%} + [{{ release.version | trim_start_matches(pat="v") }}]: \ + {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }} + {% endif -%} + {% else -%} + {% if release.previous.version -%} + [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD + {% endif -%} + {% endif -%} +{% endfor %} + +""" +# remove the leading and trailing whitespace from the templates +trim = true + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # remove issue numbers from commits + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^doc", group = "๐Ÿ“š Documentation" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^refactor", group = "๐Ÿšœ Refactor" }, + { message = "^style", group = "๐ŸŽจ Styling" }, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = "^chore\\(release\\): prepare", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|ci", group = "โš™๏ธ Miscellaneous Tasks", skip = true }, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, + { message = "^revert", group = "โ—€๏ธ Revert" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = true +# regex for matching git tags +tag_pattern = "v[0-9].*" +# regex for skipping tags +skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/.git-cliff/release-notes.toml b/.git-cliff/release-notes.toml new file mode 100644 index 0000000..4dcda95 --- /dev/null +++ b/.git-cliff/release-notes.toml @@ -0,0 +1,109 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[remote.github] +owner = "biomejs" +repo = "create-biome-reproduction" + +[bump] +features_always_bump_minor = true +breaking_always_bump_major = true +initial_tag = "0.1.0" + +[changelog] +header = "" +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {%- for commit in commits %} + - {{ commit.message | upper_first | trim }}\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%} + {% if commit.github.pr_number %} in \ + [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \ + {%- endif -%} + {% endfor %} +{% endfor %} + + + +{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + --- + ### New Contributors +{%- endif -%} + +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + * @{{ contributor.username }} made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}](({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %} +{%- endfor %}\n +""" +# template for the changelog footer +footer = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% for release in releases -%} + {% if release.version -%} + {% if release.previous.version -%} + [{{ release.version | trim_start_matches(pat="v") }}]: \ + {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }} + {% endif -%} + {% else -%} + {% if release.previous.version -%} + [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD + {% endif -%} + {% endif -%} +{% endfor %} + +""" +# remove the leading and trailing whitespace from the templates +trim = true + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # remove issue numbers from commits + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^doc", group = "๐Ÿ“š Documentation" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^refactor", group = "๐Ÿšœ Refactor" }, + { message = "^style", group = "๐ŸŽจ Styling" }, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = "^chore\\(release\\): prepare", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|ci", group = "โš™๏ธ Miscellaneous Tasks", skip = true }, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, + { message = "^revert", group = "โ—€๏ธ Revert" }, +] # regex for parsing and grouping commits +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = true +# regex for matching git tags +tag_pattern = "v[0-9].*" +# regex for skipping tags +skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml new file mode 100644 index 0000000..962117d --- /dev/null +++ b/.github/workflows/integrate.yaml @@ -0,0 +1,32 @@ +name: Integrate + +on: + push: + branches: [main] + pull_request: + +jobs: + + code-quality: + name: Code Quality + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + - name: Run Biome + run: biome ci . + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Build + run: bun run build \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..887677b --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,70 @@ +name: Release + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + + release: + name: Release + runs-on: ubuntu-latest + permissions: + pull-requests: read + contents: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Verify provenance + run: npm audit signatures + - name: Setup git-cliff + uses: kenji-miyake/setup-git-cliff@v2 + with: + version: 2.4.0 + - name: Generate next version + id: next-version + run: | + PREFIXED_VERSION=$(git-cliff --config .git-cliff/changelog.toml --unreleased --bumped-version 2>/dev/null) + echo "version=${PREFIXED_VERSION#v}" >> $GITHUB_OUTPUT + - name: Patch package.json version + run: bun scripts/patch-version.ts "${{ steps.next-version.outputs.version }}" + - name: Build + run: bun run build + - name: Package + run: npm pack + - name: Update changelog + run: git-cliff --config .git-cliff/changelog.toml --unreleased --bump --prepend CHANGELOG.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and push the changelog + uses: EndBug/add-and-commit@v9 + with: + add: ./CHANGELOG.md ./package.json + message: "chore(release): prepare v${{ steps.next-version.outputs.version }} release" + push: true + default_author: github_actions + - name: Config npm + run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Release + run: npm publish + - name: Generate release notes + run: git-cliff --config .git-cliff/release-notes.toml --unreleased --output RELEASE_NOTES.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + body_path: RELEASE_NOTES.md + name: v${{ steps.next-version.outputs.version }} + tag_name: v${{ steps.next-version.outputs.version }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4c6d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e07e46 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5f7dcef --- /dev/null +++ b/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "vcs": { + "enabled": true, + "useIgnoreFile": true, + "clientKind": "git" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..bfe8422 Binary files /dev/null and b/bun.lockb differ diff --git a/lefthook.yaml b/lefthook.yaml new file mode 100644 index 0000000..67ed45f --- /dev/null +++ b/lefthook.yaml @@ -0,0 +1,5 @@ +pre-commit: + commands: + check: + glob: "*.{js,ts,jsx,tsx,json}" + run: biome check --apply {staged_files} && git add {staged_files} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7efae7 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@biomejs/create-biome-reproduction", + "publishConfig": { + "access": "public" + }, + "version": "0.1.0", + "type": "module", + "author": "Nicolas Hedger ", + "bin": { + "create-biome-repro": "dist/index.js" + }, + "files": ["templates", "dist"], + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/biomejs/create-biome-reproduction.git" + }, + "bugs": { + "url": "https://github.com/biomejs/create-biome-reproduction/issues" + }, + "homepage": "https://github.com/biomejs/create-biome-reproduction#readme", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@types/bun": "1.1.6", + "@types/node": "22.1.0", + "@types/prompts": "2.4.9", + "lefthook": "1.7.11", + "tsup": "8.2.4", + "typescript": "^5.5.4" + }, + "dependencies": { + "@biomejs/version-utils": "0.4.0", + "kolorist": "1.8.0", + "ora": "8.0.1", + "prompts": "2.4.2" + } +} diff --git a/scripts/patch-version.ts b/scripts/patch-version.ts new file mode 100644 index 0000000..4ded4a5 --- /dev/null +++ b/scripts/patch-version.ts @@ -0,0 +1,8 @@ +const packageJsonContents = await Bun.file("package.json").text(); + +const patchedContents = packageJsonContents.replace( + /"version": ".*"/, + `"version": "${process.argv[2]}"`, +); + +Bun.write("package.json", patchedContents); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7794f3d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,114 @@ +import { cpSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { getAllVersions, getLatestVersion } from "@biomejs/version-utils"; +import { green, reset } from "kolorist"; +import ora from "ora"; +import prompts, { type Answers } from "prompts"; + +async function init() { + let result: Answers<"projectName" | "version" | "packageManager">; + + const spinner = ora("Fetching versions").start(); + const versions = await getAllVersions(); + spinner.succeed("Fetched versions"); + + try { + result = await prompts([ + { + type: "text", + name: "projectName", + message: reset("Project name:"), + initial: `biome-repro-${Date.now()}`, + }, + { + type: versions !== undefined ? "autocomplete" : "text", + name: "version", + message: reset("Biome version:"), + choices: versions?.map((version) => { + return { + title: version, + value: version, + }; + }), + initial: async () => { + const latestVersion = await getLatestVersion(); + const index = + versions?.findIndex((version) => version === latestVersion) ?? 0; + return index; + }, + }, + { + type: "select", + name: "packageManager", + message: reset("Package manager:"), + choices: [ + { + title: "npm", + value: "npm", + }, + { + title: "pnpm", + value: "pnpm", + }, + { + title: "bun", + value: "bun", + }, + { + title: "yarn", + value: "yarn", + }, + ], + }, + ]); + + const { projectName, version, packageManager } = result; + + const cwd = process.cwd(); + const targetDir = projectName; + const root = join(cwd, targetDir); + + mkdirSync(root, { recursive: true }); + console.log(`\nScaffolding project in ${root}...`); + + cpSync(join(__dirname, "../templates/biome"), root, { recursive: true }); + const packageJsonContents = readFileSync( + join(root, "package.json"), + "utf-8", + ); + const packageJson = JSON.parse(packageJsonContents); + packageJson.devDependencies["@biomejs/biome"] = version; + writeFileSync( + join(root, "package.json"), + JSON.stringify(packageJson, null, "\t"), + ); + + console.log("\nDone. Now run:\n"); + + if (root !== cwd) { + console.log( + green( + ` cd ${projectName.includes(" ") ? `"${projectName}"` : projectName}`, + ), + ); + } + + switch (packageManager) { + case "yarn": + console.log(green(` ${packageManager}`)); + break; + default: + console.log(green(` ${packageManager} install`)); + break; + } + + console.log(); + } catch (e) { + console.error(e); + process.exit(1); + } +} + +init().catch((e) => { + console.error(e); +}); diff --git a/templates/biome/biome.json b/templates/biome/biome.json new file mode 100644 index 0000000..dba9a4a --- /dev/null +++ b/templates/biome/biome.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/templates/biome/package.json b/templates/biome/package.json new file mode 100644 index 0000000..06e371e --- /dev/null +++ b/templates/biome/package.json @@ -0,0 +1,15 @@ +{ + "name": "biome-repro", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "format": "biome format .", + "lint": "biome lint", + "check": "biome check", + "ci": "biome ci" + }, + "devDependencies": { + "@biomejs/biome": "" + } +} diff --git a/templates/biome/src/index.ts b/templates/biome/src/index.ts new file mode 100644 index 0000000..b11a840 --- /dev/null +++ b/templates/biome/src/index.ts @@ -0,0 +1,4 @@ +/** + * If possible, put your minimal reproduction in this file, but feel free to + * create other files elsewhere in the project if necessary. + */ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ffc08ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..7203808 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + splitting: false, + sourcemap: true, + dts: true, + clean: true, +});