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

feat: static assets #671

Merged
merged 14 commits into from
Oct 25, 2019
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@
"build": "tsc -p ./tsconfig.build.json",
"cli": "node -r ts-node/register -r tsconfig-paths/register src/cli/index.ts",
"cli:debug": "node -r ts-node/register -r tsconfig-paths/register --inspect-brk src/cli/index.ts",
"compile-rulesets": "node ./scripts/compile-rulesets.js",
"generate-assets": "node ./scripts/generate-assets.js",
"inline-version": "./scripts/inline-version.js",
"lint.fix": "yarn lint --fix",
"lint": "tsc --noEmit && tslint 'src/**/*.ts'",
"postbuild.oas-functions": "copyfiles -u 1 \"dist/rulesets/oas*/functions/*.js\" ./",
"postbuild": "yarn build.oas-functions && yarn compile-rulesets",
"postbuild": "yarn build.oas-functions && yarn generate-assets",
"prebuild": "copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" dist && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" ./",
"prebuild.binary": "yarn build",
"pretest.karma": "node ./scripts/generate-karma-fixtures.js",
"pretest.karma": "node ./scripts/generate-karma-fixtures.js && yarn pretest",
"pretest": "node ./scripts/generate-assets.js",
"schema.update": "yarn typescript-json-schema --id \"http://stoplight.io/schemas/rule.schema.json\" --required tsconfig.json IRule --out ./src/meta/rule.schema.json",
"test.harness": "jest -c ./jest.harness.config.js",
"test.karma": "karma start",
Expand Down
38 changes: 19 additions & 19 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import typescript from 'rollup-plugin-typescript2';
import * as path from 'path';
import * as fs from 'fs';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import { terser } from 'rollup-plugin-terser';

const BASE_PATH = process.cwd();
const functions = [];

