diff --git a/.github/workflows/deploy-docusaurus.yml b/.github/workflows/deploy-docusaurus.yml index 76a10816115..5ee557c065f 100644 --- a/.github/workflows/deploy-docusaurus.yml +++ b/.github/workflows/deploy-docusaurus.yml @@ -58,7 +58,7 @@ jobs: - name: Generate reference docs working-directory: ./packages/blockly run: | - npx gulp typings + npm run typings npm run docs - name: Build the Docusaurus site diff --git a/eslint.config.mjs b/eslint.config.mjs index ade06b99230..5968b371edc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -196,6 +196,8 @@ export default defineConfig( 'packages/blockly/.prettierrc.js', 'packages/blockly/gulpfile.mjs', 'packages/blockly/scripts/helpers.js', + 'packages/blockly/scripts/lib/*.mjs', + 'packages/blockly/scripts/package.mjs', 'packages/blockly/scripts/update_github_pages.mjs', 'packages/blockly/tests/mocha/.mocharc.js', 'packages/blockly/tests/migration/validate-renamings.mjs', diff --git a/package-lock.json b/package-lock.json index 3228ca943ad..2cb464e2402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10407,9 +10407,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10424,9 +10421,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10441,9 +10435,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10458,9 +10449,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11048,9 +11036,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11065,9 +11050,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11082,9 +11064,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11099,9 +11078,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -12249,9 +12225,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12269,9 +12242,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12289,9 +12259,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12309,9 +12276,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12329,9 +12293,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12349,9 +12310,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12505,9 +12463,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12525,9 +12480,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12545,9 +12497,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12565,9 +12514,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12585,9 +12531,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12605,9 +12548,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16090,16 +16030,6 @@ "node": ">=0.8" } }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -16126,65 +16056,6 @@ "node": ">=0.10.0" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/cloneable-readable/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/cloneable-readable/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -16465,16 +16336,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/concat-with-sourcemaps": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", - "dev": true, - "license": "ISC", - "dependencies": { - "source-map": "^0.6.1" - } - }, "node_modules/concurrently": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", @@ -21302,59 +21163,6 @@ "node": ">=10" } }, - "node_modules/gulp-concat": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", - "integrity": "sha512-a2scActrQrDBpBbR3WUZGyGS1JEPLg5PZJdIa7/Bi3GuKAmPYDK6SFhy/NZq5R8KsKKFvtfR0fakbUCcKGCCjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "concat-with-sourcemaps": "^1.0.0", - "through2": "^2.0.0", - "vinyl": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-concat/node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/gulp-concat/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-concat/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/gulp-rename": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz", @@ -21425,74 +21233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/gulp-umd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-umd/-/gulp-umd-2.0.0.tgz", - "integrity": "sha512-zA0RDIITdOwpVUBQ6vy2R+iCsTXwDImPnWreNBmVJQAg3nDGefowV7KYwWoIeEVoxyHZT2CR50nEF6ovUh5/2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "concat-stream": "^1.6.2", - "lodash.template": "^4.4.0", - "through2": "^2.0.3" - } - }, - "node_modules/gulp-umd/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/gulp-umd/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/gulp-umd/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-umd/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/gulp-umd/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/gulplog": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", @@ -24143,9 +23883,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -24167,9 +23904,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -24191,9 +23925,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -24215,9 +23946,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -24396,13 +24124,6 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -24434,28 +24155,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, - "node_modules/lodash.template": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.18.1.tgz", - "integrity": "sha512-5urZrLnV/VD6zHK5KsVtZgt7H19v51mIzoS0aBNH8yp3I8tbswrEjOABOPY8m8uB7NuibubLrMX+Y0PXsU9X+w==", - "deprecated": "This package is deprecated. Use https://socket.dev/npm/package/eta instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -36719,11 +36418,9 @@ "glob": "^13.0.6", "google-closure-compiler": "^20260819.0.0", "gulp": "^5.0.1", - "gulp-concat": "^2.6.1", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", "gulp-sourcemaps": "^3.0.0", - "gulp-umd": "^2.0.0", "http-server": "^14.1.1", "jsdom": "30.0.1", "json5": "^2.2.3", diff --git a/packages/blockly/gulpfile.mjs b/packages/blockly/gulpfile.mjs index 819c9088a08..3feecf4b73b 100644 --- a/packages/blockly/gulpfile.mjs +++ b/packages/blockly/gulpfile.mjs @@ -32,12 +32,8 @@ import { tsc, } from './scripts/gulpfiles/build_tasks.mjs'; import {docs} from './scripts/gulpfiles/docs_tasks.mjs'; -import { - cleanReleaseDir, - pack, - typings, -} from './scripts/gulpfiles/package_tasks.mjs'; import {generators, test} from './scripts/gulpfiles/test_tasks.mjs'; +import {cleanReleaseDir} from './scripts/lib/release_dir.mjs'; const clean = parallel(cleanBuildDir, cleanReleaseDir); @@ -54,7 +50,6 @@ export { tsc, minify, build, - pack, // Formerly package. prepareDemos, deployDemosBeta, deployDemos, @@ -66,10 +61,10 @@ export { // prettier-ignore export { messages, // Generate msg/json/en.json et al. + cleanBuildDir, clean, test, generators as testGenerators, buildAdvancedCompilationTest, docs, - typings, } diff --git a/packages/blockly/package.json b/packages/blockly/package.json index d2c1f66d37e..adcb046f1b9 100644 --- a/packages/blockly/package.json +++ b/packages/blockly/package.json @@ -82,10 +82,11 @@ "lint:fix": "eslint . --fix", "langfiles": "gulp langfiles", "minify": "gulp minify", - "package": "gulp pack", + "package": "node scripts/package.mjs", "prepareDemos": "gulp prepareDemos", "start": "npm run package && npx nx run @blockly/dev-tools:build && concurrently -n watch,devtools-server,core-server \"npx nx watch --projects=@blockly/dev-tools --includeDependencies -- npm run build\" \"npx nx run @blockly/dev-tools:start --no-open\" \"http-server ./ -p 8081 -s -o /tests/playground.html -c-1\"", "tsc": "gulp tsc", + "typings": "node scripts/package.mjs typings", "test": "gulp test", "test:browser": "npx mocha --config tests/browser/.mocharc.js", "test:mocha:node": "npm run test:mocha:typecheck && npx mocha --config tests/mocha/.mocharc.node.cjs \"tests/mocha/**/*_test.{js,ts}\"", @@ -162,11 +163,9 @@ "glob": "^13.0.6", "google-closure-compiler": "^20260819.0.0", "gulp": "^5.0.1", - "gulp-concat": "^2.6.1", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", "gulp-sourcemaps": "^3.0.0", - "gulp-umd": "^2.0.0", "http-server": "^14.1.1", "jsdom": "30.0.1", "json5": "^2.2.3", diff --git a/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs b/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs index ae7fd943b9e..a677287f0d8 100644 --- a/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs @@ -14,8 +14,8 @@ import * as fs from 'fs'; import * as path from 'path'; import {execSync} from 'child_process'; import * as buildTasks from './build_tasks.mjs'; -import {getPackageJson} from './helper_tasks.mjs'; -import * as packageTasks from './package_tasks.mjs'; +import {getPackageJson} from '../lib/package_json.mjs'; +import {cleanReleaseDir} from '../lib/release_dir.mjs'; import {rimraf} from 'rimraf'; const demoTmpDir = '../_deploy'; @@ -171,7 +171,7 @@ export const prepareDemos = gulp.series( copyAppengineSrc), gulp.series( gulp.parallel(buildTasks.cleanBuildDir, - packageTasks.cleanReleaseDir), + cleanReleaseDir), buildTasks.build, gulp.parallel(copyBuilt, copyCompressedToOldLocation, diff --git a/packages/blockly/scripts/gulpfiles/build_tasks.mjs b/packages/blockly/scripts/gulpfiles/build_tasks.mjs index 5f141f5bf09..70522376e86 100644 --- a/packages/blockly/scripts/gulpfiles/build_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -31,9 +31,9 @@ import { TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR, } from './config.mjs'; -import {getPackageJson} from './helper_tasks.mjs'; import {posixPath, quote} from '../helpers.js'; +import {getPackageJson} from '../lib/package_json.mjs'; const argv = yargs(hideBin(process.argv)).parse(); diff --git a/packages/blockly/scripts/gulpfiles/helper_tasks.mjs b/packages/blockly/scripts/gulpfiles/helper_tasks.mjs deleted file mode 100644 index 2068de106a5..00000000000 --- a/packages/blockly/scripts/gulpfiles/helper_tasks.mjs +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Any gulp helper functions. - */ - -import Module from "node:module"; - -const require = Module.createRequire(import.meta.url); - -/** - * Load and return the contents of package.json. - * - * Uses require() rather than import, and clears the require cache, to - * ensure the loaded package.json data is up to date. - */ -export function getPackageJson() { - delete require.cache[require.resolve('../../package.json')]; - return require('../../package.json'); -} - diff --git a/packages/blockly/scripts/gulpfiles/package_tasks.mjs b/packages/blockly/scripts/gulpfiles/package_tasks.mjs deleted file mode 100644 index aebc93a37f0..00000000000 --- a/packages/blockly/scripts/gulpfiles/package_tasks.mjs +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Gulp tasks to package Blockly for distribution on NPM. - */ - -import * as gulp from 'gulp'; -import concat from 'gulp-concat'; -import replace from 'gulp-replace'; -import umd from 'gulp-umd'; - -import * as path from 'path'; -import * as fs from 'fs'; -import {rimraf} from 'rimraf'; -import * as build from './build_tasks.mjs'; -import {getPackageJson} from './helper_tasks.mjs'; -import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; - -// Path to template files for gulp-umd. -const TEMPLATE_DIR = 'scripts/package/templates'; - -/** - * A helper method for wrapping a file into a Universal Module Definition. - * @param {string} namespace The export namespace. - * @param {Array} dependencies An array of dependencies to inject. - */ -function packageUMD(namespace, dependencies, template = 'umd.template') { - return umd({ - dependencies: function () { return dependencies; }, - namespace: function () { return namespace; }, - exports: function () { return namespace; }, - template: path.join(TEMPLATE_DIR, template) - }); -}; - -/** - * This task wraps scripts/package/index.js into a UMD module. - * - * This module is the main entrypoint for the blockly package, and - * loads blockly/core, blockly/blocks and blockly/msg/en and then - * calls setLocale(en). - */ -function packageIndex() { - return gulp.src('scripts/package/index.js') - .pipe(packageUMD('Blockly', [{ - name: 'Blockly', - amd: 'blockly/core', - cjs: 'blockly/core', - },{ - name: 'en', - amd: 'blockly/msg/en', - cjs: 'blockly/msg/en', - global: 'Blockly.Msg', - },{ - name: 'blocks', - amd: 'blockly/blocks', - cjs: 'blockly/blocks', - global: 'Blockly.Blocks', - }])) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task copies scripts/package/core-node.js into into the - * package. This module will be the 'blockly/core' entrypoint for - * node.js environments. - * - * Note that, unlike index.js, this file does not get a UMD wrapper. - * This is because it is only used in node.js environments and so is - * guaranteed to be loaded as a CJS module. - */ -function packageCoreNode() { - return gulp.src('scripts/package/core-node.js') - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task wraps each of the files in ${BUILD_DIR/msg/ into a UMD module. - * @example import * as En from 'blockly/msg/en'; - */ -function packageLocales() { - // Remove references to goog.provide and goog.require. - return gulp.src(`${LANG_BUILD_DIR}/*.js`) - .pipe(replace(/goog\.[^\n]+/g, '')) - .pipe(packageUMD('Blockly.Msg', [], 'umd-msg.template')) - .pipe(gulp.dest(`${RELEASE_DIR}/msg`)); -}; - -/** - * This task creates a UMD bundle of Blockly which includes the Blockly - * core files, the built-in blocks, the JavaScript code generator and the - * English localization files. - * @example - */ -function packageUMDBundle() { - const srcs = [ - `${RELEASE_DIR}/blockly_compressed.js`, - `${RELEASE_DIR}/msg/en.js`, - `${RELEASE_DIR}/blocks_compressed.js`, - `${RELEASE_DIR}/javascript_compressed.js`, - ]; - return gulp.src(srcs) - .pipe(concat('blockly.min.js')) - .pipe(gulp.dest(`${RELEASE_DIR}`)); -}; - - -/** - * This task creates shims for the submodule entrypoints, for the - * benefit of bundlers and other build tools that do not correctly - * support the exports declaration in package.json. These shims just - * require() and reexport the corresponding *_compressed.js bundle. - * - * This should solve issues encountered by users of bundlers that don't - * support exports at all (e.g. browserify) as well as ones that don't - * support it in certain circumstances (e.g., when using webpack's - * resolve.alias configuration option to alias 'blockly' to - * 'node_modules/blockly', as we formerly did in most plugins, which - * causes webpack to ignore blockly's package.json entirely). - * - * Assumptions: - * - Such bundlers will _completely_ ignore the exports declaration. - * - The bundles are intended to be used in a browser—or at least not - * in node.js—so the core entrypoint never needs to route to - * core-node.js. This is reasonable since there's little reason to - * bundle code for node.js, and node.js has supported the exports - * clause since at least v12, consideably older than any version of - * node.js we officially support. - * - It suffices to provide only a CJS entrypoint (because we can only - * provide CJS or ESM, not both. (We could in future switch to - * providing only an ESM entrypoint instead, though.) - * - * @param {Function} done Callback to call when done. - */ -function packageLegacyEntrypoints(done) { - for (const entrypoint of [ - 'core', 'blocks', 'dart', 'javascript', 'lua', 'php', 'python' - ]) { - const bundle = - (entrypoint === 'core' ? 'blockly' : entrypoint) + '_compressed.js'; - fs.writeFileSync(path.join(RELEASE_DIR, `${entrypoint}.js`), - `// Shim for backwards-compatibility with bundlers that do not -// support the 'exports' clause in package.json, to allow them -// to load the blockly/${entrypoint} submodule entrypoint. -module.exports = require('./${bundle}'); -`); - } - done(); -} - -/** - * This task copies all the media/* files into the release directory. - */ -function packageMedia() { - return gulp.src('media/*', {encoding: false}) - .pipe(gulp.dest(`${RELEASE_DIR}/media`)); -}; - -/** - * This task copies the package.json file into the release directory, - * with modifications: - * - * - The scripts section is removed. - * - The nx section is removed. - * - * Prerequisite: buildLangfiles. - * - * @param {Function} done Callback to call when done. - */ -function packageJSON(done) { - // Copy package.json, so we can safely modify it. - const json = JSON.parse(JSON.stringify(getPackageJson())); - // Remove unwanted entries. - delete json['scripts']; - delete json['nx']; - // Update exports to match how the package will be structured - const exports = json['exports']; - if (exports) { - for (const exportKey in exports) { - const exportObj = exports[exportKey]; - for (const pathKey in exportObj) { - exportObj[pathKey] = exportObj[pathKey].replace('./dist', '.') - } - } - } - // Set "type": "commonjs", since that's what .js files in the - // package root are. This should be a no-op since that's the - // default, but by setting it explicitly we ensure that any chage to - // the repository top-level package.json to set "type": "module" - // won't break the published package accidentally. - json.type = 'commonjs'; - // Write resulting package.json file to release directory. - if (!fs.existsSync(RELEASE_DIR)) { - fs.mkdirSync(RELEASE_DIR, {recursive: true}); - } - fs.writeFileSync(`${RELEASE_DIR}/package.json`, - JSON.stringify(json, null, 2)); - done(); -}; - -/** - * This task copies the scripts/package/README.md file into the - * release directory. This file is what developers will see at - * https://www.npmjs.com/package/blockly . - */ -function packageReadme() { - return gulp.src('scripts/package/README.md') - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task copies the generated .d.ts files in build/declarations and the - * hand-written .d.ts files in typings/ into the release directory. The main - * entrypoint file (index.d.ts) is referenced in package.json in the types - * property. - */ -function packageDTS() { - const handwrittenSrcs = [ - 'typings/*.d.ts', - 'typings/msg/*.d.ts', - ]; - return gulp.src(handwrittenSrcs, {base: 'typings'}) - .pipe(gulp.src(`${TYPINGS_BUILD_DIR}/**/*.d.ts`, {ignore: [ - `${TYPINGS_BUILD_DIR}/blocks/**/*`, - ]})) - .pipe(replace('AnyDuringMigration', 'any')) - .pipe(gulp.dest(RELEASE_DIR)); -}; - -/** - * This task cleans the release directory (by deleting it). - */ -export function cleanReleaseDir() { - // Sanity check. - if (RELEASE_DIR === '.' || RELEASE_DIR === '/') { - return Promise.reject(`Refusing to rm -rf ${RELEASE_DIR}`); - } - return rimraf(RELEASE_DIR); -} - -/** - * This task prepares the files to be included in the NPM by copying - * them into the release directory. - * - * This task was formerly called "package" but was renamed in - * preparation for porting gulpfiles to ESM because "package" is a - * reserved word. - * - * Prerequisite: build. - */ -export const typings = gulp.series( - gulp.parallel(build.cleanBuildDir, cleanReleaseDir), - build.tsc, - packageDTS); - -export const pack = gulp.series( - gulp.parallel( - build.cleanBuildDir, - cleanReleaseDir), - build.build, - gulp.parallel( - packageIndex, - packageCoreNode, - packageLegacyEntrypoints, - packageMedia, - gulp.series(packageLocales, packageUMDBundle), - packageJSON, - packageReadme, - packageDTS) - ); diff --git a/packages/blockly/scripts/lib/exec.mjs b/packages/blockly/scripts/lib/exec.mjs new file mode 100644 index 00000000000..2d551b49d6d --- /dev/null +++ b/packages/blockly/scripts/lib/exec.mjs @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helpers for running other commands in a CLI. + * + * These all inherit the current working directory unless told + * otherwise. + */ + +import {execSync, spawn} from 'node:child_process'; + +/** + * Run a command, forwarding its output to this process's stdio. + * + * @param {string} command The command to run. + */ +export function run(command) { + execSync(command, {stdio: 'inherit'}); +} + +/** + * Run a command and return its trimmed stdout. + * Used when the output of a command is needed as a value. + * + * @param {string} command The command to run. + * @returns {string} The command's output. + */ +export function capture(command) { + return execSync(command, {encoding: 'utf8'}).trim(); +} + +/** + * Run a command in a subprocess without a shell, forwarding its output + * to this process's stdio. + * + * Unlike run(), this does not block the event loop, so several + * commands can be run concurrently. + * + * @param {string} command The command to run. + * @param {Array=} args Arguments to pass to the command. + * @param {object=} options Options for child_process.spawn, plus an + * optional label to use in place of the command name when + * reporting failure. + * @returns {Promise} Promise resolved when the command succeeds, + * and rejected if it fails. + */ +export function spawnAsync(command, args = [], {label, ...options} = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, {stdio: 'inherit', ...options}); + child.on('error', reject); + child.on('close', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + const reason = signal ? `signal ${signal}` : `exit code ${code}`; + reject(new Error(`${label ?? command} failed with ${reason}`)); + }); + }); +} diff --git a/packages/blockly/scripts/lib/fs_utils.mjs b/packages/blockly/scripts/lib/fs_utils.mjs new file mode 100644 index 00000000000..c37126e8c41 --- /dev/null +++ b/packages/blockly/scripts/lib/fs_utils.mjs @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Filesystem helpers for the build and packaging scripts. + */ + +import {globSync} from 'glob'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +/** + * Maximum number of file operations to have in flight at once, across + * everything in this module. Each one holds file descriptors open + * while it runs, so letting the number of files being processed decide + * how many run concurrently risks EMFILE errors on systems with a low + * file descriptor limit. + */ +const MAX_OPEN_FILES = 32; + +/** Number of file operations currently in flight. */ +let openFiles = 0; + +/** Callbacks of the operations waiting for a free slot, in order. */ +const waiting = []; + +/** + * Absolute path to the root of the blockly package. All of the paths + * in config.mjs are relative to this directory. + */ +export const PACKAGE_ROOT = path.resolve(import.meta.dirname, '..', '..'); + +/** + * Resolve a package-root-relative path to an absolute one, so that + * scripts work regardless of the current working directory. Segments + * that are already absolute are returned as-is. + * + * @param {...string} segments Path segments, relative to the package root. + * @returns {string} The corresponding absolute path. + */ +export function fromRoot(...segments) { + return path.resolve(PACKAGE_ROOT, ...segments); +} + +/** + * Run an operation that opens files, waiting first if MAX_OPEN_FILES + * of them are already in flight. This makes it safe to start an + * operation per file without regard for how many files there are. + * + * @param {function(): Promise} operation The operation to run. + * @returns {Promise} The result of the operation. + * @template T + */ +async function withOpenFile(operation) { + if (openFiles < MAX_OPEN_FILES) { + openFiles++; + } else { + await new Promise((resolve) => waiting.push(resolve)); + } + try { + return await operation(); + } finally { + const next = waiting.shift(); + if (next) { + // Hand our slot directly to the next operation in line, rather + // than releasing it and letting them race for it. + next(); + } else { + openFiles--; + } + } +} + +/** + * Write a text file, creating its parent directories if needed. + * + * @param {string} file Path of the file to write, relative to the + * package root. + * @param {string} contents Contents to write. + */ +export async function writeFile(file, contents) { + const target = fromRoot(file); + await fs.mkdir(path.dirname(target), {recursive: true}); + await fs.writeFile(target, contents); +} + +/** + * Copy the files matching one or more glob patterns into a destination + * directory, preserving each file's path relative to the source + * directory. + * + * @param {object} options Options object. + * @param {string} options.from Directory the patterns are relative to, + * itself relative to the package root. + * @param {string[]} options.patterns Glob patterns, relative to `from`. + * @param {string} options.to Destination directory, relative to the + * package root. + * @param {string[]=} options.ignore Glob patterns to exclude. + * @param {function(string): string=} options.transform If supplied, the + * contents of each file are read as UTF-8, passed through this + * function, and the result written out. Files are copied verbatim + * (and so binary-safe) if it is omitted. + * @returns {Promise} Promise resolved when copying is complete. + */ +export async function copyFiles({from, patterns, to, ignore = [], transform}) { + const files = globSync(patterns, { + cwd: fromRoot(from), + ignore, + nodir: true, + posix: true, + }); + await Promise.all( + files.map((file) => + withOpenFile(async () => { + const src = fromRoot(from, file); + const dest = fromRoot(to, file); + await fs.mkdir(path.dirname(dest), {recursive: true}); + if (transform) { + await fs.writeFile(dest, transform(await fs.readFile(src, 'utf8'))); + } else { + await fs.copyFile(src, dest); + } + }), + ), + ); +} diff --git a/packages/blockly/scripts/lib/package_json.mjs b/packages/blockly/scripts/lib/package_json.mjs new file mode 100644 index 00000000000..1bc985a7818 --- /dev/null +++ b/packages/blockly/scripts/lib/package_json.mjs @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Access to the blockly package's package.json. + */ + +import * as fs from 'node:fs'; +import {fromRoot} from './fs_utils.mjs'; + +/** + * Load and return the contents of package.json. + * + * The file is re-read on every call so that callers always see + * up-to-date data, even if it has been modified (e.g. to bump the + * version number) since the script started. + * + * @returns {object} The parsed contents of package.json. + */ +export function getPackageJson() { + return JSON.parse(fs.readFileSync(fromRoot('package.json'), 'utf8')); +} diff --git a/packages/blockly/scripts/lib/release_dir.mjs b/packages/blockly/scripts/lib/release_dir.mjs new file mode 100644 index 00000000000..febb24495df --- /dev/null +++ b/packages/blockly/scripts/lib/release_dir.mjs @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Management of the release directory, from which the + * blockly npm package is published. + */ + +import * as fs from 'node:fs/promises'; +import {RELEASE_DIR} from '../gulpfiles/config.mjs'; +import {fromRoot} from './fs_utils.mjs'; + +/** + * Clean the release directory (by deleting it). + * + * @returns {Promise} Promise resolved once the directory is gone. + */ +export async function cleanReleaseDir() { + // Sanity check. + if (RELEASE_DIR === '.' || RELEASE_DIR === '/') { + throw new Error(`Refusing to rm -rf ${RELEASE_DIR}`); + } + await fs.rm(fromRoot(RELEASE_DIR), {force: true, recursive: true}); +} diff --git a/packages/blockly/scripts/lib/run_gulp.mjs b/packages/blockly/scripts/lib/run_gulp.mjs new file mode 100644 index 00000000000..a45b808a33d --- /dev/null +++ b/packages/blockly/scripts/lib/run_gulp.mjs @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Running of gulp tasks from plain node scripts. + * + * TODO: Delete this once the build tasks have been converted to plain + * node scripts, and call them directly instead. + */ + +import Module from 'node:module'; +import {spawnAsync} from './exec.mjs'; +import {PACKAGE_ROOT} from './fs_utils.mjs'; + +const require = Module.createRequire(import.meta.url); + +/** Path to the gulp CLI entrypoint. */ +const GULP_CLI = require.resolve('gulp-cli/bin/gulp.js'); + +/** + * Run a single gulp task in a subprocess, inheriting stdio so that its + * output appears interleaved with our own. + * + * Tasks are run one per invocation because the gulp CLI runs multiple + * tasks in parallel rather than in series. + * + * @param {string} task Name of the task to run, as exported by gulpfile.mjs. + * @param {Array=} args Additional arguments to pass to gulp. + * @returns {Promise} Promise resolved when the task succeeds, and + * rejected if it fails. + */ +export function runGulpTask(task, args = []) { + return spawnAsync(process.execPath, [GULP_CLI, task, ...args], { + cwd: PACKAGE_ROOT, + label: `gulp ${task}`, + }); +} diff --git a/packages/blockly/scripts/lib/umd.mjs b/packages/blockly/scripts/lib/umd.mjs new file mode 100644 index 00000000000..2f3e689d290 --- /dev/null +++ b/packages/blockly/scripts/lib/umd.mjs @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Wrapping of scripts into Universal Module Definitions. + */ + +import * as fs from 'node:fs'; +import {fromRoot} from './fs_utils.mjs'; + +/** Path to the UMD template files, relative to the package root. */ +const TEMPLATE_DIR = 'scripts/package/templates'; + +/** Cache of template contents, keyed by template filename. */ +const templates = new Map(); + +/** + * A dependency to be injected into a UMD module. + * + * @typedef {object} UmdDependency + * @property {string} name Name of the dependency, used as the default + * for any of the other properties that are omitted. + * @property {string=} amd Module ID to require in an AMD loader. + * @property {string=} cjs Module ID to require in a CJS loader. + * @property {string=} global Property of the global object (minus the + * leading "root.") to use in a browser. + * @property {string=} param Name of the factory function parameter the + * dependency is passed as. + */ + +/** + * Wrap the given script in a Universal Module Definition, so that it + * can be loaded by an AMD loader, by a CJS loader, or directly in a + * browser. + * + * This is a replacement for the gulp-umd plugin, supporting just the + * subset of its template syntax and options that our templates use. + * + * @param {string} contents The script to wrap. + * @param {object} options Options object. + * @param {string} options.namespace The export namespace. + * @param {string=} options.exports The expression the factory function + * should return. Defaults to the namespace. + * @param {Array=} options.dependencies Dependencies to + * inject. + * @param {string=} options.template Filename of the template to use, + * within TEMPLATE_DIR. + * @returns {string} The wrapped script. + */ +export function wrapUmd( + contents, + { + namespace, + exports = namespace, + dependencies = [], + template = 'umd.template', + }, +) { + const substitutions = { + contents, + exports, + namespace, + amd: `[${dependencies.map((dep) => `'${dep.amd ?? dep.name}'`).join(', ')}]`, + cjs: dependencies + .map((dep) => `require('${dep.cjs ?? dep.name}')`) + .join(', '), + global: dependencies + .map((dep) => `root.${dep.global ?? dep.name}`) + .join(', '), + param: dependencies.map((dep) => dep.param ?? dep.name).join(', '), + }; + + return getTemplate(template).replace(/<%=\s*(\w+)\s*%>/g, (match, key) => { + if (!(key in substitutions)) { + throw new Error(`Unsupported substitution ${match} in ${template}`); + } + return substitutions[key]; + }); +} + +/** + * Load (and cache) the contents of a template file. + * + * @param {string} template Filename of the template, within TEMPLATE_DIR. + * @returns {string} The contents of the template. + */ +function getTemplate(template) { + if (!templates.has(template)) { + templates.set( + template, + fs.readFileSync(fromRoot(TEMPLATE_DIR, template), 'utf8'), + ); + } + return templates.get(template); +} diff --git a/packages/blockly/scripts/package.mjs b/packages/blockly/scripts/package.mjs new file mode 100644 index 00000000000..97a766d96bf --- /dev/null +++ b/packages/blockly/scripts/package.mjs @@ -0,0 +1,355 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Script to package Blockly for distribution on NPM. + * + * Usage: + * node scripts/package.mjs + * # build Blockly and assemble the complete npm package in the + * # release directory + * node scripts/package.mjs typings + * # assemble only the .d.ts files, which is all that is needed + * # to generate the reference documentation + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import {parseArgs} from 'node:util'; +import { + LANG_BUILD_DIR, + RELEASE_DIR, + TYPINGS_BUILD_DIR, +} from './gulpfiles/config.mjs'; +import {copyFiles, fromRoot, writeFile} from './lib/fs_utils.mjs'; +import {getPackageJson} from './lib/package_json.mjs'; +import {cleanReleaseDir} from './lib/release_dir.mjs'; +import {runGulpTask} from './lib/run_gulp.mjs'; +import {wrapUmd} from './lib/umd.mjs'; + +/** Directory containing the files to be copied into the package as-is. */ +const PACKAGE_SRC_DIR = 'scripts/package'; + +/** Directory the localised message modules are written to. */ +const RELEASE_MSG_DIR = path.join(RELEASE_DIR, 'msg'); + +/** + * The submodule entrypoints that get a legacy CJS shim. See + * packageLegacyEntrypoints for details. + */ +const LEGACY_ENTRYPOINTS = [ + 'core', + 'blocks', + 'dart', + 'javascript', + 'lua', + 'php', + 'python', +]; + +/** + * Wrap scripts/package/index.js into a UMD module. + * + * This module is the main entrypoint for the blockly package, and + * loads blockly/core, blockly/blocks and blockly/msg/en and then + * calls setLocale(en). + */ +async function packageIndex() { + const contents = await fs.readFile( + fromRoot(PACKAGE_SRC_DIR, 'index.js'), + 'utf8', + ); + await writeFile( + path.join(RELEASE_DIR, 'index.js'), + wrapUmd(contents, { + namespace: 'Blockly', + dependencies: [ + { + name: 'Blockly', + amd: 'blockly/core', + cjs: 'blockly/core', + }, + { + name: 'en', + amd: 'blockly/msg/en', + cjs: 'blockly/msg/en', + global: 'Blockly.Msg', + }, + { + name: 'blocks', + amd: 'blockly/blocks', + cjs: 'blockly/blocks', + global: 'Blockly.Blocks', + }, + ], + }), + ); +} + +/** + * Copy scripts/package/core-node.js into the package. This module + * will be the 'blockly/core' entrypoint for node.js environments. + * + * Note that, unlike index.js, this file does not get a UMD wrapper. + * This is because it is only used in node.js environments and so is + * guaranteed to be loaded as a CJS module. + */ +async function packageCoreNode() { + await copyFiles({ + from: PACKAGE_SRC_DIR, + patterns: ['core-node.js'], + to: RELEASE_DIR, + }); +} + +/** + * Wrap each of the files in ${BUILD_DIR}/msg/ into a UMD module. + * + * @example import * as En from 'blockly/msg/en'; + */ +async function packageLocales() { + await copyFiles({ + from: LANG_BUILD_DIR, + patterns: ['*.js'], + to: RELEASE_MSG_DIR, + transform: (contents) => + wrapUmd(contents, { + namespace: 'Blockly.Msg', + template: 'umd-msg.template', + }), + }); +} + +/** + * Create a UMD bundle of Blockly which includes the Blockly core + * files, the built-in blocks, the JavaScript code generator and the + * English localization files. + * + * Prerequisites: build (for the compiled chunks), packageLocales. + * + * @example + */ +async function packageUMDBundle() { + const srcs = [ + path.join(RELEASE_DIR, 'blockly_compressed.js'), + path.join(RELEASE_MSG_DIR, 'en.js'), + path.join(RELEASE_DIR, 'blocks_compressed.js'), + path.join(RELEASE_DIR, 'javascript_compressed.js'), + ]; + const contents = await Promise.all( + srcs.map((src) => fs.readFile(fromRoot(src), 'utf8')), + ); + await writeFile( + path.join(RELEASE_DIR, 'blockly.min.js'), + contents.join('\n'), + ); +} + +/** + * Create shims for the submodule entrypoints, for the benefit of + * bundlers and other build tools that do not correctly support the + * exports declaration in package.json. These shims just require() and + * reexport the corresponding *_compressed.js bundle. + * + * This should solve issues encountered by users of bundlers that don't + * support exports at all (e.g. browserify) as well as ones that don't + * support it in certain circumstances (e.g., when using webpack's + * resolve.alias configuration option to alias 'blockly' to + * 'node_modules/blockly', as we formerly did in most plugins, which + * causes webpack to ignore blockly's package.json entirely). + * + * Assumptions: + * - Such bundlers will _completely_ ignore the exports declaration. + * - The bundles are intended to be used in a browser—or at least not + * in node.js—so the core entrypoint never needs to route to + * core-node.js. This is reasonable since there's little reason to + * bundle code for node.js, and node.js has supported the exports + * clause since at least v12, consideably older than any version of + * node.js we officially support. + * - It suffices to provide only a CJS entrypoint (because we can only + * provide CJS or ESM, not both. (We could in future switch to + * providing only an ESM entrypoint instead, though.) + */ +async function packageLegacyEntrypoints() { + await Promise.all( + LEGACY_ENTRYPOINTS.map((entrypoint) => { + const bundle = + (entrypoint === 'core' ? 'blockly' : entrypoint) + '_compressed.js'; + return writeFile( + path.join(RELEASE_DIR, `${entrypoint}.js`), + `// Shim for backwards-compatibility with bundlers that do not +// support the 'exports' clause in package.json, to allow them +// to load the blockly/${entrypoint} submodule entrypoint. +module.exports = require('./${bundle}'); +`, + ); + }), + ); +} + +/** + * Copy all the media/* files into the release directory. + */ +async function packageMedia() { + await copyFiles({ + from: 'media', + patterns: ['*'], + to: path.join(RELEASE_DIR, 'media'), + }); +} + +/** + * Copy the package.json file into the release directory, with + * modifications: + * + * - The scripts section is removed. + * - The nx section is removed. + * - The exports are rewritten to match the structure of the package. + */ +async function packageJSON() { + const json = getPackageJson(); + // Remove unwanted entries. + delete json['scripts']; + delete json['nx']; + // Update exports to match how the package will be structured. + const exports = json['exports']; + if (exports) { + for (const exportKey in exports) { + const exportObj = exports[exportKey]; + for (const pathKey in exportObj) { + exportObj[pathKey] = exportObj[pathKey].replace('./dist', '.'); + } + } + } + // Set "type": "commonjs", since that's what .js files in the + // package root are. This should be a no-op since that's the + // default, but by setting it explicitly we ensure that any chage to + // the repository top-level package.json to set "type": "module" + // won't break the published package accidentally. + json.type = 'commonjs'; + // Write resulting package.json file to release directory. + await writeFile( + path.join(RELEASE_DIR, 'package.json'), + JSON.stringify(json, null, 2), + ); +} + +/** + * Copy the scripts/package/README.md file into the release directory. + * This file is what developers will see at + * https://www.npmjs.com/package/blockly . + */ +async function packageReadme() { + await copyFiles({ + from: PACKAGE_SRC_DIR, + patterns: ['README.md'], + to: RELEASE_DIR, + }); +} + +/** + * Copy the generated .d.ts files in build/declarations and the + * hand-written .d.ts files in typings/ into the release directory. The + * main entrypoint file (index.d.ts) is referenced in package.json in + * the types property. + * + * Prerequisite: tsc (for the generated declarations). + */ +async function packageDTS() { + const transform = (contents) => + contents.replaceAll('AnyDuringMigration', 'any'); + // Copy the hand-written declarations first, so that any generated + // declaration with the same name takes precedence. + await copyFiles({ + from: 'typings', + patterns: ['*.d.ts', 'msg/*.d.ts'], + to: RELEASE_DIR, + transform, + }); + await copyFiles({ + from: TYPINGS_BUILD_DIR, + patterns: ['**/*.d.ts'], + ignore: ['blocks/**'], + to: RELEASE_DIR, + transform, + }); +} + +/** + * Clean the build and release directories, ready for a fresh build. + * + * TODO: Call cleanBuildDir directly once the build tasks have been + * converted to plain node scripts. + */ +async function clean() { + await Promise.all([cleanReleaseDir(), runGulpTask('cleanBuildDir')]); +} + +/** + * Prepare the files to be included in the NPM package by building + * Blockly and copying the results into the release directory. + */ +async function pack() { + await clean(); + await runGulpTask('build'); + await Promise.all([ + packageIndex(), + packageCoreNode(), + packageLegacyEntrypoints(), + packageMedia(), + packageLocales().then(packageUMDBundle), + packageJSON(), + packageReadme(), + packageDTS(), + ]); +} + +/** + * Assemble just the .d.ts files in the release directory. This is all + * that is needed in order to generate the reference documentation, and + * is much quicker than a full pack. + */ +async function typings() { + await clean(); + await runGulpTask('tsc'); + await packageDTS(); +} + +/** The commands this script accepts, as its first argument. */ +const COMMANDS = {pack, typings}; + +const USAGE = `Usage: node scripts/package.mjs [command] + +Packages Blockly for distribution on NPM. + +Commands: + pack Build Blockly and assemble the complete package (default) + typings Build and assemble only the .d.ts files + +Options: + --help Show this message`; + +const {positionals, values} = parseArgs({ + allowPositionals: true, + options: { + 'help': {type: 'boolean'}, + }, +}); + +if (values.help) { + console.log(USAGE); +} else { + const [command = 'pack'] = positionals; + try { + if (!Object.hasOwn(COMMANDS, command)) { + throw new Error(`Unknown command '${command}'.\n${USAGE}`); + } + await COMMANDS[command](); + } catch (e) { + console.error(e.message); + process.exitCode = 1; + } +} diff --git a/packages/blockly/scripts/update_github_pages.mjs b/packages/blockly/scripts/update_github_pages.mjs index 55a4e4f48e5..50e8e59edb6 100644 --- a/packages/blockly/scripts/update_github_pages.mjs +++ b/packages/blockly/scripts/update_github_pages.mjs @@ -19,8 +19,8 @@ * # build the current branch instead of syncing main */ -import {execSync} from 'node:child_process'; import {parseArgs} from 'node:util'; +import {capture, run} from './lib/exec.mjs'; const UPSTREAM_URL = 'git@github.com:RaspberryPiFoundation/blockly.git'; @@ -40,26 +40,6 @@ Options: --use-local Build and push the current branch instead of syncing main --help Show this message`; -/** - * Runs a command, forwarding its output to this process's stdio. - * - * @param {string} command The command to run. - */ -function run(command) { - execSync(command, {stdio: 'inherit'}); -} - -/** - * Runs a command and returns its trimmed stdout. - * Used when the output of a command is needed as a value. - * - * @param {string} command The command to run. - * @returns {string} The command's output. - */ -function capture(command) { - return execSync(command, {encoding: 'utf8'}).trim(); -} - /** * Resolves which remote to use for pushing gh-pages. *