Skip to content

Fixing Windows Support #28

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 5 commits into from
Jun 21, 2017
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
22 changes: 3 additions & 19 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const missingLoaderTransformer = require('./friendly-errors/transformers/missing
const missingLoaderFormatter = require('./friendly-errors/formatters/missing-loader');
const missingPostCssConfigTransformer = require('./friendly-errors/transformers/missing-postcss-config');
const missingPostCssConfigFormatter = require('./friendly-errors/formatters/missing-postcss-config');
const pathUtil = require('./config/path-util');

class ConfigGenerator {
/**
Expand Down Expand Up @@ -407,7 +408,7 @@ class ConfigGenerator {
plugins.push(friendlyErrorsPlugin);

if (!this.webpackConfig.useDevServer()) {
const outputPath = this.webpackConfig.outputPath.replace(this.webpackConfig.getContext() + '/', '');
const outputPath = pathUtil.getRelativeOutputPath(this.webpackConfig);
plugins.push(new AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin));
}

Expand Down Expand Up @@ -440,24 +441,7 @@ class ConfigGenerator {
}

buildDevServerConfig() {
// strip trailing slash
const outputPath = this.webpackConfig.outputPath.replace(/\/$/,'');
// use the manifestKeyPrefix if available
const publicPath = this.webpackConfig.manifestKeyPrefix ? this.webpackConfig.manifestKeyPrefix.replace(/\/$/,'') : this.webpackConfig.publicPath.replace(/\/$/,'');

/*
* We use the intersection of the publicPath and outputPath to determine
* "document root" of the web server. For example:
* * outputPath = /var/www/public/build
* * publicPath = /build/
* => contentBase should be /var/www/public
*/
if (outputPath.indexOf(publicPath) === -1) {
throw new Error(`Unable to determine contentBase option for webpack's devServer configuration. The publicPath (${this.webpackConfig.publicPath}) string does not exist in the outputPath (${this.webpackConfig.outputPath}), and so the "document root" cannot be determined.`);
}

// a non-regex replace
const contentBase = outputPath.split(publicPath).join('');
const contentBase = pathUtil.getContentBase(this.webpackConfig);

return {
contentBase: contentBase,
Expand Down
116 changes: 116 additions & 0 deletions lib/config/path-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const path = require('path');

module.exports = {
/**
* Determines the "contentBase" to use for the devServer.
*
* @param {WebpackConfig} webpackConfig
* @return {String}
*/
getContentBase(webpackConfig) {
// strip trailing slash (for Unix or Windows)
const outputPath = webpackConfig.outputPath.replace(/\/$/,'').replace(/\\$/, '');
// use the manifestKeyPrefix if available
const publicPath = webpackConfig.manifestKeyPrefix ? webpackConfig.manifestKeyPrefix.replace(/\/$/,'') : webpackConfig.publicPath.replace(/\/$/,'');

/*
* We use the intersection of the publicPath (or manifestKeyPrefix) and outputPath
* to determine the "document root" of the web server. For example:
* * outputPath = /var/www/public/build
* * publicPath = /build/
Copy link
Member

Choose a reason for hiding this comment

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

what if publicPath is /subdir/build/ ? AFAIK path.join(contentBase, publicPath) === outputPath will never be true, as outputPath will use \ everywhere, while path.join(contentBase, publicPath) will have some /

Copy link
Member Author

Choose a reason for hiding this comment

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

You're correct. But, if the user has a publicPath that is not a sub-string of the outputPath, they will already have received an error forcing them to set manifestKeyPrefix (you do not need to set this manually... until you deploy under a subdirectory or set publicPath to a CDN). So, this situation won't happen. I've just pushed a commit to hopefully clarify this - it's a bit complex since different parts of the system are working together. It is possible that the user will set a manifestKeyPrefix that is not a substring of the outputPath, and then they would see this error (because this is not allowed)

* => contentBase should be /var/www/public
*
* At this point, if the publicPath is non-standard (e.g. it contains
* a sub-directory or is absolute), then the user will already see
* an error that they must set the manifestKeyPrefix.
*/

// start with outputPath, then join publicPath with it, see if it equals outputPath
// in loop, do dirname on outputPath and repeat
// eventually, you (may) get to the right path
let contentBase = outputPath;
while (path.dirname(contentBase) !== contentBase) {
contentBase = path.dirname(contentBase);

if (path.join(contentBase, publicPath) === outputPath) {
return contentBase;
}
}

throw new Error(`Unable to determine contentBase option for webpack's devServer configuration. The ${webpackConfig.manifestKeyPrefix ? 'manifestKeyPrefix' : 'publicPath'} (${webpackConfig.manifestKeyPrefix ? webpackConfig.manifestKeyPrefix : webpackConfig.publicPath}) string does not exist in the outputPath (${webpackConfig.outputPath}), and so the "document root" cannot be determined.`);
},

/**
* Returns the output path, but as a relative string (e.g. web/build)
*
* @param {WebpackConfig} webpackConfig
* @return {String}
*/
getRelativeOutputPath(webpackConfig) {
return webpackConfig.outputPath.replace(webpackConfig.getContext() + path.sep, '');
},

/**
* If the manifestKeyPrefix is not set, this uses the publicPath to generate it.
*
* Most importantly, this runs some sanity checks to make sure that it's
* ok to use the publicPath as the manifestKeyPrefix.
*
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
validatePublicPathAndManifestKeyPrefix(webpackConfig) {
if (webpackConfig.manifestKeyPrefix !== null) {
// nothing to check - they have manually set the key prefix
return;
}

if (webpackConfig.publicPath.includes('://')) {
/*
* If publicPath is absolute, you probably don't want your manifests.json
* keys to be prefixed with the CDN URL. Instead, we force you to
* choose your manifestKeyPrefix.
*/

throw new Error('Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. build/) to use when building your manifest keys. This is happening because you passed an absolute URL to setPublicPath().');
}

let outputPath = webpackConfig.outputPath;
// for comparison purposes, change \ to / on Windows
outputPath = outputPath.replace(/\\/g, '/');

// remove trailing slash on each
outputPath = outputPath.replace(/\/$/, '');
const publicPath = webpackConfig.publicPath.replace(/\/$/, '');

/*
* This is a sanity check. If, for example, you are deploying
* to a subdirectory, then you might have something like this:
* outputPath = /var/www/public/build
* publicPath = /subdir/build/
*
* In that case, you probably don't want the keys in the manifest.json
* file to be prefixed with /subdir/build - it makes more sense
* to prefix them with /build, which is the true prefix relative
* to your application (the subdirectory is a deployment detail).
*
* For that reason, we force you to choose your manifestKeyPrefix().
*/
if (outputPath.indexOf(publicPath) === -1) {
const suggestion = publicPath.substr(publicPath.lastIndexOf('/') + 1) + '/';

throw new Error(`Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. ${suggestion}) to use when building your manifest keys. This is caused by setOutputPath() (${outputPath}) and setPublicPath() (${publicPath}) containing paths that don't seem compatible.`);
}
}
};
40 changes: 5 additions & 35 deletions lib/config/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

'use strict';

const pathUtil = require('./path-util');

class Validator {
/**
* @param {WebpackConfig} webpackConfig
Expand All @@ -20,7 +22,7 @@ class Validator {
validate() {
this._validateBasic();

this._validatePublicPathConfig();
this._validatePublicPathAndManifestKeyPrefix();

this._validateDevServer();
}
Expand All @@ -39,40 +41,8 @@ class Validator {
}
}

_validatePublicPathConfig() {
if (this.webpackConfig.publicPath.includes('://') && !this.webpackConfig.manifestKeyPrefix) {
/*
* If publicPath is absolute, you probably don't want your manifests.json
* keys to be prefixed with the CDN URL. Instead, we force you to
* choose your manifestKeyPrefix.
*/

throw new Error('Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. build/) to use when building your manifest keys. This is happening because you passed an absolute URL to setPublicPath().');
}

if (!this.webpackConfig.manifestKeyPrefix) {
const outputPath = this.webpackConfig.outputPath.replace(/\/$/, '');
const publicPath = this.webpackConfig.publicPath.replace(/\/$/, '');

/*
* This is a sanity check. If, for example, you are deploying
* to a subdirectory, then you might have something like this:
* outputPath = /var/www/public/build
* publicPath = /subdir/build/
*
* In that case, you probably don't want the keys in the manifest.json
* file to be prefixed with /subdir/build - it makes more sense
* to prefix them with /build, which is the true prefix relative
* to your application (the subdirectory is a deployment detail).
*
* For that reason, we force you to choose your manifestKeyPrefix().
*/
if (outputPath.indexOf(publicPath) === -1) {
const suggestion = publicPath.substr(publicPath.lastIndexOf('/') + 1) + '/';

throw new Error(`Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. ${suggestion}) to use when building your manifest keys. This is caused by setOutputPath() and setPublicPath() containing paths that don't seem compatible.`);
}
}
_validatePublicPathAndManifestKeyPrefix() {
pathUtil.validatePublicPathAndManifestKeyPrefix(this.webpackConfig);
}

_validateDevServer() {
Expand Down
7 changes: 7 additions & 0 deletions lib/webpack/webpack-manifest-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ ManifestPlugin.prototype.apply = function(compiler) {
path.dirname(file),
path.basename(module.userRequest)
);
/* *** MODIFICATION START *** */
// for Windows, we want the keys to use /, not \
// (and path.join will obviously use \ in Windows)
if (process.platform === 'win32') {
moduleAssets[file] = moduleAssets[file].replace(/\\/g, '/');
}
/* *** MODIFICATION END *** */
});
});