const processor = (folder, array) => array.map(fn => ({
input: path.resolve(BASE_PATH, folder, `${fn}.js`),
for (const directory of ['dist/rulesets/oas/functions','dist/rulesets/oas2/functions', 'dist/rulesets/oas3/functions']) {
const targetDir = path.join(BASE_PATH, directory);
if (!fs.existsSync(targetDir)) continue;
for (const file of fs.readdirSync(targetDir)) {
const targetFile = path.join(targetDir, file);
const stat = fs.statSync(targetFile);
if (!stat.isFile()) continue;
const ext = path.extname(targetFile);
if (ext !== '.js') continue;

functions.push(targetFile);
}
}

module.exports = functions.map(fn => ({
input: fn,
plugins: [
typescript({
tsconfig: path.join(BASE_PATH, './tsconfig.rollup.json'),
Expand All @@ -18,24 +34,8 @@ const processor = (folder, array) => array.map(fn => ({
terser(),
],
output: {
file: path.resolve(BASE_PATH, folder, `${fn}.js`),
file: fn,
format: 'cjs',
exports: 'named'
},
}));

module.exports = processor('dist/rulesets/oas/functions', [
'oasOp2xxResponse',
'oasOpFormDataConsumeCheck',
'oasOpIdUnique',
'oasOpParams',
'oasOpSecurityDefined',
'oasPathParam',
'refSiblings',
])
.concat(processor('dist/rulesets/oas2/functions', [
// Add here the oas2 specific functions
]))
.concat(processor('dist/rulesets/oas3/functions', [
// Add here the oas3 specific functions
]));
26 changes: 0 additions & 26 deletions scripts/compile-rulesets.js

This file was deleted.

61 changes: 61 additions & 0 deletions scripts/generate-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* This script generates a list of assets that are needed to load spectral:oas ruleset.
* It contains all OAS custom functions and *resolved* rulesets.
* The assets are stores in a single filed call assets.json in the following format:
* `<require-call-path>: <content>`
* where the `require-call-path` is the path you'd normally pass to require(), i.e. `@stoplight/spectral/rulesets/oas/index.js` and `content` is the text data.
* Assets can be loaded using Spectral#registerStaticAssets statc method, i.e. `Spectral.registerStaticAssets(require('@stoplight/spectral/rulesets/assets/assets.json'))`;
* If you execute the code above, ruleset will be loaded fully offline, without a need to make any request.
*/

const path = require('@stoplight/path');
const fs = require('fs');
const { promisify } = require('util');
const { parse } = require('@stoplight/yaml');
const { httpAndFileResolver } = require('../dist/resolvers/http-and-file');

const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);
const readdirAsync = promisify(fs.readdir);
const statAsync = promisify(fs.stat);

const baseDir = path.join(__dirname, '../rulesets/assets/');

if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir);
}

const target = path.join(baseDir, `assets.json`);
const assets = {};

(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change needed if we are doing #561 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's still going to be needed. It's mostly for Studio to use and we don't expose it anywhere in the docs, etc.
I'll leave a note somewhere in the code on what it actually does and how it works, as right now it's not obvious, and we will forget about why we need it.
I see Windows build is failing, so need to take a look what's broken.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@collinbachi did we already switch to whatever new CircleCI payment structure that gets us Windows builds?

Copy link

@collinbachi collinbachi Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We started a 2 week trial of the new structure Wednesday afternoon

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@P0lip ok, fair enough on the still needed. If you cannot get windows build working with Azure then maybe try out the CircleCI windows build, however that works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philsturgeon I got Windows build working, but would love to try out Windows on CircleCI. Azure pipelines is tad slow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another time! :)

await Promise.all(['', '2', '3'].map(spec => processDirectory(assets, path.join(__dirname, `../rulesets/oas${spec}`))));
await writeFileAsync(target, JSON.stringify(assets, null, 2));
})();

async function processDirectory(assets, dir) {
await Promise.all((await readdirAsync(dir)).map(async name => {
if (name === 'schemas') return;
const target = path.join(dir, name);
const stats = await statAsync(target);
if (stats.isDirectory()) {
return processDirectory(assets, target);
} else {
let content = await readFileAsync(target, 'utf8');
if (path.extname(name) === '.json') {
content = JSON.stringify((await httpAndFileResolver.resolve(JSON.parse(content), {
dereferenceRemote: true,
dereferenceInline: false,
baseUri: target,
parseResolveResult(opts) {
opts.result = parse(opts.result);
return opts;
},
})).result);
}

assets[path.join('@stoplight/spectral', path.relative(path.join(__dirname, '..'), target))] = content;
}
}));
}
13 changes: 13 additions & 0 deletions src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Dictionary } from '@stoplight/types';

function resolveSpectralRuleset(ruleset: string) {
return `@stoplight/spectral/rulesets/${ruleset}/index.json`;
}

export const RESOLVE_ALIASES: Dictionary<string, string> = {
'spectral:oas': resolveSpectralRuleset('oas'),
'spectral:oas2': resolveSpectralRuleset('oas2'),
'spectral:oas3': resolveSpectralRuleset('oas3'),
};

export const STATIC_ASSETS: Dictionary<string> = {};
5 changes: 5 additions & 0 deletions src/fs/reader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isURL } from '@stoplight/path';
import AbortController from 'abort-controller';
import * as fs from 'fs';
import { STATIC_ASSETS } from '../assets';
import request from '../request';

export interface IReadOptions {
Expand All @@ -9,6 +10,10 @@ export interface IReadOptions {
}

export async function readFile(name: string, opts: IReadOptions): Promise<string> {
if (name in STATIC_ASSETS) {
return STATIC_ASSETS[name];
}

if (isURL(name)) {
let response;
let timeout: NodeJS.Timeout | number | null = null;
Expand Down
40 changes: 40 additions & 0 deletions src/rulesets/__tests__/reader.jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Dictionary } from '@stoplight/types';
import { DiagnosticSeverity } from '@stoplight/types';
import * as fs from 'fs';
import * as nock from 'nock';
import { Spectral } from '../../spectral';
import { IRule, Rule } from '../../types';
import { readRuleset } from '../reader';
const nanoid = require('nanoid');

jest.mock('nanoid');
jest.mock('fs');

const validFlatRuleset = path.join(__dirname, './__fixtures__/valid-flat-ruleset.json');
const validRequireInfo = path.join(__dirname, './__fixtures__/valid-require-info-ruleset.yaml');
Expand Down Expand Up @@ -44,6 +46,7 @@ describe('Rulesets reader', () => {

afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});

it('given flat, valid ruleset file should return rules', async () => {
Expand Down Expand Up @@ -594,4 +597,41 @@ describe('Rulesets reader', () => {
it('given invalid ruleset should output errors', () => {
return expect(readRuleset(invalidRuleset)).rejects.toThrowError(/should have required property/);
});

it('is able to load the whole ruleset from static file', async () => {
nock.disableNetConnect();

const readFileSpy = jest.spyOn(fs, 'readFile');

Spectral.registerStaticAssets(require('../../../rulesets/assets/assets.json'));

const { rules, functions } = await readRuleset('spectral:oas');

expect(rules).toMatchObject({
'openapi-tags': expect.objectContaining({
description: 'OpenAPI object should have non-empty `tags` array.',
formats: ['oas2', 'oas3'],
}),
'oas2-schema': expect.objectContaining({
description: 'Validate structure of OpenAPI v2 specification.',
formats: ['oas2'],
}),
'oas3-schema': expect.objectContaining({
description: 'Validate structure of OpenAPI v3 specification.',
formats: ['oas3'],
}),
});

expect(functions).toMatchObject({
oasOp2xxResponse: expect.any(Object),
oasOpFormDataConsumeCheck: expect.any(Object),
oasOpIdUnique: expect.any(Object),
oasOpParams: expect.any(Object),
oasOpSecurityDefined: expect.any(Object),
oasPathParam: expect.any(Object),
});

expect(readFileSpy).not.toBeCalled();
readFileSpy.mockRestore();
});
});
47 changes: 47 additions & 0 deletions src/rulesets/__tests__/reader.karma.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FetchMockSandbox } from 'fetch-mock';
import { Spectral } from '../../spectral';
import { readRuleset } from '../reader';

declare const fetch: FetchMockSandbox;

describe('Rulesets reader', () => {
afterEach(() => {
Spectral.registerStaticAssets({});
});

it('is able to load the whole ruleset from static file', async () => {
fetch.resetBehavior();
fetch.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', {
status: 404,
body: {},
});

Spectral.registerStaticAssets(require('../../../rulesets/assets/assets.json'));

const { rules, functions } = await readRuleset('spectral:oas');

expect(rules).toMatchObject({
'openapi-tags': expect.objectContaining({
description: 'OpenAPI object should have non-empty `tags` array.',
formats: ['oas2', 'oas3'],
}),
'oas2-schema': expect.objectContaining({
description: 'Validate structure of OpenAPI v2 specification.',
formats: ['oas2'],
}),
'oas3-schema': expect.objectContaining({
description: 'Validate structure of OpenAPI v3 specification.',
formats: ['oas3'],
}),
});

expect(functions).toMatchObject({
oasOp2xxResponse: expect.any(Object),
oasOpFormDataConsumeCheck: expect.any(Object),
oasOpIdUnique: expect.any(Object),
oasOpParams: expect.any(Object),
oasOpSecurityDefined: expect.any(Object),
oasPathParam: expect.any(Object),
});
});
});
16 changes: 13 additions & 3 deletions src/rulesets/finder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as path from '@stoplight/path';
import * as fs from 'fs';
import { filesMap } from './map';
import { RESOLVE_ALIASES, STATIC_ASSETS } from '../assets';

const SPECTRAL_SRC_ROOT = path.join(__dirname, '..');

// DON'T RENAME THIS FUNCTION, you can move it within this file, but it must be kept as top-level declaration
// parameter can be renamed, but don't this if you don't need to
function resolveSpectralVersion(pkg: string) {
Expand Down Expand Up @@ -32,6 +31,12 @@ async function resolveFromFS(from: string, to: string) {
}

targetPath = path.resolve(from, to);

// if found in static assets, it's fine, as readParsable will handle it just fine
if (targetPath in STATIC_ASSETS) {
return targetPath;
}

// if it's not a built-in ruleset, try to resolve the file according to the provided path
if (await exists(targetPath)) {
return targetPath;
Expand All @@ -41,11 +46,16 @@ async function resolveFromFS(from: string, to: string) {
}

export async function findFile(from: string, to: string) {
const mapped = filesMap.get(to);
const mapped = RESOLVE_ALIASES[to];

if (mapped !== void 0) {
to = mapped;
}

if (to in STATIC_ASSETS) {
return to;
}

if (path.isAbsolute(to)) {
return to;
}
Expand Down
9 changes: 0 additions & 9 deletions src/rulesets/map.ts

This file was deleted.

Loading