diff --git a/package-lock.json b/package-lock.json index 3a84f19fc..53c343417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10941,14 +10941,16 @@ } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -38878,14 +38880,14 @@ } }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } diff --git a/scripts/copy-blockly.js b/scripts/copy-blockly.js index 59c7e355d..c472d282b 100644 --- a/scripts/copy-blockly.js +++ b/scripts/copy-blockly.js @@ -1,20 +1,7 @@ #!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); - -async function copyDir(src, dest) { - await fs.mkdir(dest, {recursive: true}); - const entries = await fs.readdir(src, {withFileTypes: true}); - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - if (entry.isDirectory()) { - await copyDir(srcPath, destPath); - } else if (entry.isFile()) { - await fs.copyFile(srcPath, destPath); - } - } -} +const {copyDirectoryContents} = require('./copy-helpers'); (async () => { const destRoot = path.resolve(__dirname, '..', 'examples', 'lib'); @@ -54,7 +41,7 @@ async function copyDir(src, dest) { // Copy the entire blockly package into destRoot (preserves README, dist, msg, media, etc.) try { - await copyDir(blocklyDir, blocklyRoot); + await copyDirectoryContents(blocklyDir, blocklyRoot); console.log('Blockly copied to', blocklyRoot); } catch (e) { console.error('Failed to copy Blockly package:', e); @@ -88,7 +75,7 @@ async function copyDir(src, dest) { distExists = false; } if (distExists) { - await copyDir(srcDist, path.join(dest, 'dist')); + await copyDirectoryContents(srcDist, path.join(dest, 'dist')); copiedLocal.push(shortName); console.log( `Copied local @blockly/${shortName}/dist to ${path.join( diff --git a/scripts/copy-helpers.js b/scripts/copy-helpers.js new file mode 100644 index 000000000..80332c43e --- /dev/null +++ b/scripts/copy-helpers.js @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Shared filesystem copy helpers (binary-safe, no gulp streams). + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * True if the path looks like a glob pattern (not a literal file path). + * @param {string} filePath Absolute or normalized path. + * @returns {boolean} Whether the path contains glob metacharacters. + */ +function hasGlobPattern(filePath) { + return /[*?[\]]/.test(filePath); +} + +/** + * Expand a glob pattern to matching paths. Prefer Node's `fs.globSync` (Node + * 22+); older Node uses the `glob` package (pulled in by gulp). + * @param {string} pattern Absolute path that may contain glob metacharacters. + * @returns {!Array} Matching paths from the filesystem. + */ +function expandGlob(pattern) { + if (typeof fs.globSync === 'function') { + return fs.globSync(pattern); + } + return require('glob').sync(pattern); +} + +/** + * Expand `blocklyDemoConfig.files`-style entries to concrete paths. + * @param {!Array} sources Paths relative to cwd or absolute. + * @returns {!Array} Absolute paths to files (directories skipped). + */ +function expandSourcePaths(sources) { + const out = []; + for (const src of sources) { + const resolved = path.resolve(src); + if (!hasGlobPattern(resolved)) { + out.push(resolved); + continue; + } + const matches = expandGlob(resolved); + for (const m of matches) { + if (fs.existsSync(m) && fs.statSync(m).isFile()) { + out.push(m); + } + } + } + return out; +} + +/** + * Copy files under baseDir into destRoot, preserving directory structure + * relative to baseDir. Skips missing sources. + * Uses fs.copyFileSync so binary files are not corrupted. + * Glob patterns in `sources` are expanded. + * @param {!Array} sources Source file paths (may include globs). + * @param {string} baseDir Base directory (e.g. 'examples' or 'plugins'). + * @param {string} destRoot Destination root (e.g. 'gh-pages/examples'). + */ +function copyFilesWithBase(sources, baseDir, destRoot) { + const baseResolved = path.resolve(baseDir); + const destRootResolved = path.resolve(destRoot); + const sourceFiles = expandSourcePaths(sources); + for (const srcPath of sourceFiles) { + if (!fs.existsSync(srcPath)) { + continue; + } + if (!fs.statSync(srcPath).isFile()) { + continue; + } + const rel = path.relative(baseResolved, path.resolve(srcPath)); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error( + `Source ${srcPath} is not under base directory ${baseDir}`, + ); + } + const destPath = path.join(destRootResolved, rel); + fs.mkdirSync(path.dirname(destPath), {recursive: true}); + fs.copyFileSync(srcPath, destPath); + } +} + +/** + * Copy a directory tree into dest. If dest already exists as a directory, + * the contents of src are merged/copied into it. + * No-op if src does not exist. Binary-safe. + * @param {string} src Source directory path. + * @param {string} dest Destination directory path. + * @returns {!Promise} Resolves when the copy completes. + */ +async function copyDirectoryContents(src, dest) { + try { + await fs.promises.access(src); + } catch { + return; + } + await fs.promises.mkdir(dest, {recursive: true}); + await fs.promises.cp(src, dest, {recursive: true}); +} + +module.exports = { + copyFilesWithBase, + copyDirectoryContents, +}; diff --git a/scripts/gh-predeploy.js b/scripts/gh-predeploy.js index b6ae17aaf..a5f5e1ec7 100644 --- a/scripts/gh-predeploy.js +++ b/scripts/gh-predeploy.js @@ -15,6 +15,8 @@ const path = require('path'); const showdown = require('showdown'); gulp.header = require('gulp-header'); +const {copyFilesWithBase, copyDirectoryContents} = require('./copy-helpers'); + const appDirectory = fs.realpathSync(process.cwd()); const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); @@ -288,18 +290,16 @@ function createReadmePage(pluginDir, isLocal) { * @param {string} pluginDir The directory with the plugin source files. * @param {boolean} isLocal True if building for a local test. False if * building for gh-pages. - * @returns {*} gulp stream */ function preparePlugin(pluginDir, isLocal) { console.log(`Preparing ${pluginDir} plugin for deployment.`); createPluginPage(pluginDir, isLocal); createReadmePage(pluginDir, isLocal); - return gulp - .src(['./plugins/' + pluginDir + '/build/test_bundle.js'], { - base: './plugins/', - allowEmpty: true, - }) - .pipe(gulp.dest('./gh-pages/plugins/')); + copyFilesWithBase( + [path.join('plugins', pluginDir, 'build', 'test_bundle.js')], + 'plugins', + path.join('gh-pages', 'plugins'), + ); } /** @@ -328,8 +328,9 @@ function prepareToDeployPlugins(done) { const folders = getPluginFolders(); return gulp.parallel( folders.map(function (folder) { - return function preDeployPlugin() { - return preparePlugin(folder, false); + return function preDeployPlugin(done) { + preparePlugin(folder, false); + done(); }; }), )(done); @@ -344,8 +345,9 @@ function prepareLocalPlugins(done) { const folders = getPluginFolders(); return gulp.parallel( folders.map(function (folder) { - return function preDeployPlugin() { - return preparePlugin(folder, true); + return function preDeployPlugin(done) { + preparePlugin(folder, true); + done(); }; }), )(done); @@ -445,7 +447,6 @@ function createExamplePage(pageRoot, pagePath, demoConfig, isLocal) { * @param {boolean} isLocal True if building for a local test. False if * building for gh-pages. * @param {Function} done Completed callback. - * @returns {Function | undefined} Gulp task. */ function prepareExample(exampleDir, isLocal, done) { const baseDir = 'examples'; @@ -468,12 +469,13 @@ function prepareExample(exampleDir, isLocal, done) { // Special case: do a straight copy for the devsite demo, with no wrappers. if (packageJson.name == 'blockly-devsite-demo') { - return gulp - .src( - fileList.map((f) => path.join(baseDir, exampleDir, f)), - {base: baseDir, allowEmpty: true}, - ) - .pipe(gulp.dest('./gh-pages/examples/')); + copyFilesWithBase( + fileList.map((f) => path.join(baseDir, exampleDir, f)), + baseDir, + path.join('gh-pages', 'examples'), + ); + done(); + return; } // All other examples. @@ -487,14 +489,14 @@ function prepareExample(exampleDir, isLocal, done) { // Copy over all other files mentioned in the demoConfig to the // correct directory. const assets = fileList.filter((f) => !pageRegex.test(f)); - let stream; if (assets.length) { - stream = gulp.src( + copyFilesWithBase( assets.map((f) => path.join(baseDir, exampleDir, f)), - {base: baseDir, allowEmpty: true}, + baseDir, + path.join('gh-pages', 'examples'), ); } - return stream.pipe(gulp.dest('./gh-pages/examples/')); + done(); } /** @@ -517,20 +519,17 @@ function getExampleFolders() { * * This is treated separately from other examples because it doesn't * get the same page chrome added to it. - * @returns {Function | undefined} Gulp task. */ -function prepareDeveloperTools() { +async function prepareDeveloperTools() { const baseDir = 'examples'; const devToolsDir = 'developer-tools'; console.log(`Preparing developer-tools for deployment.`); - // Create target folder, if it doesn't exist. - fs.mkdirSync(path.join('gh-pages', baseDir, devToolsDir), {recursive: true}); - - // Copy all files from `dist/` subdirectory into the corresponding gh-pages directory - return gulp - .src('./examples/developer-tools/dist/*') - .pipe(gulp.dest('./gh-pages/examples/developer-tools')); + // Copy all files from `dist/` into the gh-pages developer-tools directory + await copyDirectoryContents( + path.join(baseDir, devToolsDir, 'dist'), + path.join('gh-pages', baseDir, devToolsDir), + ); } /**