Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102// src/plugin/loader/__tests__/es5-extends-compat.test.ts/** * Verifies that classes exposed via window.__REAMS_PLUGIN_API__ can be * extended using TypeScript's ES5 `__extends` helper (which calls the * super-class as a regular function via `_super.apply(this, args)`). * * Real-world repro: cm-typewriter-scroll-obsidian (Typewriter Scroll) * is bundled with TS target ES5. Its derived constructor body: * * function CMTypewriterScrollPlugin() { * var _this = _super !== null && _super.apply(this, arguments) || this; * ... * } * * Without the ES5-callable wrapper installed by installPluginApiGlobal, * `_super.apply(this, arguments)` throws * "TypeError: Class constructor Plugin cannot be invoked without 'new'" * because our `Plugin` is an ES2020 class. The wrapper synthesizes the ES5 * shape (callable + new'able) without changing the underlying class so * native ESM consumers (`import { Plugin } from 'obsidian'`) still see a * real class. */import { describe, it, expect, beforeAll } from 'vitest'import { installPluginApiGlobal } from '../plugin-api-global'import { Plugin as RealPlugin } from '../../plugin'
beforeAll(() => { installPluginApiGlobal()})
// Reproduce the TS-emitted __extends helper verbatim.const __extends = (function () { let extendStatics: (d: object, b: object) => void = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d: any, b: any) { d.__proto__ = b }) || function (d: any, b: any) { for (const p in b) { if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p] } } return extendStatics(d, b) } return function (d: any, b: any): void { if (typeof b !== 'function' && b !== null) { throw new TypeError( 'Class extends value ' + String(b) + ' is not a constructor or null' ) } extendStatics(d, b) function __(this: any) { this.constructor = d } d.prototype = b === null ? Object.create(b) : (((__.prototype = b.prototype), new (__ as any)())) }})()
describe('ES5 __extends compatibility for Plugin', () => { it('Plugin from window.__REAMS_PLUGIN_API__ is callable as a function via __extends', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const PluginShim = api.Plugin as new (...args: unknown[]) => unknown
// Build a derived "class" exactly the way TS-ES5 emits it. const Derived = (function (_super: any) { __extends(Derived, _super) function Derived(this: any, ...args: any[]) { // This is the line that fails on a real ES6 class: // _super.apply(this, arguments) // Our wrapper makes it construct an instance of the underlying class // with `this.constructor` (Derived) as new.target. const _this = (_super !== null && _super.apply(this, args)) || this _this._derivedField = 42 return _this } return Derived })(PluginShim) as unknown as new (...args: unknown[]) => unknown
const fakeApp = {} as never const fakeManifest = { id: 'x', name: 'x', version: '1', minAppVersion: '0', author: 'x', description: 'x' } as never
let instance: unknown expect(() => { instance = new Derived(fakeApp, fakeManifest) }).not.toThrow()
// Instance should still be `instanceof RealPlugin` so the loader's // `instance instanceof Plugin` check passes. expect(instance).toBeInstanceOf(RealPlugin) // And it should have the derived field set in the derived constructor. expect((instance as { _derivedField: number })._derivedField).toBe(42) })
it('Plugin from window.__REAMS_PLUGIN_API__ is also new-able directly (ESM consumers)', () => { const api = window.__REAMS_PLUGIN_API__ as Record<string, unknown> const PluginShim = api.Plugin as new (...args: unknown[]) => unknown class MyPlugin extends (PluginShim as unknown as { new(...args: unknown[]): object }) {} const fakeApp = {} as never const fakeManifest = { id: 'y', name: 'y', version: '1', minAppVersion: '0', author: 'y', description: 'y' } as never
const instance = new (MyPlugin as unknown as new (...a: unknown[]) => object)(fakeApp, fakeManifest) expect(instance).toBeInstanceOf(RealPlugin) })})