diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8c57e88 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + commit-message: + prefix: ci + + - package-ecosystem: npm # See documentation for possible values + directories: + - / # Location of package manifests + - /recipies/* + commit-message: + prefix: deps + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3a706d7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +# For more information see: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow + +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + lint-and-types: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [23.x] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run lint + - run: npm run format + - run: npm run test:types + + tests: + strategy: + fail-fast: false + matrix: + node: + - version: 23.x + - version: 22.x + # glob is not backported below 22.x + os: + - macos-latest + - ubuntu-latest + - windows-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - name: Set up Node.js ${{ matrix.node.version }} + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # 4.1.0 + with: + node-version: ${{ matrix.node.version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit + - run: npm run test:e2e diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56d1901 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Commonly ignored Node.js files +node_modules +npm-debug.log +.npm +.env.* + +# OSX +.DS_Store diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b393560 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +23 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4f737e8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "biomejs.biome" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2d8ad4d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always" +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c47a179 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +A recipe generally has a few things: + +* A `README.md` explaining its purpose and use (including any options, and required and optional +files). +* Tests via node's test runner (min coverage: 80%) + * unit tests (file extension: `.spec.mjs` or `.spec.mts`) + * end-to-end test(s) for accepted use-cases (file extension: `.e2e.mjs` or `.e2e.mts`) +* Code comments (js docs, etc) +* Types (either via typescript or jsdoc) + +CI will run lint & type checking and all included test files against all PRs. + +> [!NOTE] +> snapshots will be generated with the file extension `.snap.cjs`. + +New recipes are added under `./recipes` in their own folder, succinctly named for what it does. General-purpose recipes have simple names like `correct-ts-specifiers`. A suite of migrations has a name like `migrate from 18 to 20`, and more specific migrations are named like `migrate fs.readFile from 18 to 20`. + +## Before pushing a commit + +A convenient superset of checks is available via `node --run pre-commit`, which automatically fixes formatting and linting issues, checks types, and runs unit and end-to-end tests. Changes resulting from this should be committed. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1aa5b10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright Contributors to the Userland Migrations project. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b1b1780..e9e7c45 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ # Node.js userland migrations + +This repository contains codemodes (automated migrations) for "userland" code. These are intended to facilitate adopting new features and upgrading source-code affected by breaking changes. + +## Usage + +> [!CAUTION] +> These scripts change source code. Commit any unsaved changes before running them. Failing to do so may ruin your day. + +To run the transform scripts use [`codemod`](https://go.codemod.com/github) command below: + +```console +$ npx codemod --target [...options] +``` + +* `transform` - name of transform. see available transforms below. +* `path` - directory to transform. defaults to the current directory. + +See the [codemod CLI doc](https://go.codemod.com/cli-docs) for a full list of available commands. + +## Available codemods + +All Node.js codemods are also available in the [Codemod Registry](https://codemod.link/nodejs-official). diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..5ebe181 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,52 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "formatter": { + "indentStyle": "tab", + "lineWidth": 100 + }, + // Rules for the linter + "linter": { + "rules": { + "style": { + "useImportType": "error", + "useNodeAssertStrict": "error", + "useNodejsImportProtocol": "error" + }, + "suspicious": { + "noExplicitAny": "error", + "noEmptyBlock": "error", + "noDuplicateAtImportRules": "error", + "noDuplicateObjectKeys": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "useArrayLiterals": "off", + "noUnknownFunction": "error" + }, + "nursery": { + "noEnum": "error" + } + } + }, + // Language specific settings + "javascript": { + "formatter": { + "semicolons": "always", + "quoteStyle": "single", + "trailingCommas": "all" + }, + "linter": { + "enabled": true + } + }, + "json": { + "formatter": { + "enabled": false + } + }, + // VSC specific settings + "vcs": { + "enabled": true, + "clientKind": "git" + } +} diff --git a/build/snapshots.mts b/build/snapshots.mts new file mode 100644 index 0000000..d3a70f7 --- /dev/null +++ b/build/snapshots.mts @@ -0,0 +1,17 @@ +import { basename, dirname, extname, join } from 'node:path'; +import { snapshot } from 'node:test'; + +snapshot.setResolveSnapshotPath(generateSnapshotPath); +/** + * @param testFilePath '/tmp/foo.test.js' + * @returns '/tmp/foo.test.snap.cjs' + */ +function generateSnapshotPath(testFilePath?: string) { + if (!testFilePath) return ''; + + const ext = extname(testFilePath); + const filename = basename(testFilePath, ext); + const base = dirname(testFilePath); + + return join(base, `${filename}.snap.cjs`); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..36f4cfd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,216 @@ +{ + "name": "userland-migrations", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "userland-migrations", + "version": "1.0.0", + "license": "MIT", + "workspaces": [ + "./recipes/*" + ], + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.9.1", + "typescript": "^5.7.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@types/node": { + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e3a9658 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "userland-migrations", + "version": "1.0.0", + "description": "A collection of migration recipes for userland code.", + "scripts": { + "format:fix": "biome format --fix ./", + "format": "biome format ./", + "lint:fix": "biome lint --fix ./", + "lint": "biome lint ./", + "pre-commit": "node --run lint:fix; node --run format:fix; node --run test:types; node --run test:unit; node --run test:e2e", + "test:e2e": "node --no-warnings --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=./coverage.lcov --test-reporter=spec --test-reporter-destination=stdout --import './build/snapshots.ts' --test --test-coverage-include='recipes/**/*' --test-coverage-exclude='**/*.e2e.{mjs,mts}' './packages/*/*.e2e.{mjs,mts}'", + "test:types": "tsc", + "test:unit": "node --no-warnings --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=./coverage.lcov --test-reporter=spec --test-reporter-destination=stdout --experimental-test-module-mocks --import './build/snapshots.ts' --test --test-coverage-include='recipes/**/*' --test-coverage-exclude='**/*.spec.{mjs,mts}' --test-coverage-lines=0.8 './packages/*/*.spec.{mjs,mts}'" + }, + "repository": { + "type": "git", + "url": "https://github.com/nodejs/userland-migrations" + }, + "keywords": [ + "automation", + "codemod", + "migrations", + "node.js" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/nodejs/userland-migrations/issues" + }, + "homepage": "https://nodejs.org/learn/userland-migrations", + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.9.1", + "typescript": "^5.7.2" + }, + "workspaces": [ + "./recipes/*" + ] +} diff --git a/recipes/.gitkeep b/recipes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3292d56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + + "target": "ESNext", + // "moduleDetection": "force", + + /* Modules */ + "module": "NodeNext", + "moduleResolution": "nodenext", + "allowImportingTsExtensions": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + "resolveJsonModule": true, + "allowArbitraryExtensions": true, + + /* JavaScript Support */ + "allowJs": true, + "checkJs": true, + + /* Emit */ + "noEmit": true, + + /* Interop Constraints */ + "verbatimModuleSyntax": true, + "allowSyntheticDefaultImports": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + /* Type Checking */ + "strict": true, + "noImplicitAny": true, + "strictNullChecks": false, + "strictFunctionTypes": true, + + /* Completeness */ + "skipLibCheck": true + }, + "include": [ + "./build/", + "./recipes/" + ] +}