drupal/core/scripts/js/vendor-update.js

319 lines
10 KiB
JavaScript
Raw Normal View History

/**
* @file
* Copy files for JS vendor dependencies from node_modules to the assets/vendor
* folder.
*
* This script handles all dependencies except CKEditor and Modernizr, which
* require a custom build step.
*/
const path = require('path');
const { copyFile, writeFile, readFile, chmod, mkdir } = require('fs').promises;
const ckeditor5Files = require('./assets/ckeditor5Files');
const jQueryUIProcess = require('./assets/process/jqueryui');
const mapProcess = require('./assets/process/map');
const coreFolder = path.resolve(__dirname, '../../');
const packageFolder = `${coreFolder}/node_modules`;
const assetsFolder = `${coreFolder}/assets/vendor`;
(async () => {
const librariesPath = `${coreFolder}/core.libraries.yml`;
// Open the core.libraries.yml file to update version information
// automatically.
const libraries = (await readFile(librariesPath)).toString().split('\n\n');
function updateLibraryVersion(libraryName, { version }) {
const libraryIndex = libraries.findIndex((lib) =>
lib.startsWith(libraryName),
);
if (libraryIndex > 0) {
const libraryDeclaration = libraries[libraryIndex];
// Get the previous package version from the yaml file, versions can be
// declared with a yaml anchor such as `version: &yaml_anchor "xxx"`
const currentVersion = libraryDeclaration.match(/version:(?: [&\w_]+)? "(.*)"\n/)[1];
// Replace the version value and the version in the license URL.
libraries[libraryIndex] = libraryDeclaration.replace(
new RegExp(currentVersion, 'g'),
version,
);
}
}
/**
* Structure of the object defining a library to copy to the assets/ folder.
*
* @typedef DrupalLibraryAsset
*
* @prop {string} pack
* The name of the npm package (used to get the name of the folder where
* the files are situated inside of the node_modules folder). Note that we
* use `pack` and not `package` because `package` is a future reserved word.
* @prop {string} [folder]
* The folder under `assets/vendor/` where the files will be copied. If
* this
* is not defined the value of `pack` is used.
* @prop {string} [library]
* The key under which the library is declared in core.libraries.yml.
* @prop {Array} [files]
* An array of files to be copied over.
* - A string if the file has the same name and is at the same level in
* the source and target folder.
* - An object with a `from` and `to` property if the source and target
* have a different name or if the folder nesting is different.
* @prop {object} [process]
* An object containing a file extension as a key and a callback as the
* value. The callback will be called for each file matching the file
* extension. It can be used to minify the file content before saving to
* the target directory.
*/
/**
* Declare the array that defines what needs to be copied over.
*
* @type {DrupalLibraryAsset[]}
*/
const ASSET_LIST = [
{
pack: 'backbone',
library: 'internal.backbone',
files: ['backbone.js', 'backbone-min.js', 'backbone-min.map'],
},
{
pack: 'css.escape',
folder: 'css-escape',
library: 'css.escape',
files: ['css.escape.js'],
},
{
pack: 'es6-promise',
files: [
{ from: 'dist/es6-promise.auto.min.js', to: 'es6-promise.auto.min.js' },
{
from: 'dist/es6-promise.auto.min.map',
to: 'es6-promise.auto.min.map',
},
],
},
{
pack: 'farbtastic',
library: 'jquery.farbtastic',
files: [
'marker.png',
'mask.png',
'wheel.png',
'farbtastic.css',
{ from: 'farbtastic.min.js', to: 'farbtastic.js' },
],
},
{
pack: 'jquery',
files: [
{ from: 'dist/jquery.js', to: 'jquery.js' },
{ from: 'dist/jquery.min.js', to: 'jquery.min.js' },
{ from: 'dist/jquery.min.map', to: 'jquery.min.map' },
],
},
{
pack: 'jquery-form',
library: 'internal.jquery.form',
files: [
{ from: 'dist/jquery.form.min.js', to: 'jquery.form.min.js' },
{ from: 'dist/jquery.form.min.js.map', to: 'jquery.form.min.js.map' },
{ from: 'src/jquery.form.js', to: 'src/jquery.form.js' },
],
},
{
pack: 'js-cookie',
files: [{ from: 'dist/js.cookie.min.js', to: 'js.cookie.min.js' }],
},
{
pack: 'normalize.css',
folder: 'normalize-css',
library: 'normalize',
files: ['normalize.css'],
},
{
pack: '@drupal/once',
folder: 'once',
files: [
{ from: 'dist/once.js', to: 'once.js' },
{ from: 'dist/once.min.js', to: 'once.min.js' },
{ from: 'dist/once.min.js.map', to: 'once.min.js.map' },
],
},
{
pack: 'picturefill',
files: [{ from: 'dist/picturefill.min.js', to: 'picturefill.min.js' }],
},
{
pack: '@popperjs/core',
folder: 'popperjs',
files: [
{ from: 'dist/umd/popper.min.js', to: 'popper.min.js' },
{ from: 'dist/umd/popper.min.js.map', to: 'popper.min.js.map' },
],
},
{
pack: 'shepherd.js',
folder: 'shepherd',
files: [
{ from: 'dist/js/shepherd.min.js', to: 'shepherd.min.js' },
{ from: 'dist/js/shepherd.min.js.map', to: 'shepherd.min.js.map' },
],
},
{ pack: 'sortablejs', folder: 'sortable', files: ['Sortable.min.js'] },
{
pack: 'tabbable',
files: [
{ from: 'dist/index.umd.min.js', to: 'index.umd.min.js' },
{ from: 'dist/index.umd.min.js.map', to: 'index.umd.min.js.map' },
],
},
{
pack: 'underscore',
library: 'internal.underscore',
files: ['underscore-min.js', 'underscore-min.js.map'],
},
{
pack: 'loadjs',
files: [{ from: 'dist/loadjs.min.js', to: 'loadjs.min.js' }],
},
{
pack: 'jquery-ui',
folder: 'jquery.ui',
process: {
// This will automatically minify the files and update the destination
// filename before saving.
'.js': jQueryUIProcess,
},
files: [
'themes/base/autocomplete.css',
'themes/base/button.css',
'themes/base/checkboxradio.css',
'themes/base/controlgroup.css',
'themes/base/core.css',
'themes/base/dialog.css',
'themes/base/draggable.css',
'themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png',
'themes/base/images/ui-icons_444444_256x240.png',
'themes/base/images/ui-icons_555555_256x240.png',
'themes/base/images/ui-icons_777620_256x240.png',
'themes/base/images/ui-icons_777777_256x240.png',
'themes/base/images/ui-icons_cc0000_256x240.png',
'themes/base/images/ui-icons_ffffff_256x240.png',
'themes/base/menu.css',
'themes/base/resizable.css',
'themes/base/theme.css',
'ui/data.js',
'ui/disable-selection.js',
'ui/focusable.js',
'ui/form-reset-mixin.js',
'ui/form.js',
'ui/ie.js',
'ui/jquery-patch.js',
'ui/keycode.js',
'ui/labels.js',
'ui/plugin.js',
'ui/safe-active-element.js',
'ui/safe-blur.js',
'ui/scroll-parent.js',
'ui/unique-id.js',
'ui/version.js',
'ui/widget.js',
'ui/widgets/autocomplete.js',
'ui/widgets/button.js',
'ui/widgets/checkboxradio.js',
'ui/widgets/controlgroup.js',
'ui/widgets/dialog.js',
'ui/widgets/draggable.js',
'ui/widgets/menu.js',
'ui/widgets/mouse.js',
'ui/widgets/resizable.js',
],
},
// CKEditor 5 builds the list of files dynamically based on what exists
// in the filesystem.
...ckeditor5Files(packageFolder),
];
/**
* Default callback for processing map files.
*/
const defaultProcessCallbacks = {
'.map': mapProcess,
};
/**
* Return an object with a 'from' and 'to' member.
*
* @param {string|object} file
*
* @return {{from: string, to: string}}
*/
function normalizeFile(file) {
let normalized = file;
if (typeof file === 'string') {
normalized = {
from: file,
to: file,
};
}
return normalized;
}
for (const { pack, files = [], folder = false, library = false, process = {} } of ASSET_LIST) {
const sourceFolder = pack;
const libraryName = library || folder || pack;
const destFolder = folder || pack;
// Add a callback for map files by default.
const processCallbacks = { ...defaultProcessCallbacks, ...process };
// Update the library version in core.libraries.yml with the version
// from the npm package.
try {
const packageInfo = JSON.parse((await readFile(`${packageFolder}/${sourceFolder}/package.json`)).toString());
updateLibraryVersion(libraryName, packageInfo);
} catch (e) {
// The package.json file doesn't exist, so nothing to do.
}
for (const file of files.map(normalizeFile)) {
const sourceFile = `${packageFolder}/${sourceFolder}/${file.from}`;
const destFile = `${assetsFolder}/${destFolder}/${file.to}`;
const extension = path.extname(file.from);
try {
await mkdir(path.dirname(destFile), { recursive: true });
} catch (e) {
// Nothing to do if the folder already exists.
}
// There is a callback that transforms the file contents, we are not
// simply copying a file from A to B.
if (processCallbacks[extension]) {
const contents = (await readFile(sourceFile)).toString();
const results = await processCallbacks[extension]({ file, contents });
console.log(`Process ${sourceFolder}/${file.from} and save ${results.length} files:\n ${results.map(({ filename = file.to }) => filename).join(', ')}`);
for (const { filename = file.to, contents } of results) {
// The filename key can be used to change the name of the saved file.
await writeFile(`${assetsFolder}/${destFolder}/${filename}`, contents);
}
} else {
// There is no callback simply copy the file.
console.log(`Copy ${sourceFolder}/${file.from} to ${destFolder}/${file.to}`);
await copyFile(sourceFile, destFile);
}
// This file comes from a zip file that hasn't been updated in years
// hardcode the permission fix to pass the commit checks.
if (['marker.png'].includes(file.to)) {
await chmod(destFile, 0o644);
}
}
}
await writeFile(librariesPath, libraries.join('\n\n'));
})();