Skip to content

Commit

Permalink
feat(ruleset-bundler): initial release (#1819)
Browse files Browse the repository at this point in the history
* feat(ruleset-bundler): initial release

* test(karma): setup alias for karma

* style: simplify

* docs: add some
  • Loading branch information
P0lip committed Sep 15, 2021
1 parent 55ed5e4 commit f8a58f7
Show file tree
Hide file tree
Showing 30 changed files with 1,351 additions and 19 deletions.
Empty file added __karma__/fsevents.ts
Empty file.
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ module.exports = {
},
testMatch: ['<rootDir>/packages/functions/src/**/__tests__/**/*.{test,spec}.ts'],
},
{
...projectDefault,
displayName: {
name: '@stoplight/spectral-ruleset-bundler',
color: 'blueBright',
},
testMatch: ['<rootDir>/packages/ruleset-bundler/src/**/__tests__/**/*.{test,spec}.ts'],
},
{
...projectDefault,
displayName: {
Expand Down
1 change: 1 addition & 0 deletions karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = (config: Config): void => {
'node-fetch': require.resolve('./__karma__/fetch'),
fs: require.resolve('./__karma__/fs'),
process: require.resolve('./__karma__/process'),
fsevents: require.resolve('./__karma__/fsevents'),
},
},
acornOptions: {
Expand Down
15 changes: 15 additions & 0 deletions packages/ruleset-bundler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @stoplight/spectral-ruleset-bundler

**WARNING** - for the time being, the following package is meant to be used internally.

## Options

- **plugins** - any other Rollup.js plugin, i.e. a minifier.
- **target**:
- `node` - a preset suitable for the Node.js runtime
- `browser` - a preset tailored to Browsers
- `runtime` - a preset you want to use if you want to bundle & execute the ruleset at the runtime
- format - supported values are: `esm`, `commonjs`, `iife`.
- treeshake - whether to enable tree shaking. False by default.

**Bolded** options are required.
52 changes: 52 additions & 0 deletions packages/ruleset-bundler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@stoplight/spectral-ruleset-bundler",
"version": "1.0.0",
"homepage": "https://github.com/stoplightio/spectral",
"bugs": "https://github.com/stoplightio/spectral/issues",
"author": "Stoplight <support@stoplight.io>",
"engines": {
"node": ">=12.20"
},
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"type": "commonjs",
"exports": {
".": {
"default": "./dist/index.js"
},
"./presets/*": {
"default": "./dist/presets/*.js"
},
"./plugins/*": {
"default": "./dist/plugins/*.js"
}
},
"repository": {
"type": "git",
"url": "https://github.com/stoplightio/spectral.git"
},
"dependencies": {
"@stoplight/path": "1.3.2",
"@stoplight/spectral-core": ">=1",
"@stoplight/spectral-functions": ">=1",
"@stoplight/spectral-formats": ">=1",
"@stoplight/spectral-parsers": ">=1",
"@stoplight/spectral-ref-resolver": ">=1",
"@stoplight/spectral-rulesets": ">=1",
"@stoplight/spectral-runtime": "^1.1.0",
"@stoplight/types": "12.3.0",
"@types/node": "*",
"rollup": "~2.56.3",
"validate-npm-package-name": "3.0.0"
},
"devDependencies": {
"@types/validate-npm-package-name": "^3.0.3",
"fetch-mock": "^9.11.0",
"memfs": "^3.2.2",
"prettier": "^2.3.2"
}
}
312 changes: 312 additions & 0 deletions packages/ruleset-bundler/src/__tests__/index.spec.ts

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions packages/ruleset-bundler/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { rollup, Plugin } from 'rollup';
import { isURL } from '@stoplight/path';
import { isValidPackageName } from './utils/isValidPackageName';

export type BundleOptions = {
plugins: Plugin[];
target: 'node' | 'browser' | 'runtime';
format?: 'esm' | 'commonjs' | 'iife';
treeshake?: boolean; // false by default
};

export async function bundleRuleset(
input: string,
{ target = 'browser', plugins, format, treeshake = false }: BundleOptions,
): Promise<string> {
const bundle = await rollup({
input,
plugins,
treeshake,
watch: false,
perf: false,
onwarn(e, fn) {
if (e.code === 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT') {
return;
}

fn(e);
},
external:
// the iife output is meant to be evaluated as a script type at the runtime, therefore it must not contain any import/exports, we must have the entire code ready to execute
target === 'runtime'
? []
: target === 'browser'
? id => isURL(id)
: (id, importer) =>
id.startsWith('node:') ||
(!isURL(id) && isValidPackageName(id) && (importer === void 0 || !isURL(importer))),
});

return (await bundle.generate({ format: format ?? (target === 'runtime' ? 'iife' : 'esm'), exports: 'auto' }))
.output[0].code;
}
197 changes: 197 additions & 0 deletions packages/ruleset-bundler/src/plugins/__tests__/builtins.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import * as fs from 'fs';
import { serveAssets } from '@stoplight/spectral-test-utils';
import * as runtime from '@stoplight/spectral-runtime';
import * as functions from '@stoplight/spectral-functions';

jest.mock?.('fs');

import { BundleOptions, bundleRuleset } from '../../index';
import type { IO } from '../../types';
import { virtualFs } from '../virtualFs';
import { builtins } from '../builtins';

describe('Builtins Plugin', () => {
let io: IO;

beforeEach(() => {
io = {
fs,
fetch: runtime.fetch,
};
});

describe.each<BundleOptions['target']>(['browser', 'runtime'])('given %s target', target => {
it('should inline Spectral packages & expose it to the runtime', async () => {
serveAssets({
'/tmp/input.js': `import { schema } from '@stoplight/spectral-functions';
import { oas } from '@stoplight/spectral-rulesets';
export default {
extends: [oas],
rules: {
'my-rule': {
given: '$',
then: {
function: schema,
functionOptions: {
schema: {
type: 'object',
},
},
},
},
},
};`,
});

const code = await bundleRuleset('/tmp/input.js', {
format: 'esm',
target,
plugins: [builtins(), virtualFs(io)],
});

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'];
var input = {
extends: [oas],
rules: {
'my-rule': {
given: '$',
then: {
function: schema,
functionOptions: {
schema: {
type: 'object',
},
},
},
},
},
};
export { input as default };
`);

expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions);
});

it('should support overrides', async () => {
serveAssets({
'/tmp/input.js': `import { readFile } from '@stoplight/spectral-runtime';
readFile();`,
});

// eslint-disable-next-line @typescript-eslint/no-empty-function
function readFile(): void {}

const code = await bundleRuleset('/tmp/input.js', {
format: 'esm',
target,
plugins: [
builtins({
'@stoplight/spectral-runtime': {
readFile,
},
}),
virtualFs(io),
],
});

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'];
readFile();
`);

expect(globalThis[Symbol.for('@stoplight/spectral-runtime')]).toStrictEqual({
...runtime,
readFile,
});
});
});

describe('given node target', () => {
it('should be a no-op', async () => {
serveAssets({
'/tmp/input.js': `import { schema } from '@stoplight/spectral-functions';
import { oas } from '@stoplight/spectral-rulesets';
export default {
extends: [oas],
rules: {
'my-rule': {
given: '$',
then: {
function: schema,
functionOptions: {
schema: {
type: 'object',
},
},
},
},
},
};`,
});

const code = await bundleRuleset('/tmp/input.js', {
target: 'node',
plugins: [builtins(), virtualFs(io)],
});

expect(code).toEqual(`import { schema } from '@stoplight/spectral-functions';
import { oas } from '@stoplight/spectral-rulesets';
var input = {
extends: [oas],
rules: {
'my-rule': {
given: '$',
then: {
function: schema,
functionOptions: {
schema: {
type: 'object',
},
},
},
},
},
};
export { input as default };
`);

expect(globalThis[Symbol.for('@stoplight/spectral-functions')]).toStrictEqual(functions);
});
});
});

0 comments on commit f8a58f7

Please sign in to comment.