diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d44aa6982..633292d69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,11 @@ on: push: branches: - 'release/[0-9]+.[0-9]+.[0-9]+' + - 'hotfix/[0-9]+.[0-9]+.[0-9]+' + - 'pre-release/[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+' + - 'pre-release/[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' + - 'pre-release/[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+' + - 'pre-release/[0-9]+.[0-9]+.[0-9]+-hotfix.[0-9]+' jobs: build: @@ -11,107 +16,225 @@ jobs: permissions: contents: write pull-requests: write + id-token: write strategy: matrix: - node-version: [18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' cache-dependency-path: './common/config/rush/pnpm-lock.yaml' + registry-url: 'https://registry.npmjs.org/' + + - name: Install native deps for node-canvas (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev pkg-config - # Install rush - name: Install rush run: node common/scripts/install-run-rush.js install --bypass-policy - - name: Parse semver version from branch name + # Release flow + - name: Parse semver version from branch name (release) + if: startsWith(github.ref_name, 'release/') id: semver_parser uses: xile611/read-package-version-action@main with: path: packages/vgrammar semver_string: ${{ github.ref_name }} - semver_pattern: '^release/(.*)$' # ^v?(.*)$ by default + semver_pattern: '^release/(.*)$' - - name: update nextBump of version policies + - name: update nextBump of version policies (release) + if: startsWith(github.ref_name, 'release/') uses: xile611/set-next-bump-of-rush@main with: release_version: ${{ steps.semver_parser.outputs.full }} write_next_bump: true - - name: Generate changelog by rush version + - name: Generate changelog by rush version (release) + if: startsWith(github.ref_name, 'release/') run: node common/scripts/install-run-rush.js version --bump - - name: Update version + - name: Update version (release) + if: startsWith(github.ref_name, 'release/') run: node common/scripts/apply-release-version.js 'none' ${{ steps.semver_parser.outputs.main }} + # Hotfix flow + - name: Parse semver (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + id: semver_hotfix + uses: xile611/read-package-version-action@main + with: + path: packages/vgrammar + semver_string: ${{ github.ref_name }} + semver_pattern: '^hotfix/(.*)$' + + - name: update nextBump (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + uses: xile611/set-next-bump-of-rush@main + with: + release_version: ${{ steps.semver_hotfix.outputs.full }} + write_next_bump: true + + - name: Generate changelog (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + run: node common/scripts/install-run-rush.js version --bump + + - name: Update version (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + run: node common/scripts/apply-release-version.js 'none' ${{ steps.semver_hotfix.outputs.main }} + + # Pre-release flow + - name: Parse semver (pre-release) + if: startsWith(github.ref_name, 'pre-release/') + id: semver_prerelease + uses: xile611/read-package-version-action@main + with: + path: packages/vgrammar + semver_string: ${{ github.ref_name }} + semver_pattern: '^pre-release/(.*)$' + + - name: Apply prereleaseName (pre-release) + if: startsWith(github.ref_name, 'pre-release/') + run: node common/scripts/apply-release-version.js ${{ steps.semver_prerelease.outputs.pre_release_name }} ${{ steps.semver_prerelease.outputs.main }} + + # Build (Shared) - name: Build packages + env: + NODE_OPTIONS: '--max_old_space_size=4096' run: node common/scripts/install-run-rush.js build --only tag:package - # - name: Run bug server - # working-directory: ./packages/vgrammar-full - # env: - # BUG_SERVER_TOKEN: ${{ secrets.BUG_SERVER_TOKEN }} - # run: node ../../common/scripts/install-run-rushx.js ci - - - name: Publish to npm + # Publish - Release + - name: Publish to npm with provenance (release) + if: startsWith(github.ref_name, 'release/') env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - NPM_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + NPM_CONFIG_PROVENANCE: true run: node common/scripts/install-run-rush.js publish --publish --include-all --tag latest + # Publish - Hotfix + - name: Publish to npm with provenance (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + env: + NPM_CONFIG_PROVENANCE: true + run: node common/scripts/install-run-rush.js publish --publish --include-all --tag hotfix + + # Publish - Pre-release + - name: Publish to npm with provenance (pre-release) + if: startsWith(github.ref_name, 'pre-release/') + env: + NPM_CONFIG_PROVENANCE: true + run: node common/scripts/install-run-rush.js publish --publish --include-all --tag ${{ steps.semver_prerelease.outputs.pre_release_type }} + - name: Update shrinkwrap run: node common/scripts/install-run-rush.js update - - name: Get npm version - id: package-version + # Git Push & Release Creation - Release + - name: Get npm version (release) + if: startsWith(github.ref_name, 'release/') + id: package_version_release uses: xile611/read-package-version-action@main with: path: packages/vgrammar - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - message: 'build: release version ${{ steps.package-version.outputs.current_version }}' - branch: ${{ github.ref_name }} + - name: Commit & Push changes (release) + if: startsWith(github.ref_name, 'release/') + run: | + git add . + git commit -m 'build: release version ${{ steps.package_version_release.outputs.current_version }}' + git push origin ${{ github.ref_name }} - - name: Collect changelog of rush + - name: Collect changelog of rush (release) + if: startsWith(github.ref_name, 'release/') uses: xile611/collect-rush-changlog@main - id: changelog + id: changelog_release with: - version: ${{ steps.package-version.outputs.current_version }} + version: ${{ steps.package_version_release.outputs.current_version }} - - name: Create Release for Tag - id: release_tag + - name: Create Release for Tag (release) + if: startsWith(github.ref_name, 'release/') + id: release_tag_release uses: ncipollo/release-action@v1.12.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag: v${{ steps.package-version.outputs.current_version }} + tag: v${{ steps.package_version_release.outputs.current_version }} commit: main prerelease: false body: | - ${{ steps.changelog.outputs.markdown }} - draft: true # + ${{ steps.changelog_release.outputs.markdown }} + draft: true - - name: Create Pull Request + - name: Create Pull Request (release) + if: startsWith(github.ref_name, 'release/') uses: dustinirving/create-pr@v1.0.2 with: token: ${{ secrets.GITHUB_TOKEN }} - title: '[Auto release] release ${{ steps.package-version.outputs.current_version }}' + title: '[Auto release] release ${{ steps.package_version_release.outputs.current_version }}' base: main head: ${{ github.ref_name }} - labels: release # default labels, the action will throw error if not specified - reviewers: xile611 # default reviewers, the action will throw error if not specified + labels: release + reviewers: xile611 body: | - ${{ steps.changelog.outputs.markdown }} + ${{ steps.changelog_release.outputs.markdown }} + + # Git Push & Release Creation - Hotfix + - name: Get npm version (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + id: package_version_hotfix + uses: xile611/read-package-version-action@main + with: + path: packages/vgrammar + + - name: Commit & Push changes (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + run: | + git add . + git commit -m 'build: prelease version ${{ steps.package_version_hotfix.outputs.current_version }}' + git push origin ${{ github.ref_name }} + + - name: Collect changelog of rush (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + uses: xile611/collect-rush-changlog@main + id: changelog_hotfix + with: + version: ${{ steps.package_version_hotfix.outputs.current_version }} + + - name: Create Release for Tag (hotfix) + if: startsWith(github.ref_name, 'hotfix/') + id: release_tag_hotfix + uses: ncipollo/release-action@v1.12.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag: v${{ steps.package_version_hotfix.outputs.current_version }} + commit: ${{ github.ref_name }} + prerelease: false + body: | + ${{ steps.changelog_hotfix.outputs.markdown }} + draft: true + + # Git Push - Pre-release + - name: Get npm version (pre-release) + if: startsWith(github.ref_name, 'pre-release/') + id: package_version_prerelease + uses: xile611/read-package-version-action@main + with: + path: packages/vgrammar + + - name: Commit & Push changes (pre-release) + if: startsWith(github.ref_name, 'pre-release/') + run: | + git add . + git commit -m 'build: prerelease version ${{ steps.package_version_prerelease.outputs.current_version }}' + git push origin ${{ github.ref_name }} diff --git a/common/git-hooks/commit-msg b/common/git-hooks/commit-msg old mode 100644 new mode 100755 diff --git a/common/git-hooks/pre-commit b/common/git-hooks/pre-commit old mode 100644 new mode 100755 diff --git a/common/git-hooks/pre-push b/common/git-hooks/pre-push old mode 100644 new mode 100755 diff --git a/common/scripts/install-run-rush-pnpm.js b/common/scripts/install-run-rush-pnpm.js index 5c149955d..2356649f4 100644 --- a/common/scripts/install-run-rush-pnpm.js +++ b/common/scripts/install-run-rush-pnpm.js @@ -10,6 +10,9 @@ // node common/scripts/install-run-rush-pnpm.js pnpm-command // // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. /******/ (() => { // webpackBootstrap /******/ "use strict"; @@ -19,7 +22,7 @@ var __webpack_exports__ = {}; \*****************************************************/ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for license information. +// See LICENSE in the project root for license information. require('./install-run-rush'); //# sourceMappingURL=install-run-rush-pnpm.js.map module.exports = __webpack_exports__; diff --git a/common/scripts/install-run-rush.js b/common/scripts/install-run-rush.js index cada1eded..9676fc718 100644 --- a/common/scripts/install-run-rush.js +++ b/common/scripts/install-run-rush.js @@ -8,6 +8,9 @@ // node common/scripts/install-run-rush.js install // // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. /******/ (() => { // webpackBootstrap /******/ "use strict"; @@ -113,7 +116,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for license information. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); @@ -136,8 +140,8 @@ function _getRushVersion(logger) { return rushJsonMatches[1]; } catch (e) { - throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` + - "The 'rushVersion' field is either not assigned in rush.json or was specified " + + throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + + `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + 'using an unexpected syntax.'); } } @@ -196,7 +200,7 @@ function _run() { } runWithErrorAndStatusCode(logger, () => { const version = _getRushVersion(logger); - logger.info(`The rush.json configuration requests Rush version ${version}`); + logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; if (lockFilePath) { logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); diff --git a/common/scripts/install-run-rushx.js b/common/scripts/install-run-rushx.js index b05df262b..6581521f3 100644 --- a/common/scripts/install-run-rushx.js +++ b/common/scripts/install-run-rushx.js @@ -10,6 +10,9 @@ // node common/scripts/install-run-rushx.js custom-command // // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. /******/ (() => { // webpackBootstrap /******/ "use strict"; @@ -19,7 +22,7 @@ var __webpack_exports__ = {}; \*************************************************/ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for license information. +// See LICENSE in the project root for license information. require('./install-run-rush'); //# sourceMappingURL=install-run-rushx.js.map module.exports = __webpack_exports__; diff --git a/common/scripts/install-run.js b/common/scripts/install-run.js index 68b1b56fc..9283c4452 100644 --- a/common/scripts/install-run.js +++ b/common/scripts/install-run.js @@ -8,6 +8,9 @@ // node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io // // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +// +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for details. /******/ (() => { // webpackBootstrap /******/ "use strict"; @@ -21,6 +24,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "isVariableSetInNpmrcFile": () => (/* binding */ isVariableSetInNpmrcFile), /* harmony export */ "syncNpmrc": () => (/* binding */ syncNpmrc) /* harmony export */ }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 657147); @@ -33,23 +37,30 @@ __webpack_require__.r(__webpack_exports__); /** - * As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims + * This function reads the content for given .npmrc file path, and also trims * unusable lines from the .npmrc file. * - * Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in - * the .npmrc file to provide different authentication tokens for different registry. - * However, if the environment variable is undefined, it expands to an empty string, which - * produces a valid-looking mapping with an invalid URL that causes an error. Instead, - * we'd prefer to skip that line and continue looking in other places such as the user's - * home directory. - * * @returns * The text of the the .npmrc. */ -function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) { - logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose - logger.info(` --> "${targetNpmrcPath}"`); - let npmrcFileLines = fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n'); +// create a global _combinedNpmrc for cache purpose +const _combinedNpmrcMap = new Map(); +function _trimNpmrcFile(options) { + const { sourceNpmrcPath, linesToPrepend, linesToAppend } = options; + const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); + if (combinedNpmrcFromCache !== undefined) { + return combinedNpmrcFromCache; + } + let npmrcFileLines = []; + if (linesToPrepend) { + npmrcFileLines.push(...linesToPrepend); + } + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); + } + if (linesToAppend) { + npmrcFileLines.push(...linesToAppend); + } npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); const resultLines = []; // This finds environment variable tokens that look like "${VAR_NAME}" @@ -57,8 +68,13 @@ function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) { // Comment lines start with "#" or ";" const commentRegExp = /^\s*[#;]/; // Trim out lines that reference environment variables that aren't defined - for (const line of npmrcFileLines) { + for (let line of npmrcFileLines) { let lineShouldBeTrimmed = false; + //remove spaces before or after key and value + line = line + .split('=') + .map((lineToTrim) => lineToTrim.trim()) + .join('='); // Ignore comment lines if (!commentRegExp.test(line)) { const environmentVariables = line.match(expansionRegExp); @@ -85,27 +101,44 @@ function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) { } } const combinedNpmrc = resultLines.join('\n'); + //save the cache + _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); + return combinedNpmrc; +} +function _copyAndTrimNpmrcFile(options) { + const { logger, sourceNpmrcPath, targetNpmrcPath, linesToPrepend, linesToAppend } = options; + logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose + logger.info(` --> "${targetNpmrcPath}"`); + const combinedNpmrc = _trimNpmrcFile({ + sourceNpmrcPath, + linesToPrepend, + linesToAppend + }); fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); return combinedNpmrc; } -/** - * syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file. - * If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder. - * - * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc() - * - * @returns - * The text of the the synced .npmrc, if one exists. If one does not exist, then undefined is returned. - */ -function syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { - info: console.log, - error: console.error -}) { +function syncNpmrc(options) { + const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { + // eslint-disable-next-line no-console + info: console.log, + // eslint-disable-next-line no-console + error: console.error + }, createIfMissing = false, linesToAppend, linesToPrepend } = options; const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); try { - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - return _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath); + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { + // Ensure the target folder exists + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { + fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); + } + return _copyAndTrimNpmrcFile({ + sourceNpmrcPath, + targetNpmrcPath, + logger, + linesToAppend, + linesToPrepend + }); } else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target @@ -117,6 +150,16 @@ function syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger throw new Error(`Error syncing .npmrc file: ${e}`); } } +function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey) { + const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; + //if .npmrc file does not exist, return false directly + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + return false; + } + const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath }); + const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); + return trimmedNpmrcFile.match(variableKeyRegExp) !== null; +} //# sourceMappingURL=npmrcUtilities.js.map /***/ }), @@ -253,7 +296,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 679877); // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See the @microsoft/rush package's LICENSE file for license information. +// See LICENSE in the project root for license information. +/* eslint-disable no-console */ @@ -297,7 +341,7 @@ let _npmPath = undefined; function getNpmPath() { if (!_npmPath) { try { - if (os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32') { + if (_isWindows()) { // We're on Windows const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); @@ -359,6 +403,23 @@ function _getRushTempFolder(rushCommonFolder) { return _ensureAndJoinPath(rushCommonFolder, 'temp'); } } +/** + * Compare version strings according to semantic versioning. + * Returns a positive integer if "a" is a later version than "b", + * a negative integer if "b" is later than "a", + * and 0 otherwise. + */ +function _compareVersionStrings(a, b) { + const aParts = a.split(/[.-]/); + const bParts = b.split(/[.-]/); + const numberOfParts = Math.max(aParts.length, bParts.length); + for (let i = 0; i < numberOfParts; i++) { + if (aParts[i] !== bParts[i]) { + return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); + } + } + return 0; +} /** * Resolve a package specifier to a static version */ @@ -376,32 +437,55 @@ function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { try { const rushTempFolder = _getRushTempFolder(rushCommonFolder); const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)(sourceNpmrcFolder, rushTempFolder, undefined, logger); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: rushTempFolder, + logger + }); const npmPath = getNpmPath(); // This returns something that looks like: - // @microsoft/rush@3.0.0 '3.0.0' - // @microsoft/rush@3.0.1 '3.0.1' - // ... - // @microsoft/rush@3.0.20 '3.0.20' - // - const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], { + // ``` + // [ + // "3.0.0", + // "3.0.1", + // ... + // "3.0.20" + // ] + // ``` + // + // if multiple versions match the selector, or + // + // ``` + // "3.0.0" + // ``` + // + // if only a single version matches. + const spawnSyncOptions = { cwd: rushTempFolder, - stdio: [] - }); + stdio: [], + shell: _isWindows() + }; + const platformNpmPath = _getPlatformPath(npmPath); + const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); if (npmVersionSpawnResult.status !== 0) { throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); } const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); - const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line); - const latestVersion = versionLines[versionLines.length - 1]; + const parsedVersionOutput = JSON.parse(npmViewVersionOutput); + const versions = Array.isArray(parsedVersionOutput) + ? parsedVersionOutput + : [parsedVersionOutput]; + let latestVersion = versions[0]; + for (let i = 1; i < versions.length; i++) { + const latestVersionCandidate = versions[i]; + if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { + latestVersion = latestVersionCandidate; + } + } if (!latestVersion) { throw new Error('No versions found for the specified version range.'); } - const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/); - if (!versionMatches) { - throw new Error(`Invalid npm output ${latestVersion}`); - } - return versionMatches[1]; + return latestVersion; } catch (e) { throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); @@ -427,7 +511,7 @@ function findRushJsonFolder() { } } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root if (!_rushJsonFolder) { - throw new Error('Unable to find rush.json.'); + throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); } } return _rushJsonFolder; @@ -515,10 +599,12 @@ function _installPackage(logger, packageInstallFolder, name, version, command) { try { logger.info(`Installing ${name}...`); const npmPath = getNpmPath(); - const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(npmPath, [command], { + const platformNpmPath = _getPlatformPath(npmPath); + const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { stdio: 'inherit', cwd: packageInstallFolder, - env: process.env + env: process.env, + shell: _isWindows() }); if (result.status !== 0) { throw new Error(`"npm ${command}" encountered an error`); @@ -534,9 +620,18 @@ function _installPackage(logger, packageInstallFolder, name, version, command) { */ function _getBinPath(packageInstallFolder, binName) { const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - const resolvedBinName = os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32' ? `${binName}.cmd` : binName; + const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); } +/** + * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. + */ +function _getPlatformPath(platformPath) { + return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; +} +function _isWindows() { + return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; +} /** * Write a flag file to the package's install directory, signifying that the install was successful. */ @@ -558,7 +653,11 @@ function installAndRun(logger, packageName, packageVersion, packageBinName, pack // The package isn't already installed _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)(sourceNpmrcFolder, packageInstallFolder, undefined, logger); + (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: packageInstallFolder, + logger + }); _createPackageJson(packageInstallFolder, packageName, packageVersion); const command = lockFilePath ? 'ci' : 'install'; _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); @@ -574,15 +673,14 @@ function installAndRun(logger, packageName, packageVersion, packageBinName, pack const originalEnvPath = process.env.PATH || ''; let result; try { - // Node.js on Windows can not spawn a file when the path has a space on it - // unless the path gets wrapped in a cmd friendly way and shell mode is used - const shouldUseShell = binPath.includes(' ') && os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; - const platformBinPath = shouldUseShell ? `"${binPath}"` : binPath; + // `npm` bin stubs on Windows are `.cmd` files + // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` + const platformBinPath = _getPlatformPath(binPath); process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { stdio: 'inherit', windowsVerbatimArguments: false, - shell: shouldUseShell, + shell: _isWindows(), cwd: process.cwd(), env: process.env }); diff --git a/packages/vgrammar-wordcloud-shape/src/filling.ts b/packages/vgrammar-wordcloud-shape/src/filling.ts index 8eff08f1f..ffec0acaf 100644 --- a/packages/vgrammar-wordcloud-shape/src/filling.ts +++ b/packages/vgrammar-wordcloud-shape/src/filling.ts @@ -81,7 +81,7 @@ export function filling( for (let y = startY; y <= y2; y += fillingYStep) { for (let x = startX; x <= x2; x += fillingXStep) { // 测量填充词的 bounds - measureSprite(canvas, ctx, fillingWords, wi); + measureSprite(canvas, ctx, fillingWords, wi, layoutConfig.measureCache); const word = fillingWords[wi]; word.x = x; word.y = y; diff --git a/packages/vgrammar-wordcloud-shape/src/interface.ts b/packages/vgrammar-wordcloud-shape/src/interface.ts index cbd3bddcd..4068fd320 100644 --- a/packages/vgrammar-wordcloud-shape/src/interface.ts +++ b/packages/vgrammar-wordcloud-shape/src/interface.ts @@ -69,6 +69,25 @@ export interface SegmentationOutputType extends SegmentationInputType { fillingInitialFontSize?: number; fillingDeltaFontSize?: number; } + +export type CachedWordMeasure = { + sprite: number[]; + bounds: { + dTop: number; + dBottom: number; + dLeft: number; + dRight: number; + }; + wordSize: [number, number]; +}; + +export interface IWordMeasureCache { + get: (key: string) => CachedWordMeasure | undefined; + set: (key: string, value: CachedWordMeasure) => void; + clear: () => void; + size?: () => number; +} + export type wordsConfigType = { getText: TagItemFunction; getFontSize?: TagItemFunction; @@ -132,6 +151,8 @@ export type LayoutConfigType = { minInitFontSize: number; minFontSize: number; minFillFontSize: number; + + measureCache?: IWordMeasureCache; }; export type CloudWordType = { x: number; @@ -233,5 +254,7 @@ export interface WordCloudShapeOptions { // 填充词词最小布局字号 minFillFontSize?: number; + measureCache?: IWordMeasureCache; + onUpdateMaskCanvas?: (canvas?: HTMLCanvasElement) => void; } diff --git a/packages/vgrammar-wordcloud-shape/src/layout.ts b/packages/vgrammar-wordcloud-shape/src/layout.ts index 5c843e99a..8f43e51f4 100644 --- a/packages/vgrammar-wordcloud-shape/src/layout.ts +++ b/packages/vgrammar-wordcloud-shape/src/layout.ts @@ -10,7 +10,15 @@ import type { wordsConfigType } from './interface'; import { removeBorder, scaleAndMiddleShape, segmentation } from './segmentation'; -import { WORDCLOUD_SHAPE_HOOK_EVENT, calTextLength, colorListEqual, fakeRandom, functor, loadImage } from './util'; +import { + MapWordMeasureCache, + WORDCLOUD_SHAPE_HOOK_EVENT, + calTextLength, + colorListEqual, + fakeRandom, + functor, + loadImage +} from './util'; import { LinearScale, OrdinalScale, SqrtScale } from '@visactor/vscale'; import cloud from './cloud-shape-layout'; import { type IProgressiveTransformResult, type IView } from '@visactor/vgrammar-core'; @@ -205,6 +213,8 @@ export class Layout implements IProgressiveTransformResult { size: options.size, ratio: options.ratio || 0.8, + measureCache: options.measureCache ?? new MapWordMeasureCache(), + // layout 相关 shapeUrl: options.shape, random: typeof options.random === 'undefined' ? true : options.random, diff --git a/packages/vgrammar-wordcloud-shape/src/util.ts b/packages/vgrammar-wordcloud-shape/src/util.ts index 972b81a91..8cd9eac9f 100644 --- a/packages/vgrammar-wordcloud-shape/src/util.ts +++ b/packages/vgrammar-wordcloud-shape/src/util.ts @@ -1,6 +1,12 @@ import { vglobal, createImage } from '@visactor/vrender-core'; import { isBase64, isNil, isValidUrl, Logger } from '@visactor/vutils'; -import type { CloudWordType, LayoutConfigType, SegmentationOutputType } from './interface'; +import type { + CachedWordMeasure, + CloudWordType, + IWordMeasureCache, + LayoutConfigType, + SegmentationOutputType +} from './interface'; export enum WORDCLOUD_SHAPE_HOOK_EVENT { BEFORE_WORDCLOUD_SHAPE_LAYOUT = 'beforeWordcloudShapeLayout', @@ -9,6 +15,53 @@ export enum WORDCLOUD_SHAPE_HOOK_EVENT { AFTER_WORDCLOUD_SHAPE_DRAW = 'afterWordcloudShapeDraw' } +export class MapWordMeasureCache implements IWordMeasureCache { + private _map: Map; + private _maxSize: number; + + constructor(maxSize: number = 1000) { + this._map = new Map(); + this._maxSize = maxSize; + } + + get(key: string): CachedWordMeasure | undefined { + const value = this._map.get(key); + if (value === undefined) { + return undefined; + } + + // 简单 LRU:命中时更新插入顺序 + this._map.delete(key); + this._map.set(key, value); + + return value; + } + + set(key: string, value: CachedWordMeasure): void { + if (this._map.has(key)) { + this._map.set(key, value); + return; + } + + if (this._map.size >= this._maxSize) { + const firstKey = this._map.keys().next().value as string | undefined; + if (firstKey !== undefined) { + this._map.delete(firstKey); + } + } + + this._map.set(key, value); + } + + clear(): void { + this._map.clear(); + } + + size(): number { + return this._map.size; + } +} + export const colorListEqual = (arr0: string[], arr1: string[]) => { if (arr1.length === 1 && arr1[0] === '#537EF5') { // 填充词默认值认为与核心词一致 diff --git a/packages/vgrammar-wordcloud-shape/src/wordle.ts b/packages/vgrammar-wordcloud-shape/src/wordle.ts index 9f6ab7cb0..239353082 100644 --- a/packages/vgrammar-wordcloud-shape/src/wordle.ts +++ b/packages/vgrammar-wordcloud-shape/src/wordle.ts @@ -1,4 +1,4 @@ -import type { CloudWordType, LayoutConfigType, SegmentationOutputType } from './interface'; +import type { CloudWordType, IWordMeasureCache, LayoutConfigType, SegmentationOutputType } from './interface'; export function layout( words: CloudWordType[], @@ -23,7 +23,7 @@ export function layout( for (let i = 0; i < regionWords.length; i++) { // 批量测量单词的 bounds - measureSprite(canvas, ctx, words, i); + measureSprite(canvas, ctx, words, i, layoutConfig.measureCache); const word = regionWords[i]; word.x = center[0]; word.y = center[1]; @@ -53,7 +53,7 @@ export function layout( for (let i = 0; i < failedWords.length; i++) { const word = failedWords[i]; - measureSprite(canvas, ctx, failedWords, i); + measureSprite(canvas, ctx, failedWords, i, layoutConfig.measureCache); word.x = shapeCenter[0]; word.y = shapeCenter[1]; if (word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor)) { @@ -86,7 +86,7 @@ export function layoutSelfShrink( for (let i = 0; i < regionWords.length; i++) { // 批量测量单词的 bounds - measureSprite(canvas, ctx, words, i); + measureSprite(canvas, ctx, words, i, layoutConfig.measureCache); const word = regionWords[i]; word.x = center[0]; word.y = center[1]; @@ -181,7 +181,7 @@ export function layoutGlobalShrink( let restartTag = false; for (let i = 0; i < regionWords.length; i++) { // 批量测量单词的 bounds - measureSprite(canvas, ctx, words, i); + measureSprite(canvas, ctx, words, i, layoutConfig.measureCache); const word = regionWords[i]; word.x = center[0]; word.y = center[1]; @@ -252,7 +252,7 @@ export function layoutGlobalShrink( for (let i = 0; i < failedWords.length; i++) { const word = failedWords[i]; - measureSprite(canvas, ctx, failedWords, i); + measureSprite(canvas, ctx, failedWords, i, layoutConfig.measureCache); word.x = shapeCenter[0]; word.y = shapeCenter[1]; if (word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor)) { @@ -309,7 +309,7 @@ export function layoutSelfEnlarge( let restartTag = false; for (let i = 0; i < regionWords.length; i++) { // 批量测量单词的 bounds - measureSprite(canvas, ctx, words, i); + measureSprite(canvas, ctx, words, i, layoutConfig.measureCache); const word = regionWords[i]; word.x = center[0]; word.y = center[1]; @@ -390,7 +390,7 @@ export function layoutSelfEnlarge( for (let i = 0; i < failedWords.length; i++) { const word = failedWords[i]; - measureSprite(canvas, ctx, failedWords, i); + measureSprite(canvas, ctx, failedWords, i, layoutConfig.measureCache); word.x = shapeCenter[0]; word.y = shapeCenter[1]; if (word.hasText && place(board, word, shapeMaxR, shapeRatio, size, boardSize, stepFactor)) { @@ -547,12 +547,25 @@ export function measureSprite( canvas: HTMLCanvasElement | any, ctx: CanvasRenderingContext2D | null, words: CloudWordType[] | any, - wi: number + wi: number, + cache?: IWordMeasureCache ) { - if (words[wi].sprite || words[wi].fontSize === 0) { + const targetWord = words[wi]; + if (targetWord.sprite || targetWord.fontSize === 0) { return; } + if (cache) { + const cached = cache.get(getWordMeasureKey(targetWord)); + if (cached) { + targetWord.sprite = cached.sprite; + targetWord.bounds = cached.bounds; + targetWord.wordSize = cached.wordSize; + targetWord.hasText = true; + return; + } + } + const cw = 2048; const ch = 2048; const radians = Math.PI / 180; @@ -709,6 +722,16 @@ export function measureSprite( dRight: dRight - (wordSize[0] >> 1) }; word.sprite = sprite; + + if (cache) { + const key = getWordMeasureKey(word); + cache.set(key, { + sprite, + bounds: word.bounds, + wordSize + }); + } + // 后续操作中 LT 无意义 delete word.LT; } @@ -743,6 +766,24 @@ export function measureSprite( // document.body.prepend(canvas) } +function getWordMeasureKey(word: CloudWordType | any) { + return ( + String(word.text) + + '|' + + String(word.fontFamily) + + '|' + + String(word.fontStyle) + + '|' + + String(word.fontWeight) + + '|' + + String(word.fontSize) + + '|' + + String(word.rotate) + + '|' + + String(word.padding) + ); +} + /** * 根据 shape 相关的信息初始化 board */ diff --git a/rush.json b/rush.json index 367c1d0eb..956e84cff 100644 --- a/rush.json +++ b/rush.json @@ -1,8 +1,8 @@ { "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", - "rushVersion": "5.94.1", - "pnpmVersion": "7.32.1", - "nodeSupportedVersionRange": ">=14.15.0 <15.0.0 || >=16.13.0 <17.0.0 || >=18.15.0 <19.0.0", + "rushVersion": "5.133.1", + "pnpmVersion": "9.12.3", + "nodeSupportedVersionRange": ">=18.15.0 <19.0.0 || >=20.0.0 <21.0.0", "suppressNodeLtsWarning": true, "ensureConsistentVersions": true, "projectFolderMinDepth": 2,