From f8c20e5414464cf36a1dd48845ff57bf4e87d158 Mon Sep 17 00:00:00 2001 From: Carl-Erik Kopseng Date: Tue, 3 Oct 2023 09:56:26 +0200 Subject: [PATCH] New article: making Sinon work with complex setups (#2540) Co-authored-by: Morgan Roderick --- docs/CONTRIBUTING.md | 1 + docs/_howto/link-seams-commonjs.md | 15 +- docs/_howto/typescript-swc.md | 264 ++++++++++++++ docs/_includes/header.html | 1 - docs/_sass/_prism.scss | 284 +-------------- docs/assets/js/prism.js | 542 +--------------------------- docs/current/migrating-to-2.0.md | 63 ---- docs/guides/index.md | 7 - docs/{releases.html => releases.md} | 14 +- 9 files changed, 294 insertions(+), 897 deletions(-) create mode 100644 docs/_howto/typescript-swc.md delete mode 100644 docs/current/migrating-to-2.0.md delete mode 100644 docs/guides/index.md rename docs/{releases.html => releases.md} (94%) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 383db3d90..7d4057a38 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -9,6 +9,7 @@ When you're contributing documentation changes for code in `main` branch, then d If you're contributing documentation for an existing release, then your documentation changes should go into the documentation for that release in `_releases/` folder, and possibly several of the following releases also. ### Where are all the _releases_? + All the files that used to be under `_releases` in the `main` can be found in the `releases` branch. The `main` branch now only keeps the latest version. That means, to fix the docs of published releases you need to checkout the _relases branch_ and supply a PR against that. ## Running the documentation site locally diff --git a/docs/_howto/link-seams-commonjs.md b/docs/_howto/link-seams-commonjs.md index b022c6792..6eb34d6aa 100644 --- a/docs/_howto/link-seams-commonjs.md +++ b/docs/_howto/link-seams-commonjs.md @@ -1,13 +1,19 @@ --- layout: page -title: How to use Link Seams with CommonJS +title: How to stub out CommonJS modules --- -This page describes how to isolate your system under test, by stubbing out dependencies with [link seams][seams]. +This page describes how to isolate your system under test, by targetting the [link seams][seams]; replacing your dependencies with stubs you control. -This is the CommonJS version, so we will be using [proxyquire][proxyquire] to construct our seams. +> If you want a better understand of the example and get a good description of what _seams_ are, we recommend that you read the [seams (all 3 web pages)][seams] excerpt from the classic [Working Effectively with Legacy Code][legacy], though it is not strictly necessary. -To better understand the example and get a good description of what seams are, we recommend that you read the [seams (all 3 web pages)][seams] excerpt from [Working Effectively with Legacy Code][legacy] before proceeding. +This guide targets the CommonJS module system, made popular by NodeJS. There are other module systems, but until recent years this was the de-facto module system and even when the actual EcmaScript Module standard arrived in 2015, transpilers and bundlers can still _output_ code as CJS modules. For instance, Typescript outputs CJS modules per default as of 2023, so it is still relevant, as your `import foo from './foo'` might still end up being transpiled into `const foo = require('./foo')` in the end. + + + +## Hooking into `require` + +For us to replace the underlying calls done by `require` we need a tool to hook into the process. There are many tools that can do this: rewire, proxyquire, [Quibble][quibble], etc. This example will be using [proxyquire][proxyquire] to construct our _seams_ (i.e. replace the modules), but the actual mechanics will be very similar for the other tools. Read it? @@ -83,3 +89,4 @@ describe("example", function () { [proxyquire]: https://github.com/thlorenz/proxyquire [demo-proxyquire]: https://github.com/sinonjs/demo-proxyquire [legacy]: https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code +[quibble]: https://www.npmjs.com/package/quibble diff --git a/docs/_howto/typescript-swc.md b/docs/_howto/typescript-swc.md new file mode 100644 index 000000000..316d8a54d --- /dev/null +++ b/docs/_howto/typescript-swc.md @@ -0,0 +1,264 @@ +--- +layout: page +title: "Case study: real world dependency stubbing" +--- + +Sinon is a simple tool that only tries to do a few things and do them well: creating and injecting test doubles (spies, fakes, stubs) into objects. Unfortunately, in todays world of build pipelines, complex tooling, transpilers and different module systems, doing the simple thing quickly becomes difficult. + +This article is a detailed step-by-step guide on how one can approach the typical issues that arise and various approaches for debugging and solving them. The real-world case chosen is using Sinon along with [SWC][swc-project], running tests written in TypeScript in the [Mocha test runner][mocha] and wanting to replace dependencies in this ([SUT][sut]). The essence is that there are always _many_ approaches for achieving what you want. Some require tooling, some can get away with almost no tooling, some are general in nature (not specific to SWC for instance) and some are a blend. This means you can usually make some of these approaches work for other combinations of tooling as well, once you understand what is going on. Draw inspiration from the approach and figure out what works for you! + +## On Typescript + +The Sinon project does not explicitly list TypeScript as a supported target environment. That does not mean Sinon will not run, just that there are so many complications that we cannot come up with guides on figuring out the details for you on every system :) + +Typescript is a super-set of EcmaScript (JavaScript) and can be transpiled in a wide variety of ways into EcmaScript, both by targetting different runtimes (ES5, ES2015, ES2023, etc) and module systems (CommonJS, ESM, AMD, ...). Some transpiler are closer to the what the standard TypeScript compiler produces, some are laxer in various ways and additionally they have all kinds of options to tweak the result. This is indeed complex, so before you dig yourself done in this matter, it is essential that you try to figure out what the resulting code _actually_ looks like. + +As you will see in this guide, adding a few sprinkles of `console.log` with the output of [`Object.getOwnPropertyDescriptor(object, propname)`][get-own] is usually sufficient to understand what is going on! + +All code and working setups described in this guide are on [Github][master-branch] and links to the correct branch can be found in each section. + +## Scenario + +### Tech + +- Mocha: drives the tests +- SWC: very fast Rust-based transpiler able to target different module systems (CJS, ESM, ...) and target runtimes (ES5, ES2020, ...) +- Typescript: Type-safe EcmaScript superset +- Sinon: library for creating and injecting test doubles (stubs, mocks, spies and fakes) +- Module system: CommonJS + +### Desired outcome + +Being able to replace exports on the dependency `./other` with a Sinon created test double in `main.ts` when running tests (see code below). + +### Problem + +Running tests with `ts-node` works fine, but changing the setup to using SWC instead results in the tests failing with the following output from Mocha: + +``` +1) main + should mock: + TypeError: Descriptor for property toBeMocked is non-configurable and non-writable +``` + +### Original code + +**main.ts** + +```typescript +import { toBeMocked } from "./other"; + +export function main() { + const out = toBeMocked(); + console.log(out); +} +``` + +**other.ts** + +```typescript +export function toBeMocked() { + return "I am the original function"; +} +``` + +**main.spec.ts** + +```typescript +import sinon from "sinon"; +import "./init"; +import * as Other from "./other"; +import { main } from "./main"; +import { expect } from "chai"; + +const sandbox = sinon.createSandbox(); + +describe("main", () => { + let mocked; + it("should mock", () => { + mocked = sandbox.stub(Other, "toBeMocked").returns("mocked"); + main(); + expect(mocked.called).to.be.true; + }); +}); +``` + +Additionally, both the `.swcrc` file used by SWC and the `tsconfig.json` file used by `ts-node` is configured to produce modules of the CommonJS form, not ES Modules. + +### Brief Analysis + +The error message indicates the resulting output of transpilation is different from that of `ts-node`, as this is Sinon telling us that it is unable to do anything with the property of an object, when the [property descriptor][descriptor] is essentially immutable. + +Let us sprinkle some debugging statements to figure out what the differences between the two tools are. First we will add these some debugging output to the beginning of the test, for instance just after `it("should mock", () => {`, to see what the state is _before_ we attempt to do any modifications: + +```javascript +console.log("Other", Other); +console.log( + "Other property descriptors", + Object.getOwnPropertyDescriptors(Other) +); +``` + +Now let's try what happens when running this again, once with the existing SWC setup and a second time after changing the config file for Mocha, `.mocharc.json`, to use `'ts-node'` instead of `'@swc/register` in its `'require'` array. This `--require` option of Node is for modules that will be run by Node before _your_ code, making it possible to do stuff like hook into `require` and transpile code on-the-fly. + +#### Output of a SWC configured run of `npm test` + +``` +Other { toBeMocked: [Getter] } +Other property descriptors { + __esModule: { + value: true, + writable: false, + enumerable: false, + configurable: false + }, + toBeMocked: { + get: [Function: get], + set: undefined, + enumerable: true, + configurable: false + } +} + 1) should mock + + + 0 passing (4ms) + 1 failing + + 1) main + should mock: + TypeError: Descriptor for property toBeMocked is non-configurable and non-writable +``` + +#### Output of a `ts-node` configured run of `npm test` + +``` +Other { toBeMocked: [Function: toBeMocked] } +Other property descriptors { + __esModule: { + value: true, + writable: false, + enumerable: false, + configurable: false + }, + toBeMocked: { + value: [Function: toBeMocked], + writable: true, + enumerable: true, + configurable: true + } +} +mocked + ✔ should mock +``` + +The important difference to note about the object `Other` is that the property `toBeMocked` is a simple writable _value_ in the case of `ts-node` and a non-configurable _getter_ in the case of SWC. It being a getter is not a problem for Sinon, as we have a multitude of options for replacing those, but if `configurable` is set to `false` Sinon cannot really do anything about it. + +Let us take a closer look. + +#### Conclusion of analysis + +SWC transforms the imports on the form `import * as Other from './other'` into objects where the individual exports are exposed through immutable accessors (_getters_). + +We can address this issue in mainly 3 ways: + +1. somehow reconfigure SWC to produce different output when running tests that we can work with, either making writable values or configurable getters +2. use pure dependency injection, opening up `./other.ts` to be changed from the inside +3. address how modules are loaded, injecting [an additional `require` "hook"](https://levelup.gitconnected.com/how-to-add-hooks-to-node-js-require-function-dee7acd12698) + +# Solutions + +## Mutating the output from the transpiler + +> [Working code][swc-mutable-export] + +If we can just flip the `configurable` flag to `true` during transpilation, Sinon could be instructed to replace the getter. It turns out, there is a SWC _plugin_ that does just that: [swc_mut_cjs_exports](https://www.npmjs.com/package/swc_mut_cjs_exports). By installing that and adding the following under the `jsc` key in `.swcrc`, you know get a configurable property descriptor. + +```json +"experimental": { + "plugins": [[ "swc_mut_cjs_exports", {} ]] +}, +``` + +A getter _is_ different from a value, so you need to change your testcode slightly to replace the getter: + +```js +const stub = sandbox.fake.returns("mocked"); +sandbox.replaceGetter(Other, "toBeMocked", () => stub); +``` + +## Use pure dependency injection + +> [Working code][pure-di] + +This technique works regardless of language, module systems, bundlers and tool chains, but requires slight modifications of the SUT to allow modifying it. Sinon cannot help with resetting state automatically in this scenario. + +**other.ts** + +```typescript +function _toBeMocked() { + return "I am the original function"; +} + +export let toBeMocked = _toBeMocked; + +export function _setToBeMocked(mockImplementation) { + toBeMocked = mockImplementation; +} +``` + +**main.spec.ts** + +```typescript +describe("main", () => { + let mocked; + let original = Other.toBeMocked; + + after(() => Other._setToBeMocked(original)) + + it("should mock", () => { + mocked = sandbox.stub().returns("mocked"); + Other._setToBeMocked(mocked) + main(); + expect(mocked.called).to.be.true; + }); +``` + +## Hooking into Node's module loading + +> [Working code][cjs-mocking] + +This is what [the article on _targetting the link seams_][link-seams-cjs] is about. The only difference here is using Quibble instead of Proxyquire. Quibble is slightly terser and also supports being used as a ESM _loader_, making it a bit more modern and useful. The end result: + +```typescript +describe("main module", () => { + let mocked, main; + + before(() => { + mocked = sandbox.stub().returns("mocked"); + quibble("./other", { toBeMocked: mocked }); + ({ main } = require("./main")); + }); + + it("should mock", () => { + main(); + expect(mocked.called).to.be.true; + }); +}); +``` + +# Final remarks + +As can be seen, there are lots of different paths to walk in order to achieve the same basic goal. Find the one that works for your case. + +[link-seams-cjs]: /how-to/link-seams-commonjs/ +[master-branch]: https://github.com/fatso83/sinon-swc-bug +[descriptor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty +[get-own]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor +[swc-project]: https://swc.rs/ +[mocha]: https://mochajs.org/ +[sut]: http://xunitpatterns.com/SUT.html +[require-hook]: https://levelup.gitconnected.com/how-to-add-hooks-to-node-js-require-function-dee7acd12698 +[swc-mutable-export]: https://github.com/fatso83/sinon-swc-bug/tree/swc-with-mutable-exports +[pure-di]: https://github.com/fatso83/sinon-swc-bug/tree/pure-di +[cjs-mocking]: https://github.com/fatso83/sinon-swc-bug/tree/cjs-mocking diff --git a/docs/_includes/header.html b/docs/_includes/header.html index 67430ca00..4d6029b38 100644 --- a/docs/_includes/header.html +++ b/docs/_includes/header.html @@ -9,7 +9,6 @@ diff --git a/docs/_sass/_prism.scss b/docs/_sass/_prism.scss index a4b5b9014..ddefe1639 100644 --- a/docs/_sass/_prism.scss +++ b/docs/_sass/_prism.scss @@ -1,281 +1,3 @@ -/* http://prismjs.com/download.html?themes=prism-twilight&languages=clike+javascript */ -/** - * prism.js Twilight theme - * Based (more or less) on the Twilight theme originally of Textmate fame. - * @author Remy Bach - */ - -code[class*="language-"] { - color: white; - background: none; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*="language-"] { - color: white; - background: none; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - background: hsl(0, 0%, 8%); - /* #141414 */ -} - -:not(pre) > code[class*="language-"] { - background: hsl(0, 0%, 8%); - /* #141414 */ -} - -/* Code blocks */ - -pre[class*="language-"] { - border-radius: 5px; - /* #282A2B */ - box-shadow: 1px 1px 0.5em black inset; - margin: 0.5em 0; - overflow: auto; - padding: 1em; - &::-moz-selection { - /* Firefox */ - background: hsl(200, 4%, 16%); - /* #282A2B */ - } - &::selection { - /* Safari */ - background: hsl(200, 4%, 16%); - /* #282A2B */ - } - &::-moz-selection, - ::-moz-selection { - background: hsla(0, 0%, 93%, 0.15); - /* #EDEDED */ - } -} - -/* Text Selection colour */ - -code[class*="language-"] { - &::-moz-selection, - ::-moz-selection { - background: hsla(0, 0%, 93%, 0.15); - /* #EDEDED */ - } -} - -pre[class*="language-"] { - &::selection, - ::selection { - background: hsla(0, 0%, 93%, 0.15); - /* #EDEDED */ - } -} - -code[class*="language-"] { - &::selection, - ::selection { - background: hsla(0, 0%, 93%, 0.15); - /* #EDEDED */ - } -} - -/* Inline code */ - -:not(pre) > code[class*="language-"] { - border-radius: 0.3em; - border: 0.13em solid hsl(0, 0%, 33%); - /* #545454 */ - box-shadow: 1px 1px 0.3em -0.1em black inset; - padding: 0.15em 0.2em 0.05em; - white-space: normal; -} - -.token { - &.comment, - &.prolog, - &.doctype, - &.cdata { - color: hsl(0, 0%, 47%); - /* #777777 */ - } - &.punctuation { - opacity: 0.7; - } -} - -.namespace { - opacity: 0.7; -} - -.token { - &.tag, - &.boolean, - &.number, - &.deleted { - color: hsl(14, 58%, 55%); - /* #CF6A4C */ - } - &.keyword, - &.property, - &.selector, - &.constant, - &.symbol, - &.builtin { - color: hsl(53, 89%, 79%); - /* #F9EE98 */ - } - &.attr-name, - &.attr-value, - &.string, - &.char, - &.operator, - &.entity, - &.url { - color: hsl(76, 21%, 52%); - /* #8F9D6A */ - } -} - -.language-css .token.string, -.style .token.string { - color: hsl(76, 21%, 52%); - /* #8F9D6A */ -} - -.token { - &.variable, - &.inserted { - color: hsl(76, 21%, 52%); - /* #8F9D6A */ - } - &.atrule { - color: hsl(218, 22%, 55%); - /* #7587A6 */ - } - &.regex { - color: hsl(42, 75%, 65%); - /* #E9C062 */ - } - &.important { - color: hsl(42, 75%, 65%); - /* #E9C062 */ - font-weight: bold; - } - &.bold { - font-weight: bold; - } - &.italic { - font-style: italic; - } - &.entity { - cursor: help; - } -} - -pre[data-line] { - padding: 1em 0 1em 3em; - position: relative; -} - -/* Markup */ - -.language-markup .token { - &.tag, - &.attr-name, - &.punctuation { - color: hsl(33, 33%, 52%); - /* #AC885B */ - } -} - -/* Make the tokens sit above the line highlight so the colours don't look faded. */ - -.token { - position: relative; - z-index: 1; -} - -.line-highlight { - background: hsla(0, 0%, 33%, 0.25); - /* #545454 */ - background: linear-gradient( - to right, - hsla(0, 0%, 33%, 0.1) 70%, - hsla(0, 0%, 33%, 0) - ); - /* #545454 */ - border-bottom: 1px dashed hsl(0, 0%, 33%); - /* #545454 */ - border-top: 1px dashed hsl(0, 0%, 33%); - /* #545454 */ - left: 0; - line-height: inherit; - margin-top: 0.75em; - /* Same as .prism’s padding-top */ - padding: inherit 0; - pointer-events: none; - position: absolute; - right: 0; - white-space: pre; - z-index: 0; - &:before { - background-color: hsl(215, 15%, 59%); - /* #8794A6 */ - border-radius: 999px; - box-shadow: 0 1px white; - color: hsl(24, 20%, 95%); - /* #F5F2F0 */ - content: attr(data-start); - font: bold 65%/1.5 sans-serif; - left: 0.6em; - min-width: 1em; - padding: 0 0.5em; - position: absolute; - text-align: center; - text-shadow: none; - top: 0.4em; - vertical-align: 0.3em; - } - &[data-end]:after { - background-color: hsl(215, 15%, 59%); - /* #8794A6 */ - border-radius: 999px; - box-shadow: 0 1px white; - color: hsl(24, 20%, 95%); - /* #F5F2F0 */ - content: attr(data-start); - font: bold 65%/1.5 sans-serif; - left: 0.6em; - min-width: 1em; - padding: 0 0.5em; - position: absolute; - text-align: center; - text-shadow: none; - top: 0.4em; - vertical-align: 0.3em; - bottom: 0.4em; - content: attr(data-end); - top: auto; - } -} +/* PrismJS 1.29.0 +https://prismjs.com/download.html?#themes=prism-twilight&languages=clike+javascript+typescript */ +code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;text-shadow:0 -.1em .2em #000;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}:not(pre)>code[class*=language-],pre[class*=language-]{background:#141414}pre[class*=language-]{border-radius:.5em;border:.3em solid #545454;box-shadow:1px 1px .5em #000 inset;margin:.5em 0;overflow:auto;padding:1em}pre[class*=language-]::-moz-selection{background:#27292a}pre[class*=language-]::selection{background:#27292a}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:hsla(0,0%,93%,.15)}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:hsla(0,0%,93%,.15)}:not(pre)>code[class*=language-]{border-radius:.3em;border:.13em solid #545454;box-shadow:1px 1px .3em -.1em #000 inset;padding:.15em .2em .05em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#777}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.deleted,.token.number,.token.tag{color:#ce6849}.token.builtin,.token.constant,.token.keyword,.token.property,.token.selector,.token.symbol{color:#f9ed99}.language-css .token.string,.style .token.string,.token.attr-name,.token.attr-value,.token.char,.token.entity,.token.inserted,.token.operator,.token.string,.token.url,.token.variable{color:#909e6a}.token.atrule{color:#7385a5}.token.important,.token.regex{color:#e8c062}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.language-markup .token.attr-name,.language-markup .token.punctuation,.language-markup .token.tag{color:#ac885c}.token{position:relative;z-index:1}.line-highlight.line-highlight{background:hsla(0,0%,33%,.25);background:linear-gradient(to right,hsla(0,0%,33%,.1) 70%,hsla(0,0%,33%,0));border-bottom:1px dashed #545454;border-top:1px dashed #545454;margin-top:.75em;z-index:0}.line-highlight.line-highlight:before,.line-highlight.line-highlight[data-end]:after{background-color:#8693a6;color:#f4f1ef} diff --git a/docs/assets/js/prism.js b/docs/assets/js/prism.js index 0e6974556..5f84b9078 100644 --- a/docs/assets/js/prism.js +++ b/docs/assets/js/prism.js @@ -1,536 +1,6 @@ -/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ -var _self = - "undefined" != typeof window - ? window - : "undefined" != typeof WorkerGlobalScope && - self instanceof WorkerGlobalScope - ? self - : {}, - Prism = (function () { - var e = /\blang(?:uage)?-(\w+)\b/i, - t = 0, - n = (_self.Prism = { - util: { - encode: function (e) { - return e instanceof a - ? new a(e.type, n.util.encode(e.content), e.alias) - : "Array" === n.util.type(e) - ? e.map(n.util.encode) - : e - .replace(/&/g, "&") - .replace(/ e.length) break e; - if (!(v instanceof a)) { - u.lastIndex = 0; - var b = u.exec(v), - k = 1; - if (!b && h && m != r.length - 1) { - if ( - ((u.lastIndex = y), - (b = u.exec(e)), - !b) - ) - break; - for ( - var w = - b.index + - (c ? b[1].length : 0), - _ = b.index + b[0].length, - A = m, - P = y, - x = r.length; - x > A && _ > P; - ++A - ) - (P += r[A].length), - w >= P && (++m, (y = P)); - if ( - r[m] instanceof a || - r[A - 1].greedy - ) - continue; - (k = A - m), - (v = e.slice(y, P)), - (b.index -= y); - } - if (b) { - c && (f = b[1].length); - var w = b.index + f, - b = b[0].slice(f), - _ = w + b.length, - O = v.slice(0, w), - S = v.slice(_), - j = [m, k]; - O && j.push(O); - var N = new a( - l, - g ? n.tokenize(b, g) : b, - d, - b, - h - ); - j.push(N), - S && j.push(S), - Array.prototype.splice.apply( - r, - j - ); - } - } - } - } - } - return r; - }, - hooks: { - all: {}, - add: function (e, t) { - var a = n.hooks.all; - (a[e] = a[e] || []), a[e].push(t); - }, - run: function (e, t) { - var a = n.hooks.all[e]; - if (a && a.length) - for (var r, i = 0; (r = a[i++]); ) r(t); - }, - }, - }), - a = (n.Token = function (e, t, n, a, r) { - (this.type = e), - (this.content = t), - (this.alias = n), - (this.length = 0 | (a || "").length), - (this.greedy = !!r); - }); - if ( - ((a.stringify = function (e, t, r) { - if ("string" == typeof e) return e; - if ("Array" === n.util.type(e)) - return e - .map(function (n) { - return a.stringify(n, t, e); - }) - .join(""); - var i = { - type: e.type, - content: a.stringify(e.content, t, r), - tag: "span", - classes: ["token", e.type], - attributes: {}, - language: t, - parent: r, - }; - if ( - ("comment" == i.type && (i.attributes.spellcheck = "true"), - e.alias) - ) { - var l = - "Array" === n.util.type(e.alias) ? e.alias : [e.alias]; - Array.prototype.push.apply(i.classes, l); - } - n.hooks.run("wrap", i); - var o = ""; - for (var s in i.attributes) - o += - (o ? " " : "") + - s + - '="' + - (i.attributes[s] || "") + - '"'; - return ( - "<" + - i.tag + - ' class="' + - i.classes.join(" ") + - '"' + - (o ? " " + o : "") + - ">" + - i.content + - "" - ); - }), - !_self.document) - ) - return _self.addEventListener - ? (_self.addEventListener( - "message", - function (e) { - var t = JSON.parse(e.data), - a = t.language, - r = t.code, - i = t.immediateClose; - _self.postMessage(n.highlight(r, n.languages[a], a)), - i && _self.close(); - }, - !1 - ), - _self.Prism) - : _self.Prism; - var r = - document.currentScript || - [].slice.call(document.getElementsByTagName("script")).pop(); - return ( - r && - ((n.filename = r.src), - document.addEventListener && - !r.hasAttribute("data-manual") && - ("loading" !== document.readyState - ? window.requestAnimationFrame - ? window.requestAnimationFrame(n.highlightAll) - : window.setTimeout(n.highlightAll, 16) - : document.addEventListener( - "DOMContentLoaded", - n.highlightAll - ))), - _self.Prism - ); - })(); -"undefined" != typeof module && module.exports && (module.exports = Prism), - "undefined" != typeof global && (global.Prism = Prism); -(Prism.languages.markup = { - comment: //, - prolog: /<\?[\w\W]+?\?>/, - doctype: //i, - cdata: //i, - tag: { - pattern: /<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i, - inside: { - tag: { - pattern: /^<\/?[^\s>\/]+/i, - inside: { punctuation: /^<\/?/, namespace: /^[^\s>\/:]+:/ }, - }, - "attr-value": { - pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i, - inside: { punctuation: /[=>"']/ }, - }, - punctuation: /\/?>/, - "attr-name": { - pattern: /[^\s>\/]+/, - inside: { namespace: /^[^\s>\/:]+:/ }, - }, - }, - }, - entity: /&#?[\da-z]{1,8};/i, -}), - Prism.hooks.add("wrap", function (a) { - "entity" === a.type && - (a.attributes.title = a.content.replace(/&/, "&")); - }), - (Prism.languages.xml = Prism.languages.markup), - (Prism.languages.html = Prism.languages.markup), - (Prism.languages.mathml = Prism.languages.markup), - (Prism.languages.svg = Prism.languages.markup); -(Prism.languages.css = { - comment: /\/\*[\w\W]*?\*\//, - atrule: { - pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i, - inside: { rule: /@[\w-]+/ }, - }, - url: /url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i, - selector: /[^\{\}\s][^\{\};]*?(?=\s*\{)/, - string: { - pattern: /("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/, - greedy: !0, - }, - property: /(\b|\B)[\w-]+(?=\s*:)/i, - important: /\B!important\b/i, - function: /[-a-z0-9]+(?=\()/i, - punctuation: /[(){};:]/, -}), - (Prism.languages.css.atrule.inside.rest = Prism.util.clone( - Prism.languages.css - )), - Prism.languages.markup && - (Prism.languages.insertBefore("markup", "tag", { - style: { - pattern: /()[\w\W]*?(?=<\/style>)/i, - lookbehind: !0, - inside: Prism.languages.css, - alias: "language-css", - }, - }), - Prism.languages.insertBefore( - "inside", - "attr-value", - { - "style-attr": { - pattern: /\s*style=("|').*?\1/i, - inside: { - "attr-name": { - pattern: /^\s*style/i, - inside: Prism.languages.markup.tag.inside, - }, - punctuation: /^\s*=\s*['"]|['"]\s*$/, - "attr-value": { - pattern: /.+/i, - inside: Prism.languages.css, - }, - }, - alias: "language-css", - }, - }, - Prism.languages.markup.tag - )); -Prism.languages.clike = { - comment: [ - { pattern: /(^|[^\\])\/\*[\w\W]*?\*\//, lookbehind: !0 }, - { pattern: /(^|[^\\:])\/\/.*/, lookbehind: !0 }, - ], - string: { - pattern: /(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, - greedy: !0, - }, - "class-name": { - pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i, - lookbehind: !0, - inside: { punctuation: /(\.|\\)/ }, - }, - keyword: /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/, - boolean: /\b(true|false)\b/, - function: /[a-z0-9_]+(?=\()/i, - number: /\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i, - operator: /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/, - punctuation: /[{}[\];(),.:]/, -}; -(Prism.languages.javascript = Prism.languages.extend("clike", { - keyword: /\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/, - number: /\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/, - function: /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i, - operator: /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/, -})), - Prism.languages.insertBefore("javascript", "keyword", { - regex: { - pattern: /(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/, - lookbehind: !0, - greedy: !0, - }, - }), - Prism.languages.insertBefore("javascript", "string", { - "template-string": { - pattern: /`(?:\\\\|\\?[^\\])*?`/, - greedy: !0, - inside: { - interpolation: { - pattern: /\$\{[^}]+\}/, - inside: { - "interpolation-punctuation": { - pattern: /^\$\{|\}$/, - alias: "punctuation", - }, - rest: Prism.languages.javascript, - }, - }, - string: /[\s\S]+/, - }, - }, - }), - Prism.languages.markup && - Prism.languages.insertBefore("markup", "tag", { - script: { - pattern: /()[\w\W]*?(?=<\/script>)/i, - lookbehind: !0, - inside: Prism.languages.javascript, - alias: "language-javascript", - }, - }), - (Prism.languages.js = Prism.languages.javascript); +/* PrismJS 1.29.0 +https://prismjs.com/download.html?#themes=prism-twilight&languages=clike+javascript+typescript */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; +!function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism); diff --git a/docs/current/migrating-to-2.0.md b/docs/current/migrating-to-2.0.md deleted file mode 100644 index b2bfc39af..000000000 --- a/docs/current/migrating-to-2.0.md +++ /dev/null @@ -1,63 +0,0 @@ -# Migrating to v2.0 - -Sinon v2.0 is the second major release, we have made several breaking changes in this release as a result of modernising the internals of Sinon. This guide is intended to walk you through the changes. - -## sinon.log and sinon.logError Removed - -`sinon.log` and `sinon.logError` were used in Sinon v1.x to globally configure `FakeServer`, `FakeXMLHttpRequest` and `FakeXDomainRequest`; these three functions now allow the logger to be configured on a per-use basis. In v1.x you may have written: - -```js -sinon.log = function (msg) { // your logging impl }; -``` - -You would now individually import and configure the utility upon creation: - -```js -var sinon = require("sinon"); - -var myFakeServer = sinon.fakeServer.create({ - logger: function (msg) { // your logging impl } -}); -``` - -## sinon.test, sinon.testCase and sinon.config Removed - -`sinon.test` and `sinon.testCase` have been extracted from the Sinon API and moved into their own node module, [sinon-test](https://www.npmjs.com/package/sinon-test). Please refer to the [sinon-test README](https://github.com/sinonjs/sinon-test/blob/master/README.md) for migration examples. - -## stub.callsFake replaces stub(obj, 'meth', fn) - -`sinon.stub(obj, 'meth', fn)` return a spy, not a full stub. Behavior could not be redefined. `stub.callsFake` -now returns a full stub. Here's a [codemod script](https://github.com/hurrymaplelad/sinon-codemod) to help you migrate. -See [discussion](https://github.com/sinonjs/sinon/pull/823). - -```js -// Old -sinon.stub(obj, "meth", fn); -// New -sinon.stub(obj, "meth").callsFake(fn); -``` - -## Deprecation of internal helpers - -The following utility functions are being marked as deprecated and are planned for removal in Sinon v3.0; please check your codebase for usage to ease future migrations: - -- `sinon.calledInOrder` -- `sinon.create` -- `sinon.deepEqual` -- `sinon.format` -- `sinon.functionName` -- `sinon.functionToString` -- `sinon.getConfig` -- `sinon.getPropertyDescriptor` -- `sinon.objectKeys` -- `sinon.orderByFirstCall` -- `sinon.timesInWorlds` -- `sinon.valueToString` -- `sinon.walk` -- `sinon.wrapMethod` -- `sinon.Event` -- `sinon.CustomEvent` -- `sinon.EventTarget` -- `sinon.ProgressEvent` -- `sinon.typeOf` -- `sinon.extend` diff --git a/docs/guides/index.md b/docs/guides/index.md deleted file mode 100644 index 440ed4322..000000000 --- a/docs/guides/index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: page -title: Guides -breadcrumb: guides ---- - -{% include docs/migration-guides.md %} diff --git a/docs/releases.html b/docs/releases.md similarity index 94% rename from docs/releases.html rename to docs/releases.md index 8d74b9155..2597e2e7a 100644 --- a/docs/releases.html +++ b/docs/releases.md @@ -3,11 +3,12 @@ title: Releases - Sinon.JS permalink: /releases/index.html redirect_from: - - /docs - - /docs/ - - /download - - /download/ - - /releases/download/ + - /docs + - /docs/ + - /download + - /download/ + - /releases/download/ + - /guides ---
@@ -17,8 +18,10 @@

Releases

Changelog

You can see the full log of changes for each release on our separate changelog page. +
+{% include docs/migration-guides.md %}
    @@ -50,6 +53,7 @@

    Changelog

    {% endif %} {% endfor %}
+