diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 00000000..788cd335 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +node scripts/post-checkout.mjs "$1" "$2" "$3" diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 00000000..48f9bf50 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +pnpm typecheck --minimumFailingSeverity warning diff --git a/.remarkrc.mjs b/.remarkrc.mjs index 64af7285..3a3379bb 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -1,5 +1,3 @@ -// @ts-check - import remarkFrontmatter from 'remark-frontmatter'; import remarkMdx from 'remark-mdx'; import remarkPresetLintRecommended from 'remark-preset-lint-recommended'; diff --git a/eslint.config.mjs b/eslint.config.mjs index 553da4b3..0efff8c4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,12 @@ import js from '@eslint/js'; -import tseslint from 'typescript-eslint'; import astro from 'eslint-plugin-astro'; +import globals from 'globals'; +import process from 'node:process'; +import tseslint from 'typescript-eslint'; -export default [ - { ignores: ['dist/', '.astro/', 'node_modules/', 'scripts/'] }, +import { defineConfig } from 'eslint/config'; +export default defineConfig([ + { ignores: ['dist/', '.astro/', 'node_modules/'] }, js.configs.recommended, @@ -18,16 +21,29 @@ export default [ { argsIgnorePattern: '^_' }, ], '@typescript-eslint/no-explicit-any': 'error', - 'no-undef': 'off', + // ci runs `astro check` which runs a full typescript checker + 'no-undef': process.env.CI ? 'off' : 'error', }, }, - { - files: ['**/*.astro'], + files: ['scripts/*', 'src/plugins/*'], + languageOptions: { + globals: { + // scripts and plugins run in a node env + ...globals.node, + }, + }, + }, + { + // also catch astro virtual files + files: ['**/*.astro', '**/*.astro/**/*.ts'], languageOptions: { globals: { ImageMetadata: 'readonly', }, }, + rules: { + 'no-undef': 'off', + }, }, -]; +]); diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs new file mode 100644 index 00000000..1b56c8b9 --- /dev/null +++ b/lint-staged.config.mjs @@ -0,0 +1,17 @@ +/** @type {import('lint-staged').Configuration} */ +export default { + '**/*': (files) => `prettier --write --ignore-unknown ${files.join(' ')}`, + '**/*.{astro,ts,mjs,js}': (files) => `eslint --fix ${files.join(' ')}`, + 'src/content/**/*.{md,mdx}': (files) => [ + `pnpm remark ${files.join(' ')} --ext mdx --frail --no-stdout --quiet`, + ], + 'examples/**/*.{java,gradle}': () => [ + `./examples/gradlew -p examples spotlessApply`, + ], + 'src/data/glossary.ts': () => [ + 'pnpm generate:glossary', + 'git add src/content/docs/resources/glossary.mdx', + ], + // Yes, I know this should be a FunctionTask but those are kinda bad until https://github.com/lint-staged/lint-staged/issues/1826 is resolved + 'package.json': () => 'pnpm tsx scripts/syncLockfile.lint.ts', +}; diff --git a/package.json b/package.json index d2793a6e..31979789 100644 --- a/package.json +++ b/package.json @@ -55,22 +55,12 @@ "remark-frontmatter": "^5.0.0", "remark-lint-no-dead-urls": "^2.0.1", "remark-mdx": "^3.1.1", + "globals": "^16.5.0", "remark-preset-lint-recommended": "^7.0.1", "starlight-links-validator": "^0.20.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "typescript-eslint": "^8.61.1", "vfile": "^6.0.3" - }, - "lint-staged": { - "**/*": [ - "pnpm format", - "pnpm lint" - ], - "examples/**/*.java": "sh -c 'cd examples && ./gradlew spotlessApply'", - "src/data/glossary.ts": [ - "pnpm generate:glossary", - "git add src/content/docs/resources/glossary.mdx" - ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03e40d4b..5ba09bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: eslint-plugin-astro: specifier: ^1.7.0 version: 1.7.0(eslint@10.5.0) + globals: + specifier: ^16.5.0 + version: 16.5.0 husky: specifier: ^9.1.7 version: 9.1.7 diff --git a/scripts/post-checkout.mjs b/scripts/post-checkout.mjs new file mode 100644 index 00000000..fb6558b3 --- /dev/null +++ b/scripts/post-checkout.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { execSync } from 'node:child_process'; +import { hashLockfile, readStamp } from './stamp.mjs'; + +function main() { + const [, , , , isBranchCheckoutArg] = process.argv; + + if (isBranchCheckoutArg !== '1') { + process.exit(0); + } + + const currentHash = hashLockfile(); + if (!currentHash) { + process.exit(0); + } + + if (currentHash === readStamp()) { + process.exit(0); + } + + console.log('lockfile changed since last checkout, running `pnpm install`'); + + try { + execSync('pnpm install', { stdio: 'inherit' }); + } catch { + console.error('pnpm install failed. retry manually.'); + process.exit(1); + } +} + +if (!process.env.CI) { + main(); +} diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts new file mode 100644 index 00000000..a7e27e4f --- /dev/null +++ b/scripts/postinstall.ts @@ -0,0 +1,22 @@ +import { hashLockfile, STAMP, writeStamp } from './stamp.mjs'; +import { existsSync, unlinkSync } from 'node:fs'; + +function updateHash(): void { + const hash = hashLockfile(); + if (!hash) { + // no lockfile for some reason, remove stale stamp if it exists + if (existsSync(STAMP)) { + unlinkSync(STAMP); + } + process.exit(0); + } + writeStamp(hash); +} +// main function. put other functions in here to run on postinstall. do not put loose code in main, make your own method +function main(): void { + updateHash(); +} + +if (!process.env.CI) { + main(); +} diff --git a/scripts/stamp.mjs b/scripts/stamp.mjs new file mode 100644 index 00000000..efc69968 --- /dev/null +++ b/scripts/stamp.mjs @@ -0,0 +1,27 @@ +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +export const LOCKFILE = resolve(process.cwd(), 'pnpm-lock.yaml'); +export const STAMP = resolve(process.cwd(), 'node_modules', '.checksum.sha1'); + +/** + * @returns {string | null} the hash or null if the lockfile doesn't exist + */ +export function hashLockfile() { + if (!existsSync(LOCKFILE)) return null; + const contents = readFileSync(LOCKFILE); + return createHash('sha1').update(contents).digest('hex'); +} +/** + * @returns {string} the hash from the stamp file, or an empty string if it doesn't exist + */ +export function readStamp() { + return existsSync(STAMP) ? readFileSync(STAMP, 'utf-8').trim() : ''; +} +/** + * @param {string} hash + */ +export function writeStamp(hash) { + writeFileSync(STAMP, hash, 'utf-8'); +} diff --git a/scripts/syncLockfile.lint.ts b/scripts/syncLockfile.lint.ts new file mode 100644 index 00000000..e2f0e005 --- /dev/null +++ b/scripts/syncLockfile.lint.ts @@ -0,0 +1,13 @@ +// Script to check if pnpm-lock.yaml is in sync with package.json. +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; + +await promisify(exec)( + // check if lockfile is in sync, don't run scripts, don't modify node_modules or lockfile + 'pnpm install --frozen-lockfile --lockfile-only --ignore-scripts', +).catch(() => { + console.error( + 'pnpm-lock.yaml is out of sync with package.json. Run `pnpm install` to update the lockfile.', + ); + process.exit(1); +}); diff --git a/src/plugins/remark-no-inline-code-fences.mjs b/src/plugins/remark-no-inline-code-fences.mjs index f4eacb81..d8543659 100644 --- a/src/plugins/remark-no-inline-code-fences.mjs +++ b/src/plugins/remark-no-inline-code-fences.mjs @@ -1,4 +1,3 @@ -// @ts-check /// import { visit } from 'unist-util-visit';