Unverified Commit 7b1a32be authored by Joe Haddad's avatar Joe Haddad Committed by GitHub
Browse files

Polish webpack message output (#5174)

* Only install react-scripts in CI mode

* Link locally

* Re-enable all output tests

* :lipstick: Polish webpack output

* Test sass support message

* Add more tests, but disabled

* Format missing default export error

* Format aliased import

* Why was node-sass required? Odd

* Format webpack rejection error

* Re-enable unknown package test

* Format file not found error and catch module scope plugin error

* Re-disable case sensitive paths

* Intercept and format case sensitive path errors

* Test out of scope message formatting

* Run behavior on macOS

* Run behavior on Node 8 and 10, only Node 8 for macOS

* Add some debugging

* Update matcher

* Only check stderr

* Remove old snapshot

* More debug

* Remove debug

* Add new debug

* Disable test on linux

* Add comment for future
parent 5abff641
3 merge requests!12191Lim.Pisey.168:/Identified - We are currently investigating reports of missing build logs. The issue has been identified and a resolution is in progress. We will provide a further update when available.Mar 21, 09:02 UTC,!12853brikk,!5717Automatically extract project file structure from build bundle file
Showing with 467 additions and 50 deletions
+467 -50
...@@ -28,5 +28,8 @@ env: ...@@ -28,5 +28,8 @@ env:
- TEST_SUITE=behavior - TEST_SUITE=behavior
matrix: matrix:
include: include:
- os: osx
node_js: 8
env: TEST_SUITE=behavior
- node_js: 4 - node_js: 4
env: TEST_SUITE=old-node env: TEST_SUITE=old-node
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`webpack message formatting formats aliased unknown export 1`] = `
Object {
"stderr": "Creating an optimized production build...
Failed to compile.
./src/App.js
Attempted import error: 'bar' is not exported from './AppUnknownExport' (imported as 'bar2').
",
"stdout": "",
}
`;
exports[`webpack message formatting formats babel syntax error 1`] = ` exports[`webpack message formatting formats babel syntax error 1`] = `
Object { Object {
"stderr": "Creating an optimized production build... "stderr": "Creating an optimized production build...
Failed to compile. Failed to compile.
./src/App.js ./src/App.js
Syntax error: Unterminated JSX contents (8:12) Syntax error: Unterminated JSX contents (8:13)
6 | <div> 6 | <div>
7 | <span> 7 | <span>
...@@ -25,16 +39,16 @@ Syntax error: Unterminated JSX contents (8:12) ...@@ -25,16 +39,16 @@ Syntax error: Unterminated JSX contents (8:12)
exports[`webpack message formatting formats css syntax error 1`] = ` exports[`webpack message formatting formats css syntax error 1`] = `
Object { Object {
"stderr": "Creating an optimized production build... "stderr": "Creating an optimized production build...
Failed to compile. Failed to compile.

./src/AppCss.css
Syntax Error: (3:2) Unexpected }
 1 | .App { ./src/AppCss.css
 2 |  color: red; Syntax error: Unexpected } (3:2)
> 3 | }}
 |  ^ 1 | .App {
 4 |  2 | color: red;
> 3 | }}
| ^
4 |
", ",
...@@ -74,12 +88,58 @@ To ignore, add // eslint-disable-next-line to the line before. ...@@ -74,12 +88,58 @@ To ignore, add // eslint-disable-next-line to the line before.
} }
`; `;
exports[`webpack message formatting formats file not found error 1`] = `
Object {
"stderr": "Creating an optimized production build...
Failed to compile.
./src/App.js
Cannot find file './ThisFileSouldNotExist' in './src'.
",
"stdout": "",
}
`;
exports[`webpack message formatting formats missing package 1`] = ` exports[`webpack message formatting formats missing package 1`] = `
Object { Object {
"stderr": "Creating an optimized production build... "stderr": "Creating an optimized production build...
Failed to compile. Failed to compile.

Module not found: Error: Can't resolve 'unknown-package' in '/private/var/folders/c3/vytj6_h56b77f_g72smntm3m0000gn/T/bf26e1d3700ad14275f6eefb5e4417c1/src' ./src/App.js
Cannot find module: 'unknown-package'. Make sure this package is installed.
You can install this package by running: yarn add unknown-package.
",
"stdout": "",
}
`;
exports[`webpack message formatting formats no default export 1`] = `
Object {
"stderr": "Creating an optimized production build...
Failed to compile.
./src/App.js
Attempted import error: './ExportNoDefault' does not contain a default export (imported as 'myImport').
",
"stdout": "",
}
`;
exports[`webpack message formatting formats out of scope error 1`] = `
Object {
"stderr": "Creating an optimized production build...
Failed to compile.
./src/App.js
You attempted to import ../OutOfScopeImport which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.
You can either move it inside src/, or add a symlink to it from project's node_modules/.
", ",
...@@ -93,7 +153,22 @@ Object { ...@@ -93,7 +153,22 @@ Object {
Failed to compile. Failed to compile.
./src/App.js ./src/App.js
1:1677-1680 './AppUnknownExport' does not contain an export named 'bar'. Attempted import error: 'bar' is not exported from './AppUnknownExport'.
",
"stdout": "",
}
`;
exports[`webpack message formatting helps when users tries to use sass 1`] = `
Object {
"stderr": "Creating an optimized production build...
Failed to compile.
./src/AppSass.scss
To import Sass files, you first need to install node-sass.
Run \`npm install node-sass\` or \`yarn add node-sass\` inside your workspace.
", ",
......
const { bootstrap, getOutputProduction } = require('../../utils'); const {
bootstrap,
getOutputDevelopment,
getOutputProduction,
} = require('../../utils');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const Semaphore = require('async-sema'); const Semaphore = require('async-sema');
...@@ -19,7 +23,7 @@ describe('webpack message formatting', () => { ...@@ -19,7 +23,7 @@ describe('webpack message formatting', () => {
semaphore.release(); semaphore.release();
}); });
xit('formats babel syntax error', async () => { it('formats babel syntax error', async () => {
fs.copySync( fs.copySync(
path.join(__dirname, 'src', 'AppBabel.js'), path.join(__dirname, 'src', 'AppBabel.js'),
path.join(testDirectory, 'src', 'App.js') path.join(testDirectory, 'src', 'App.js')
...@@ -29,8 +33,7 @@ describe('webpack message formatting', () => { ...@@ -29,8 +33,7 @@ describe('webpack message formatting', () => {
expect(response).toMatchSnapshot(); expect(response).toMatchSnapshot();
}); });
xit('formats css syntax error', async () => { it('formats css syntax error', async () => {
// TODO: fix me!
fs.copySync( fs.copySync(
path.join(__dirname, 'src', 'AppCss.js'), path.join(__dirname, 'src', 'AppCss.js'),
path.join(testDirectory, 'src', 'App.js') path.join(testDirectory, 'src', 'App.js')
...@@ -40,8 +43,7 @@ describe('webpack message formatting', () => { ...@@ -40,8 +43,7 @@ describe('webpack message formatting', () => {
expect(response).toMatchSnapshot(); expect(response).toMatchSnapshot();
}); });
xit('formats unknown export', async () => { it('formats unknown export', async () => {
// TODO: fix me!
fs.copySync( fs.copySync(
path.join(__dirname, 'src', 'AppUnknownExport.js'), path.join(__dirname, 'src', 'AppUnknownExport.js'),
path.join(testDirectory, 'src', 'App.js') path.join(testDirectory, 'src', 'App.js')
...@@ -51,8 +53,27 @@ describe('webpack message formatting', () => { ...@@ -51,8 +53,27 @@ describe('webpack message formatting', () => {
expect(response).toMatchSnapshot(); expect(response).toMatchSnapshot();
}); });
xit('formats missing package', async () => { it('formats aliased unknown export', async () => {
// TODO: fix me! fs.copySync(
path.join(__dirname, 'src', 'AppAliasUnknownExport.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot();
});
it('formats no default export', async () => {
fs.copySync(
path.join(__dirname, 'src', 'AppNoDefault.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot();
});
it('formats missing package', async () => {
fs.copySync( fs.copySync(
path.join(__dirname, 'src', 'AppMissingPackage.js'), path.join(__dirname, 'src', 'AppMissingPackage.js'),
path.join(testDirectory, 'src', 'App.js') path.join(testDirectory, 'src', 'App.js')
...@@ -85,4 +106,52 @@ describe('webpack message formatting', () => { ...@@ -85,4 +106,52 @@ describe('webpack message formatting', () => {
const response = await getOutputProduction({ directory: testDirectory }); const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot(); expect(response).toMatchSnapshot();
}); });
it('helps when users tries to use sass', async () => {
fs.copySync(
path.join(__dirname, 'src', 'AppSass.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot();
});
it('formats file not found error', async () => {
fs.copySync(
path.join(__dirname, 'src', 'AppUnknownFile.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot();
});
it('formats case sensitive path error', async () => {
fs.copySync(
path.join(__dirname, 'src', 'AppIncorrectCase.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputDevelopment({ directory: testDirectory });
if (process.platform === 'darwin') {
expect(response.stderr).toMatch(
`Cannot find file: 'export5.js' does not match the corresponding name on disk: './src/Export5.js'.`
);
} else {
expect(response.stderr).not.toEqual(''); // TODO: figure out how we can test this on Linux/Windows
// I believe getting this working requires we tap into enhanced-resolve
// pipeline, which is debt we don't want to take on right now.
}
});
it('formats out of scope error', async () => {
fs.copySync(
path.join(__dirname, 'src', 'AppOutOfScopeImport.js'),
path.join(testDirectory, 'src', 'App.js')
);
const response = await getOutputProduction({ directory: testDirectory });
expect(response).toMatchSnapshot();
});
}); });
{ {
"dependencies": { "dependencies": {
"node-sass": "4.x",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest"
"react-scripts": "latest"
}, },
"browserslist": [ "browserslist": [
">0.2%" ">0.2%"
......
import React, { Component } from 'react';
import { bar as bar2 } from './AppUnknownExport';
class App extends Component {
componentDidMount() {
bar2();
}
render() {
return <div />;
}
}
export default App;
import React, { Component } from 'react';
import five from './export5';
class App extends Component {
render() {
return <div className="App">{five}</div>;
}
}
export default App;
import React, { Component } from 'react';
import myImport from './ExportNoDefault';
class App extends Component {
render() {
return <div className="App">{myImport}</div>;
}
}
export default App;
import React, { Component } from 'react';
import myImport from '../OutOfScopeImport';
class App extends Component {
render() {
return <div className="App">{myImport}</div>;
}
}
export default App;
import React, { Component } from 'react';
import './AppSass.scss';
class App extends Component {
render() {
return <div className="App" />;
}
}
export default App;
.App {
color: red;
}
import React, { Component } from 'react';
import DefaultExport from './ThisFileSouldNotExist';
class App extends Component {
render() {
return <div className="App" />;
}
}
export default App;
export default 5;
export const six = 6;
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
"bootstrap": "4.x", "bootstrap": "4.x",
"node-sass": "4.x", "node-sass": "4.x",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest"
"react-scripts": "latest"
} }
} }
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
"dva": "2.4.0", "dva": "2.4.0",
"ky": "0.3.0", "ky": "0.3.0",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest"
"react-scripts": "latest"
} }
} }
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
"graphql": "14.0.2", "graphql": "14.0.2",
"react-apollo": "2.2.1", "react-apollo": "2.2.1",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest"
"react-scripts": "latest"
} }
} }
{ {
"dependencies": { "dependencies": {},
"react-scripts": "latest"
},
"homepage": "." "homepage": "."
} }
...@@ -6,12 +6,36 @@ const os = require('os'); ...@@ -6,12 +6,36 @@ const os = require('os');
const stripAnsi = require('strip-ansi'); const stripAnsi = require('strip-ansi');
async function bootstrap({ directory, template }) { async function bootstrap({ directory, template }) {
const shouldInstallScripts = process.env.CI && process.env.CI !== 'false';
await Promise.all( await Promise.all(
['public/', 'src/', 'package.json'].map(async file => ['public/', 'src/', 'package.json'].map(async file =>
fs.copy(path.join(template, file), path.join(directory, file)) fs.copy(path.join(template, file), path.join(directory, file))
) )
); );
if (shouldInstallScripts) {
const packageJson = fs.readJsonSync(path.join(directory, 'package.json'));
packageJson.dependencies = Object.assign(packageJson.dependencies, {
'react-scripts': 'latest',
});
fs.writeJsonSync(path.join(directory, 'package.json'), packageJson);
}
await execa('yarnpkg', ['install', '--mutex', 'network'], { cwd: directory }); await execa('yarnpkg', ['install', '--mutex', 'network'], { cwd: directory });
if (!shouldInstallScripts) {
fs.ensureSymlinkSync(
path.resolve(
path.join(
__dirname,
'..',
'packages',
'react-scripts',
'bin',
'react-scripts.js'
)
),
path.join(directory, 'node_modules', '.bin', 'react-scripts')
);
await execa('yarnpkg', ['link', 'react-scripts'], { cwd: directory });
}
} }
async function isSuccessfulDevelopment({ directory }) { async function isSuccessfulDevelopment({ directory }) {
...@@ -43,6 +67,39 @@ async function isSuccessfulProduction({ directory }) { ...@@ -43,6 +67,39 @@ async function isSuccessfulProduction({ directory }) {
} }
} }
async function getOutputDevelopment({ directory, env = {} }) {
try {
const { stdout, stderr } = await execa(
'./node_modules/.bin/react-scripts',
['start', '--smoke-test'],
{
cwd: directory,
env: Object.assign(
{},
{
BROWSER: 'none',
PORT: await getPort(),
CI: 'false',
FORCE_COLOR: '0',
},
env
),
}
);
return { stdout: stripAnsi(stdout), stderr: stripAnsi(stderr) };
} catch (err) {
return {
stdout: '',
stderr: stripAnsi(
err.message
.split(os.EOL)
.slice(2)
.join(os.EOL)
),
};
}
}
async function getOutputProduction({ directory, env = {} }) { async function getOutputProduction({ directory, env = {} }) {
try { try {
const { stdout, stderr } = await execa( const { stdout, stderr } = await execa(
...@@ -71,5 +128,6 @@ module.exports = { ...@@ -71,5 +128,6 @@ module.exports = {
bootstrap, bootstrap,
isSuccessfulDevelopment, isSuccessfulDevelopment,
isSuccessfulProduction, isSuccessfulProduction,
getOutputDevelopment,
getOutputProduction, getOutputProduction,
}; };
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const chalk = require('chalk');
const findUp = require('find-up');
const path = require('path');
class ModuleNotFoundPlugin {
constructor(appPath, yarnLockFile) {
this.appPath = appPath;
this.yarnLockFile = yarnLockFile;
this.useYarnCommand = this.useYarnCommand.bind(this);
this.getRelativePath = this.getRelativePath.bind(this);
this.prettierError = this.prettierError.bind(this);
}
useYarnCommand() {
try {
return findUp.sync('yarn.lock', { cwd: this.appPath }) != null;
} catch (_) {
return false;
}
}
getRelativePath(_file) {
let file = path.relative(this.appPath, _file);
if (file.startsWith('..')) {
file = _file;
} else if (!file.startsWith('.')) {
file = '.' + path.sep + file;
}
return file;
}
prettierError(err) {
let { details: _details = '', origin } = err;
if (origin == null) {
const caseSensitivity =
err.message &&
/\[CaseSensitivePathsPlugin\] `(.*?)` .* `(.*?)`/.exec(err.message);
if (caseSensitivity) {
const [, incorrectPath, actualName] = caseSensitivity;
const actualFile = this.getRelativePath(
path.join(path.dirname(incorrectPath), actualName)
);
const incorrectName = path.basename(incorrectPath);
err.message = `Cannot find file: '${incorrectName}' does not match the corresponding name on disk: '${actualFile}'.`;
}
return err;
}
const file = this.getRelativePath(origin.resource);
let details = _details.split('\n');
const request = /resolve '(.*?)' in '(.*?)'/.exec(details);
if (request) {
const isModule = details[1] && details[1].includes('module');
const isFile = details[1] && details[1].includes('file');
let [, target, context] = request;
context = this.getRelativePath(context);
if (isModule) {
const isYarn = this.useYarnCommand();
details = [
`Cannot find module: '${target}'. Make sure this package is installed.`,
'',
'You can install this package by running: ' +
(isYarn
? chalk.bold(`yarn add ${target}`)
: chalk.bold(`npm install ${target}`)) +
'.',
];
} else if (isFile) {
details = [`Cannot find file '${target}' in '${context}'.`];
} else {
details = [err.message];
}
} else {
details = [err.message];
}
err.message = [file, ...details].join('\n').replace('Error: ', '');
const isModuleScopePluginError =
err.error && err.error.__module_scope_plugin;
if (isModuleScopePluginError) {
err.message = err.message.replace('Module not found: ', '');
}
return err;
}
apply(compiler) {
const { prettierError } = this;
compiler.hooks.make.intercept({
register(tap) {
if (
!(tap.name === 'MultiEntryPlugin' || tap.name === 'SingleEntryPlugin')
) {
return tap;
}
return Object.assign({}, tap, {
fn: (compilation, callback) => {
tap.fn(compilation, (err, ...args) => {
if (err && err.name === 'ModuleNotFoundError') {
err = prettierError(err);
}
callback(err, ...args);
});
},
});
},
});
compiler.hooks.normalModuleFactory.tap('ModuleNotFoundPlugin', nmf => {
nmf.hooks.afterResolve.intercept({
register(tap) {
if (tap.name !== 'CaseSensitivePathsPlugin') {
return tap;
}
return Object.assign({}, tap, {
fn: (compilation, callback) => {
tap.fn(compilation, (err, ...args) => {
if (
err &&
err.message &&
err.message.includes('CaseSensitivePathsPlugin')
) {
err = prettierError(err);
}
callback(err, ...args);
});
},
});
},
});
});
}
}
module.exports = ModuleNotFoundPlugin;
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
const chalk = require('chalk'); const chalk = require('chalk');
const path = require('path'); const path = require('path');
const os = require('os');
class ModuleScopePlugin { class ModuleScopePlugin {
constructor(appSrc, allowedFiles = []) { constructor(appSrc, allowedFiles = []) {
...@@ -63,24 +64,28 @@ class ModuleScopePlugin { ...@@ -63,24 +64,28 @@ class ModuleScopePlugin {
); );
}) })
) { ) {
callback( const scopeError = new Error(
new Error( `You attempted to import ${chalk.cyan(
`You attempted to import ${chalk.cyan( request.__innerRequest_request
request.__innerRequest_request )} which falls outside of the project ${chalk.cyan(
)} which falls outside of the project ${chalk.cyan( 'src/'
)} directory. ` +
`Relative imports outside of ${chalk.cyan(
'src/' 'src/'
)} directory. ` + )} are not supported.` +
`Relative imports outside of ${chalk.cyan( os.EOL +
'src/' `You can either move it inside ${chalk.cyan(
)} are not supported. ` + 'src/'
`You can either move it inside ${chalk.cyan( )}, or add a symlink to it from project's ${chalk.cyan(
'src/' 'node_modules/'
)}, or add a symlink to it from project's ${chalk.cyan( )}.`
'node_modules/'
)}.`
),
request
); );
Object.defineProperty(scopeError, '__module_scope_plugin', {
value: true,
writable: false,
enumerable: false,
});
callback(scopeError, request);
} else { } else {
callback(); callback();
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment