Plugin API for Parcel bundler - provides base classes for creating Parcel plugins including transformers, resolvers, bundlers, namers, runtimes, packagers, optimizers, compressors, reporters, and validators
67
The Validator plugin validates code for errors, style issues, and other quality concerns during the build process. Validators can run linting, type checking, and other code quality analysis.
Base class for creating code validation plugins.
/**
* Base class for code validation plugins
* @template T - Configuration type for this validator
*/
export declare class Validator<T> {
constructor(opts: ValidatorOpts);
}
/**
* Validator plugin configuration (union of two validator types)
*/
type ValidatorOpts = DedicatedThreadValidator | MultiThreadValidator;
/**
* Validator that processes all assets at once in a dedicated thread
*/
interface DedicatedThreadValidator {
/** Validate all assets together */
validateAll(args: {
assets: Asset[];
resolveConfigWithPath: ResolveConfigWithPathFn;
options: PluginOptions;
logger: PluginLogger;
tracer: PluginTracer;
}): Promise<Array<ValidateResult | null>>;
}
/**
* Validator that processes assets individually across multiple threads
*/
interface MultiThreadValidator {
/** Get configuration for an asset */
getConfig?(args: {
asset: Asset;
resolveConfig: ResolveConfigFn;
options: PluginOptions;
logger: PluginLogger;
tracer: PluginTracer;
}): Promise<ConfigResult>;
/** Validate a single asset */
validate(args: {
asset: Asset;
config: ConfigResult | void;
options: PluginOptions;
logger: PluginLogger;
tracer: PluginTracer;
}): Promise<ValidateResult | void>;
}/**
* Result from validating an asset
*/
interface ValidateResult {
/** Validation diagnostics (errors, warnings) */
diagnostics: Array<Diagnostic>;
/** Whether validation passed */
isValid: boolean;
/** Additional metadata */
meta?: Record<string, any>;
}
/**
* Configuration loading result
*/
interface ConfigResult {
/** Configuration object */
config: any;
/** Files that were read to load this config */
files: Array<FilePath>;
}
/**
* Function to resolve configuration with path information
*/
type ResolveConfigWithPathFn = (
configNames: Array<string>,
fromPath: FilePath
) => Promise<ConfigResult | null>;
/**
* Function to resolve configuration
*/
type ResolveConfigFn = (
configNames: Array<string>
) => Promise<any>;Single Asset Validator Example:
import { Validator } from "@parcel/plugin";
import eslint from "eslint";
export default new Validator({
// Get configuration for validation
async getConfig({asset, resolveConfig}) {
// Only validate JavaScript files
if (!asset.filePath.endsWith('.js')) {
return null;
}
// Load ESLint configuration
const config = await resolveConfig([
'.eslintrc.js',
'.eslintrc.json',
'.eslintrc'
]);
return { config, files: [] };
},
// Validate single asset
async validate({asset, config, logger}) {
if (!config) {
return null; // Skip validation if no config
}
const code = await asset.getCode();
const linter = new eslint.ESLint({
baseConfig: config.config,
useEslintrc: false
});
try {
const results = await linter.lintText(code, {
filePath: asset.filePath
});
const diagnostics = [];
for (const result of results) {
for (const message of result.messages) {
diagnostics.push({
level: message.severity === 2 ? 'error' : 'warning',
message: message.message,
filePath: asset.filePath,
start: {
line: message.line,
column: message.column
},
hints: message.fix ? ['Run ESLint with --fix to auto-correct this issue'] : undefined
});
}
}
return {
diagnostics,
isValid: diagnostics.every(d => d.level !== 'error')
};
} catch (error) {
return {
diagnostics: [{
level: 'error',
message: `ESLint validation failed: ${error.message}`,
filePath: asset.filePath
}],
isValid: false
};
}
}
});Batch Validator Example:
import { Validator } from "@parcel/plugin";
import typescript from "typescript";
export default new Validator({
// Validate all TypeScript assets together
async validateAll({assets, resolveConfigWithPath, options, logger}) {
// Filter TypeScript assets
const tsAssets = assets.filter(asset =>
asset.filePath.endsWith('.ts') || asset.filePath.endsWith('.tsx')
);
if (tsAssets.length === 0) {
return [];
}
// Load TypeScript configuration
const configResult = await resolveConfigWithPath([
'tsconfig.json'
], options.projectRoot);
const tsConfig = configResult?.config || {};
// Create TypeScript program
const fileNames = tsAssets.map(asset => asset.filePath);
const program = typescript.createProgram(fileNames, tsConfig.compilerOptions || {});
// Get diagnostics
const diagnostics = typescript.getPreEmitDiagnostics(program);
// Convert to validation results
const results = [];
for (const asset of tsAssets) {
const assetDiagnostics = diagnostics
.filter(d => d.file?.fileName === asset.filePath)
.map(d => this.convertTSDiagnostic(d));
results.push({
diagnostics: assetDiagnostics,
isValid: assetDiagnostics.every(d => d.level !== 'error')
});
}
return results;
},
convertTSDiagnostic(diagnostic) {
const message = typescript.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
);
let start = undefined;
if (diagnostic.file && diagnostic.start != null) {
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
start = {
line: pos.line + 1,
column: pos.character
};
}
return {
level: diagnostic.category === typescript.DiagnosticCategory.Error ? 'error' : 'warning',
message,
filePath: diagnostic.file?.fileName,
start,
documentationURL: `https://typescript.tv/errors/#TS${diagnostic.code}`
};
}
});File Type Filtering:
// Only validate specific file types
if (!asset.filePath.match(/\.(js|ts|jsx|tsx)$/)) {
return null;
}Configuration Loading:
// Load validation configuration
const config = await resolveConfig([
'.eslintrc.js',
'.eslintrc.json',
'package.json' // Look for eslintConfig key
]);Error Aggregation:
// Collect errors from multiple sources
const allDiagnostics = [
...syntaxErrors,
...lintingErrors,
...typeErrors
];
return {
diagnostics: allDiagnostics,
isValid: allDiagnostics.every(d => d.level !== 'error')
};Install with Tessl CLI
npx tessl i tessl/npm-parcel--plugindocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10