diff --git a/declarations/getLoaderOptions.d.ts b/declarations/getLoaderOptions.d.ts new file mode 100644 index 0000000..92051aa --- /dev/null +++ b/declarations/getLoaderOptions.d.ts @@ -0,0 +1,4 @@ +export function getLoaderOptions( + options: string | object | null | undefined, + schema: import('./validate').Schema | null | undefined +): object; diff --git a/declarations/index.d.ts b/declarations/index.d.ts index cf47ded..3d8b6ca 100644 --- a/declarations/index.d.ts +++ b/declarations/index.d.ts @@ -1,3 +1,4 @@ +import { getLoaderOptions } from './getLoaderOptions'; import { validate } from './validate'; import { ValidationError } from './validate'; -export { validate, ValidationError }; +export { getLoaderOptions, validate, ValidationError }; diff --git a/package-lock.json b/package-lock.json index f033226..faa4973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2267,6 +2267,11 @@ "@types/istanbul-lib-report": "*" } }, + "@types/json-parse-better-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/json-parse-better-errors/-/json-parse-better-errors-1.0.0.tgz", + "integrity": "sha512-JAsGXEMsiw2ttNrlady/Z8ztrSEl1y8IoG9ge7hpBQ4I+ilKxelOPGKnVmt9TX69lkXdJioDWCkzkktWcdAshA==" + }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -8719,8 +8724,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-parse-even-better-errors": { "version": "2.3.1", diff --git a/package.json b/package.json index 5b667b4..e1fe6a7 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,11 @@ "declarations" ], "dependencies": { + "@types/json-parse-better-errors": "^1.0.0", + "@types/json-schema": "^7.0.7", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2", - "@types/json-schema": "^7.0.7" + "json-parse-better-errors": "^1.0.2" }, "devDependencies": { "@babel/cli": "^7.12.13", diff --git a/src/getLoaderOptions.js b/src/getLoaderOptions.js new file mode 100644 index 0000000..9c555b1 --- /dev/null +++ b/src/getLoaderOptions.js @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ + +const querystring = require('querystring'); + +const parseJson = require('json-parse-better-errors'); + +const { validate } = require('./validate'); + +/** + * Parses the loader option if necessary, and validates it. + * + * @param {string | object | null | undefined} options a raw object, a JSON string, or a query string representing a loader option + * @param {import("./validate").Schema | null | undefined} schema a schema to validate the option against + * @return {object} the parsed loader option + */ +module.exports.getLoaderOptions = function getLoaderOptions(options, schema) { + if (typeof options === 'string') { + if (options.substr(0, 1) === '{' && options.substr(-1) === '}') { + try { + options = parseJson(options); + } catch (e) { + throw new Error(`Cannot parse string options: ${e.message}`); + } + } else { + options = querystring.parse(options, '&', '=', { + maxKeys: 0, + }); + } + } + + // eslint-disable-next-line no-undefined + if (options === null || options === undefined) { + options = {}; + } + + if (schema) { + let name = 'Loader'; + let baseDataPath = 'options'; + let match; + // eslint-disable-next-line no-cond-assign + if (schema.title && (match = /^(.+) (.+)$/.exec(schema.title))) { + [, name, baseDataPath] = match; + } + // @ts-expect-error + validate(schema, options, { + name, + baseDataPath, + }); + } + + // @ts-expect-error + return options; +}; diff --git a/src/index.js b/src/index.js index f994bea..24dcfca 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ const { validate, ValidationError } = require('./validate'); +const { getLoaderOptions } = require('./getLoaderOptions'); -module.exports = { validate, ValidationError }; +module.exports = { getLoaderOptions, validate, ValidationError }; diff --git a/test/__snapshots__/getLoaderOptions.test.js.snap b/test/__snapshots__/getLoaderOptions.test.js.snap new file mode 100644 index 0000000..0fb666b --- /dev/null +++ b/test/__snapshots__/getLoaderOptions.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getLoaderOptions parses and validates given JSON string 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options has an unknown property 'namee'. These properties are valid: + object { name? }" +`; + +exports[`getLoaderOptions parses and validates given query string 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options has an unknown property 'namee'. These properties are valid: + object { name? }" +`; + +exports[`getLoaderOptions returns and validates given object as-is 1`] = ` +"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema. + - options has an unknown property 'namee'. These properties are valid: + object { name? }" +`; + +exports[`getLoaderOptions throws an error for invalid JSON 1`] = `"Cannot parse string options: Unexpected token u in JSON at position 10 while parsing near '{\\"name\\":faulse}'"`; diff --git a/test/getLoaderOptions.test.js b/test/getLoaderOptions.test.js new file mode 100644 index 0000000..c35438e --- /dev/null +++ b/test/getLoaderOptions.test.js @@ -0,0 +1,59 @@ +import { getLoaderOptions } from '../src'; + +import schemaTitle from './fixtures/schema-title.json'; + +describe('getLoaderOptions', () => { + it('returns given object as-is', () => { + const options = getLoaderOptions({ name: false }); + expect(options).toEqual({ name: false }); + }); + + it('returns and validates given object as-is', () => { + const options = getLoaderOptions({ name: false }, schemaTitle); + expect(options).toEqual({ name: false }, schemaTitle); + expect(() => + getLoaderOptions({ namee: false }, schemaTitle) + ).toThrowErrorMatchingSnapshot(); + }); + + it('parses given JSON string', () => { + const options = getLoaderOptions('{"name":false}'); + expect(options).toEqual({ name: false }); + }); + + it('parses and validates given JSON string', () => { + const options = getLoaderOptions('{"name":false}', schemaTitle); + expect(options).toEqual({ name: false }, schemaTitle); + expect(() => + getLoaderOptions('{"namee":false}', schemaTitle) + ).toThrowErrorMatchingSnapshot(); + }); + + it('throws an error for invalid JSON', () => { + expect(() => + getLoaderOptions('{"name":faulse}') + ).toThrowErrorMatchingSnapshot(); + }); + + it('parses given query string', () => { + const options = getLoaderOptions('name=false'); + expect(options).toEqual({ name: 'false' }); + }); + + it('parses and validates given query string', () => { + expect(() => + getLoaderOptions('namee=false', schemaTitle) + ).toThrowErrorMatchingSnapshot(); + }); + + it('returns an empty object for null', () => { + const options = getLoaderOptions(null); + expect(options).toEqual({}); + }); + + it('returns an empty object for undefined', () => { + // eslint-disable-next-line no-undefined + const options = getLoaderOptions(undefined); + expect(options).toEqual({}); + }); +});