Repository: stoplightio/spectral
Pull request #2048 (2022-02-03 23:28:50)

feat(ruleset-bundler): a number of minor enhancements
Related to https://github.com/stoplightio/platform-internal/pull/7403

This is an accumulated set of changes I've kept track of after the initial release of the package and the actual integrations attempts in Studio and Spectral CLI.
I suggest reviewing this PR commit by commit because each change is somewhat different.

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No





Patch:
diff --git a/__karma__/perf_hooks.js b/__karma__/perf_hooks.js
new file mode 100644
index 00000000..e69de29b
diff --git a/__karma__/process.js b/__karma__/process.js
index f237ddf5..98594c82 100644
--- a/__karma__/process.js
+++ b/__karma__/process.js
@@ -1,1 +1,1 @@
-export default undefined;
+export const on = Function();
diff --git a/karma.conf.ts b/karma.conf.ts
index 45917c49..2581b4bb 100644
--- a/karma.conf.ts
+++ b/karma.conf.ts
@@ -17,7 +17,7 @@ module.exports = (config: Config): void => {
     files: ['./__karma__/jest.ts', 'packages/*/src/**/*.ts'],
 
     // list of files / patterns to exclude
-    exclude: ['packages/cli/**', '**/*.jest.test.ts'],
+    exclude: ['packages/cli/**', 'packages/ruleset-bundler/src/plugins/commonjs.ts', '**/*.jest.test.ts'],
 
     // preprocess matching files before serving them to the browser
     // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
@@ -41,6 +41,7 @@ module.exports = (config: Config): void => {
             'node-fetch': require.resolve('./__karma__/fetch'),
             fs: require.resolve('./__karma__/fs'),
             process: require.resolve('./__karma__/process'),
+            perf_hooks: require.resolve('./__karma__/perf_hooks'),
             fsevents: require.resolve('./__karma__/fsevents'),
           },
         },
diff --git a/packages/ruleset-bundler/package.json b/packages/ruleset-bundler/package.json
index 0761b7dc..f2108a2f 100644
--- a/packages/ruleset-bundler/package.json
+++ b/packages/ruleset-bundler/package.json
@@ -38,6 +38,7 @@
     "release": "semantic-release -e semantic-release-monorepo"
   },
   "dependencies": {
+    "@rollup/plugin-commonjs": "^21.0.1",
     "@stoplight/path": "1.3.2",
     "@stoplight/spectral-core": ">=1",
     "@stoplight/spectral-formats": ">=1",
@@ -50,7 +51,7 @@
     "@stoplight/types": "^12.3.0",
     "@types/node": "*",
     "pony-cause": "1.1.1",
-    "rollup": "~2.60.2",
+    "rollup": "~2.67.0",
     "tslib": "^2.3.1",
     "validate-npm-package-name": "3.0.0"
   },
diff --git a/packages/ruleset-bundler/src/index.ts b/packages/ruleset-bundler/src/index.ts
index 2228defd..38acec50 100644
--- a/packages/ruleset-bundler/src/index.ts
+++ b/packages/ruleset-bundler/src/index.ts
@@ -1,6 +1,7 @@
 import { rollup, Plugin } from 'rollup';
 import { isURL } from '@stoplight/path';
 import { isPackageImport } from './utils/isPackageImport';