Expand Down
33 changes: 0 additions & 33 deletions test/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,39 +466,6 @@ describe('The config-generator function', () => {
const actualConfig = configGenerator(config);
expect(actualConfig.devServer).to.be.undefined;
});

it('contentBase is calculated correctly', () => {
const config = createConfig();
config.runtimeConfig.useDevServer = true;
config.runtimeConfig.devServerUrl = 'http://localhost:8080/';
config.outputPath = '/tmp/public/build';
config.setPublicPath('/build/');
config.addEntry('main', './main');

const actualConfig = configGenerator(config);
// contentBase should point to the "document root", which
// is calculated as outputPath, but without the publicPath portion
expect(actualConfig.devServer.contentBase).to.equal('/tmp/public');
expect(actualConfig.devServer.publicPath).to.equal('/build/');

});

it('contentBase works ok with manifestKeyPrefix', () => {
const config = createConfig();
config.runtimeConfig.useDevServer = true;
config.runtimeConfig.devServerUrl = 'http://localhost:8080/';
config.outputPath = '/tmp/public/build';
config.setPublicPath('/subdirectory/build');
// this "fixes" the incompatibility between outputPath and publicPath
config.setManifestKeyPrefix('/build/');
config.addEntry('main', './main');

const actualConfig = configGenerator(config);
// contentBase should point to the "document root", which
// is calculated as outputPath, but without the publicPath portion
expect(actualConfig.devServer.contentBase).to.equal('/tmp/public');
expect(actualConfig.devServer.publicPath).to.equal('/subdirectory/build/');
});
});

