Skip to content

fix: babel config lookup #364

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

Merged
merged 14 commits into from
Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ Just remember to configure your `netlify.toml` to point to the `Next.js` build f

## Webpack Configuration

By default the webpack configuration uses `babel-loader` to load all js files. Any `.babelrc` in the directory `netlify-lambda` is run from will be respected. If no `.babelrc` is found, a [few basic settings are used](https://github.com/netlify/netlify-lambda/blob/master/lib/build.js#L11-L15a).
By default the webpack configuration uses `babel-loader` to load all js files.
`netlify-lambda` will search for [a valid babel config file](https://babeljs.io/docs/en/config-files) in the functions directory first and look upwards up to the directory `netlify-lambda` is run from (similar to how `babel-loader` looks for a Babel config file).
If no babel config file is found, a [few basic settings are used](https://github.com/netlify/netlify-lambda/blob/master/lib/build.js#L11-L15a).

If you need to use additional webpack modules or loaders, you can specify an additional webpack config with the `-c`/`--config` option when running either `serve` or `build`.

Expand Down Expand Up @@ -383,7 +385,7 @@ The additional webpack config will be merged into the default config via [webpac

The default webpack configuration uses `babel-loader` with a [few basic settings](https://github.com/netlify/netlify-lambda/blob/master/lib/build.js#L19-L33).

However, if any `.babelrc` is found in the directory `netlify-lambda` is run from, or [folders above it](https://github.com/netlify/netlify-lambda/pull/92) (useful for monorepos), it will be used instead of the default one.
However, if any valid Babel config file is found in the directory `netlify-lambda` is run from, or [folders above it](https://github.com/netlify/netlify-lambda/pull/92) (useful for monorepos), it will be used instead of the default one.

It is possible to disable this behaviour by passing `--babelrc false`.

Expand All @@ -401,7 +403,7 @@ npm install --save-dev @babel/preset-typescript

You may also want to add `typescript @types/node @types/aws-lambda`.

2. Create a custom `.babelrc` file:
2. Create a Babel config file, e.g. `.babelrc`:

```diff
{
Expand Down Expand Up @@ -465,7 +467,7 @@ If you need an escape hatch and are building your lambda in some way that is inc

Defaults to `true`

Use a `.babelrc` found in the directory `netlify-lambda` is run from. This can be useful when you have conflicting babel-presets, more info [here](#babel-configuration)
Use a Babel config file found in the directory `netlify-lambda` is run from. This can be useful when you have conflicting babel-presets, more info [here](#babel-configuration)

## Netlify Identity

Expand Down
83 changes: 66 additions & 17 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@ var path = require('path');
var conf = require('./config');
var webpack = require('webpack');
var merge = require('webpack-merge');
const findUp = require('find-up');

/*
* Possible babel files were taken from
* https://github.com/babel/babel/blob/master/packages/babel-core/src/config/files/configuration.js#L24
*/

const BABEL_ROOT_CONFIG_FILENAMES = [
'babel.config.js',
'babel.config.cjs',
'babel.config.mjs',
'babel.config.json',
];

const BABEL_RELATIVE_CONFIG_FILENAMES = [
'.babelrc',
'.babelrc.js',
'.babelrc.cjs',
'.babelrc.mjs',
'.babelrc.json',
];

const BABEL_CONFIG_FILENAMES = [
...BABEL_ROOT_CONFIG_FILENAMES,
...BABEL_RELATIVE_CONFIG_FILENAMES,
];

const testFilePattern = '\\.(test|spec)\\.?';

Expand All @@ -15,25 +41,52 @@ function getBabelTarget(envConfig) {
return unknown ? '8.15.0' : current.replace(/^nodejs/, '');
}

function haveBabelrc(functionsDir) {
const cwd = process.cwd();
function getRepositoryRoot(functionsDir, cwd) {
const gitDirectory = findUp.sync('.git', {
cwd: functionsDir,
type: 'directory',
});
if (gitDirectory === undefined) {
return cwd;
}

return (
fs.existsSync(path.join(cwd, '.babelrc')) ||
functionsDir.split('/').some((dir) => {
const indexOf = functionsDir.indexOf(dir);
const dirToSearch = functionsDir.substr(0, indexOf);
return path.dirname(gitDirectory);
}

return fs.existsSync(path.join(cwd, dirToSearch, '.babelrc'));
})
function existsBabelConfig(functionsDir, cwd) {
const repositoryRoot = getRepositoryRoot(functionsDir, cwd);
const babelConfigFile = findUp.sync(
(dir) => {
const babelConfigFile = BABEL_CONFIG_FILENAMES.find(
(babelConfigFilename) =>
findUp.sync.exists(path.join(dir, babelConfigFilename)),
);
if (babelConfigFile) {
return path.join(dir, babelConfigFile);
}
// Don't search higher than the repository root
if (dir === repositoryRoot) {
return findUp.stop;
}
return undefined;
},
{
cwd: functionsDir,
},
);
return Boolean(babelConfigFile);
}

function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
function webpackConfig(
dir,
{ userWebpackConfig, useBabelrc, cwd = process.cwd() } = {},
Copy link
Contributor

Choose a reason for hiding this comment

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

cwd argument is only here so we can test the monorepo setup

) {
var config = conf.load();
var envConfig = conf.loadContext(config).environment;
var babelOpts = { cacheDirectory: true };
if (!haveBabelrc(dir)) {

var dirPath = path.resolve(path.join(cwd, dir));
if (!existsBabelConfig(dirPath, cwd)) {
babelOpts.presets = [
[
require.resolve('@babel/preset-env'),
Expand All @@ -49,8 +102,7 @@ function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
}

var functionsDir = config.build.functions || config.build.Functions;
var functionsPath = path.resolve(path.join(process.cwd(), functionsDir));
var dirPath = path.resolve(path.join(process.cwd(), dir));
var functionsPath = path.resolve(path.join(cwd, functionsDir));

if (dirPath === functionsPath) {
throw new Error(
Expand Down Expand Up @@ -140,10 +192,7 @@ function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
);
}
if (userWebpackConfig) {
var webpackAdditional = require(path.join(
process.cwd(),
userWebpackConfig,
));
var webpackAdditional = require(path.join(cwd, userWebpackConfig));

return merge.smart(webpackConfig, webpackAdditional);
}
Expand Down
136 changes: 120 additions & 16 deletions lib/build.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const util = require('util');
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const rimraf = util.promisify(require('rimraf'));
const tempy = require('tempy');
const build = require('./build');

jest.mock('./config', () => {
Expand All @@ -16,26 +18,62 @@ jest.mock('./config', () => {
const buildTemp = path.join('.temp', 'build');
const functions = path.join(buildTemp, 'functions');

const setupFunction = (script, filename) => {
fs.mkdirSync(functions, { recursive: true });
fs.writeFileSync(path.join(functions, filename), script);
const mkdir = util.promisify(fs.mkdir);
const pWriteFile = util.promisify(fs.writeFile);

const writeFile = async (fullPath, content) => {
await mkdir(path.dirname(fullPath), { recursive: true });
await pWriteFile(fullPath, content);
};

const writeFileInBuild = async (content, file) => {
const fullPath = `${buildTemp}/${file}`;
await writeFile(fullPath, content);
return fullPath;
};

const writeFileInFunctions = async (content, file) => {
const fullPath = `${functions}/${file}`;
await writeFile(fullPath, content);
return fullPath;
};

const findBabelLoaderRule = (rules) =>
rules.find((rule) => rule.use.loader.includes('babel-loader'));

const validateNotDetectedBabelConfig = (stats) => {
const babelLoaderRuleOptions = findBabelLoaderRule(
stats.compilation.options.module.rules,
).use.options;

expect(babelLoaderRuleOptions.presets).toBeDefined();
expect(babelLoaderRuleOptions.plugins).toBeDefined();
};

const validateDetectedBabelConfig = (stats) => {
const babelLoaderRuleOptions = findBabelLoaderRule(
stats.compilation.options.module.rules,
).use.options;

expect(babelLoaderRuleOptions.presets).toBeUndefined();
expect(babelLoaderRuleOptions.plugins).toBeUndefined();
};

describe('build', () => {
const functionsBuildOutputDir = require('./config').load().build.functions;

beforeEach(() => {
fs.mkdirSync(buildTemp, { recursive: true });
beforeEach(async () => {
await mkdir(buildTemp, { recursive: true });
});

afterEach(() => {
rimraf.sync(buildTemp);
afterEach(async () => {
await rimraf(buildTemp);
});

describe('run', () => {
it('should return webpack stats on successful build', async () => {
const script = `module.exports = () => console.log("hello world")`;
setupFunction(script, 'index.js');
await writeFileInFunctions(script, 'index.js');

const stats = await build.run(functions);
expect(stats.compilation.errors).toHaveLength(0);
Expand All @@ -46,7 +84,7 @@ describe('build', () => {

it('should throw error on complication errors', async () => {
const script = `module.exports = () => console.log("hello`;
setupFunction(script, 'index.js');
await writeFileInFunctions(script, 'index.js');

expect.assertions(1);

Expand All @@ -55,7 +93,7 @@ describe('build', () => {

it('should throw error on invalid config', async () => {
const script = `module.exports = () => console.log("hello world")`;
setupFunction(script, 'index.js');
await writeFileInFunctions(script, 'index.js');

expect.assertions(1);

Expand All @@ -68,13 +106,13 @@ describe('build', () => {

it('should merge webpack custom config', async () => {
const script = `module.exports = () => console.log("hello world")`;
setupFunction(script, 'index.js');
await writeFileInFunctions(script, 'index.js');

const webpackConfig = `module.exports = { resolve: { extensions: ['.custom'] } }`;
const customWebpackConfigDir = path.join(buildTemp, 'webpack');
const userWebpackConfig = path.join(customWebpackConfigDir, 'webpack.js');
fs.mkdirSync(customWebpackConfigDir, { recursive: true });
fs.writeFileSync(userWebpackConfig, webpackConfig);
const userWebpackConfig = await writeFileInBuild(
webpackConfig,
'webpack/webpack.js',
);

const stats = await build.run(functions, {
userWebpackConfig,
Expand All @@ -89,5 +127,71 @@ describe('build', () => {
'.custom',
]);
});

describe('babel config file resolution', () => {
it('should alter the default babelOpts when no valid babel config file is found', async () => {
await writeFileInFunctions('', 'not-babel.config.js');

const stats = await build.run(functions);

validateNotDetectedBabelConfig(stats);
});

it('should not alter the default babelOpts when a valid babel config file is found in same directory as the functions directory', async () => {
await writeFileInFunctions('', 'babel.config.js');

const stats = await build.run(functions);

validateDetectedBabelConfig(stats);
});

it('should not alter the default babelOpts when a valid babel config is found in directory above the functions directory', async () => {
const [, fullPath] = await Promise.all([
writeFileInFunctions('', 'babel.config.js'),
writeFileInFunctions('', `sub-dir/index.js`),
]);

const stats = await build.run(path.dirname(fullPath));

validateDetectedBabelConfig(stats);
});

it('should not alter the default babelOpts when a valid babel config is found in a monorepo', async () => {
const stats = await tempy.directory.task(async (directory) => {
await Promise.all([
writeFile(`${directory}/.git/HEAD`, ''),
writeFile(
`${directory}/packages/netlify-site/functions/index.js`,
'module.exports = () => console.log("hello world")',
),
writeFile(`${directory}/babel.config.js`, ''),
]);

return await build.run(`packages/netlify-site/functions`, {
cwd: directory,
});
});

validateDetectedBabelConfig(stats);
});

it('should not alter the default babelOpts when a valid babel config is found in a non git project', async () => {
const stats = await tempy.directory.task(async (directory) => {
await Promise.all([
writeFile(
`${directory}/packages/netlify-site/functions/index.js`,
'module.exports = () => console.log("hello world")',
),
writeFile(`${directory}/babel.config.js`, ''),
]);

return await build.run(`packages/netlify-site/functions`, {
cwd: directory,
});
});

validateDetectedBabelConfig(stats);
});
});
});
});
Loading