Skip to content

Commit 7878f4e

Browse files
committed
Add Encore.copyFiles() method to the public API
1 parent 5fcd4cb commit 7878f4e

11 files changed

+436
-6
lines changed

fixtures/images/symfony_logo_alt.png

15.6 KB
Loading

index.js

+58
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,64 @@ class Encore {
404404
return this;
405405
}
406406

407+
/**
408+
* Copy files or folders to the build directory.
409+
*
410+
* For example:
411+
*
412+
* // Copy the content of a whole directory and its subdirectories
413+
* Encore.copyFiles({ from: './images' });
414+
*
415+
* // Only copy files matching a given pattern
416+
* Encore.copyFiles({ from: './images', pattern: /\.(png|jpg|jpeg)$/ })
417+
*
418+
* // Set the path the files are copied to
419+
* Encore.copyFiles({
420+
* from: './images',
421+
* pattern: /\.(png|jpg|jpeg)$/,
422+
* to: 'assets/images/[path][name].[ext]'
423+
* })
424+
*
425+
* // Version files
426+
* Encore.copyFiles(
427+
* from: './images',
428+
* to: 'assets/images/[path][name].[hash:8].[ext]'
429+
* })
430+
*
431+
* // Add multiple configs in a single call
432+
* Encore.copyFiles([
433+
* { from: './images' },
434+
* { from: './txt', pattern: /\.txt$/ },
435+
* ]);
436+
*
437+
* Notes:
438+
* * No transformation is applied to the copied files (for instance
439+
* copying a CSS file won't minify it)
440+
* * By default files won't be versioned even if enableVersioning()
441+
* has been called. In order to add a hash to your filenames you
442+
* must use the "[hash]" placeholder in the "to" option (see below)
443+
*
444+
* Supported options:
445+
* * {string} from (mandatory)
446+
* The path of the source directory (mandatory)
447+
* * {RegExp} pattern (default: all files)
448+
* A pattern that the filenames must match in order to be copied
449+
* * {string} to (default: [path][name].[ext])
450+
* Where the files must be copied to. You can add all the
451+
* placeholders supported by the file-loader.
452+
* https://github.com./webpack-contrib/file-loader#placeholders
453+
* * {boolean} includeSubdirectories (default: true)
454+
* Whether or not the copy should include subdirectories.
455+
*
456+
* @param {object|Array} configs
457+
* @returns {Encore}
458+
*/
459+
copyFiles(configs) {
460+
webpackConfig.copyFiles(configs);
461+
462+
return this;
463+
}
464+
407465
/**
408466
* Tell Webpack to output a separate runtime.js file.
409467
*

lib/WebpackConfig.js

+34
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class WebpackConfig {
6969
this.useHandlebarsLoader = false;
7070

7171
// Features/Loaders options
72+
this.copyFilesConfigs = [];
7273
this.sassOptions = {
7374
resolveUrlLoader: true
7475
};
@@ -365,6 +366,39 @@ class WebpackConfig {
365366
this.addEntry(name, file);
366367
}
367368

369+
copyFiles(configs = []) {
370+
if (!Array.isArray(configs)) {
371+
configs = [configs];
372+
}
373+
374+
if (configs.some(elt => typeof elt !== 'object')) {
375+
throw new Error('copyFiles() must be called with either a config object or an array of config objects.');
376+
}
377+
378+
const defaultConfig = {
379+
from: null,
380+
pattern: /.*/,
381+
to: '[path][name].[ext]',
382+
includeSubdirectories: true
383+
};
384+
385+
for (const config of configs) {
386+
if (!config.from) {
387+
throw new Error('Config objects passed to copyFiles() must have a "from" property.');
388+
}
389+
390+
for (const configKey of Object.keys(config)) {
391+
if (!(configKey in defaultConfig)) {
392+
throw new Error(`Invalid config option "${configKey}" passed to copyFiles(). Valid keys are ${Object.keys(defaultConfig).join(', ')}`);
393+
}
394+
}
395+
396+
this.copyFilesConfigs.push(
397+
Object.assign({}, defaultConfig, config)
398+
);
399+
}
400+
}
401+
368402
enablePostCssLoader(postCssLoaderOptionsCallback = () => {}) {
369403
this.usePostCssLoader = true;
370404

lib/config-generator.js

+27
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const sharedEntryConcatPuginUtil = require('./plugins/shared-entry-concat');
4141
const PluginPriorities = require('./plugins/plugin-priorities');
4242
const applyOptionsCallback = require('./utils/apply-options-callback');
4343
const sharedEntryTmpName = require('./utils/sharedEntryTmpName');
44+
const copyEntryTmpName = require('./utils/copyEntryTmpName');
4445
const tmp = require('tmp');
4546
const fs = require('fs');
4647
const path = require('path');
@@ -140,6 +141,32 @@ class ConfigGenerator {
140141
entry[sharedEntryTmpName] = tmpFileObject.name;
141142
}
142143

144+
if (this.webpackConfig.copyFilesConfigs.length > 0) {
145+
const tmpFileObject = tmp.fileSync();
146+
fs.writeFileSync(
147+
tmpFileObject.name,
148+
this.webpackConfig.copyFilesConfigs.reduce((buffer, entry, index) => {
149+
const copyFrom = path.resolve(
150+
this.webpackConfig.getContext(),
151+
entry.from
152+
);
153+
154+
const requireContextParam = `!${require.resolve('file-loader')}?context=${copyFrom}&name=${entry.to}!${copyFrom}`;
155+
156+
return buffer + `
157+
const context_${index} = require.context(
158+
'${stringEscaper(requireContextParam)}',
159+
${entry.includeSubdirectories},
160+
${entry.pattern}
161+
);
162+
context_${index}.keys().forEach(context_${index});
163+
`;
164+
}, '')
165+
);
166+
167+
entry[copyEntryTmpName] = tmpFileObject.name;
168+
}
169+
143170
return entry;
144171
}
145172

