Skip to content

Commit 15001e3

Browse files
authored
fix: babel config lookup (#364)
1 parent 3965644 commit 15001e3

File tree

5 files changed

+511
-217
lines changed

5 files changed

+511
-217
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,9 @@ Just remember to configure your `netlify.toml` to point to the `Next.js` build f
351351

352352
## Webpack Configuration
353353

354-
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).
354+
By default the webpack configuration uses `babel-loader` to load all js files.
355+
`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).
356+
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).
355357

356358
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`.
357359

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

384386
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).
385387

386-
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.
388+
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.
387389

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

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

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

404-
2. Create a custom `.babelrc` file:
406+
2. Create a Babel config file, e.g. `.babelrc`:
405407

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

466468
Defaults to `true`
467469

468-
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)
470+
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)
469471

470472
## Netlify Identity
471473

lib/build.js

+66-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@ var path = require('path');
33
var conf = require('./config');
44
var webpack = require('webpack');
55
var merge = require('webpack-merge');
6+
const findUp = require('find-up');
7+
8+
/*
9+
* Possible babel files were taken from
10+
* https://github.com/babel/babel/blob/master/packages/babel-core/src/config/files/configuration.js#L24
11+
*/
12+
13+
const BABEL_ROOT_CONFIG_FILENAMES = [
14+
'babel.config.js',
15+
'babel.config.cjs',
16+
'babel.config.mjs',
17+
'babel.config.json',
18+
];
19+
20+
const BABEL_RELATIVE_CONFIG_FILENAMES = [
21+
'.babelrc',
22+
'.babelrc.js',
23+
'.babelrc.cjs',
24+
'.babelrc.mjs',
25+
'.babelrc.json',
26+
];
27+
28+
const BABEL_CONFIG_FILENAMES = [
29+
...BABEL_ROOT_CONFIG_FILENAMES,
30+
...BABEL_RELATIVE_CONFIG_FILENAMES,
31+
];
632

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

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