+import { dedupeRollupPlugins } from './utils/dedupeRollupPlugins';
 
 export type BundleOptions = {
   plugins: Plugin[];
@@ -17,7 +18,7 @@ export async function bundleRuleset(
 ): Promise<string> {
   const bundle = await rollup({
     input,
-    plugins,
+    plugins: dedupeRollupPlugins(plugins),
     treeshake,
     watch: false,
     perf: false,
diff --git a/packages/ruleset-bundler/src/plugins/builtins.ts b/packages/ruleset-bundler/src/plugins/builtins.ts
index 66708f18..937a831c 100644
--- a/packages/ruleset-bundler/src/plugins/builtins.ts
+++ b/packages/ruleset-bundler/src/plugins/builtins.ts
@@ -11,15 +11,21 @@ type Module = 'core' | 'formats' | 'functions' | 'parsers' | 'ref-resolver' | 'r
 type GlobalModules = Record<`@stoplight/spectral-${Module}`, string>;
 type Overrides = Record<keyof GlobalModules, Record<string, unknown>>;
 
+const NAME = '@stoplight-spectral/builtins';
+
 function registerModule(
+  instanceId: number,
   id: keyof GlobalModules,
   members: Record<string, unknown>,
   overrides: Partial<Overrides>,
 ): [string, string] {
   const actualOverrides = overrides[id];
-  globalThis[Symbol.for(id)] = actualOverrides ? { ...members, ...actualOverrides } : members;
+  const instances = (globalThis[Symbol.for(NAME)] ??= {}) as Record<string, Partial<Overrides>>;
+  const root = (instances[instanceId] ??= {});
+
+  root[id] = actualOverrides ? { ...members, ...actualOverrides } : members;
 
-  const m = `globalThis[Symbol.for('${id}')]`;
+  const m = `globalThis[Symbol.for('${NAME}')]['${instanceId}']['${id}']`;
   let code = '';
   for (const member of Object.keys(members)) {
     code += `export const ${member} = ${m}['${member}'];\n`;
@@ -29,26 +35,28 @@ function registerModule(
 }
 
 export const builtins = (overrides: Partial<Overrides> = {}): Plugin => {
+  const instanceId = Math.round(Math.random() * 1_000_000);
+
   const modules = Object.fromEntries([
-    registerModule('@stoplight/spectral-core', core, overrides),
-    registerModule('@stoplight/spectral-formats', formats, overrides),
-    registerModule('@stoplight/spectral-functions', functions, overrides),
-    registerModule('@stoplight/spectral-parsers', parsers, overrides),
-    registerModule('@stoplight/spectral-ref-resolver', refResolver, overrides),
-    registerModule('@stoplight/spectral-rulesets', rulesets, overrides),
-    registerModule('@stoplight/spectral-runtime', runtime, overrides),
+    registerModule(instanceId, '@stoplight/spectral-core', core, overrides),
+    registerModule(instanceId, '@stoplight/spectral-formats', formats, overrides),
+    registerModule(instanceId, '@stoplight/spectral-functions', functions, overrides),
+    registerModule(instanceId, '@stoplight/spectral-parsers', parsers, overrides),
+    registerModule(instanceId, '@stoplight/spectral-ref-resolver', refResolver, overrides),
+    registerModule(instanceId, '@stoplight/spectral-rulesets', rulesets, overrides),
+    registerModule(instanceId, '@stoplight/spectral-runtime', runtime, overrides),
   ]) as GlobalModules;
 
   return {
-    name: '@stoplight-spectral/builtins',
-    resolveId(id) {
+    name: NAME,
+    resolveId(id): string | null {
       if (id in modules) {
         return id;
       }
 
       return null;
     },
-    load(id) {
+    load(id): string | undefined {
       if (id in modules) {
         return modules[id] as string;
       }
diff --git a/packages/ruleset-bundler/src/plugins/commonjs.ts b/packages/ruleset-bundler/src/plugins/commonjs.ts
new file mode 100644
index 00000000..19a107ba
--- /dev/null
+++ b/packages/ruleset-bundler/src/plugins/commonjs.ts
@@ -0,0 +1,1 @@
+export { default as commonjs } from '@rollup/plugin-commonjs';
diff --git a/packages/ruleset-bundler/src/plugins/virtualFs.ts b/packages/ruleset-bundler/src/plugins/virtualFs.ts
index 6ba92dfc..6c64d851 100644
--- a/packages/ruleset-bundler/src/plugins/virtualFs.ts
+++ b/packages/ruleset-bundler/src/plugins/virtualFs.ts
@@ -1,35 +1,48 @@
 import { dirname, parse, join, normalize, isAbsolute, isURL } from '@stoplight/path';
-import type { Plugin } from 'rollup';
+import type { Plugin, PluginContext } from 'rollup';
 import type { IO } from '../types';
 
-export const virtualFs = ({ fs }: IO): Plugin => ({
-  name: '@stoplight-spectral/virtual-fs',
-  resolveId(source, importer) {
-    const { protocol } = parse(source);
-
-    if (protocol === 'http' || protocol === 'https') {
-      return null;
-    }
-
-    if (protocol !== 'file' && !/^[./]/.test(source)) {
-      return null;
-    }
-
-    if (isAbsolute(source)) {
-      return normalize(source);
-    }
-
-    if (importer !== void 0) {
-      return join(dirname(importer), source);
-    }
-
-    return source;
-  },
-  load(id) {
-    if (!isURL(id)) {
-      return fs.promises.readFile(id, 'utf8');
-    }
-
-    return;
-  },
-});
+export const virtualFs = ({ fs }: IO): Plugin => {
+  const recognized = new WeakMap<PluginContext, string[]>();
+
+  return {
+    name: '@stoplight-spectral/virtual-fs',
+
+    resolveId(source, importer): string | null {
+      const { protocol } = parse(source);
+
+      if (protocol === 'http' || protocol === 'https') {
+        return null;
+      }
+
+      if (protocol !== 'file' && !/^[./]/.test(source)) {
+        return null;
+      }
+
+      let resolvedSource = source;
+
+      if (isAbsolute(source)) {
+        resolvedSource = normalize(source);
+      } else if (importer !== void 0) {
+        resolvedSource = join(dirname(importer), source);
+      }
+
+      let existingEntries = recognized.get(this);
+      if (existingEntries === void 0) {
+        existingEntries = [];
+        recognized.set(this, existingEntries);
+      }
+
+      existingEntries.push(resolvedSource);
+
+      return resolvedSource;
+    },
+    load(id): Promise<string> | undefined {
+      if (!isURL(id) && recognized.get(this)?.includes(id) === true) {
+        return fs.promises.readFile(id, 'utf8');
+      }
+
+      return;
+    },
+  };
+};
diff --git a/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts
new file mode 100644
index 00000000..12ba67f6
--- /dev/null
+++ b/packages/ruleset-bundler/src/utils/dedupeRollupPlugins.ts
@@ -0,0 +1,12 @@
+// this function makes sure we can only have one plugin with the same name
+// the last plugin definition has a precedence
+import type { Plugin } from 'rollup';
+
+export function dedupeRollupPlugins(plugins: Plugin[]): Plugin[] {
+  const map = new Map<string, Plugin>();
+  for (const plugin of plugins) {
+    map.set(plugin.name, plugin);
+  }
+
+  return Array.from(map.values());
+}
diff --git a/yarn.lock b/yarn.lock
index 6191b0f5..815b0186 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2077,6 +2077,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/plugin-commonjs@npm:^21.0.1":
+  version: 21.0.1
+  resolution: "@rollup/plugin-commonjs@npm:21.0.1"
+  dependencies:
+    "@rollup/pluginutils": ^3.1.0
+    commondir: ^1.0.1
+    estree-walker: ^2.0.1
+    glob: ^7.1.6
+    is-reference: ^1.2.1
+    magic-string: ^0.25.7
+    resolve: ^1.17.0
+  peerDependencies:
+    rollup: ^2.38.3
+  checksum: 3e56be58c72d655face6f361f85923ddcc3cc07760b5a3a91cfc728115dfed358fc595781148c512d53a03be8c703133379f228e78fd2aed8655fae9d83800b6
+  languageName: node
+  linkType: hard
+
 "@rollup/pluginutils@npm:^3.1.0":
   version: 3.1.0
   resolution: "@rollup/pluginutils@npm:3.1.0"
@@ -2449,6 +2466,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@stoplight/spectral-ruleset-bundler@workspace:packages/ruleset-bundler"
   dependencies:
+    "@rollup/plugin-commonjs": ^21.0.1
     "@stoplight/path": 1.3.2
     "@stoplight/spectral-core": ">=1"
     "@stoplight/spectral-formats": ">=1"
@@ -2465,7 +2483,7 @@ __metadata:
     memfs: ^3.3.0
     pony-cause: 1.1.1
     prettier: ^2.4.1
-    rollup: ~2.60.2
+    rollup: ~2.67.0
     tslib: ^2.3.1
     validate-npm-package-name: 3.0.0
   languageName: unknown
@@ -11067,9 +11085,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"rollup@npm:~2.60.2":
-  version: 2.60.2
-  resolution: "rollup@npm:2.60.2"
+"rollup@npm:~2.67.0":
+  version: 2.67.2
+  resolution: "rollup@npm:2.67.2"
   dependencies:
     fsevents: ~2.3.2
   dependenciesMeta:
@@ -11077,7 +11095,7 @@ __metadata:
       optional: true
   bin:
     rollup: dist/bin/rollup
-  checksum: bcd41dfe8afb7e0d97ce2237752165bdda689bcce6321d96821d565de3e0c865a49b544923f315985be2bfde086f72b54aae4ae7c87f798b3cb9558a5bec4e65
+  checksum: 9aca5251ba4b441064183cde2394b91567259002d68086bdd3906db66d55dd148ab27e57c51eb53830d7b9b813c2d4e834b7735d65e2a869780bc639d4a20c38
   languageName: node
   linkType: hard
 


Test patch:
diff --git a/packages/ruleset-bundler/src/__tests__/index.jest.test.ts b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts
new file mode 100644
index 00000000..9afc25fe
--- /dev/null
+++ b/packages/ruleset-bundler/src/__tests__/index.jest.test.ts
@@ -0,0 +1,100 @@
+import { serveAssets } from '@stoplight/spectral-test-utils';
+import { fetch } from '@stoplight/spectral-runtime';
+import * as fs from 'fs';
+import { bundleRuleset } from '../index';
+import { IO } from '../types';
+import { node } from '../presets/node';
+import { browser } from '../presets/browser';
+import { commonjs } from '../plugins/commonjs';
+import { virtualFs } from '../plugins/virtualFs';
+import { runtime } from '../presets/runtime';
+
+jest.mock('fs');
+
+describe('Ruleset Bundler', () => {
+  let io: IO;
+
+  beforeEach(() => {
+    io = {
+      fs,
+      fetch,
+    };
+
+    serveAssets({
+      '/p/.spectral/my-fn.js': `module.exports = function f() { return [] };`,
+
+      '/p/spectral.js': `import myFn from './.spectral/my-fn.js';
+
+export default {
+  rules: {
+    rule: {
+       given: '$',
+       then: { function: myFn },
+    }
+  },
+};`,
+    });
+  });
+
+  it('given runtime target, should support commonjs', async () => {
+    const code = await bundleRuleset('/p/spectral.js', {
+      target: 'runtime',
+      plugins: [...runtime(io), commonjs()],
+    });
+
+    expect(code).toContain(`\tvar myFn = function f() { return [] };
+
+\tvar spectral = {
+\t  rules: {
+\t    rule: {
+\t       given: '$',
+\t       then: { function: myFn },
+\t    }
+\t  },
+\t};
+
+\treturn spectral;
+
+})();`);
+  });
+
+  it('given browser target, should support commonjs', async () => {
+    const code = await bundleRuleset('/p/spectral.js', {
+      target: 'browser',
+      plugins: [...browser(io), commonjs()],
+    });
+
+    expect(code).toContain(`var myFn = function f() { return [] };
+
+var spectral = {
+  rules: {
+    rule: {
+       given: '$',
+       then: { function: myFn },
+    }
+  },
+};
+
+export { spectral as default };`);
+  });
+
+  it('given node target, should support commonjs', async () => {
+    const code = await bundleRuleset('/p/spectral.js', {
+      target: 'node',
+      plugins: [...node(io), virtualFs(io), commonjs()],
+    });
+
+    expect(code).toContain(`var myFn = function f() { return [] };
+
+var spectral = {
+  rules: {
+    rule: {
+       given: '$',
+       then: { function: myFn },
+    }
+  },
+};
+
+export { spectral as default };`);
+  });
+});
diff --git a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts
index 8a122f18..44f3fc9b 100644
--- a/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts
+++ b/packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts
@@ -12,12 +12,23 @@ import { builtins } from '../builtins';
 
 describe('Builtins Plugin', () => {
   let io: IO;
+  let randomSpy: jest.SpyInstance;
 
   beforeEach(() => {
     io = {
       fs,
       fetch: runtime.fetch,
     };
+
+    randomSpy = jest
+      .spyOn(Math, 'random')
+      .mockReturnValueOnce(0.8229275205939697)
+      .mockReturnValueOnce(0.7505242801973444)
+      .mockReturnValueOnce(0.5647855410879519);
+  });
+
+  afterEach(() => {
+    randomSpy.mockRestore();
   });
 
   describe.each<BundleOptions['target']>(['browser', 'runtime'])('given %s target', target => {
@@ -51,21 +62,21 @@ export default {
       });
 
       expect(code)
-        .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight/spectral-functions')]['alphabetical'];
-const casing = globalThis[Symbol.for('@stoplight/spectral-functions')]['casing'];
-const defined = globalThis[Symbol.for('@stoplight/spectral-functions')]['defined'];
-const enumeration = globalThis[Symbol.for('@stoplight/spectral-functions')]['enumeration'];
-const falsy = globalThis[Symbol.for('@stoplight/spectral-functions')]['falsy'];
-const length = globalThis[Symbol.for('@stoplight/spectral-functions')]['length'];
-const pattern = globalThis[Symbol.for('@stoplight/spectral-functions')]['pattern'];
-const schema = globalThis[Symbol.for('@stoplight/spectral-functions')]['schema'];
-const truthy = globalThis[Symbol.for('@stoplight/spectral-functions')]['truthy'];
-const undefined$1 = globalThis[Symbol.for('@stoplight/spectral-functions')]['undefined'];
-const unreferencedReusableObject = globalThis[Symbol.for('@stoplight/spectral-functions')]['unreferencedReusableObject'];
-const xor = globalThis[Symbol.for('@stoplight/spectral-functions')]['xor'];
-
-const oas = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['oas'];
-const asyncapi = globalThis[Symbol.for('@stoplight/spectral-rulesets')]['asyncapi'];
+        .toEqual(`const alphabetical = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['alphabetical'];
+const casing = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['casing'];
+const defined = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['defined'];
+const enumeration = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['enumeration'];
+const falsy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['falsy'];
+const length = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['length'];
+const pattern = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['pattern'];
+const schema = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['schema'];
+const truthy = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['truthy'];
+const undefined$1 = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['undefined'];
+const unreferencedReusableObject = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['unreferencedReusableObject'];
+const xor = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions']['xor'];
+
+const oas = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['oas'];
+const asyncapi = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-rulesets']['asyncapi'];
 
 var input = {
   extends: [oas],
@@ -87,7 +98,9 @@ var input = {
 export { input as default };
 `);
 
-      expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions);
+      expect(
+        globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'],
+      ).toStrictEqual(functions);
     });
 
     it('should support overrides', async () => {
@@ -113,30 +126,78 @@ readFile();`,
         ],
       });
 
-      expect(code).toEqual(`const fetch = globalThis[Symbol.for('@stoplight/spectral-runtime')]['fetch'];
-const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight/spectral-runtime')]['DEFAULT_REQUEST_OPTIONS'];
-const decodeSegmentFragment = globalThis[Symbol.for('@stoplight/spectral-runtime')]['decodeSegmentFragment'];
-const printError = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printError'];
-const PrintStyle = globalThis[Symbol.for('@stoplight/spectral-runtime')]['PrintStyle'];
-const printPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printPath'];
-const printValue = globalThis[Symbol.for('@stoplight/spectral-runtime')]['printValue'];
-const startsWithProtocol = globalThis[Symbol.for('@stoplight/spectral-runtime')]['startsWithProtocol'];
-const isAbsoluteRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['isAbsoluteRef'];
-const traverseObjUntilRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['traverseObjUntilRef'];
-const getEndRef = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getEndRef'];
-const safePointerToPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['safePointerToPath'];
-const getClosestJsonPath = globalThis[Symbol.for('@stoplight/spectral-runtime')]['getClosestJsonPath'];
-const readFile = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readFile'];
-const readParsable = globalThis[Symbol.for('@stoplight/spectral-runtime')]['readParsable'];
+      expect(code)
+        .toEqual(`const fetch = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['fetch'];
+const DEFAULT_REQUEST_OPTIONS = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['DEFAULT_REQUEST_OPTIONS'];
+const decodeSegmentFragment = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['decodeSegmentFragment'];
+const printError = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printError'];
+const PrintStyle = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['PrintStyle'];
+const printPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printPath'];
+const printValue = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['printValue'];
+const startsWithProtocol = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['startsWithProtocol'];
+const isAbsoluteRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['isAbsoluteRef'];
+const traverseObjUntilRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['traverseObjUntilRef'];
+const getEndRef = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getEndRef'];
+const safePointerToPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['safePointerToPath'];
+const getClosestJsonPath = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['getClosestJsonPath'];
+const readFile = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readFile'];
+const readParsable = globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime']['readParsable'];
 
 readFile();
 `);
 
-      expect(globalThis[Symbol.for('@stoplight/spectral-runtime')]).toStrictEqual({
+      expect(
+        globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'],
+      ).toStrictEqual({
         ...runtime,
         readFile,
       });
     });
+
+    it('should isolate each instance', async () => {
+      serveAssets({
+        '/tmp/input.js': `import { readFile } from '@stoplight/spectral-runtime';
+
+readFile();`,
+      });
+
+      // eslint-disable-next-line @typescript-eslint/no-empty-function
+      function readFile(): void {}
+      // eslint-disable-next-line @typescript-eslint/no-empty-function
+      function readFile2(): void {}
+
+      await bundleRuleset('/tmp/input.js', {
+        format: 'esm',
+        target,
+        plugins: [
+          builtins({
+            '@stoplight/spectral-runtime': {
+              readFile,
+            },
+          }),
+          builtins({
+            '@stoplight/spectral-runtime': {
+              readFile: readFile2,
+            },
+          }),
+          virtualFs(io),
+        ],
+      });
+
+      expect(
+        globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-runtime'],
+      ).toStrictEqual({
+        ...runtime,
+        readFile,
+      });
+
+      expect(
+        globalThis[Symbol.for('@stoplight-spectral/builtins')]['750524']['@stoplight/spectral-runtime'],
+      ).toStrictEqual({
+        ...runtime,
+        readFile: readFile2,
+      });
+    });
   });
 
   describe('given node target', () => {
@@ -191,7 +252,9 @@ var input = {
 export { input as default };
 `);
 
-      expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions);
+      expect(
+        globalThis[Symbol.for('@stoplight-spectral/builtins')]['822928']['@stoplight/spectral-functions'],
+      ).toStrictEqual(functions);
     });
   });
 });
diff --git a/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts
new file mode 100644
index 00000000..aef57779
--- /dev/null
+++ b/packages/ruleset-bundler/src/utils/__tests__/dedupeRollupPlugins.spec.ts
@@ -0,0 +1,51 @@
+import type { Plugin } from 'rollup';
+
+import { dedupeRollupPlugins } from '../dedupeRollupPlugins';
+
+describe('dedupeRollupPlugins util', () => {
+  it('should keep plugins with different names', () => {
+    const plugins: Plugin[] = [
+      {
+        name: 'plugin 1',
+      },
+      {
+        name: 'plugin 2',
+      },
+      {
+        name: 'plugin 3',
+      },
+    ];
+
+    expect(dedupeRollupPlugins([...plugins])).toStrictEqual(plugins);
+  });
+
+  it('given the same plugin, should replace the first declaration', () => {
+    const plugins: Plugin[] = [
+      {
+        name: 'plugin 1',
+        cacheKey: 'key 1',
+      },
+      {
+        name: 'plugin 2',
+      },
+      {
+        name: 'plugin 1',
+        cacheKey: 'key 2',
+      },
+      {
+        name: 'plugin 1',
+        cacheKey: 'key 3',
+      },
+    ];
+
+    expect(dedupeRollupPlugins([...plugins])).toStrictEqual([
+      {
+        name: 'plugin 1',
+        cacheKey: 'key 3',
+      },
+      {
+        name: 'plugin 2',
+      },
+    ]);
+  });
+});