lib/plugins/delete-unused-entries.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@
1111

1212
const DeleteUnusedEntriesJSPlugin = require('../webpack/delete-unused-entries-js-plugin');
1313
const PluginPriorities = require('./plugin-priorities');
14+
const copyEntryTmpName = require('../utils/copyEntryTmpName');
1415

1516
/**
1617
* @param {Array} plugins
1718
* @param {WebpackConfig} webpackConfig
1819
* @return {void}
1920
*/
2021
module.exports = function(plugins, webpackConfig) {
22+
const entries = [... webpackConfig.styleEntries.keys()];
23+
24+
if (webpackConfig.copyFilesConfigs.length > 0) {
25+
entries.push(copyEntryTmpName);
26+
}
2127

2228
plugins.push({
23-
plugin: new DeleteUnusedEntriesJSPlugin(
24-
// transform into an Array
25-
[... webpackConfig.styleEntries.keys()]
26-
),
29+
plugin: new DeleteUnusedEntriesJSPlugin(entries),
2730
priority: PluginPriorities.DeleteUnusedEntriesJSPlugin
2831
});
2932
};

lib/plugins/entry-files-manifest.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const EntryFilesManifestPlugin = require('../webpack/entry-files-manifest-plugin
1313
const PluginPriorities = require('./plugin-priorities');
1414
const path = require('path');
1515
const sharedEntryTmpName = require('../utils/sharedEntryTmpName');
16+
const copyEntryTmpName = require('../utils/copyEntryTmpName');
1617
const manifestKeyPrefixHelper = require('../utils/manifest-key-prefix-helper');
1718

1819
/**
@@ -26,7 +27,7 @@ module.exports = function(plugins, webpackConfig) {
2627
plugin: new EntryFilesManifestPlugin(
2728
path.join(webpackConfig.outputPath, 'entrypoints.json'),
2829
manifestKeyPrefixHelper(webpackConfig),
29-
[sharedEntryTmpName],
30+
[sharedEntryTmpName, copyEntryTmpName],
3031
webpackConfig.styleEntries
3132
),
3233
priority: PluginPriorities.EntryFilesManifestPlugin

lib/plugins/manifest.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ManifestPlugin = require('webpack-manifest-plugin');
1313
const PluginPriorities = require('./plugin-priorities');
1414
const applyOptionsCallback = require('../utils/apply-options-callback');
1515
const sharedEntryTmpName = require('../utils/sharedEntryTmpName');
16+
const copyEntryTmpName = require('../utils/copyEntryTmpName');
1617
const manifestKeyPrefixHelper = require('../utils/manifest-key-prefix-helper');
1718

1819
/**
@@ -26,7 +27,7 @@ module.exports = function(plugins, webpackConfig) {
2627
// always write a manifest.json file, even with webpack-dev-server
2728
writeToFileEmit: true,
2829
filter: (file) => {
29-
return (!file.isChunk || file.chunk.id !== sharedEntryTmpName);
30+
return (!file.isChunk || ![sharedEntryTmpName, copyEntryTmpName].includes(file.chunk.id));
3031
}
3132
};
3233

lib/utils/copyEntryTmpName.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* This file is part of the Symfony Webpack Encore package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
module.exports = '_tmp_copy';

test/WebpackConfig.js

+64
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,70 @@ describe('WebpackConfig object', () => {
359359
});
360360
});
361361

362+
describe('copyFiles', () => {
363+
it('Calling it add files to be copied', () => {
364+
const config = createConfig();
365+
366+
// With multiple config objects
367+
config.copyFiles([
368+
{ from: './foo', pattern: /.*/ },
369+
{ from: './bar', pattern: /abc/, to: 'bar', includeSubdirectories: false },
370+
]);
371+
372+
// With a single config object
373+
config.copyFiles({ from: './baz' });
374+
375+
expect(config.copyFilesConfigs).to.deep.equal([{
376+
from: './foo',
377+
pattern: /.*/,
378+
to: '[path][name].[ext]',
379+
includeSubdirectories: true
380+
}, {
381+
from: './bar',
382+
pattern: /abc/,
383+
to: 'bar',
384+
includeSubdirectories: false
385+
}, {
386+
from: './baz',
387+
pattern: /.*/,
388+
to: '[path][name].[ext]',
389+
includeSubdirectories: true
390+
}]);
391+
});
392+
393+
it('Calling it with an invalid parameter', () => {
394+
const config = createConfig();
395+
396+
expect(() => {
397+
config.copyFiles('foo');
398+
}).to.throw('must be called with either a config object or an array of config objects');
399+
400+
expect(() => {
401+
config.copyFiles([{ from: 'foo' }, 'foo']);
402+
}).to.throw('must be called with either a config object or an array of config objects');
403+
});
404+
405+
it('Calling it with a missing from key', () => {
406+
const config = createConfig();
407+
408+
expect(() => {
409+
config.copyFiles({ to: 'foo' });
410+
}).to.throw('must have a "from" property');
411+
412+
expect(() => {
413+
config.copyFiles([{ from: 'foo' }, { to: 'foo' }]);
414+
}).to.throw('must have a "from" property');
415+
});
416+
417+
it('Calling it with an unknown config property', () => {
418+
const config = createConfig();
419+
420+
expect(() => {
421+
config.copyFiles({ from: 'images', foo: 'foo' });
422+
}).to.throw('Invalid config option "foo"');
423+
});
424+
});
425+
362426
describe('autoProvideVariables', () => {
363427
it('Calling multiple times merges', () => {
364428
const config = createConfig();

0 commit comments

Comments
 (0)