From e38fb1c629db91305ff403bf4b49c33471df9c25 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 1 Jun 2026 18:17:56 +0400 Subject: [PATCH 1/5] Speed up generated JS demos check --- .github/workflows/visual-tests-demos.yml | 91 ++++++++++-------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index cdc7a9025584..933bdaed1996 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -416,13 +416,12 @@ jobs: name: Check generated demos (changed only) runs-on: devextreme-shr2 timeout-minutes: 10 - needs: [check-should-run, get-changes, build-devextreme, determine-framework-tests-scope] + needs: [check-should-run, get-changes, determine-framework-tests-scope] if: | always() && needs.check-should-run.outputs.should-run == 'true' && needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' && - needs.build-devextreme.result == 'success' + needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' steps: - name: Get sources @@ -435,26 +434,52 @@ jobs: path: apps/demos continue-on-error: true + - name: Detect changed React TS demos + id: changed-react-demos + working-directory: apps/demos + run: | + if [ ! -f "changed-files.json" ]; then + echo "changed-files.json not found, skipping generated JS demos check" + echo "has-react-demos=false" >> $GITHUB_OUTPUT + exit 0 + fi + + jq -r '.[].filename' changed-files.json \ + | grep '/React/' \ + | grep -E '\.tsx?$' \ + | sed 's|^apps/demos/||' \ + | sed -E 's|/[^/]*\.tsx?$||' \ + | sort \ + | uniq > changed-react-demos.txt || true + + if [ -s changed-react-demos.txt ]; then + echo "Changed React demos:" + cat changed-react-demos.txt + echo "has-react-demos=true" >> $GITHUB_OUTPUT + else + echo "No React demos found in changed files, skipping conversion" + echo "has-react-demos=false" >> $GITHUB_OUTPUT + fi + - uses: pnpm/action-setup@v6 + if: steps.changed-react-demos.outputs.has-react-demos == 'true' with: run_install: false - name: Use Node.js + if: steps.changed-react-demos.outputs.has-react-demos == 'true' uses: actions/setup-node@v6 with: node-version-file: '.node-version' - - name: Download devextreme sources - uses: actions/download-artifact@v8 - with: - name: devextreme-sources - - name: Get pnpm store directory + if: steps.changed-react-demos.outputs.has-react-demos == 'true' shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - uses: actions/cache/restore@v5 + if: steps.changed-react-demos.outputs.has-react-demos == 'true' name: Restore pnpm cache with: path: ${{ env.STORE_PATH }} @@ -463,38 +488,15 @@ jobs: ${{ runner.os }}-pnpm-cache - name: Install dependencies + if: steps.changed-react-demos.outputs.has-react-demos == 'true' run: pnpm install --frozen-lockfile - - name: Install tgz - working-directory: apps/demos - run: pnpm add ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz - - - name: Prepare JS - working-directory: apps/demos - run: pnpm run prepare-js - - name: Check generated JS demos + if: steps.changed-react-demos.outputs.has-react-demos == 'true' working-directory: apps/demos run: | - if [ -f "changed-files.json" ]; then - echo "Running convert-to-js on changed files only" - - CHANGED_DEMOS=$(jq -r '.[].filename' changed-files.json | grep '/React/' | grep '\.tsx$' | sed 's|^apps/demos/||' | sed 's|/[^/]*\.tsx$||' | sort | uniq) - - if [ -z "$CHANGED_DEMOS" ]; then - echo "No React demos found in changed files, skipping conversion" - else - echo "Changed React demos:" - echo "$CHANGED_DEMOS" - - echo "$CHANGED_DEMOS" | while read -r demo_dir; do - if [ ! -z "$demo_dir" ]; then - echo "Converting: $demo_dir" - pnpm run convert-to-js "$demo_dir" - fi - done - fi - fi + echo "Running convert-to-js on changed files only" + xargs pnpm run convert-to-js < changed-react-demos.txt git add ./Demos -N @@ -510,13 +512,12 @@ jobs: name: Check generated demos (${{ matrix.CONSTEL }}) runs-on: devextreme-shr2 timeout-minutes: 15 - needs: [check-should-run, build-devextreme, determine-framework-tests-scope] + needs: [check-should-run, determine-framework-tests-scope] if: | always() && needs.check-should-run.outputs.should-run == 'true' && needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' && - needs.build-devextreme.result == 'success' + needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' strategy: fail-fast: false matrix: @@ -535,11 +536,6 @@ jobs: with: node-version-file: '.node-version' - - name: Download devextreme sources - uses: actions/download-artifact@v8 - with: - name: devextreme-sources - - name: Get pnpm store directory shell: bash run: | @@ -556,14 +552,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Install tgz - working-directory: apps/demos - run: pnpm add ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz - - - name: Prepare JS - working-directory: apps/demos - run: pnpm run prepare-js - - name: Check generated JS demos working-directory: apps/demos env: @@ -1300,4 +1288,3 @@ jobs: name: csp-violations-report path: apps/demos/csp-reports/ if-no-files-found: ignore - From 717152d36f1d606ce3b5e458ffc7f89318df9f6a Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 1 Jun 2026 18:26:14 +0400 Subject: [PATCH 2/5] check lint at once --- .github/workflows/visual-tests-demos.yml | 39 ++++++++- apps/demos/utils/ts-to-js-converter/cli.ts | 38 +++++--- .../utils/ts-to-js-converter/converter.ts | 86 ++++++++++++++----- apps/demos/utils/ts-to-js-converter/types.ts | 4 + 4 files changed, 129 insertions(+), 38 deletions(-) diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index 933bdaed1996..103e1f584b64 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -416,12 +416,13 @@ jobs: name: Check generated demos (changed only) runs-on: devextreme-shr2 timeout-minutes: 10 - needs: [check-should-run, get-changes, determine-framework-tests-scope] + needs: [check-should-run, get-changes, build-devextreme, determine-framework-tests-scope] if: | always() && needs.check-should-run.outputs.should-run == 'true' && needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' + needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' && + needs.build-devextreme.result == 'success' steps: - name: Get sources @@ -472,6 +473,12 @@ jobs: with: node-version-file: '.node-version' + - name: Download devextreme sources + if: steps.changed-react-demos.outputs.has-react-demos == 'true' + uses: actions/download-artifact@v8 + with: + name: devextreme-sources + - name: Get pnpm store directory if: steps.changed-react-demos.outputs.has-react-demos == 'true' shell: bash @@ -491,6 +498,16 @@ jobs: if: steps.changed-react-demos.outputs.has-react-demos == 'true' run: pnpm install --frozen-lockfile + - name: Install tgz + if: steps.changed-react-demos.outputs.has-react-demos == 'true' + working-directory: apps/demos + run: pnpm add ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz + + - name: Prepare JS + if: steps.changed-react-demos.outputs.has-react-demos == 'true' + working-directory: apps/demos + run: pnpm run prepare-js + - name: Check generated JS demos if: steps.changed-react-demos.outputs.has-react-demos == 'true' working-directory: apps/demos @@ -512,12 +529,13 @@ jobs: name: Check generated demos (${{ matrix.CONSTEL }}) runs-on: devextreme-shr2 timeout-minutes: 15 - needs: [check-should-run, determine-framework-tests-scope] + needs: [check-should-run, build-devextreme, determine-framework-tests-scope] if: | always() && needs.check-should-run.outputs.should-run == 'true' && needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' + needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' && + needs.build-devextreme.result == 'success' strategy: fail-fast: false matrix: @@ -536,6 +554,11 @@ jobs: with: node-version-file: '.node-version' + - name: Download devextreme sources + uses: actions/download-artifact@v8 + with: + name: devextreme-sources + - name: Get pnpm store directory shell: bash run: | @@ -552,6 +575,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Install tgz + working-directory: apps/demos + run: pnpm add ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz + + - name: Prepare JS + working-directory: apps/demos + run: pnpm run prepare-js + - name: Check generated JS demos working-directory: apps/demos env: diff --git a/apps/demos/utils/ts-to-js-converter/cli.ts b/apps/demos/utils/ts-to-js-converter/cli.ts index a15262238887..62fd8753b76d 100644 --- a/apps/demos/utils/ts-to-js-converter/cli.ts +++ b/apps/demos/utils/ts-to-js-converter/cli.ts @@ -5,7 +5,7 @@ import { glob } from 'glob'; import { consola } from 'consola'; import fs from 'fs'; -import { converter } from './converter'; +import { converter, prettifyOutputs } from './converter'; function findFoldersWithTsxFiles(directory) { const foldersWithTsxFiles = []; @@ -81,19 +81,29 @@ const performConversion = async (patterns) => { // @ts-ignore )).flat(1); - await Promise.all( - entries.map(async ({ source, out }) => { - logger.start(`converting ${source}`); - await converter(source, out, logger); - logger.success(`${source} complete`); - }), - ) - // eslint-disable-next-line no-void - .then(void 0) - .catch((error) => { - logger.error(error); - process.exit(1); - }); + let convertedOutDirs: string[]; + + try { + const outDirs = await Promise.all( + entries.map(async ({ source, out }) => { + logger.start(`converting ${source}`); + const converted = await converter(source, out, logger, { prettify: false }); + if (converted) { + logger.success(`${source} complete`); + } + return converted ? out : null; + }), + ); + + convertedOutDirs = outDirs.filter( + (outDir): outDir is string => outDir != null, + ); + } catch (error) { + logger.error(error); + process.exit(1); + } + + await prettifyOutputs(convertedOutDirs, logger); }; function splitArrayIntoSubarrays(array, subarrayLength) { diff --git a/apps/demos/utils/ts-to-js-converter/converter.ts b/apps/demos/utils/ts-to-js-converter/converter.ts index cd7319aacb69..baf86f95e314 100644 --- a/apps/demos/utils/ts-to-js-converter/converter.ts +++ b/apps/demos/utils/ts-to-js-converter/converter.ts @@ -9,7 +9,12 @@ import { promisify } from 'util'; import _ from 'lodash'; import os from 'os'; -import { Logger, PathResolver, PathResolvers } from './types'; +import { + ConverterOptions, + Logger, + PathResolver, + PathResolvers, +} from './types'; let platformGlob = glob; const makePathArrayPosix = (pathArray) => pathArray.map( @@ -34,6 +39,8 @@ const bundleAssets = [ const redundantAssets = ['*tsconfig*', 'types.js']; +const quoteShellArg = (value: string) => `"${value.replace(/(["\\$`])/g, '\\$1')}"`; + const makeConfig = ( resolve: PathResolvers, include: string[], @@ -80,12 +87,28 @@ const pipeSource = async ( const execTsc = async (directory: string, args: string[]): Promise => { const tscScript = require.resolve('typescript/bin/tsc'); - const { stdout } = await promisify(cps.execFile)( - process.execPath, - [tscScript, ...args], - { cwd: directory }, - ); - return stdout; + try { + const { stdout } = await promisify(cps.execFile)( + process.execPath, + [tscScript, ...args], + { cwd: directory }, + ); + return stdout; + } catch (error) { + const { stdout, stderr } = error as { stdout?: string; stderr?: string }; + + if (stdout) { + // eslint-disable-next-line no-console + console.error(stdout); + } + + if (stderr) { + // eslint-disable-next-line no-console + console.error(stderr); + } + + throw error; + } }; const compile = async (resolve: PathResolvers, log: Logger) => { @@ -182,14 +205,36 @@ const patchImports = async (resolve: PathResolvers, log: Logger) => { log.debug('imports patching done'); }; -const prettify = async (resolve: PathResolvers, log: Logger) => { +export const prettifyOutputs = async ( + outDirs: string[], + log: Logger, + cwd = process.cwd(), +) => { + const uniqueOutDirs = [...new Set(outDirs)]; + + if (uniqueOutDirs.length === 0) { + return; + } + log.debug('running Prettier'); - await exec(`prettier --write "${resolve.out('')}${path.sep}!(*.{css,json,md,tsbuildinfo})" --single-attribute-per-line --print-width 100`, { - cwd: resolve.out(''), - }); - await exec(`eslint --fix "${resolve.out('')}" --ignore-pattern "config.js" --ignore-pattern "*.tsbuildinfo"`, { - cwd: resolve.out(''), - }); + + const prettierPatterns = uniqueOutDirs + .map((outDir) => quoteShellArg( + `${path.resolve(outDir)}${path.sep}!(*.{css,json,md,tsbuildinfo})`, + )) + .join(' '); + const eslintPatterns = uniqueOutDirs + .map((outDir) => quoteShellArg(path.resolve(outDir))) + .join(' '); + + await exec( + `prettier --write ${prettierPatterns} --single-attribute-per-line --print-width 100`, + { cwd }, + ); + await exec( + `eslint --fix ${eslintPatterns} --ignore-pattern "**/config.js" --ignore-pattern "*.tsbuildinfo"`, + { cwd }, + ); }; const hasTypescriptFiles = async (resolve: PathResolver) => { @@ -203,7 +248,8 @@ export const converter = async ( sourceDir: string, outDir: string, log: Logger, -): Promise => { + options: ConverterOptions = {}, +): Promise => { log.debug('TS to JS example converter starting'); log.debug(`sourceDir: ${sourceDir}`); log.debug(`outDir: ${outDir}`); @@ -218,7 +264,7 @@ export const converter = async ( if (!await hasTypescriptFiles(sourceDirResolver)) { log.info(`No TypeScript files found in ${sourceDir}. Skipping...`); - return; + return false; } log.debug(`touching ${outDir}`); @@ -238,11 +284,11 @@ export const converter = async ( await compile(resolve, log); await copyAssets(resolve, log); await patchImports(resolve, log); - await prettify(resolve, log); + if (options.prettify !== false) { + await prettifyOutputs([outDir], log, outDir); + } await strip(resolve, log); - } catch (error) { - log.error(error); - return; + return true; } finally { log.debug(`removing temp directory: ${tempDir}`); await remove(tempDir); diff --git a/apps/demos/utils/ts-to-js-converter/types.ts b/apps/demos/utils/ts-to-js-converter/types.ts index 4f06614729ad..6e6573369c97 100644 --- a/apps/demos/utils/ts-to-js-converter/types.ts +++ b/apps/demos/utils/ts-to-js-converter/types.ts @@ -16,3 +16,7 @@ export type ActionConverterEntry = { source: string out: string }; + +export type ConverterOptions = { + prettify?: boolean +}; From 86d38e90da4d0568c16f7f000050054460949adf Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 1 Jun 2026 20:31:04 +0400 Subject: [PATCH 3/5] refactor --- .github/workflows/visual-tests-demos.yml | 2 +- .../React/devextreme-vectormap-data.d.ts | 27 +++++++ .../React/devextreme-vectormap-data.d.ts | 27 +++++++ apps/demos/utils/ts-to-js-converter/cli.ts | 73 ++++++++++++------- .../utils/ts-to-js-converter/converter.ts | 59 +++++++++++---- 5 files changed, 144 insertions(+), 44 deletions(-) create mode 100644 apps/demos/Demos/VectorMap/ImageMarkers/React/devextreme-vectormap-data.d.ts create mode 100644 apps/demos/Demos/VectorMap/MultipleLayers/React/devextreme-vectormap-data.d.ts diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index 103e1f584b64..e00eb36c6b63 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -539,7 +539,7 @@ jobs: strategy: fail-fast: false matrix: - CONSTEL: ['1/5', '2/5', '3/5', '4/5', '5/5'] + CONSTEL: ['1/2', '2/2'] steps: - name: Get sources diff --git a/apps/demos/Demos/VectorMap/ImageMarkers/React/devextreme-vectormap-data.d.ts b/apps/demos/Demos/VectorMap/ImageMarkers/React/devextreme-vectormap-data.d.ts new file mode 100644 index 000000000000..e5de5ff1844d --- /dev/null +++ b/apps/demos/Demos/VectorMap/ImageMarkers/React/devextreme-vectormap-data.d.ts @@ -0,0 +1,27 @@ +declare module 'devextreme-dist/js/vectormap-data/usa.js' { + type Position = number[]; + type Geometry = + | { type: 'Point'; coordinates: Position } + | { type: 'MultiPoint'; coordinates: Position[] } + | { type: 'LineString'; coordinates: Position[] } + | { type: 'MultiLineString'; coordinates: Position[][] } + | { type: 'Polygon'; coordinates: Position[][] } + | { type: 'MultiPolygon'; coordinates: Position[][][] }; + + interface Feature { + type: 'Feature'; + geometry: Geometry; + properties: Record; + } + + interface FeatureCollection { + type: 'FeatureCollection'; + features: Feature[]; + // eslint-disable-next-line spellcheck/spell-checker + bbox?: number[]; + } + + const usa: FeatureCollection; + export { usa }; + export default usa; +} diff --git a/apps/demos/Demos/VectorMap/MultipleLayers/React/devextreme-vectormap-data.d.ts b/apps/demos/Demos/VectorMap/MultipleLayers/React/devextreme-vectormap-data.d.ts new file mode 100644 index 000000000000..370ec277e5ea --- /dev/null +++ b/apps/demos/Demos/VectorMap/MultipleLayers/React/devextreme-vectormap-data.d.ts @@ -0,0 +1,27 @@ +declare module 'devextreme-dist/js/vectormap-data/world.js' { + type Position = number[]; + type Geometry = + | { type: 'Point'; coordinates: Position } + | { type: 'MultiPoint'; coordinates: Position[] } + | { type: 'LineString'; coordinates: Position[] } + | { type: 'MultiLineString'; coordinates: Position[][] } + | { type: 'Polygon'; coordinates: Position[][] } + | { type: 'MultiPolygon'; coordinates: Position[][][] }; + + interface Feature { + type: 'Feature'; + geometry: Geometry; + properties: Record; + } + + interface FeatureCollection { + type: 'FeatureCollection'; + features: Feature[]; + // eslint-disable-next-line spellcheck/spell-checker + bbox?: number[]; + } + + const world: FeatureCollection; + export { world }; + export default world; +} diff --git a/apps/demos/utils/ts-to-js-converter/cli.ts b/apps/demos/utils/ts-to-js-converter/cli.ts index 62fd8753b76d..c2157f8f72b5 100644 --- a/apps/demos/utils/ts-to-js-converter/cli.ts +++ b/apps/demos/utils/ts-to-js-converter/cli.ts @@ -7,6 +7,17 @@ import fs from 'fs'; import { converter, prettifyOutputs } from './converter'; +const defaultConversionConcurrency = 8; + +const logger = { + warning: consola.warn, + error: consola.error, + debug: consola.debug, + info: consola.info, + start: consola.start, + success: consola.success, +}; + function findFoldersWithTsxFiles(directory) { const foldersWithTsxFiles = []; const filesAndFolders = fs.readdirSync(directory); @@ -51,16 +62,7 @@ const getPatterns = () => { return filteredDemos.map((demoName) => demoName.split(path.sep).join(path.posix.sep)); }; -const performConversion = async (patterns) => { - const logger = { - warning: consola.warn, - error: consola.error, - debug: consola.debug, - info: consola.info, - start: consola.start, - success: consola.success, - }; - +const performConversion = async (patterns, conversionConcurrency) => { const args = minimist(patterns); const sourceDirs = args._ || [process.cwd()]; @@ -81,29 +83,30 @@ const performConversion = async (patterns) => { // @ts-ignore )).flat(1); - let convertedOutDirs: string[]; - try { - const outDirs = await Promise.all( - entries.map(async ({ source, out }) => { - logger.start(`converting ${source}`); - const converted = await converter(source, out, logger, { prettify: false }); - if (converted) { - logger.success(`${source} complete`); - } - return converted ? out : null; - }), - ); + const outDirs: (string | null)[] = []; + const entryBatches = splitArrayIntoSubarrays(entries, conversionConcurrency); + + for (const entryBatch of entryBatches) { + outDirs.push(...await Promise.all( + entryBatch.map(async ({ source, out }) => { + logger.start(`converting ${source}`); + const converted = await converter(source, out, logger, { prettify: false }); + if (converted) { + logger.success(`${source} complete`); + } + return converted ? out : null; + }), + )); + } - convertedOutDirs = outDirs.filter( + return outDirs.filter( (outDir): outDir is string => outDir != null, ); } catch (error) { logger.error(error); process.exit(1); } - - await prettifyOutputs(convertedOutDirs, logger); }; function splitArrayIntoSubarrays(array, subarrayLength) { @@ -116,6 +119,17 @@ function splitArrayIntoSubarrays(array, subarrayLength) { return result; } +function getConversionConcurrency() { + const rawValue = process.env.CONVERT_TO_JS_CONCURRENCY; + const parsedValue = rawValue == null ? defaultConversionConcurrency : Number(rawValue); + + if (!Number.isInteger(parsedValue) || parsedValue < 1) { + throw new Error(`CONVERT_TO_JS_CONCURRENCY must be a positive integer. Received: ${rawValue}`); + } + + return parsedValue; +} + async function startScript() { const userFlags = process.argv.slice(2); if (userFlags[0] === 'split') { @@ -138,10 +152,15 @@ async function startScript() { async function batchPatternsAndConvert() { const allPatterns = getPatterns(); - const batches = splitArrayIntoSubarrays(allPatterns, 10); + const conversionConcurrency = getConversionConcurrency(); + const batches = splitArrayIntoSubarrays(allPatterns, conversionConcurrency); + const convertedOutDirs: string[] = []; + for (const batch of batches) { - await performConversion(batch); + convertedOutDirs.push(...await performConversion(batch, conversionConcurrency)); } + + await prettifyOutputs(convertedOutDirs, process.cwd(), logger); } startScript(); diff --git a/apps/demos/utils/ts-to-js-converter/converter.ts b/apps/demos/utils/ts-to-js-converter/converter.ts index baf86f95e314..0d8f8d89f3d2 100644 --- a/apps/demos/utils/ts-to-js-converter/converter.ts +++ b/apps/demos/utils/ts-to-js-converter/converter.ts @@ -41,6 +41,8 @@ const redundantAssets = ['*tsconfig*', 'types.js']; const quoteShellArg = (value: string) => `"${value.replace(/(["\\$`])/g, '\\$1')}"`; +const toPosixPath = (value: string) => value.split(path.sep).join(path.posix.sep); + const makeConfig = ( resolve: PathResolvers, include: string[], @@ -66,6 +68,7 @@ const makeConfig = ( skipLibCheck: true, allowSyntheticDefaultImports: true, resolveJsonModule: true, + preserveSymlinks: true, }, }); @@ -98,12 +101,10 @@ const execTsc = async (directory: string, args: string[]): Promise => { const { stdout, stderr } = error as { stdout?: string; stderr?: string }; if (stdout) { - // eslint-disable-next-line no-console console.error(stdout); } if (stderr) { - // eslint-disable-next-line no-console console.error(stderr); } @@ -205,10 +206,21 @@ const patchImports = async (resolve: PathResolvers, log: Logger) => { log.debug('imports patching done'); }; +const prettify = async (resolve: PathResolvers, log: Logger) => { + log.debug('running Prettier'); + const prettierCommand = `prettier --write "${resolve.out('')}${path.sep}!(*.{css,json,md,tsbuildinfo})" --single-attribute-per-line --print-width 100`; + await exec(prettierCommand, { + cwd: resolve.out(''), + }); + await exec(`eslint --fix "${resolve.out('')}" --ignore-pattern "config.js" --ignore-pattern "*.tsbuildinfo"`, { + cwd: resolve.out(''), + }); +}; + export const prettifyOutputs = async ( outDirs: string[], + demosRootDir: string, log: Logger, - cwd = process.cwd(), ) => { const uniqueOutDirs = [...new Set(outDirs)]; @@ -219,22 +231,37 @@ export const prettifyOutputs = async ( log.debug('running Prettier'); const prettierPatterns = uniqueOutDirs - .map((outDir) => quoteShellArg( - `${path.resolve(outDir)}${path.sep}!(*.{css,json,md,tsbuildinfo})`, - )) + .map((outDir) => quoteShellArg(`${path.resolve(outDir)}${path.sep}!(*.{css,json,md,tsbuildinfo})`)) .join(' '); const eslintPatterns = uniqueOutDirs - .map((outDir) => quoteShellArg(path.resolve(outDir))) + .map((outDir) => quoteShellArg( + toPosixPath(path.relative(demosRootDir, outDir)), + )) .join(' '); - await exec( - `prettier --write ${prettierPatterns} --single-attribute-per-line --print-width 100`, - { cwd }, - ); - await exec( - `eslint --fix ${eslintPatterns} --ignore-pattern "**/config.js" --ignore-pattern "*.tsbuildinfo"`, - { cwd }, - ); + const prettierConfig = quoteShellArg(path.join(demosRootDir, '.prettierrc.json')); + const eslintConfig = quoteShellArg(path.join(demosRootDir, 'eslint.config.mjs')); + const prettierCommand = [ + 'prettier', + '--write', + prettierPatterns, + '--config', + prettierConfig, + '--single-attribute-per-line', + '--print-width 100', + ].join(' '); + const eslintCommand = [ + 'eslint', + '--fix', + eslintPatterns, + '--config', + eslintConfig, + '--ignore-pattern "**/config.js"', + '--ignore-pattern "*.tsbuildinfo"', + ].join(' '); + + await exec(prettierCommand, { cwd: uniqueOutDirs[0] }); + await exec(eslintCommand, { cwd: demosRootDir }); }; const hasTypescriptFiles = async (resolve: PathResolver) => { @@ -285,7 +312,7 @@ export const converter = async ( await copyAssets(resolve, log); await patchImports(resolve, log); if (options.prettify !== false) { - await prettifyOutputs([outDir], log, outDir); + await prettify(resolve, log); } await strip(resolve, log); return true; From ed5c7eb0a19f63ea04c5c301e01b8756e4b0e953 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 2 Jun 2026 13:00:53 +0400 Subject: [PATCH 4/5] refactor --- .github/workflows/visual-tests-demos.yml | 2 +- .../Diagram/AdvancedDataBinding/React/App.tsx | 2 +- apps/demos/utils/ts-to-js-converter/cli.ts | 103 +++++++++--------- .../utils/ts-to-js-converter/converter.ts | 79 +++++++++----- apps/demos/utils/ts-to-js-converter/types.ts | 4 - 5 files changed, 102 insertions(+), 88 deletions(-) diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index e00eb36c6b63..e5bd8d265f3a 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -513,7 +513,7 @@ jobs: working-directory: apps/demos run: | echo "Running convert-to-js on changed files only" - xargs pnpm run convert-to-js < changed-react-demos.txt + xargs -r pnpm run convert-to-js < changed-react-demos.txt git add ./Demos -N diff --git a/apps/demos/Demos/Diagram/AdvancedDataBinding/React/App.tsx b/apps/demos/Demos/Diagram/AdvancedDataBinding/React/App.tsx index c000ee118b3e..0453eabd0ad4 100644 --- a/apps/demos/Demos/Diagram/AdvancedDataBinding/React/App.tsx +++ b/apps/demos/Demos/Diagram/AdvancedDataBinding/React/App.tsx @@ -46,7 +46,7 @@ function itemTextStyleExpr(obj: { level: string; }) { } function itemStyleExpr(obj: { type: string; }) { - const style: React.CSSProperties = { stroke: '#444444' }; + const style: { stroke: string; fill?: string } = { stroke: '#444444' }; if (obj.type === 'group') { style.fill = '#f3f3f3'; } diff --git a/apps/demos/utils/ts-to-js-converter/cli.ts b/apps/demos/utils/ts-to-js-converter/cli.ts index c2157f8f72b5..fec37f8784e1 100644 --- a/apps/demos/utils/ts-to-js-converter/cli.ts +++ b/apps/demos/utils/ts-to-js-converter/cli.ts @@ -5,7 +5,8 @@ import { glob } from 'glob'; import { consola } from 'consola'; import fs from 'fs'; -import { converter, prettifyOutputs } from './converter'; +import { converter, prettifyOutputs, splitArrayIntoSubarrays } from './converter'; +import { ActionConverterEntry } from './types'; const defaultConversionConcurrency = 8; @@ -83,41 +84,37 @@ const performConversion = async (patterns, conversionConcurrency) => { // @ts-ignore )).flat(1); - try { - const outDirs: (string | null)[] = []; - const entryBatches = splitArrayIntoSubarrays(entries, conversionConcurrency); - - for (const entryBatch of entryBatches) { - outDirs.push(...await Promise.all( - entryBatch.map(async ({ source, out }) => { - logger.start(`converting ${source}`); - const converted = await converter(source, out, logger, { prettify: false }); + const outDirs: (string | null)[] = []; + let failedCount = 0; + const entryBatches = splitArrayIntoSubarrays( + entries, + conversionConcurrency, + ); + + for (const entryBatch of entryBatches) { + outDirs.push(...await Promise.all( + entryBatch.map(async ({ source, out }) => { + logger.start(`converting ${source}`); + try { + const converted = await converter(source, out, logger); if (converted) { logger.success(`${source} complete`); } return converted ? out : null; - }), - )); - } - - return outDirs.filter( - (outDir): outDir is string => outDir != null, - ); - } catch (error) { - logger.error(error); - process.exit(1); - } -}; - -function splitArrayIntoSubarrays(array, subarrayLength) { - const result = []; - - for (let i = 0; i < array.length; i += subarrayLength) { - result.push(array.slice(i, i + subarrayLength)); + } catch { + logger.error(`failed converting ${source}`); + failedCount += 1; + return null; + } + }), + )); } - return result; -} + return { + outDirs: outDirs.filter((outDir): outDir is string => outDir != null), + failedCount, + }; +}; function getConversionConcurrency() { const rawValue = process.env.CONVERT_TO_JS_CONCURRENCY; @@ -132,35 +129,37 @@ function getConversionConcurrency() { async function startScript() { const userFlags = process.argv.slice(2); - if (userFlags[0] === 'split') { - process.env.CONSTEL = '1/4'; - consola.log('Start converting Part', process.env.CONSTEL); - await batchPatternsAndConvert(); - process.env.CONSTEL = '2/4'; - consola.log('Start converting Part', process.env.CONSTEL); - await batchPatternsAndConvert(); - process.env.CONSTEL = '3/4'; - consola.log('Start converting Part', process.env.CONSTEL); - await batchPatternsAndConvert(); - process.env.CONSTEL = '4/4'; - consola.log('Start converting Part', process.env.CONSTEL); - await batchPatternsAndConvert(); - } else { - await batchPatternsAndConvert(); + const parts = userFlags[0] === 'split' ? ['1/4', '2/4', '3/4', '4/4'] : [null]; + let failedCount = 0; + + for (const part of parts) { + if (part != null) { + process.env.CONSTEL = part; + consola.log('Start converting Part', process.env.CONSTEL); + } + failedCount += await batchPatternsAndConvert(); } + + return failedCount; } async function batchPatternsAndConvert() { const allPatterns = getPatterns(); const conversionConcurrency = getConversionConcurrency(); - const batches = splitArrayIntoSubarrays(allPatterns, conversionConcurrency); - const convertedOutDirs: string[] = []; + const { outDirs, failedCount } = await performConversion(allPatterns, conversionConcurrency); - for (const batch of batches) { - convertedOutDirs.push(...await performConversion(batch, conversionConcurrency)); - } + await prettifyOutputs(outDirs, process.cwd(), logger); - await prettifyOutputs(convertedOutDirs, process.cwd(), logger); + return failedCount; } -startScript(); +startScript() + .then((failedCount) => { + if (failedCount > 0) { + process.exit(1); + } + }) + .catch((error) => { + logger.error(error); + process.exit(1); + }); diff --git a/apps/demos/utils/ts-to-js-converter/converter.ts b/apps/demos/utils/ts-to-js-converter/converter.ts index 0d8f8d89f3d2..264936e66a94 100644 --- a/apps/demos/utils/ts-to-js-converter/converter.ts +++ b/apps/demos/utils/ts-to-js-converter/converter.ts @@ -10,7 +10,6 @@ import _ from 'lodash'; import os from 'os'; import { - ConverterOptions, Logger, PathResolver, PathResolvers, @@ -43,6 +42,16 @@ const quoteShellArg = (value: string) => `"${value.replace(/(["\\$`])/g, '\\$1') const toPosixPath = (value: string) => value.split(path.sep).join(path.posix.sep); +export function splitArrayIntoSubarrays(array: T[], subarrayLength: number): T[][] { + const result: T[][] = []; + + for (let i = 0; i < array.length; i += subarrayLength) { + result.push(array.slice(i, i + subarrayLength)); + } + + return result; +} + const makeConfig = ( resolve: PathResolvers, include: string[], @@ -206,41 +215,28 @@ const patchImports = async (resolve: PathResolvers, log: Logger) => { log.debug('imports patching done'); }; -const prettify = async (resolve: PathResolvers, log: Logger) => { - log.debug('running Prettier'); - const prettierCommand = `prettier --write "${resolve.out('')}${path.sep}!(*.{css,json,md,tsbuildinfo})" --single-attribute-per-line --print-width 100`; - await exec(prettierCommand, { - cwd: resolve.out(''), - }); - await exec(`eslint --fix "${resolve.out('')}" --ignore-pattern "config.js" --ignore-pattern "*.tsbuildinfo"`, { - cwd: resolve.out(''), - }); -}; +// Format converted demos in chunks so a single `/bin/sh -c` command stays well under the OS +// argument-length limit (Linux MAX_ARG_STRLEN ~128KB) even when a CONSTEL slice converts +// hundreds of dirs. Prettier/ESLint `--fix` are per-file, so splitting the file list does not +// change output -- provided each tool's cwd stays fixed across chunks (see prettierCwd below). +const PRETTIFY_CHUNK_SIZE = 50; -export const prettifyOutputs = async ( +const formatOutputs = async ( outDirs: string[], demosRootDir: string, - log: Logger, + prettierCwd: string, + prettierConfig: string, + eslintConfig: string, ) => { - const uniqueOutDirs = [...new Set(outDirs)]; - - if (uniqueOutDirs.length === 0) { - return; - } - - log.debug('running Prettier'); - - const prettierPatterns = uniqueOutDirs + const prettierPatterns = outDirs .map((outDir) => quoteShellArg(`${path.resolve(outDir)}${path.sep}!(*.{css,json,md,tsbuildinfo})`)) .join(' '); - const eslintPatterns = uniqueOutDirs + const eslintPatterns = outDirs .map((outDir) => quoteShellArg( toPosixPath(path.relative(demosRootDir, outDir)), )) .join(' '); - const prettierConfig = quoteShellArg(path.join(demosRootDir, '.prettierrc.json')); - const eslintConfig = quoteShellArg(path.join(demosRootDir, 'eslint.config.mjs')); const prettierCommand = [ 'prettier', '--write', @@ -260,10 +256,37 @@ export const prettifyOutputs = async ( '--ignore-pattern "*.tsbuildinfo"', ].join(' '); - await exec(prettierCommand, { cwd: uniqueOutDirs[0] }); + await exec(prettierCommand, { cwd: prettierCwd }); await exec(eslintCommand, { cwd: demosRootDir }); }; +export const prettifyOutputs = async ( + outDirs: string[], + demosRootDir: string, + log: Logger, +) => { + const uniqueOutDirs = [...new Set(outDirs)]; + + if (uniqueOutDirs.length === 0) { + return; + } + + log.debug('running Prettier'); + + const prettierConfig = quoteShellArg(path.join(demosRootDir, '.prettierrc.json')); + const eslintConfig = quoteShellArg(path.join(demosRootDir, 'eslint.config.mjs')); + // Prettier resolves `.prettierrc.json` `overrides[].files` globs relative to its cwd. The + // committed JS demos were formatted with cwd set to an output dir (so the per-file relative + // path is just `App.jsx`, which does NOT match the `**/ReactJs/**` override). Keep that cwd + // FIXED across every chunk so override matching -- and therefore output -- is identical to a + // single un-chunked run. + const prettierCwd = uniqueOutDirs[0]; + + for (const chunk of splitArrayIntoSubarrays(uniqueOutDirs, PRETTIFY_CHUNK_SIZE)) { + await formatOutputs(chunk, demosRootDir, prettierCwd, prettierConfig, eslintConfig); + } +}; + const hasTypescriptFiles = async (resolve: PathResolver) => { const filenamePatterns = ['./*.ts', './*.tsx']; // @ts-ignore @@ -275,7 +298,6 @@ export const converter = async ( sourceDir: string, outDir: string, log: Logger, - options: ConverterOptions = {}, ): Promise => { log.debug('TS to JS example converter starting'); log.debug(`sourceDir: ${sourceDir}`); @@ -311,9 +333,6 @@ export const converter = async ( await compile(resolve, log); await copyAssets(resolve, log); await patchImports(resolve, log); - if (options.prettify !== false) { - await prettify(resolve, log); - } await strip(resolve, log); return true; } finally { diff --git a/apps/demos/utils/ts-to-js-converter/types.ts b/apps/demos/utils/ts-to-js-converter/types.ts index 6e6573369c97..4f06614729ad 100644 --- a/apps/demos/utils/ts-to-js-converter/types.ts +++ b/apps/demos/utils/ts-to-js-converter/types.ts @@ -16,7 +16,3 @@ export type ActionConverterEntry = { source: string out: string }; - -export type ConverterOptions = { - prettify?: boolean -}; From 56689d404a8944665f83ce117e0b629443e9dc19 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 2 Jun 2026 14:42:33 +0400 Subject: [PATCH 5/5] refactor --- apps/demos/utils/ts-to-js-converter/converter.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/demos/utils/ts-to-js-converter/converter.ts b/apps/demos/utils/ts-to-js-converter/converter.ts index 264936e66a94..1b215c54aaba 100644 --- a/apps/demos/utils/ts-to-js-converter/converter.ts +++ b/apps/demos/utils/ts-to-js-converter/converter.ts @@ -215,10 +215,6 @@ const patchImports = async (resolve: PathResolvers, log: Logger) => { log.debug('imports patching done'); }; -// Format converted demos in chunks so a single `/bin/sh -c` command stays well under the OS -// argument-length limit (Linux MAX_ARG_STRLEN ~128KB) even when a CONSTEL slice converts -// hundreds of dirs. Prettier/ESLint `--fix` are per-file, so splitting the file list does not -// change output -- provided each tool's cwd stays fixed across chunks (see prettierCwd below). const PRETTIFY_CHUNK_SIZE = 50; const formatOutputs = async ( @@ -275,11 +271,7 @@ export const prettifyOutputs = async ( const prettierConfig = quoteShellArg(path.join(demosRootDir, '.prettierrc.json')); const eslintConfig = quoteShellArg(path.join(demosRootDir, 'eslint.config.mjs')); - // Prettier resolves `.prettierrc.json` `overrides[].files` globs relative to its cwd. The - // committed JS demos were formatted with cwd set to an output dir (so the per-file relative - // path is just `App.jsx`, which does NOT match the `**/ReactJs/**` override). Keep that cwd - // FIXED across every chunk so override matching -- and therefore output -- is identical to a - // single un-chunked run. + const prettierCwd = uniqueOutDirs[0]; for (const chunk of splitArrayIntoSubarrays(uniqueOutDirs, PRETTIFY_CHUNK_SIZE)) {