Skip to content
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
18 changes: 10 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 3 additions & 16 deletions scripts/copy-blockly.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
112 changes: 112 additions & 0 deletions scripts/copy-helpers.js
Original file line number Diff line number Diff line change
@@ -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<string>} 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<string>} sources Paths relative to cwd or absolute.
* @returns {!Array<string>} 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<string>} 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<void>} 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,
};
61 changes: 30 additions & 31 deletions scripts/gh-predeploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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'),
);
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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();
}

/**
Expand All @@ -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),
);
}

/**
Expand Down
Loading