// @flow import assert from 'assert'; import nullthrows from 'nullthrows'; import path from 'path'; import ParcelConfig from '../src/ParcelConfig'; import { validateConfigFile, mergePipelines, mergeMaps, mergeConfigs, resolveExtends, parseAndProcessConfig, resolveParcelConfig, processConfig, } from '../src/requests/ParcelConfigRequest'; import {validatePackageName} from '../src/ParcelConfig.schema'; import {DEFAULT_OPTIONS, relative} from './test-utils'; import {toProjectPath} from '../src/projectPath'; describe('ParcelConfigRequest', () => { describe('validatePackageName', () => { it('should error on an invalid official package', () => { assert.throws(() => { validatePackageName('@parcel/foo-bar', 'transform', 'transformers'); }, /Official parcel transform packages must be named according to "@parcel\/transform-{name}"/); assert.throws(() => { validatePackageName('@parcel/transformer', 'transform', 'transformers'); }, /Official parcel transform packages must be named according to "@parcel\/transform-{name}"/); }); it('should succeed on a valid official package', () => { validatePackageName('@parcel/transform-bar', 'transform', 'transformers'); }); it('should error on an invalid community package', () => { assert.throws(() => { validatePackageName('foo-bar', 'transform', 'transformers'); }, /Parcel transform packages must be named according to "parcel-transform-{name}"/); assert.throws(() => { validatePackageName('parcel-foo-bar', 'transform', 'transformers'); }, /Parcel transform packages must be named according to "parcel-transform-{name}"/); assert.throws(() => { validatePackageName('parcel-transform', 'transform', 'transformers'); }, /Parcel transform packages must be named according to "parcel-transform-{name}"/); }); it('should succeed on a valid community package', () => { validatePackageName('parcel-transform-bar', 'transform', 'transformers'); }); it('should error on an invalid scoped package', () => { assert.throws(() => { validatePackageName('@test/foo-bar', 'transform', 'transformers'); }, /Scoped parcel transform packages must be named according to "@test\/parcel-transform\[-{name}\]"/); assert.throws(() => { validatePackageName( '@test/parcel-foo-bar', 'transform', 'transformers', ); }, /Scoped parcel transform packages must be named according to "@test\/parcel-transform\[-{name}\]"/); }); it('should succeed on a valid scoped package', () => { validatePackageName( '@test/parcel-transform-bar', 'transform', 'transformers', ); validatePackageName( '@test/parcel-transform', 'transform', 'transformers', ); }); }); describe('validateConfigFile', () => { it('should throw on invalid config', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', extends: 'parcel-config-foo', transformers: { '*.js': ['parcel-invalid-plugin'], }, }, '.parcelrc', ); }); }); it('should require pipeline to be an array', () => { assert.throws(() => { validateConfigFile( // $FlowExpectedError[incompatible-call] { filePath: '.parcelrc', resolvers: '123', }, '.parcelrc', ); }); }); it('should require pipeline elements to be strings', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', // $FlowExpectedError[incompatible-call] resolvers: [1, '123', 5], }, '.parcelrc', ); }); }); it('should require package names to be valid', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', resolvers: ['parcel-foo-bar'], }, '.parcelrc', ); }); }); it('should succeed with an array of valid package names', () => { validateConfigFile( { filePath: '.parcelrc', resolvers: ['parcel-resolver-test'], }, '.parcelrc', ); }); it('should support spread elements', () => { validateConfigFile( { filePath: '.parcelrc', resolvers: ['parcel-resolver-test', '...'], }, '.parcelrc', ); }); it('should require glob map to be an object', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', // $FlowExpectedError[incompatible-call] transformers: ['parcel-transformer-test', '...'], }, '.parcelrc', ); }); }); it('should trigger the validator function for each key', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', transformers: { 'types:*.{ts,tsx}': ['@parcel/transformer-typescript-types'], 'bundle-text:*': ['-inline-string', '...'], }, }, '.parcelrc', ); }); }); it('should require extends to be a string or array of strings', () => { assert.throws(() => { validateConfigFile( // $FlowExpectedError[incompatible-call] { filePath: '.parcelrc', extends: 2, }, '.parcelrc', ); }); assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', // $FlowExpectedError[incompatible-call] extends: [2, 7], }, '.parcelrc', ); }); }); it('should support relative paths', () => { validateConfigFile( { filePath: '.parcelrc', extends: './foo', }, '.parcelrc', ); validateConfigFile( { filePath: '.parcelrc', extends: ['./foo', './bar'], }, '.parcelrc', ); }); it('should validate package names', () => { assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', extends: 'foo', }, '.parcelrc', ); }); assert.throws(() => { validateConfigFile( { filePath: '.parcelrc', extends: ['foo', 'bar'], }, '.parcelrc', ); }); validateConfigFile( { filePath: '.parcelrc', extends: 'parcel-config-foo', }, '.parcelrc', ); validateConfigFile( { filePath: '.parcelrc', extends: ['parcel-config-foo', 'parcel-config-bar'], }, '.parcelrc', ); }); it('should throw for invalid top level keys', () => { assert.throws( () => { validateConfigFile( // $FlowExpectedError { extends: '@parcel/config-default', '@parcel/transformer-js': { inlineEnvironment: false, }, }, '.parcelrc', ); }, e => { assert.strictEqual( e.diagnostics[0].codeFrames[0].codeHighlights[0].message, `Did you mean "transformers"?`, ); return true; }, ); }); it('should succeed on valid config', () => { validateConfigFile( { filePath: '.parcelrc', extends: 'parcel-config-foo', transformers: { '*.js': ['parcel-transformer-foo'], }, }, '.parcelrc', ); }); it('should throw error on empty config file', () => { assert.throws(() => { validateConfigFile({}, '.parcelrc'); }, /.parcelrc can't be empty/); }); }); describe('mergePipelines', () => { it('should return an empty array if base and extension are null', () => { assert.deepEqual(mergePipelines(null, null), []); }); it('should return base if extension is null', () => { assert.deepEqual( mergePipelines( [ { packageName: 'parcel-transform-foo', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], null, ), [ { packageName: 'parcel-transform-foo', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, ], ); }); it('should return extension if base is null', () => { assert.deepEqual( mergePipelines(null, [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ]), [ { packageName: 'parcel-transform-bar', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, ], ); }); it('should return extension if there are no spread elements', () => { assert.deepEqual( mergePipelines( [ { packageName: 'parcel-transform-foo', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], ), [ { packageName: 'parcel-transform-bar', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, ], ); }); it('should return merge base into extension if there are spread elements', () => { assert.deepEqual( mergePipelines( [ { packageName: 'parcel-transform-foo', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, ], ), [ { packageName: 'parcel-transform-bar', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transform-foo', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transform-baz', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/2', }, ], ); }); it('should throw if more than one spread element is in a pipeline', () => { assert.throws(() => { mergePipelines( [ { packageName: 'parcel-transform-foo', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, '...', ], ); }, /Only one spread element can be included in a config pipeline/); }); it('should remove spread element even without a base map', () => { assert.deepEqual( mergePipelines(null, [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, ]), [ { packageName: 'parcel-transform-bar', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transform-baz', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/2', }, ], ); }); it('should throw if more than one spread element is in a pipeline even without a base map', () => { assert.throws(() => { mergePipelines(null, [ { packageName: 'parcel-transform-bar', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, '...', ]); }, /Only one spread element can be included in a config pipeline/); }); }); describe('mergeMaps', () => { it('should return an empty object if base and extension are null', () => { assert.deepEqual(mergeMaps(null, null), {}); }); it('should return base if extension is null', () => { assert.deepEqual(mergeMaps({'*.js': 'foo'}, null), { '*.js': 'foo', }); }); it('should return extension if base is null', () => { assert.deepEqual(mergeMaps(null, {'*.js': 'foo'}), { '*.js': 'foo', }); }); it('should merge the objects', () => { assert.deepEqual( mergeMaps({'*.css': 'css', '*.js': 'base-js'}, {'*.js': 'ext-js'}), {'*.js': 'ext-js', '*.css': 'css'}, ); }); it('should ensure that extension properties have a higher precedence than base properties', () => { let merged = mergeMaps({'*.{js,jsx}': 'base-js'}, {'*.js': 'ext-js'}); assert.deepEqual(merged, {'*.js': 'ext-js', '*.{js,jsx}': 'base-js'}); assert.deepEqual(Object.keys(merged), ['*.js', '*.{js,jsx}']); }); it('should call a merger function if provided', () => { let merger = (a, b) => [a, b]; assert.deepEqual( mergeMaps({'*.js': 'base-js'}, {'*.js': 'ext-js'}, merger), {'*.js': ['base-js', 'ext-js']}, ); }); }); describe('mergeConfigs', () => { it('should merge configs', () => { let base = new ParcelConfig( { filePath: toProjectPath('/', '/.parcelrc'), resolvers: [ { packageName: 'parcel-resolver-base', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/resolvers/0', }, ], transformers: { '*.js': [ { packageName: 'parcel-transform-base', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], '*.css': [ { packageName: 'parcel-transform-css', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.css/0', }, ], }, bundler: { packageName: 'parcel-bundler-base', resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/bundler', }, }, DEFAULT_OPTIONS, ); let ext = { filePath: '.parcelrc', resolvers: [ { packageName: 'parcel-resolver-ext', resolveFrom: '.parcelrc', keyPath: '/resolvers/0', }, '...', ], transformers: { '*.js': [ { packageName: 'parcel-transform-ext', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, '...', ], }, }; let merged = { filePath: '.parcelrc', resolvers: [ { packageName: 'parcel-resolver-ext', resolveFrom: '.parcelrc', keyPath: '/resolvers/0', }, { packageName: 'parcel-resolver-base', resolveFrom: '.parcelrc', keyPath: '/resolvers/0', }, ], transformers: { '*.js': [ { packageName: 'parcel-transform-ext', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transform-base', resolveFrom: '.parcelrc', keyPath: '/transformers/*.js/0', }, ], '*.css': [ { packageName: 'parcel-transform-css', resolveFrom: '.parcelrc', keyPath: '/transformers/*.css/0', }, ], }, bundler: { packageName: 'parcel-bundler-base', resolveFrom: '.parcelrc', keyPath: '/bundler', }, runtimes: [], namers: [], optimizers: {}, compressors: {}, packagers: {}, reporters: [], validators: {}, }; // $FlowFixMe assert.deepEqual(mergeConfigs(base, ext), merged); }); }); describe('resolveExtends', () => { it('should resolve a relative path', async () => { let resolved = await resolveExtends( '../.parcelrc', path.join(__dirname, 'fixtures', 'config', 'subfolder', '.parcelrc'), '/extends', DEFAULT_OPTIONS, ); assert.equal( resolved, path.join(__dirname, 'fixtures', 'config', '.parcelrc'), ); }); it('should resolve a package name', async () => { let resolved = await resolveExtends( '@parcel/config-default', path.join(__dirname, 'fixtures', 'config', 'subfolder', '.parcelrc'), '/extends', DEFAULT_OPTIONS, ); assert.equal(resolved, require.resolve('@parcel/config-default')); }); }); describe('parseAndProcessConfig', () => { it('should load and merge configs', async () => { let defaultConfigPath = require.resolve('@parcel/config-default'); let defaultConfig = await processConfig( { ...require('@parcel/config-default'), filePath: defaultConfigPath, }, DEFAULT_OPTIONS, ); let configFilePath = path.join( __dirname, 'fixtures', 'config', '.parcelrc', ); let subConfigFilePath = path.join( __dirname, 'fixtures', 'config', 'subfolder', '.parcelrc', ); let {config} = await parseAndProcessConfig( subConfigFilePath, DEFAULT_OPTIONS.inputFS.readFileSync(subConfigFilePath, 'utf8'), DEFAULT_OPTIONS, ); let transformers = nullthrows(config.transformers); assert.deepEqual(transformers['*.js'], [ { packageName: 'parcel-transformer-sub', resolveFrom: relative(subConfigFilePath), keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transformer-base', resolveFrom: relative(configFilePath), keyPath: '/transformers/*.js/0', }, '...', ]); assert(Object.keys(transformers).length > 1); assert.deepEqual(config.resolvers, defaultConfig.resolvers); assert.deepEqual(config.bundler, defaultConfig.bundler); assert.deepEqual(config.namers, defaultConfig.namers || []); assert.deepEqual(config.packagers, defaultConfig.packagers || {}); assert.deepEqual(config.optimizers, defaultConfig.optimizers || {}); assert.deepEqual(config.reporters, defaultConfig.reporters || []); }); it('should emit a codeframe.codeHighlights when a malformed .parcelrc was found', async () => { let configFilePath = path.join( __dirname, 'fixtures', 'config-malformed', '.parcelrc', ); let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8'); let pos = { line: 2, column: 14, }; // $FlowFixMe[prop-missing] await assert.rejects( () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS), { name: 'Error', diagnostics: [ { message: 'Failed to parse .parcelrc', origin: '@parcel/core', codeFrames: [ { filePath: configFilePath, language: 'json5', code, codeHighlights: [ { message: "JSON5: invalid character 'b' at 2:14", start: pos, end: pos, }, ], }, ], }, ], }, ); }); it('should emit a codeframe when an extended parcel config file is not found', async () => { let configFilePath = path.join( __dirname, 'fixtures', 'config-extends-not-found', '.parcelrc', ); let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8'); // $FlowFixMe[prop-missing] await assert.rejects( () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS), { name: 'Error', diagnostics: [ { message: 'Cannot find extended parcel config', origin: '@parcel/core', codeFrames: [ { filePath: configFilePath, language: 'json5', code, codeHighlights: [ { message: '"./.parclrc-node-modules" does not exist, did you mean "./.parcelrc-node-modules"?', start: {line: 2, column: 14}, end: {line: 2, column: 38}, }, ], }, ], }, ], }, ); }); it('should emit a codeframe when an extended parcel config node module is not found', async () => { let configFilePath = path.join( __dirname, 'fixtures', 'config-extends-not-found', '.parcelrc-node-modules', ); let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8'); // $FlowFixMe[prop-missing] await assert.rejects( () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS), { name: 'Error', diagnostics: [ { message: 'Cannot find extended parcel config', origin: '@parcel/core', codeFrames: [ { filePath: configFilePath, language: 'json5', code, codeHighlights: [ { message: 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', start: {line: 2, column: 14}, end: {line: 2, column: 35}, }, ], }, ], }, ], }, ); }); it('should emit multiple codeframes when multiple extended configs are not found', async () => { let configFilePath = path.join( __dirname, 'fixtures', 'config-extends-not-found', '.parcelrc-multiple', ); let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8'); // $FlowFixMe[prop-missing] await assert.rejects( () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS), { name: 'Error', diagnostics: [ { message: 'Cannot find extended parcel config', origin: '@parcel/core', codeFrames: [ { filePath: configFilePath, language: 'json5', code, codeHighlights: [ { message: 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', start: {line: 2, column: 15}, end: {line: 2, column: 36}, }, ], }, ], }, { message: 'Cannot find extended parcel config', origin: '@parcel/core', codeFrames: [ { filePath: configFilePath, language: 'json5', code, codeHighlights: [ { message: '"./.parclrc" does not exist, did you mean "./.parcelrc"?', start: {line: 2, column: 39}, end: {line: 2, column: 50}, }, ], }, ], }, ], }, ); }); }); describe('resolve', () => { it('should return null if there is no .parcelrc file found', async () => { let resolved = await resolveParcelConfig(DEFAULT_OPTIONS); assert.equal(resolved, null); }); it('should resolve a config if a .parcelrc file is found', async () => { let resolved = await resolveParcelConfig({ ...DEFAULT_OPTIONS, projectRoot: path.join(__dirname, 'fixtures', 'config', 'subfolder'), }); assert(resolved !== null); }); }); });