Skip to content

Commit

Permalink
more validation of plugin objects (#594)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 14, 2020
1 parent 66170fd commit ef6eead
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 48 deletions.
4 changes: 2 additions & 2 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ export const startService: typeof types.startService = options => {
return {
build: (options: types.BuildOptions): Promise<any> =>
new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrServe(null, options, false, (err, res) =>
service.buildOrServe('build', null, options, false, (err, res) =>
err ? reject(err) : resolve(res as types.BuildResult))),
transform: (input, options) => {
input += '';
return new Promise((resolve, reject) =>
service.transform(input, options || {}, false, {
service.transform('transform', input, options || {}, false, {
readFile(_, callback) { callback(new Error('Internal error'), null); },
writeFile(_, callback) { callback(null); },
}, (err, res) => err ? reject(err) : resolve(res!)))
Expand Down
97 changes: 56 additions & 41 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ function getFlag<T, K extends keyof T>(object: T, keys: OptionKeys, key: K, must
return value;
}

function checkForInvalidFlags(object: Object, keys: OptionKeys): void {
function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string): void {
for (let key in object) {
if (!(key in keys)) {
throw new Error(`Invalid option: "${key}"`);
throw new Error(`Invalid option ${where}: "${key}"`);
}
}
}
Expand All @@ -68,7 +68,7 @@ export function validateServiceOptions(options: types.ServiceOptions): types.Ser
let keys: OptionKeys = Object.create(null);
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeString);
let worker = getFlag(options, keys, 'worker', mustBeBoolean);
checkForInvalidFlags(options, keys);
checkForInvalidFlags(options, keys, 'in startService() call');
return {
wasmURL,
worker,
Expand Down Expand Up @@ -137,7 +137,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
if (footer) flags.push(`--footer=${footer}`);
}

function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean, logLevelDefault: types.LogLevel, writeDefault: boolean):
function flagsForBuildOptions(callName: string, options: types.BuildOptions, isTTY: boolean, logLevelDefault: types.LogLevel, writeDefault: boolean):
[string[], boolean, types.Plugin[] | undefined, string | null, string | null, boolean] {
let flags: string[] = [];
let keys: OptionKeys = Object.create(null);
Expand Down Expand Up @@ -167,7 +167,7 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean, logLe
let write = getFlag(options, keys, 'write', mustBeBoolean) ?? writeDefault; // Default to true if not specified
let incremental = getFlag(options, keys, 'incremental', mustBeBoolean) === true;
let plugins = getFlag(options, keys, 'plugins', mustBeArray);
checkForInvalidFlags(options, keys);
checkForInvalidFlags(options, keys, `in ${callName}() call`);

if (sourcemap) flags.push(`--sourcemap${sourcemap === true ? '' : `=${sourcemap}`}`);
if (bundle) flags.push('--bundle');
Expand Down Expand Up @@ -226,7 +226,7 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean, logLe
let resolveDir = getFlag(stdin, stdinKeys, 'resolveDir', mustBeString);
let sourcefile = getFlag(stdin, stdinKeys, 'sourcefile', mustBeString);
let loader = getFlag(stdin, stdinKeys, 'loader', mustBeString);
checkForInvalidFlags(stdin, stdinKeys);
checkForInvalidFlags(stdin, stdinKeys, 'in "stdin" object');

if (sourcefile) flags.push(`--sourcefile=${sourcefile}`);
if (loader) flags.push(`--loader=${loader}`);
Expand All @@ -237,7 +237,7 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean, logLe
return [flags, write, plugins, stdinContents, stdinResolveDir, incremental];
}

function flagsForTransformOptions(options: types.TransformOptions, isTTY: boolean, logLevelDefault: types.LogLevel): string[] {
function flagsForTransformOptions(callName: string, options: types.TransformOptions, isTTY: boolean, logLevelDefault: types.LogLevel): string[] {
let flags: string[] = [];
let keys: OptionKeys = Object.create(null);
pushLogFlags(flags, options, keys, isTTY, logLevelDefault);
Expand All @@ -247,7 +247,7 @@ function flagsForTransformOptions(options: types.TransformOptions, isTTY: boolea
let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject);
let sourcefile = getFlag(options, keys, 'sourcefile', mustBeString);
let loader = getFlag(options, keys, 'loader', mustBeString);
checkForInvalidFlags(options, keys);
checkForInvalidFlags(options, keys, `in ${callName}() call`);

if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`);
if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`);
Expand Down Expand Up @@ -277,13 +277,15 @@ export interface StreamFS {

export interface StreamService {
buildOrServe(
callName: string,
serveOptions: types.ServeOptions | null,
options: types.BuildOptions,
isTTY: boolean,
callback: (err: Error | null, res: types.BuildResult | types.ServeResult | null) => void,
): void;

transform(
callName: string,
input: string,
options: types.TransformOptions,
isTTY: boolean,
Expand Down Expand Up @@ -447,50 +449,61 @@ export function createChannel(streamIn: StreamIn): StreamOut {
if (streamIn.isSync) throw new Error('Cannot use plugins in synchronous API calls');

let onResolveCallbacks: {
[id: number]: (args: types.OnResolveArgs) =>
(types.OnResolveResult | null | undefined | Promise<types.OnResolveResult | null | undefined>)
[id: number]: {
name: string,
callback: (args: types.OnResolveArgs) =>
(types.OnResolveResult | null | undefined | Promise<types.OnResolveResult | null | undefined>),
},
} = {};
let onLoadCallbacks: {
[id: number]: (args: types.OnLoadArgs) =>
(types.OnLoadResult | null | undefined | Promise<types.OnLoadResult | null | undefined>)
[id: number]: {
name: string,
callback: (args: types.OnLoadArgs) =>
(types.OnLoadResult | null | undefined | Promise<types.OnLoadResult | null | undefined>),
},
} = {};
let nextCallbackID = 0;
let i = 0;

request.plugins = [];

for (let item of plugins) {
let name = item.name;
let setup = item.setup;
let keys: OptionKeys = {};
if (typeof item !== 'object') throw new Error(`Plugin at index ${i} must be an object`);
let name = getFlag(item, keys, 'name', mustBeString);
let setup = getFlag(item, keys, 'setup', mustBeFunction);

if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`);
if (typeof setup !== 'function') throw new Error(`[${name}] Plugin is missing a setup function`);
checkForInvalidFlags(item, keys, `on plugin ${JSON.stringify(name)}`);

let plugin: protocol.BuildPlugin = {
name: name + '',
name,
onResolve: [],
onLoad: [],
};
if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`);
if (typeof setup !== 'function') throw new Error(`[${plugin.name}] Missing a setup function`);
i++;

setup({
onResolve(options, callback) {
let keys: OptionKeys = {};
let filter = getFlag(options, keys, 'filter', mustBeRegExp);
let namespace = getFlag(options, keys, 'namespace', mustBeString);
checkForInvalidFlags(options, keys);
if (filter == null) throw new Error(`[${plugin.name}] "onResolve" is missing a filter`);
checkForInvalidFlags(options, keys, `in onResolve() call for plugin ${JSON.stringify(name)}`);
if (filter == null) throw new Error(`[${plugin.name}] onResolve() call is missing a filter`);
let id = nextCallbackID++;
onResolveCallbacks[id] = callback;
onResolveCallbacks[id] = { name: name!, callback };
plugin.onResolve.push({ id, filter: filter.source, namespace: namespace || '' });
},

onLoad(options, callback) {
let keys: OptionKeys = {};
let filter = getFlag(options, keys, 'filter', mustBeRegExp);
let namespace = getFlag(options, keys, 'namespace', mustBeString);
checkForInvalidFlags(options, keys);
if (filter == null) throw new Error(`[${plugin.name}] "onLoad" is missing a filter`);
checkForInvalidFlags(options, keys, `in onLoad() call for plugin ${JSON.stringify(name)}`);
if (filter == null) throw new Error(`[${plugin.name}] onLoad() call is missing a filter`);
let id = nextCallbackID++;
onLoadCallbacks[id] = callback;
onLoadCallbacks[id] = { name: name!, callback };
plugin.onLoad.push({ id, filter: filter.source, namespace: namespace || '' });
},
});
Expand All @@ -504,7 +517,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
let response: protocol.OnResolveResponse = {};
for (let id of request.ids) {
try {
let callback = onResolveCallbacks[id];
let { name, callback } = onResolveCallbacks[id];
let result = await callback({
path: request.path,
importer: request.importer,
Expand All @@ -513,23 +526,23 @@ export function createChannel(streamIn: StreamIn): StreamOut {
});

if (result != null) {
if (typeof result !== 'object') throw new Error('Expected resolver plugin to return an object');
if (typeof result !== 'object') throw new Error(`Expected onResolve() callback in plugin ${JSON.stringify(name)} to return an object`);
let keys: OptionKeys = {};
let pluginName = getFlag(result, keys, 'pluginName', mustBeString);
let path = getFlag(result, keys, 'path', mustBeString);
let namespace = getFlag(result, keys, 'namespace', mustBeString);
let external = getFlag(result, keys, 'external', mustBeBoolean);
let errors = getFlag(result, keys, 'errors', mustBeArray);
let warnings = getFlag(result, keys, 'warnings', mustBeArray);
checkForInvalidFlags(result, keys);
checkForInvalidFlags(result, keys, `from onResolve() callback in plugin ${JSON.stringify(name)}`);

response.id = id;
if (pluginName != null) response.pluginName = pluginName;
if (path != null) response.path = path;
if (namespace != null) response.namespace = namespace;
if (external != null) response.external = external;
if (errors != null) response.errors = sanitizeMessages(errors);
if (warnings != null) response.warnings = sanitizeMessages(warnings);
if (errors != null) response.errors = sanitizeMessages(errors, 'errors');
if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings');
break;
}
} catch (e) {
Expand All @@ -543,31 +556,31 @@ export function createChannel(streamIn: StreamIn): StreamOut {
let response: protocol.OnLoadResponse = {};
for (let id of request.ids) {
try {
let callback = onLoadCallbacks[id];
let { name, callback } = onLoadCallbacks[id];
let result = await callback({
path: request.path,
namespace: request.namespace,
});

if (result != null) {
if (typeof result !== 'object') throw new Error('Expected loader plugin to return an object');
if (typeof result !== 'object') throw new Error(`Expected onLoad() callback in plugin ${JSON.stringify(name)} to return an object`);
let keys: OptionKeys = {};
let pluginName = getFlag(result, keys, 'pluginName', mustBeString);
let contents = getFlag(result, keys, 'contents', mustBeStringOrUint8Array);
let resolveDir = getFlag(result, keys, 'resolveDir', mustBeString);
let loader = getFlag(result, keys, 'loader', mustBeString);
let errors = getFlag(result, keys, 'errors', mustBeArray);
let warnings = getFlag(result, keys, 'warnings', mustBeArray);
checkForInvalidFlags(result, keys);
checkForInvalidFlags(result, keys, `from onLoad() callback in plugin ${JSON.stringify(name)}`);

response.id = id;
if (pluginName != null) response.pluginName = pluginName;
if (contents instanceof Uint8Array) response.contents = contents;
else if (contents != null) response.contents = protocol.encodeUTF8(contents);
if (resolveDir != null) response.resolveDir = resolveDir;
if (loader != null) response.loader = loader;
if (errors != null) response.errors = sanitizeMessages(errors);
if (warnings != null) response.warnings = sanitizeMessages(warnings);
if (errors != null) response.errors = sanitizeMessages(errors, 'errors');
if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings');
break;
}
} catch (e) {
Expand Down Expand Up @@ -605,7 +618,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
};
});
request.serve = { serveID };
checkForInvalidFlags(options, keys);
checkForInvalidFlags(options, keys, `in serve() call`);
if (port !== void 0) request.serve.port = port;
if (host !== void 0) request.serve.host = host;
serveCallbacks.set(serveID, {
Expand All @@ -627,12 +640,12 @@ export function createChannel(streamIn: StreamIn): StreamOut {
afterClose,

service: {
buildOrServe(serveOptions, options, isTTY, callback) {
buildOrServe(callName, serveOptions, options, isTTY, callback) {
const logLevelDefault = 'info';
try {
let key = nextBuildKey++;
let writeDefault = !streamIn.isBrowser;
let [flags, write, plugins, stdin, resolveDir, incremental] = flagsForBuildOptions(options, isTTY, logLevelDefault, writeDefault);
let [flags, write, plugins, stdin, resolveDir, incremental] = flagsForBuildOptions(callName, options, isTTY, logLevelDefault, writeDefault);
let request: protocol.BuildRequest = { command: 'build', key, flags, write, stdin, resolveDir, incremental };
let serve = serveOptions && buildServeData(serveOptions, request);
let pluginCleanup = plugins && plugins.length > 0 && handlePlugins(plugins, request, key);
Expand Down Expand Up @@ -703,7 +716,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
}
},

transform(input, options, isTTY, fs, callback) {
transform(callName, input, options, isTTY, fs, callback) {
const logLevelDefault = 'silent';

// Ideally the "transform()" API would be faster than calling "build()"
Expand All @@ -723,7 +736,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
// that doesn't work.
let start = (inputPath: string | null) => {
try {
let flags = flagsForTransformOptions(options, isTTY, logLevelDefault);
let flags = flagsForTransformOptions(callName, options, isTTY, logLevelDefault);
let request: protocol.TransformRequest = {
command: 'transform',
flags,
Expand Down Expand Up @@ -855,14 +868,15 @@ function failureErrorWithLog(text: string, errors: types.Message[], warnings: ty
return error;
}

function sanitizeMessages(messages: types.PartialMessage[]): types.Message[] {
function sanitizeMessages(messages: types.PartialMessage[], property: string): types.Message[] {
let messagesClone: types.Message[] = [];
let index = 0;

for (const message of messages) {
let keys: OptionKeys = {};
let text = getFlag(message, keys, 'text', mustBeString);
let location = getFlag(message, keys, 'location', mustBeObjectOrNull);
checkForInvalidFlags(message, keys);
checkForInvalidFlags(message, keys, `in element ${index} of "${property}"`);

let locationClone: types.Message['location'] = null;
if (location != null) {
Expand All @@ -873,7 +887,7 @@ function sanitizeMessages(messages: types.PartialMessage[]): types.Message[] {
let column = getFlag(location, keys, 'column', mustBeInteger);
let length = getFlag(location, keys, 'length', mustBeInteger);
let lineText = getFlag(location, keys, 'lineText', mustBeString);
checkForInvalidFlags(location, keys);
checkForInvalidFlags(location, keys, `in element ${index} of "${property}"`);

locationClone = {
file: file || '',
Expand All @@ -889,6 +903,7 @@ function sanitizeMessages(messages: types.PartialMessage[]): types.Message[] {
text: text || '',
location: locationClone,
});
index++;
}

return messagesClone;
Expand Down
10 changes: 5 additions & 5 deletions lib/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export let buildSync: typeof types.buildSync = (options: types.BuildOptions): an

// Otherwise, fall back to running a dedicated child process
let result: types.BuildResult;
runServiceSync(service => service.buildOrServe(null, options, isTTY(), (err, res) => {
runServiceSync(service => service.buildOrServe('buildSync', null, options, isTTY(), (err, res) => {
if (err) throw err;
result = res as types.BuildResult;
}));
Expand All @@ -135,7 +135,7 @@ export let transformSync: typeof types.transformSync = (input, options) => {

// Otherwise, fall back to running a dedicated child process
let result: types.TransformResult;
runServiceSync(service => service.transform(input, options || {}, isTTY(), {
runServiceSync(service => service.transform('transformSync', input, options || {}, isTTY(), {
readFile(tempFile, callback) {
try {
let contents = fs.readFileSync(tempFile, 'utf8');
Expand Down Expand Up @@ -189,19 +189,19 @@ export let startService: typeof types.startService = options => {
return Promise.resolve({
build: (options: types.BuildOptions): Promise<any> =>
new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrServe(null, options, isTTY(), (err, res) =>
service.buildOrServe('build', null, options, isTTY(), (err, res) =>
err ? reject(err) : resolve(res as types.BuildResult))),
serve: (serveOptions, buildOptions) => {
if (serveOptions === null || typeof serveOptions !== 'object')
throw new Error('The first argument must be an object')
return new Promise((resolve, reject) =>
service.buildOrServe(serveOptions, buildOptions, isTTY(), (err, res) =>
service.buildOrServe('serve', serveOptions, buildOptions, isTTY(), (err, res) =>
err ? reject(err) : resolve(res as types.ServeResult)))
},
transform: (input, options) => {
input += '';
return new Promise((resolve, reject) =>
service.transform(input, options || {}, isTTY(), {
service.transform('transform', input, options || {}, isTTY(), {
readFile(tempFile, callback) {
try {
fs.readFile(tempFile, 'utf8', (err, contents) => {
Expand Down
Loading

0 comments on commit ef6eead

Please sign in to comment.