18-
function haveBabelrc(functionsDir) {
19-
const cwd = process.cwd();
44+
function getRepositoryRoot(functionsDir, cwd) {
45+
const gitDirectory = findUp.sync('.git', {
46+
cwd: functionsDir,
47+
type: 'directory',
48+
});
49+
if (gitDirectory === undefined) {
50+
return cwd;
51+
}
2052

21-
return (
22-
fs.existsSync(path.join(cwd, '.babelrc')) ||
23-
functionsDir.split('/').some((dir) => {
24-
const indexOf = functionsDir.indexOf(dir);
25-
const dirToSearch = functionsDir.substr(0, indexOf);
53+
return path.dirname(gitDirectory);
54+
}
2655

27-
return fs.existsSync(path.join(cwd, dirToSearch, '.babelrc'));
28-
})
56+
function existsBabelConfig(functionsDir, cwd) {
57+
const repositoryRoot = getRepositoryRoot(functionsDir, cwd);
58+
const babelConfigFile = findUp.sync(
59+
(dir) => {
60+
const babelConfigFile = BABEL_CONFIG_FILENAMES.find(
61+
(babelConfigFilename) =>
62+
findUp.sync.exists(path.join(dir, babelConfigFilename)),
63+
);
64+
if (babelConfigFile) {
65+
return path.join(dir, babelConfigFile);
66+
}
67+
// Don't search higher than the repository root
68+
if (dir === repositoryRoot) {
69+
return findUp.stop;
70+
}
71+
return undefined;
72+
},
73+
{
74+
cwd: functionsDir,
75+
},
2976
);
77+
return Boolean(babelConfigFile);
3078
}
3179

32-
function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
80+
function webpackConfig(
81+
dir,
82+
{ userWebpackConfig, useBabelrc, cwd = process.cwd() } = {},
83+
) {
3384
var config = conf.load();
3485
var envConfig = conf.loadContext(config).environment;
3586
var babelOpts = { cacheDirectory: true };
36-
if (!haveBabelrc(dir)) {
87+
88+
var dirPath = path.resolve(path.join(cwd, dir));
89+
if (!existsBabelConfig(dirPath, cwd)) {
3790
babelOpts.presets = [
3891
[
3992
require.resolve('@babel/preset-env'),
@@ -49,8 +102,7 @@ function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
49102
}
50103

51104
var functionsDir = config.build.functions || config.build.Functions;
52-
var functionsPath = path.resolve(path.join(process.cwd(), functionsDir));
53-
var dirPath = path.resolve(path.join(process.cwd(), dir));
105+
var functionsPath = path.resolve(path.join(cwd, functionsDir));
54106

55107
if (dirPath === functionsPath) {
56108
throw new Error(
@@ -140,10 +192,7 @@ function webpackConfig(dir, { userWebpackConfig, useBabelrc } = {}) {
140192
);
141193
}
142194
if (userWebpackConfig) {
143-
var webpackAdditional = require(path.join(
144-
process.cwd(),
145-
userWebpackConfig,
146-
));
195+
var webpackAdditional = require(path.join(cwd, userWebpackConfig));
147196

148197
return merge.smart(webpackConfig, webpackAdditional);
149198
}

lib/build.spec.js

+120-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
const util = require('util');
12
const fs = require('fs');
23
const path = require('path');
3-
const rimraf = require('rimraf');
4+
const rimraf = util.promisify(require('rimraf'));
5+
const tempy = require('tempy');
46
const build = require('./build');
57

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

19-
const setupFunction = (script, filename) => {
20-
fs.mkdirSync(functions, { recursive: true });
21-
fs.writeFileSync(path.join(functions, filename), script);
21+
const mkdir = util.promisify(fs.mkdir);
22+
const pWriteFile = util.promisify(fs.writeFile);
23+
24+
const writeFile = async (fullPath, content) => {
25+
await mkdir(path.dirname(fullPath), { recursive: true });
26+
await pWriteFile(fullPath, content);
27+
};
28+
29+
const writeFileInBuild = async (content, file) => {
30+
const fullPath = `${buildTemp}/${file}`;
31+
await writeFile(fullPath, content);
32+
return fullPath;
33+
};
34+
35+
const writeFileInFunctions = async (content, file) => {
36+
const fullPath = `${functions}/${file}`;
37+
await writeFile(fullPath, content);
38+
return fullPath;
39+
};
40+
41+
const findBabelLoaderRule = (rules) =>
42+
rules.find((rule) => rule.use.loader.includes('babel-loader'));
43+
44+
const validateNotDetectedBabelConfig = (stats) => {
45+
const babelLoaderRuleOptions = findBabelLoaderRule(
46+
stats.compilation.options.module.rules,
47+
).use.options;
48+
49+
expect(babelLoaderRuleOptions.presets).toBeDefined();
50+
expect(babelLoaderRuleOptions.plugins).toBeDefined();
51+
};
52+
53+
const validateDetectedBabelConfig = (stats) => {
54+
const babelLoaderRuleOptions = findBabelLoaderRule(
55+
stats.compilation.options.module.rules,
56+
).use.options;
57+
58+
expect(babelLoaderRuleOptions.presets).toBeUndefined();
59+
expect(babelLoaderRuleOptions.plugins).toBeUndefined();
2260
};
2361

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

27-
beforeEach(() => {
28-
fs.mkdirSync(buildTemp, { recursive: true });
65+
beforeEach(async () => {
66+
await mkdir(buildTemp, { recursive: true });
2967
});
3068

31-
afterEach(() => {
32-
rimraf.sync(buildTemp);
69+
afterEach(async () => {
70+
await rimraf(buildTemp);
3371
});
3472

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

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

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

5189
expect.assertions(1);
5290

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

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

6098
expect.assertions(1);
6199

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

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

73111
const webpackConfig = `module.exports = { resolve: { extensions: ['.custom'] } }`;
74-
const customWebpackConfigDir = path.join(buildTemp, 'webpack');
75-
const userWebpackConfig = path.join(customWebpackConfigDir, 'webpack.js');
76-
fs.mkdirSync(customWebpackConfigDir, { recursive: true });
77-
fs.writeFileSync(userWebpackConfig, webpackConfig);
112+
const userWebpackConfig = await writeFileInBuild(
113+
webpackConfig,
114+
'webpack/webpack.js',
115+
);
78116

79117
const stats = await build.run(functions, {
80118
userWebpackConfig,
@@ -89,5 +127,71 @@ describe('build', () => {
89127
'.custom',
90128
]);
91129
});
130+
131+
describe('babel config file resolution', () => {
132+
it('should alter the default babelOpts when no valid babel config file is found', async () => {
133+
await writeFileInFunctions('', 'not-babel.config.js');
134+
135+
const stats = await build.run(functions);
136+
137+
validateNotDetectedBabelConfig(stats);
138+
});
139+
140+
it('should not alter the default babelOpts when a valid babel config file is found in same directory as the functions directory', async () => {
141+
await writeFileInFunctions('', 'babel.config.js');
142+
143+
const stats = await build.run(functions);
144+
145+
validateDetectedBabelConfig(stats);
146+
});
147+
148+
it('should not alter the default babelOpts when a valid babel config is found in directory above the functions directory', async () => {
149+
const [, fullPath] = await Promise.all([
150+
writeFileInFunctions('', 'babel.config.js'),
151+
writeFileInFunctions('', `sub-dir/index.js`),
152+
]);
153+
154+
const stats = await build.run(path.dirname(fullPath));
155+
156+
validateDetectedBabelConfig(stats);
157+
});
158+
159+
it('should not alter the default babelOpts when a valid babel config is found in a monorepo', async () => {
160+
const stats = await tempy.directory.task(async (directory) => {
161+
await Promise.all([
162+
writeFile(`${directory}/.git/HEAD`, ''),
163+
writeFile(
164+
`${directory}/packages/netlify-site/functions/index.js`,
165+
'module.exports = () => console.log("hello world")',
166+
),
167+
writeFile(`${directory}/babel.config.js`, ''),
168+
]);
169+
170+
return await build.run(`packages/netlify-site/functions`, {
171+
cwd: directory,
172+
});
173+
});
174+
175+
validateDetectedBabelConfig(stats);
176+
});
177+
178+
it('should not alter the default babelOpts when a valid babel config is found in a non git project', async () => {
179+
const stats = await tempy.directory.task(async (directory) => {
180+
await Promise.all([
181+
writeFile(
182+
`${directory}/packages/netlify-site/functions/index.js`,
183+
'module.exports = () => console.log("hello world")',
184+
),
185+
writeFile(`${directory}/babel.config.js`, ''),
186+
]);
187+
188+
return await build.run(`packages/netlify-site/functions`, {
189+
cwd: directory,
190+
});
191+
});
192+
193+
validateDetectedBabelConfig(stats);
194+
});
195+
});
92196
});
93197
});

0 commit comments

Comments
 (0)