describe('test for addPlugin config', () => {
Expand Down
112 changes: 112 additions & 0 deletions test/config/path-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const expect = require('chai').expect;
const WebpackConfig = require('../../lib/WebpackConfig');
const RuntimeConfig = require('../../lib/config/RuntimeConfig');
const pathUtil = require('../../lib/config/path-util');
const process = require('process');

function createConfig() {
const runtimeConfig = new RuntimeConfig();
runtimeConfig.context = __dirname;
runtimeConfig.environment = 'dev';
runtimeConfig.babelRcFileExists = false;

return new WebpackConfig(runtimeConfig);
}

const isWindows = (process.platform === 'win32');

describe('path-util getContentBase()', () => {
describe('getContentBase()', () => {
it('contentBase is calculated correctly', function() {
const config = createConfig();
config.runtimeConfig.useDevServer = true;
config.runtimeConfig.devServerUrl = 'http://localhost:8080/';
config.outputPath = isWindows ? 'C:\\tmp\\public\\build' : '/tmp/public/build';
config.setPublicPath('/build/');
config.addEntry('main', './main');

const actualContentBase = pathUtil.getContentBase(config);
// contentBase should point to the "document root", which
// is calculated as outputPath, but without the publicPath portion
expect(actualContentBase).to.equal(isWindows ? 'C:\\tmp\\public' : '/tmp/public');
});

it('contentBase works ok with manifestKeyPrefix', function() {
const config = createConfig();
config.runtimeConfig.useDevServer = true;
config.runtimeConfig.devServerUrl = 'http://localhost:8080/';
config.outputPath = isWindows ? 'C:\\tmp\\public\\build' : '/tmp/public/build';
config.setPublicPath('/subdirectory/build');
// this "fixes" the incompatibility between outputPath and publicPath
config.setManifestKeyPrefix('/build/');
config.addEntry('main', './main');

const actualContentBase = pathUtil.getContentBase(config);
expect(actualContentBase).to.equal(isWindows ? 'C:\\tmp\\public' : '/tmp/public');
});
});

describe('validatePublicPathAndManifestKeyPrefix', () => {
it('manifestKeyPrefix is correctly not required on windows', () => {
const config = createConfig();
config.outputPath = 'C:\\projects\\webpack-encore\\web\\build';
config.setPublicPath('/build/');
config.addEntry('main', './main');

// NOT throwing an error is the assertion
pathUtil.validatePublicPathAndManifestKeyPrefix(config);
});

it('with absolute publicPath, manifestKeyPrefix must be set', () => {
const config = createConfig();
config.outputPath = '/tmp/public/build';
config.setPublicPath('/build');
config.addEntry('main', './main');
config.setPublicPath('https://cdn.example.com');

expect(() => {
pathUtil.validatePublicPathAndManifestKeyPrefix(config);
}).to.throw('Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. build/) to use');
});

it('when outputPath and publicPath are incompatible, manifestKeyPrefix must be set', () => {
const config = createConfig();

config.outputPath = isWindows ? 'C:\\tmp\\public\\build' : '/tmp/public/build';
config.addEntry('main', './main');
// pretend we're installed to a subdirectory
config.setPublicPath('/subdirectory/build');

expect(() => {
pathUtil.validatePublicPathAndManifestKeyPrefix(config);
}).to.throw('Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. build/) to use');
});
});

describe('getRelativeOutputPath', () => {
it('basic usage', function() {
const config = createConfig();
if (isWindows) {
config.runtimeConfig.context = 'C:\\projects\\webpack-encore';
config.outputPath = 'C:\\projects\\webpack-encore\\public\\build';
} else {
config.runtimeConfig.context = '/tmp/webpack-encore';
config.outputPath = '/tmp/webpack-encore/public/build';
}

const actualPath = pathUtil.getRelativeOutputPath(config);
expect(actualPath).to.equal(isWindows ? 'public\\build' : 'public/build');
});
});
});
Loading