Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial working version #1

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches:
- main
- master
pull_request: {}

concurrency:
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
lint:
name: 'Lint'
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- run: pnpm i --frozen-lockfile
- run: pnpm run lint

test:
name: 'Test'
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- run: pnpm i --frozen-lockfile
- run: pnpm test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules/
/dist/
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# compiled output
/dist/

# misc
/pnpm-lock.yaml
3 changes: 3 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
singleQuote: true,
};
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# semver-deprecate

This is a tiny micro-library that extracts the "Ember way" of doing deprecations into a re-usable library. The philosophy of this library is that if you have marked something to be deprecated "until" a certain major version that deprecation call should throw an error once the library's version has exceeded that major version. This allows you to remove code in minor PRs after a major release has been cut because any code-path that used to hit this deprecation is now throwing and thus no longer supported.

## Usage

```js
import { makeDeprecate } from 'semver-deprecate';

import pkg from './package.json' with { type: 'json' };

const deprecate = makeDeprecate(pkg.name, pkg.version);

deprecate('The `foo` method is deprecated.', false, {
for: 'your-lib',
id: 'your-lib.foo-method', // a unique ID for the deprecation
since: {
available: '4.1.0',
enabled: '4.2.0',
},
until: '5.0.0',
url: 'https://example.com',
});
```
35 changes: 35 additions & 0 deletions assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Verify that a certain condition is met, or throw an error if otherwise.
*
* This is useful for communicating expectations in the code to other human
* readers as well as catching bugs that accidentally violate these expectations.
*
* ```js
* const { assert } = require('ember-cli/lib/debug');
*
* // Test for truthiness:
* assert('Must pass a string.', typeof str === 'string');
*
* // Fail unconditionally:
* assert('This code path should never run.');
* ```
*
* @method assert
* @param {String} description Describes the condition.
* This will become the message of the error thrown if the assertion fails.
* @param {Any} condition Must be truthy for the assertion to pass.
* If falsy, an error will be thrown.
*/
export default function assert(description, condition) {
if (!description) {
throw new Error(
'When calling `assert`, you must provide a description as the first argument.',
);
}

if (condition) {
return;
}

throw new Error(`ASSERTION FAILED: ${description}`);
}
18 changes: 18 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import globals from 'globals';
import pluginJs from '@eslint/js';

/** @type {import('eslint').Linter.Config[]} */
export default [
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
{
files: ['tests/*.cjs'],
languageOptions: {
// yes it's a cjs file but vitest needs it to have imports for some things to this makes eslint happy
sourceType: 'module',
},
},
{
ignores: ['dist/*'],
},
];
159 changes: 159 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import chalk from 'chalk';
import semver from 'semver';
import assert from './assert.js';

/**
* Create a new deprecate function for your library.
*
* ```js
* import { makeDeprecate } from 'semver-deprecate';
* import pkg from "./package.json" with { type: "json" };
*
* const deprecate = makeDeprecate(pkg.name, pkg.version);
* ```
*
* @param {string} deprecateLib the library that you are creating a deprecate function for (usually name in package.json)
* @param {*} currentVersion the current version of the library you are creating a deprecate function for (usually read from package.json)
*/
export function makeDeprecate(deprecateLib, currentVersion) {
/**
* Display a deprecation message.
*
* ```js
* deprecate('The `foo` method is deprecated.', false, {
* for: 'ember-cli',
* id: 'ember-cli.foo-method',
* since: {
* available: '4.1.0',
* enabled: '4.2.0',
* },
* until: '5.0.0',
* url: 'https://example.com',
* });
* ```
*
* @param {String} description Describes the deprecation.
* @param {Any} condition If falsy, the deprecation message will be displayed.
* @param {Object} options An object including the deprecation's details:
* - `for` The library that the deprecation is for
* - `id` The deprecation's unique id
* - `since.available` A SemVer version indicating when the deprecation was made available
* - `since.enabled` A SemVer version indicating when the deprecation was enabled
* - `until` A SemVer version indicating until when the deprecation will be active
* - `url` A URL that refers to additional information about the deprecation
*/
return function deprecate(description, condition, options) {
assert(
'When calling `deprecate`, you must provide a description as the first argument.',
description,
);
assert(
'When calling `deprecate`, you must provide a condition as the second argument.',
arguments.length > 1,
);

assert(
'When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).',
options,
);

assert(
'When calling `deprecate`, you must provide the `for` option.',
options.for,
);
assert(
'When calling `deprecate`, you must provide the `id` option.',
options.id,
);

assert(
'When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.',
options.since,
);

assert(
'When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.',
options.since.available || options.since.enabled,
);

assert(
'`since.available` must be a valid SemVer version.',
!options.since.available || isSemVer(options.since.available),
);

assert(
'`since.enabled` must be a valid SemVer version.',
!options.since.enabled || isSemVer(options.since.enabled),
);

assert(
'When calling `deprecate`, you must provide a valid SemVer version for the `until` option.',
isSemVer(options.until),
);

if (condition) {
return;
}

if (
options.for === deprecateLib &&
isDeprecationRemoved(options.until, currentVersion)
) {
throw new Error(
`The API deprecated by ${options.id} was removed in ${deprecateLib}@${options.until}. The message was: ${description}. Please see ${options.url} for more details.`,
);
}

let message = formatMessage(description, options);

warn(message);
warn('');
warn(getStackTrace());

// Return the message for testing purposes.
// This can be removed once we can register deprecation handlers.
return message;
};
}

function isSemVer(version) {
return semver.valid(version) !== null;
}

function formatMessage(description, options) {
let message = [
chalk.inverse(' DEPRECATION '),
'\n\n',
description,
'\n\n',
`ID ${options.id}`,
'\n',
`UNTIL ${options.until}`,
];

if (options.url) {
message.push('\n', `URL ${options.url}`);
}

return message.join('');
}

function getStackTrace() {
let error = new Error();
let lines = error.stack.split('\n');

lines.shift(); // Remove the word `Error`.

return lines.map((line) => line.trim()).join('\n');
}

function warn(message) {
console.warn(chalk.yellow(message));
}

function isDeprecationRemoved(until, currentVersion) {
return semver.gte(
semver.coerce(currentVersion, { includePrerelease: false }),
until,
);
}
35 changes: 31 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,30 @@
"main": "index.js",
"type": "module",
"scripts": {
"pretest": "pnpm build",
"test": "vitest",
"lint": "eslint .",
"prepublishOnly": "tsc"
"prepublishOnly": "pnpm build",
"build": "concurrently \"pnpm:build:*\" --names \"build:\" --prefixColors auto",
"build:ts": "tsc",
"build:cjs": "npx rollup index.js --file dist/index.cjs --format cjs --plugin @rollup/plugin-commonjs --plugin @rollup/plugin-node-resolve",
"format": "prettier . --cache --write",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:js": "eslint .",
"lint:js:fix": "eslint . --fix",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format",
"lint:format": "prettier . --cache --check"
},
"files": [
"dist",
"index.js",
"assert.js"
],
"exports": {
".": "./index.js"
".": {
"import": "./index.js",
"require": "./dist/index.cjs",
"types": "./index.d.ts"
}
},
"keywords": [],
"author": "Chris Manson <[email protected]>",
Expand All @@ -22,13 +40,22 @@
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.17.0",
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@types/node": "^22.10.5",
"concurrently": "^9.1.2",
"eslint": "^9.17.0",
"fixturify-project": "^7.1.3",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"rollup": "^4.30.0",
"strip-ansi": "^7.1.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
},
"dependencies": {
"chalk": "^5.4.1",
"semver": "^7.6.3"
}
},
"packageManager": "[email protected]+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
}
Loading
Loading