From c6429cd0f786caae95b9ef374823e7cfe49a9113 Mon Sep 17 00:00:00 2001 From: Orkun Date: Fri, 15 May 2026 15:41:06 +0300 Subject: [PATCH 01/13] test: add angular test matrix --- .github/workflows/test-matrix.yml | 132 +++++++++++++++ .gitignore | 2 + .node-version | 1 + bin/test-matrix.sh | 152 ++++++++++++++++++ package.json | 3 + .../tsconfig.spec.json | 3 +- 6 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-matrix.yml create mode 100644 .node-version create mode 100755 bin/test-matrix.sh diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml new file mode 100644 index 0000000..ed41957 --- /dev/null +++ b/.github/workflows/test-matrix.yml @@ -0,0 +1,132 @@ +name: Test Angular Matrix + +on: + pull_request: + +jobs: + test-matrix: + name: Angular ${{ matrix.angular-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + angular-version: [15, 16, 17, 18, 19, 20, 21] + include: + - angular-version: 15 + node-version: 18 + - angular-version: 16 + node-version: 18 + - angular-version: 17 + node-version: 18 + - angular-version: 18 + node-version: 20 + - angular-version: 19 + node-version: 20 + - angular-version: 20 + node-version: 20 + - angular-version: 21 + node-version: 22 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup environment variables + run: | + echo "CI=true" >> $GITHUB_ENV + echo "NG_CLI_ANALYTICS=false" >> $GITHUB_ENV + echo "LIB_NAME=fingerprintjs-pro-angular" >> $GITHUB_ENV + + - name: Run tests for Angular ${{ matrix.angular-version }} + shell: bash + run: | + VERSION=${{ matrix.angular-version }} + LIB_NAME="fingerprintjs-pro-angular" + + SOURCE_PROJECT_DIR="$(pwd)/projects/$LIB_NAME" + SOURCE_LIB_DIR="$SOURCE_PROJECT_DIR/src/lib" + SOURCE_PUBLIC_API="$SOURCE_PROJECT_DIR/src/public-api.ts" + SOURCE_TEST_TS="$(pwd)/test.ts" + SOURCE_JEST_CONFIG="$(pwd)/jest.config.js" + SOURCE_ROOT_TSCONFIG="$(pwd)/tsconfig.json" + SOURCE_ROOT_TSCONFIG_SPEC="$(pwd)/tsconfig.spec.json" + + # Cross-platform sedi (though GHA is linux, keeping it for compatibility) + sedi() { + sed -i "$@" + } + + TEMP_DIR=$(mktemp -d) + cd "$TEMP_DIR" + + echo "Creating new Angular workspace for version $VERSION..." + npx -y @angular/cli@$VERSION new test-workspace --create-application=false --skip-git --skip-install --defaults --package-manager=pnpm + cd test-workspace + + pnpm install --config.fund=false + pnpm exec ng generate library "$LIB_NAME" --skip-install + pnpm install --config.fund=false --config.strict-peer-dependencies=false --no-frozen-lockfile + pnpm add @fingerprint/agent --config.fund=false --config.strict-peer-dependencies=false + pnpm add @angular/platform-browser-dynamic@$VERSION zone.js --config.fund=false --config.strict-peer-dependencies=false + pnpm add -D jest jest-preset-angular jest-environment-jsdom @types/jest @types/node --config.fund=false --config.strict-peer-dependencies=false + + rm -rf projects/"$LIB_NAME"/src/lib/* + cp -r "$SOURCE_LIB_DIR/"* projects/"$LIB_NAME"/src/lib/ + cp "$SOURCE_PUBLIC_API" projects/"$LIB_NAME"/src/public-api.ts + + [ -f "$SOURCE_TEST_TS" ] && cp "$SOURCE_TEST_TS" test.ts + [ -f "$SOURCE_JEST_CONFIG" ] && cp "$SOURCE_JEST_CONFIG" jest.config.js + [ -f "$SOURCE_ROOT_TSCONFIG" ] && cp "$SOURCE_ROOT_TSCONFIG" tsconfig.json + [ -f "$SOURCE_ROOT_TSCONFIG_SPEC" ] && cp "$SOURCE_ROOT_TSCONFIG_SPEC" tsconfig.spec.json + + if [ ! -f "tsconfig.spec.json" ]; then + echo '{"extends": "./tsconfig.json", "compilerOptions": {"types": ["jest", "node"]}}' > tsconfig.spec.json + fi + + cp "$SOURCE_PROJECT_DIR/ng-package.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/package.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.lib.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.lib.prod.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.spec.json" projects/"$LIB_NAME"/ + + for FILE in "tsconfig.json" "tsconfig.base.json" "projects/$LIB_NAME/tsconfig.spec.json"; do + if [ -f "$FILE" ]; then + sedi '/"types":/d' "$FILE" + sedi '/"esModuleInterop":/d' "$FILE" + sedi '/"allowSyntheticDefaultImports":/d' "$FILE" + sedi '/"skipLibCheck":/d' "$FILE" + sedi 's/"compilerOptions":\s*{/"compilerOptions": { "types": ["node", "jest"], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true,/g' "$FILE" + fi + done + + find . -name "tsconfig.spec.json" -exec bash -c 'sed -i "s/\"types\":\s*\[/\"types\": [\"node\", \"jest\", /g" "$1"' _ {} \; + find . -name "tsconfig.spec.json" -exec bash -c 'sed -i "s/\"compilerOptions\":\s*{/\"compilerOptions\": { \"types\": [\"node\", \"jest\"], \"esModuleInterop\": true, \"allowSyntheticDefaultImports\": true, \"skipLibCheck\": true,/g" "$1"' _ {} \; + + if [ -f "projects/$LIB_NAME/tsconfig.spec.json" ]; then + if [ ! -f "../../tsconfig.json" ] && [ -f "../../tsconfig.base.json" ]; then + sedi 's/"extends": "..\/..\/tsconfig.json"/"extends": "..\/..\/tsconfig.base.json"/g' projects/"$LIB_NAME"/tsconfig.spec.json + fi + fi + + find projects/"$LIB_NAME" -name "*.spec.ts" -exec bash -c 'sed -i "1s|^|/// \n|" "$1"' _ {} \; + + if [ "$VERSION" -ge "21" ]; then + sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.json + [ -f "tsconfig.base.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.base.json + [ -f "projects/$LIB_NAME/tsconfig.lib.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' projects/"$LIB_NAME"/tsconfig.lib.json + fi + + echo "Building library..." + pnpm exec ng build "$LIB_NAME" + + echo "Running tests..." + pnpm exec jest diff --git a/.gitignore b/.gitignore index fd133a5..3d62395 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,13 @@ /tmp /out-tsc /bazel-out +.pnpm-store # Node node_modules npm-debug.log yarn-error.log +test-logs # IDEs and editors .idea/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/bin/test-matrix.sh b/bin/test-matrix.sh new file mode 100755 index 0000000..72916b0 --- /dev/null +++ b/bin/test-matrix.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +export CI=true +export NG_CLI_ANALYTICS=false +export PNPM_STORE_DIR="$(pwd)/.pnpm-store" + +VERSIONS=("15" "16" "17" "18" "19" "20" "21") +LIB_NAME="fingerprintjs-pro-angular" + +SOURCE_PROJECT_DIR="$(pwd)/projects/$LIB_NAME" +SOURCE_LIB_DIR="$SOURCE_PROJECT_DIR/src/lib" +SOURCE_PUBLIC_API="$SOURCE_PROJECT_DIR/src/public-api.ts" +SOURCE_TEST_TS="$(pwd)/test.ts" +SOURCE_JEST_CONFIG="$(pwd)/jest.config.js" +SOURCE_ROOT_TSCONFIG="$(pwd)/tsconfig.json" +SOURCE_ROOT_TSCONFIG_SPEC="$(pwd)/tsconfig.spec.json" + +LOG_DIR="$(pwd)/test-logs" +mkdir -p "$LOG_DIR" +rm -f "$LOG_DIR"/* + +echo "Starting tests for Angular versions: ${VERSIONS[*]}" +echo "Using pnpm store at: $PNPM_STORE_DIR" +echo "Logs: $LOG_DIR" +echo "------------------------------------------------------------" + +if ! command -v pnpm &> /dev/null; then + npm install -g pnpm +fi + +pnpm config set store-dir "$PNPM_STORE_DIR" + +# Cross-platform sed -i +sedi() { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i "" "$@" + else + sed -i "$@" + fi +} + +test_version() { + local VERSION=$1 + local LOG_FILE="$LOG_DIR/angular-$VERSION.log" + + if [ "$VERSION" -ge "20" ]; then + NODE_VERSION=$(node -v | cut -d'v' -f2) + NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'.' -f1) + NODE_MINOR=$(echo "$NODE_VERSION" | cut -d'.' -f2) + if [ "$NODE_MAJOR" -lt 20 ] || ([ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 19 ]); then + echo "Angular $VERSION: Skipped (Node.js $NODE_VERSION < v20.19)" + return 0 + fi + fi + + ( + set -e + TEMP_DIR=$(mktemp -d) + cd "$TEMP_DIR" + + npx -y @angular/cli@$VERSION new test-workspace --create-application=false --skip-git --skip-install --defaults --package-manager=pnpm + cd test-workspace + + pnpm install --config.fund=false + pnpm exec ng generate library "$LIB_NAME" --skip-install + pnpm install --config.fund=false --config.strict-peer-dependencies=false --no-frozen-lockfile + pnpm add @fingerprint/agent --config.fund=false --config.strict-peer-dependencies=false + pnpm add @angular/platform-browser-dynamic@$VERSION zone.js --config.fund=false --config.strict-peer-dependencies=false + pnpm add -D jest jest-preset-angular jest-environment-jsdom @types/jest @types/node --config.fund=false --config.strict-peer-dependencies=false + + rm -rf projects/"$LIB_NAME"/src/lib/* + cp -r "$SOURCE_LIB_DIR/"* projects/"$LIB_NAME"/src/lib/ + cp "$SOURCE_PUBLIC_API" projects/"$LIB_NAME"/src/public-api.ts + + [ -f "$SOURCE_TEST_TS" ] && cp "$SOURCE_TEST_TS" test.ts + [ -f "$SOURCE_JEST_CONFIG" ] && cp "$SOURCE_JEST_CONFIG" jest.config.js + [ -f "$SOURCE_ROOT_TSCONFIG" ] && cp "$SOURCE_ROOT_TSCONFIG" tsconfig.json + [ -f "$SOURCE_ROOT_TSCONFIG_SPEC" ] && cp "$SOURCE_ROOT_TSCONFIG_SPEC" tsconfig.spec.json + + if [ ! -f "tsconfig.spec.json" ]; then + echo '{"extends": "./tsconfig.json", "compilerOptions": {"types": ["jest", "node"]}}' > tsconfig.spec.json + fi + + cp "$SOURCE_PROJECT_DIR/ng-package.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/package.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.lib.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.lib.prod.json" projects/"$LIB_NAME"/ + cp "$SOURCE_PROJECT_DIR/tsconfig.spec.json" projects/"$LIB_NAME"/ + + for FILE in "tsconfig.json" "tsconfig.base.json" "projects/$LIB_NAME/tsconfig.spec.json"; do + if [ -f "$FILE" ]; then + sedi '/"types":/d' "$FILE" + sedi '/"esModuleInterop":/d' "$FILE" + sedi '/"allowSyntheticDefaultImports":/d' "$FILE" + sedi '/"skipLibCheck":/d' "$FILE" + sedi 's/"compilerOptions":\s*{/"compilerOptions": { "types": ["node", "jest"], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true,/g' "$FILE" + fi + done + + find . -name "tsconfig.spec.json" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "s/\"types\":\s*\[/\"types\": [\"node\", \"jest\", /g" "$1"' _ {} \; + find . -name "tsconfig.spec.json" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "s/\"compilerOptions\":\s*{/\"compilerOptions\": { \"types\": [\"node\", \"jest\"], \"esModuleInterop\": true, \"allowSyntheticDefaultImports\": true, \"skipLibCheck\": true,/g" "$1"' _ {} \; + + if [ -f "projects/$LIB_NAME/tsconfig.spec.json" ]; then + if [ ! -f "../../tsconfig.json" ] && [ -f "../../tsconfig.base.json" ]; then + sedi 's/"extends": "..\/..\/tsconfig.json"/"extends": "..\/..\/tsconfig.base.json"/g' projects/"$LIB_NAME"/tsconfig.spec.json + fi + fi + + find projects/"$LIB_NAME" -name "*.spec.ts" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "1s|^|/// \n|" "$1"' _ {} \; + + if [ "$VERSION" -ge "21" ]; then + sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.json + [ -f "tsconfig.base.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.base.json + [ -f "projects/$LIB_NAME/tsconfig.lib.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' projects/"$LIB_NAME"/tsconfig.lib.json + fi + + pnpm exec ng build "$LIB_NAME" + pnpm exec jest + rm -rf "$TEMP_DIR" + ) > "$LOG_FILE" 2>&1 + + local STATUS=$? + if [ $STATUS -eq 0 ]; then + echo "Angular $VERSION: PASSED" + else + echo "Angular $VERSION: FAILED" + echo "Log: $LOG_FILE" + tail -n 15 "$LOG_FILE" + fi + + return $STATUS +} + +PIDS=() +for VERSION in "${VERSIONS[@]}"; do + test_version "$VERSION" & + PIDS+=($!) +done + +OVERALL_EXIT_CODE=0 +for PID in "${PIDS[@]}"; do + wait "$PID" || OVERALL_EXIT_CODE=$? +done + +echo "------------------------------------------------------------" +if [ $OVERALL_EXIT_CODE -ne 0 ]; then + echo "CI Pipeline Failed" + exit $OVERALL_EXIT_CODE +else + echo "Success: All Angular versions passed" + exit 0 +fi diff --git a/package.json b/package.json index b8b3322..e78e87f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "fingerprintjs-pro-angular-demo", "version": "0.0.0", + "engines": { + "node": ">=22.0.0" + }, "scripts": { "prepare": "husky install", "ng": "ng", diff --git a/projects/fingerprintjs-pro-angular/tsconfig.spec.json b/projects/fingerprintjs-pro-angular/tsconfig.spec.json index bc876cc..035e16f 100644 --- a/projects/fingerprintjs-pro-angular/tsconfig.spec.json +++ b/projects/fingerprintjs-pro-angular/tsconfig.spec.json @@ -4,7 +4,8 @@ "compilerOptions": { "outDir": "../../out-tsc/spec", "types": [ - "jest" + "jest", + "node" ] }, "files": [ From b46b7f35d8d6391ad72cfeaa73650c6a5629d6e4 Mon Sep 17 00:00:00 2001 From: Orkun Date: Fri, 15 May 2026 15:43:25 +0300 Subject: [PATCH 02/13] test: add gen project version command before tests ran --- .github/workflows/test-matrix.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index ed41957..8a22ac1 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -40,6 +40,9 @@ jobs: with: version: 8 + - name: Generate project version + run: pnpm generate:version + - name: Setup environment variables run: | echo "CI=true" >> $GITHUB_ENV From 5e5762c729cf6a67fb4b455627ae84622d8c8400 Mon Sep 17 00:00:00 2001 From: Orkun Date: Tue, 23 Jun 2026 12:51:16 +0300 Subject: [PATCH 03/13] refactor: test-matrix bash to javascript --- .github/workflows/test-matrix.yml | 87 +------------- bin/test-matrix.mjs | 92 ++++++++++++++ bin/test-matrix.sh | 152 ------------------------ bin/utils/command.mjs | 22 ++++ bin/utils/commands/ensurePnpm.mjs | 12 ++ bin/utils/commands/jestRun.mjs | 5 + bin/utils/commands/ngBuild.mjs | 5 + bin/utils/commands/ngGenerate.mjs | 10 ++ bin/utils/commands/ngNew.mjs | 23 ++++ bin/utils/commands/pnpmAdd.mjs | 6 + bin/utils/commands/pnpmInstall.mjs | 10 ++ bin/utils/constants.mjs | 17 +++ bin/utils/fs.mjs | 76 ++++++++++++ bin/utils/logErrorSummary.mjs | 15 +++ bin/utils/setupLogDir.mjs | 12 ++ bin/utils/steps/copySourceFiles.mjs | 49 ++++++++ bin/utils/steps/installDependencies.mjs | 23 ++++ bin/utils/steps/updateTsConfigs.mjs | 101 ++++++++++++++++ package.json | 5 +- 19 files changed, 485 insertions(+), 237 deletions(-) create mode 100755 bin/test-matrix.mjs delete mode 100755 bin/test-matrix.sh create mode 100644 bin/utils/command.mjs create mode 100644 bin/utils/commands/ensurePnpm.mjs create mode 100644 bin/utils/commands/jestRun.mjs create mode 100644 bin/utils/commands/ngBuild.mjs create mode 100644 bin/utils/commands/ngGenerate.mjs create mode 100644 bin/utils/commands/ngNew.mjs create mode 100644 bin/utils/commands/pnpmAdd.mjs create mode 100644 bin/utils/commands/pnpmInstall.mjs create mode 100644 bin/utils/constants.mjs create mode 100644 bin/utils/fs.mjs create mode 100644 bin/utils/logErrorSummary.mjs create mode 100644 bin/utils/setupLogDir.mjs create mode 100644 bin/utils/steps/copySourceFiles.mjs create mode 100644 bin/utils/steps/installDependencies.mjs create mode 100644 bin/utils/steps/updateTsConfigs.mjs diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 8a22ac1..1c05bd6 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -40,6 +40,9 @@ jobs: with: version: 8 + - name: Install dependencies + run: pnpm install + - name: Generate project version run: pnpm generate:version @@ -50,86 +53,4 @@ jobs: echo "LIB_NAME=fingerprintjs-pro-angular" >> $GITHUB_ENV - name: Run tests for Angular ${{ matrix.angular-version }} - shell: bash - run: | - VERSION=${{ matrix.angular-version }} - LIB_NAME="fingerprintjs-pro-angular" - - SOURCE_PROJECT_DIR="$(pwd)/projects/$LIB_NAME" - SOURCE_LIB_DIR="$SOURCE_PROJECT_DIR/src/lib" - SOURCE_PUBLIC_API="$SOURCE_PROJECT_DIR/src/public-api.ts" - SOURCE_TEST_TS="$(pwd)/test.ts" - SOURCE_JEST_CONFIG="$(pwd)/jest.config.js" - SOURCE_ROOT_TSCONFIG="$(pwd)/tsconfig.json" - SOURCE_ROOT_TSCONFIG_SPEC="$(pwd)/tsconfig.spec.json" - - # Cross-platform sedi (though GHA is linux, keeping it for compatibility) - sedi() { - sed -i "$@" - } - - TEMP_DIR=$(mktemp -d) - cd "$TEMP_DIR" - - echo "Creating new Angular workspace for version $VERSION..." - npx -y @angular/cli@$VERSION new test-workspace --create-application=false --skip-git --skip-install --defaults --package-manager=pnpm - cd test-workspace - - pnpm install --config.fund=false - pnpm exec ng generate library "$LIB_NAME" --skip-install - pnpm install --config.fund=false --config.strict-peer-dependencies=false --no-frozen-lockfile - pnpm add @fingerprint/agent --config.fund=false --config.strict-peer-dependencies=false - pnpm add @angular/platform-browser-dynamic@$VERSION zone.js --config.fund=false --config.strict-peer-dependencies=false - pnpm add -D jest jest-preset-angular jest-environment-jsdom @types/jest @types/node --config.fund=false --config.strict-peer-dependencies=false - - rm -rf projects/"$LIB_NAME"/src/lib/* - cp -r "$SOURCE_LIB_DIR/"* projects/"$LIB_NAME"/src/lib/ - cp "$SOURCE_PUBLIC_API" projects/"$LIB_NAME"/src/public-api.ts - - [ -f "$SOURCE_TEST_TS" ] && cp "$SOURCE_TEST_TS" test.ts - [ -f "$SOURCE_JEST_CONFIG" ] && cp "$SOURCE_JEST_CONFIG" jest.config.js - [ -f "$SOURCE_ROOT_TSCONFIG" ] && cp "$SOURCE_ROOT_TSCONFIG" tsconfig.json - [ -f "$SOURCE_ROOT_TSCONFIG_SPEC" ] && cp "$SOURCE_ROOT_TSCONFIG_SPEC" tsconfig.spec.json - - if [ ! -f "tsconfig.spec.json" ]; then - echo '{"extends": "./tsconfig.json", "compilerOptions": {"types": ["jest", "node"]}}' > tsconfig.spec.json - fi - - cp "$SOURCE_PROJECT_DIR/ng-package.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/package.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.lib.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.lib.prod.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.spec.json" projects/"$LIB_NAME"/ - - for FILE in "tsconfig.json" "tsconfig.base.json" "projects/$LIB_NAME/tsconfig.spec.json"; do - if [ -f "$FILE" ]; then - sedi '/"types":/d' "$FILE" - sedi '/"esModuleInterop":/d' "$FILE" - sedi '/"allowSyntheticDefaultImports":/d' "$FILE" - sedi '/"skipLibCheck":/d' "$FILE" - sedi 's/"compilerOptions":\s*{/"compilerOptions": { "types": ["node", "jest"], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true,/g' "$FILE" - fi - done - - find . -name "tsconfig.spec.json" -exec bash -c 'sed -i "s/\"types\":\s*\[/\"types\": [\"node\", \"jest\", /g" "$1"' _ {} \; - find . -name "tsconfig.spec.json" -exec bash -c 'sed -i "s/\"compilerOptions\":\s*{/\"compilerOptions\": { \"types\": [\"node\", \"jest\"], \"esModuleInterop\": true, \"allowSyntheticDefaultImports\": true, \"skipLibCheck\": true,/g" "$1"' _ {} \; - - if [ -f "projects/$LIB_NAME/tsconfig.spec.json" ]; then - if [ ! -f "../../tsconfig.json" ] && [ -f "../../tsconfig.base.json" ]; then - sedi 's/"extends": "..\/..\/tsconfig.json"/"extends": "..\/..\/tsconfig.base.json"/g' projects/"$LIB_NAME"/tsconfig.spec.json - fi - fi - - find projects/"$LIB_NAME" -name "*.spec.ts" -exec bash -c 'sed -i "1s|^|/// \n|" "$1"' _ {} \; - - if [ "$VERSION" -ge "21" ]; then - sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.json - [ -f "tsconfig.base.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.base.json - [ -f "projects/$LIB_NAME/tsconfig.lib.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' projects/"$LIB_NAME"/tsconfig.lib.json - fi - - echo "Building library..." - pnpm exec ng build "$LIB_NAME" - - echo "Running tests..." - pnpm exec jest + run: pnpm run test:matrix -- --version=${{ matrix.angular-version }} diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs new file mode 100755 index 0000000..5bc3578 --- /dev/null +++ b/bin/test-matrix.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +import { joinPath, mkdtemp, createWriteStream, rm } from './utils/fs.mjs' +import { PNPM_STORE_DIR, VERSIONS, LIB_NAME, LOG_DIR } from './utils/constants.mjs' +import { ensurePnpm } from './utils/commands/ensurePnpm.mjs' +import { ngNew } from './utils/commands/ngNew.mjs' +import { ngBuild } from './utils/commands/ngBuild.mjs' +import { jestRun } from './utils/commands/jestRun.mjs' +import { copySourceFiles } from './utils/steps/copySourceFiles.mjs' +import { updateTsConfigs } from './utils/steps/updateTsConfigs.mjs' +import { installDependencies } from './utils/steps/installDependencies.mjs' +import { setupLogDir } from './utils/setupLogDir.mjs' +import { logErrorSummary } from './utils/logErrorSummary.mjs' + +process.env.CI = 'true' +process.env.NG_CLI_ANALYTICS = 'false' +process.env.PNPM_STORE_DIR = PNPM_STORE_DIR + +async function testVersion(version) { + const logFile = joinPath(LOG_DIR, `angular-${version}.log`) + const logStream = createWriteStream(logFile) + + // Node version check for Angular 20+ + if (parseInt(version) >= 20) { + const nodeVersion = process.versions.node + const [major, minor] = nodeVersion.split('.').map(Number) + if (major < 20 || (major === 20 && minor < 19)) { + console.log(`Angular ${version}: Skipped (Node.js ${nodeVersion} < v20.19)`) + return 0 + } + } + + const tempDir = mkdtemp('angular-test-') + const workspaceDir = joinPath(tempDir, 'test-workspace') + + try { + const log = (data) => logStream.write(data) + + await ngNew(version, workspaceDir, log) + await installDependencies(version, workspaceDir, log) + await copySourceFiles(workspaceDir) + await updateTsConfigs(workspaceDir, version) + await ngBuild(workspaceDir, log, LIB_NAME) + await jestRun(workspaceDir, log) + + console.log(`Angular ${version}: PASSED`) + return 0 + } catch (err) { + logErrorSummary(logStream, logFile, err, version) + return 1 + } finally { + logStream.end() + rm(tempDir) + } +} + +async function main() { + setupLogDir() + + const args = process.argv.slice(2) + const versionArgs = [] + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--version=')) { + versionArgs.push(args[i].split('=')[1]) + } else if (args[i] === '--version' && i + 1 < args.length) { + versionArgs.push(args[++i]) + } + } + + const versionsToTest = versionArgs.length > 0 ? versionArgs : VERSIONS + + console.log(`Starting tests for Angular versions: ${versionsToTest.join(', ')}`) + console.log(`Using pnpm store at: ${PNPM_STORE_DIR}`) + console.log(`Logs: ${LOG_DIR}`) + console.log('------------------------------------------------------------') + + ensurePnpm() + + const results = await Promise.all(versionsToTest.map((v) => testVersion(v))) + const failed = results.some((code) => code !== 0) + + console.log('------------------------------------------------------------') + if (failed) { + console.log('CI Pipeline Failed') + process.exit(1) + } else { + console.log('Success: All Angular versions passed') + process.exit(0) + } +} + +main() diff --git a/bin/test-matrix.sh b/bin/test-matrix.sh deleted file mode 100755 index 72916b0..0000000 --- a/bin/test-matrix.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/bin/bash - -export CI=true -export NG_CLI_ANALYTICS=false -export PNPM_STORE_DIR="$(pwd)/.pnpm-store" - -VERSIONS=("15" "16" "17" "18" "19" "20" "21") -LIB_NAME="fingerprintjs-pro-angular" - -SOURCE_PROJECT_DIR="$(pwd)/projects/$LIB_NAME" -SOURCE_LIB_DIR="$SOURCE_PROJECT_DIR/src/lib" -SOURCE_PUBLIC_API="$SOURCE_PROJECT_DIR/src/public-api.ts" -SOURCE_TEST_TS="$(pwd)/test.ts" -SOURCE_JEST_CONFIG="$(pwd)/jest.config.js" -SOURCE_ROOT_TSCONFIG="$(pwd)/tsconfig.json" -SOURCE_ROOT_TSCONFIG_SPEC="$(pwd)/tsconfig.spec.json" - -LOG_DIR="$(pwd)/test-logs" -mkdir -p "$LOG_DIR" -rm -f "$LOG_DIR"/* - -echo "Starting tests for Angular versions: ${VERSIONS[*]}" -echo "Using pnpm store at: $PNPM_STORE_DIR" -echo "Logs: $LOG_DIR" -echo "------------------------------------------------------------" - -if ! command -v pnpm &> /dev/null; then - npm install -g pnpm -fi - -pnpm config set store-dir "$PNPM_STORE_DIR" - -# Cross-platform sed -i -sedi() { - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i "" "$@" - else - sed -i "$@" - fi -} - -test_version() { - local VERSION=$1 - local LOG_FILE="$LOG_DIR/angular-$VERSION.log" - - if [ "$VERSION" -ge "20" ]; then - NODE_VERSION=$(node -v | cut -d'v' -f2) - NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'.' -f1) - NODE_MINOR=$(echo "$NODE_VERSION" | cut -d'.' -f2) - if [ "$NODE_MAJOR" -lt 20 ] || ([ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 19 ]); then - echo "Angular $VERSION: Skipped (Node.js $NODE_VERSION < v20.19)" - return 0 - fi - fi - - ( - set -e - TEMP_DIR=$(mktemp -d) - cd "$TEMP_DIR" - - npx -y @angular/cli@$VERSION new test-workspace --create-application=false --skip-git --skip-install --defaults --package-manager=pnpm - cd test-workspace - - pnpm install --config.fund=false - pnpm exec ng generate library "$LIB_NAME" --skip-install - pnpm install --config.fund=false --config.strict-peer-dependencies=false --no-frozen-lockfile - pnpm add @fingerprint/agent --config.fund=false --config.strict-peer-dependencies=false - pnpm add @angular/platform-browser-dynamic@$VERSION zone.js --config.fund=false --config.strict-peer-dependencies=false - pnpm add -D jest jest-preset-angular jest-environment-jsdom @types/jest @types/node --config.fund=false --config.strict-peer-dependencies=false - - rm -rf projects/"$LIB_NAME"/src/lib/* - cp -r "$SOURCE_LIB_DIR/"* projects/"$LIB_NAME"/src/lib/ - cp "$SOURCE_PUBLIC_API" projects/"$LIB_NAME"/src/public-api.ts - - [ -f "$SOURCE_TEST_TS" ] && cp "$SOURCE_TEST_TS" test.ts - [ -f "$SOURCE_JEST_CONFIG" ] && cp "$SOURCE_JEST_CONFIG" jest.config.js - [ -f "$SOURCE_ROOT_TSCONFIG" ] && cp "$SOURCE_ROOT_TSCONFIG" tsconfig.json - [ -f "$SOURCE_ROOT_TSCONFIG_SPEC" ] && cp "$SOURCE_ROOT_TSCONFIG_SPEC" tsconfig.spec.json - - if [ ! -f "tsconfig.spec.json" ]; then - echo '{"extends": "./tsconfig.json", "compilerOptions": {"types": ["jest", "node"]}}' > tsconfig.spec.json - fi - - cp "$SOURCE_PROJECT_DIR/ng-package.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/package.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.lib.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.lib.prod.json" projects/"$LIB_NAME"/ - cp "$SOURCE_PROJECT_DIR/tsconfig.spec.json" projects/"$LIB_NAME"/ - - for FILE in "tsconfig.json" "tsconfig.base.json" "projects/$LIB_NAME/tsconfig.spec.json"; do - if [ -f "$FILE" ]; then - sedi '/"types":/d' "$FILE" - sedi '/"esModuleInterop":/d' "$FILE" - sedi '/"allowSyntheticDefaultImports":/d' "$FILE" - sedi '/"skipLibCheck":/d' "$FILE" - sedi 's/"compilerOptions":\s*{/"compilerOptions": { "types": ["node", "jest"], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true,/g' "$FILE" - fi - done - - find . -name "tsconfig.spec.json" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "s/\"types\":\s*\[/\"types\": [\"node\", \"jest\", /g" "$1"' _ {} \; - find . -name "tsconfig.spec.json" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "s/\"compilerOptions\":\s*{/\"compilerOptions\": { \"types\": [\"node\", \"jest\"], \"esModuleInterop\": true, \"allowSyntheticDefaultImports\": true, \"skipLibCheck\": true,/g" "$1"' _ {} \; - - if [ -f "projects/$LIB_NAME/tsconfig.spec.json" ]; then - if [ ! -f "../../tsconfig.json" ] && [ -f "../../tsconfig.base.json" ]; then - sedi 's/"extends": "..\/..\/tsconfig.json"/"extends": "..\/..\/tsconfig.base.json"/g' projects/"$LIB_NAME"/tsconfig.spec.json - fi - fi - - find projects/"$LIB_NAME" -name "*.spec.ts" -exec bash -c 'sedi() { if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "$@"; else sed -i "$@"; fi; }; sedi "1s|^|/// \n|" "$1"' _ {} \; - - if [ "$VERSION" -ge "21" ]; then - sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.json - [ -f "tsconfig.base.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' tsconfig.base.json - [ -f "projects/$LIB_NAME/tsconfig.lib.json" ] && sedi 's/"moduleResolution": "node"/"moduleResolution": "bundler"/g' projects/"$LIB_NAME"/tsconfig.lib.json - fi - - pnpm exec ng build "$LIB_NAME" - pnpm exec jest - rm -rf "$TEMP_DIR" - ) > "$LOG_FILE" 2>&1 - - local STATUS=$? - if [ $STATUS -eq 0 ]; then - echo "Angular $VERSION: PASSED" - else - echo "Angular $VERSION: FAILED" - echo "Log: $LOG_FILE" - tail -n 15 "$LOG_FILE" - fi - - return $STATUS -} - -PIDS=() -for VERSION in "${VERSIONS[@]}"; do - test_version "$VERSION" & - PIDS+=($!) -done - -OVERALL_EXIT_CODE=0 -for PID in "${PIDS[@]}"; do - wait "$PID" || OVERALL_EXIT_CODE=$? -done - -echo "------------------------------------------------------------" -if [ $OVERALL_EXIT_CODE -ne 0 ]; then - echo "CI Pipeline Failed" - exit $OVERALL_EXIT_CODE -else - echo "Success: All Angular versions passed" - exit 0 -fi diff --git a/bin/utils/command.mjs b/bin/utils/command.mjs new file mode 100644 index 0000000..4c35ba4 --- /dev/null +++ b/bin/utils/command.mjs @@ -0,0 +1,22 @@ +import { spawn, execSync } from 'child_process' + +export function executeCommand(cmd, args, opts, log) { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, opts) + if (log) { + child.stdout.on('data', log) + child.stderr.on('data', log) + } + child.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`Command failed with code ${code}: ${cmd} ${args.join(' ')}`)) + } + }) + }) +} + +export function exec(cmd, options) { + return execSync(cmd, options) +} diff --git a/bin/utils/commands/ensurePnpm.mjs b/bin/utils/commands/ensurePnpm.mjs new file mode 100644 index 0000000..d786f09 --- /dev/null +++ b/bin/utils/commands/ensurePnpm.mjs @@ -0,0 +1,12 @@ +import { exec } from '../command.mjs' +import { PNPM_STORE_DIR } from '../constants.mjs' + +export function ensurePnpm() { + try { + exec('pnpm --version', { stdio: 'ignore' }) + } catch (e) { + console.log('pnpm not found, installing...') + exec('npm install -g pnpm', { stdio: 'inherit' }) + } + exec(`pnpm config set store-dir "${PNPM_STORE_DIR}"`, { stdio: 'inherit' }) +} diff --git a/bin/utils/commands/jestRun.mjs b/bin/utils/commands/jestRun.mjs new file mode 100644 index 0000000..99d0896 --- /dev/null +++ b/bin/utils/commands/jestRun.mjs @@ -0,0 +1,5 @@ +import { executeCommand } from '../command.mjs' + +export async function jestRun(workspaceDir, log) { + return executeCommand('pnpm', ['exec', 'jest'], { cwd: workspaceDir, env: { ...process.env } }, log) +} diff --git a/bin/utils/commands/ngBuild.mjs b/bin/utils/commands/ngBuild.mjs new file mode 100644 index 0000000..efe3f29 --- /dev/null +++ b/bin/utils/commands/ngBuild.mjs @@ -0,0 +1,5 @@ +import { executeCommand } from '../command.mjs' + +export async function ngBuild(workspaceDir, log, project) { + return executeCommand('pnpm', ['exec', 'ng', 'build', project], { cwd: workspaceDir, env: { ...process.env } }, log) +} diff --git a/bin/utils/commands/ngGenerate.mjs b/bin/utils/commands/ngGenerate.mjs new file mode 100644 index 0000000..4681c31 --- /dev/null +++ b/bin/utils/commands/ngGenerate.mjs @@ -0,0 +1,10 @@ +import { executeCommand } from '../command.mjs' + +export async function ngGenerate(workspaceDir, log, type, name, options = []) { + return executeCommand( + 'pnpm', + ['exec', 'ng', 'generate', type, name, ...options], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) +} diff --git a/bin/utils/commands/ngNew.mjs b/bin/utils/commands/ngNew.mjs new file mode 100644 index 0000000..62a01c6 --- /dev/null +++ b/bin/utils/commands/ngNew.mjs @@ -0,0 +1,23 @@ +import { executeCommand } from '../command.mjs' + +export async function ngNew(version, workspaceDir, log) { + const parentDir = workspaceDir.split('/').slice(0, -1).join('/') + const workspaceName = workspaceDir.split('/').pop() + + return executeCommand( + 'npx', + [ + '-y', + `@angular/cli@${version}`, + 'new', + workspaceName, + '--create-application=false', + '--skip-git', + '--skip-install', + '--defaults', + '--package-manager=pnpm', + ], + { cwd: parentDir, env: { ...process.env } }, + log + ) +} diff --git a/bin/utils/commands/pnpmAdd.mjs b/bin/utils/commands/pnpmAdd.mjs new file mode 100644 index 0000000..33d8530 --- /dev/null +++ b/bin/utils/commands/pnpmAdd.mjs @@ -0,0 +1,6 @@ +import { executeCommand } from '../command.mjs' + +export async function pnpmAdd(workspaceDir, log, packages, options = []) { + const args = ['add', ...packages, '--config.fund=false', ...options] + return executeCommand('pnpm', args, { cwd: workspaceDir, env: { ...process.env } }, log) +} diff --git a/bin/utils/commands/pnpmInstall.mjs b/bin/utils/commands/pnpmInstall.mjs new file mode 100644 index 0000000..5ba9370 --- /dev/null +++ b/bin/utils/commands/pnpmInstall.mjs @@ -0,0 +1,10 @@ +import { executeCommand } from '../command.mjs' + +export async function pnpmInstall(workspaceDir, log, options = []) { + return executeCommand( + 'pnpm', + ['install', '--config.fund=false', ...options], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) +} diff --git a/bin/utils/constants.mjs b/bin/utils/constants.mjs new file mode 100644 index 0000000..be98747 --- /dev/null +++ b/bin/utils/constants.mjs @@ -0,0 +1,17 @@ +import path from 'path' + +export const LIB_NAME = 'fingerprintjs-pro-angular' +export const VERSIONS = ['15', '16', '17', '18', '19', '20', '21'] + +export const ROOT_DIR = process.cwd() +export const PNPM_STORE_DIR = path.join(ROOT_DIR, '.pnpm-store') + +export const SOURCE_PROJECT_DIR = path.join(ROOT_DIR, 'projects', LIB_NAME) +export const SOURCE_LIB_DIR = path.join(SOURCE_PROJECT_DIR, 'src', 'lib') +export const SOURCE_PUBLIC_API = path.join(SOURCE_PROJECT_DIR, 'src', 'public-api.ts') +export const SOURCE_TEST_TS = path.join(ROOT_DIR, 'test.ts') +export const SOURCE_JEST_CONFIG = path.join(ROOT_DIR, 'jest.config.js') +export const SOURCE_ROOT_TSCONFIG = path.join(ROOT_DIR, 'tsconfig.json') +export const SOURCE_ROOT_TSCONFIG_SPEC = path.join(ROOT_DIR, 'tsconfig.spec.json') + +export const LOG_DIR = path.join(ROOT_DIR, 'test-logs') diff --git a/bin/utils/fs.mjs b/bin/utils/fs.mjs new file mode 100644 index 0000000..8e73406 --- /dev/null +++ b/bin/utils/fs.mjs @@ -0,0 +1,76 @@ +import fs from 'fs' +import path from 'path' +import os from 'os' + +export function joinPath(...args) { + return path.join(...args) +} + +export function exists(path) { + return fs.existsSync(path) +} + +export function mkdir(path, options = { recursive: true }) { + return fs.mkdirSync(path, options) +} + +export function rm(path, options = { recursive: true, force: true }) { + return fs.rmSync(path, options) +} + +export function readDir(path) { + return fs.readdirSync(path) +} + +export function unlink(path) { + return fs.unlinkSync(path) +} + +export function stat(path) { + return fs.statSync(path) +} + +export function readFile(path, encoding = 'utf8') { + return fs.readFileSync(path, encoding) +} + +export function writeFile(path, data, encoding = 'utf8') { + return fs.writeFileSync(path, data, encoding) +} + +export function mkdtemp(prefix) { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)) +} + +export function createWriteStream(path) { + return fs.createWriteStream(path) +} + +export function copyFile(src, dest) { + return fs.copyFileSync(src, dest) +} + +export function updateJsonFile(filePath, updater) { + if (!exists(filePath)) { + return + } + const content = readFile(filePath) + const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1') + const json = JSON.parse(cleanContent) + updater(json) + writeFile(filePath, JSON.stringify(json, null, 2)) +} + +export function copyRecursive(src, dest) { + const isDirectory = exists(src) && stat(src).isDirectory() + if (isDirectory) { + if (!exists(dest)) { + mkdir(dest) + } + readDir(src).forEach((childItemName) => { + copyRecursive(joinPath(src, childItemName), joinPath(dest, childItemName)) + }) + } else { + copyFile(src, dest) + } +} diff --git a/bin/utils/logErrorSummary.mjs b/bin/utils/logErrorSummary.mjs new file mode 100644 index 0000000..8621d93 --- /dev/null +++ b/bin/utils/logErrorSummary.mjs @@ -0,0 +1,15 @@ +import { readFile } from './fs.mjs' + +export function logErrorSummary(logStream, logFile, err, version) { + logStream.write(`\nError: ${err.message}\n`) + console.log(`Angular ${version}: FAILED`) + console.log(`Log: ${logFile}`) + + try { + const logContent = readFile(logFile) + const lines = logContent.split('\n') + console.log(lines.slice(-15).join('\n')) + } catch (e) { + // ignore + } +} diff --git a/bin/utils/setupLogDir.mjs b/bin/utils/setupLogDir.mjs new file mode 100644 index 0000000..ab9e58b --- /dev/null +++ b/bin/utils/setupLogDir.mjs @@ -0,0 +1,12 @@ +import { exists, joinPath, mkdir, readDir, unlink } from './fs.mjs' +import { LOG_DIR } from './constants.mjs' + +export function setupLogDir() { + if (!exists(LOG_DIR)) { + mkdir(LOG_DIR) + } else { + readDir(LOG_DIR).forEach((file) => { + unlink(joinPath(LOG_DIR, file)) + }) + } +} diff --git a/bin/utils/steps/copySourceFiles.mjs b/bin/utils/steps/copySourceFiles.mjs new file mode 100644 index 0000000..541b30e --- /dev/null +++ b/bin/utils/steps/copySourceFiles.mjs @@ -0,0 +1,49 @@ +import { copyRecursive, joinPath, exists, mkdir, copyFile, rm, writeFile } from '../fs.mjs' +import { + LIB_NAME, + SOURCE_PROJECT_DIR, + SOURCE_LIB_DIR, + SOURCE_PUBLIC_API, + SOURCE_TEST_TS, + SOURCE_JEST_CONFIG, + SOURCE_ROOT_TSCONFIG, + SOURCE_ROOT_TSCONFIG_SPEC, +} from '../constants.mjs' + +export async function copySourceFiles(workspaceDir) { + const libDestDir = joinPath(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') + if (exists(libDestDir)) { + rm(libDestDir) + } + mkdir(libDestDir) + + copyRecursive(SOURCE_LIB_DIR, libDestDir) + copyFile(SOURCE_PUBLIC_API, joinPath(workspaceDir, 'projects', LIB_NAME, 'src', 'public-api.ts')) + + if (exists(SOURCE_TEST_TS)) { + copyFile(SOURCE_TEST_TS, joinPath(workspaceDir, 'test.ts')) + } + if (exists(SOURCE_JEST_CONFIG)) { + copyFile(SOURCE_JEST_CONFIG, joinPath(workspaceDir, 'jest.config.js')) + } + if (exists(SOURCE_ROOT_TSCONFIG)) { + copyFile(SOURCE_ROOT_TSCONFIG, joinPath(workspaceDir, 'tsconfig.json')) + } + if (exists(SOURCE_ROOT_TSCONFIG_SPEC)) { + copyFile(SOURCE_ROOT_TSCONFIG_SPEC, joinPath(workspaceDir, 'tsconfig.spec.json')) + } + + if (!exists(joinPath(workspaceDir, 'tsconfig.spec.json'))) { + writeFile( + joinPath(workspaceDir, 'tsconfig.spec.json'), + JSON.stringify({ extends: './tsconfig.json', compilerOptions: { types: ['jest', 'node'] } }) + ) + } + + const projectDir = joinPath(workspaceDir, 'projects', LIB_NAME) + copyFile(joinPath(SOURCE_PROJECT_DIR, 'ng-package.json'), joinPath(projectDir, 'ng-package.json')) + copyFile(joinPath(SOURCE_PROJECT_DIR, 'package.json'), joinPath(projectDir, 'package.json')) + copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.lib.json'), joinPath(projectDir, 'tsconfig.lib.json')) + copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), joinPath(projectDir, 'tsconfig.lib.prod.json')) + copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.spec.json'), joinPath(projectDir, 'tsconfig.spec.json')) +} diff --git a/bin/utils/steps/installDependencies.mjs b/bin/utils/steps/installDependencies.mjs new file mode 100644 index 0000000..f06ab09 --- /dev/null +++ b/bin/utils/steps/installDependencies.mjs @@ -0,0 +1,23 @@ +import { pnpmInstall } from '../commands/pnpmInstall.mjs' +import { ngGenerate } from '../commands/ngGenerate.mjs' +import { pnpmAdd } from '../commands/pnpmAdd.mjs' +import { LIB_NAME } from '../constants.mjs' + +export async function installDependencies(version, workspaceDir, log) { + await pnpmInstall(workspaceDir, log) + await ngGenerate(workspaceDir, log, 'library', LIB_NAME, ['--skip-install']) + await pnpmInstall(workspaceDir, log, ['--config.strict-peer-dependencies=false', '--no-frozen-lockfile']) + await pnpmAdd(workspaceDir, log, ['@fingerprint/agent'], ['--config.strict-peer-dependencies=false']) + await pnpmAdd( + workspaceDir, + log, + [`@angular/platform-browser-dynamic@${version}`, 'zone.js'], + ['--config.strict-peer-dependencies=false'] + ) + const jestPackages = + parseInt(version) >= 18 + ? ['jest@^29.5.0', 'jest-preset-angular@^14.1.0', 'jest-environment-jsdom@^29.5.0', '@types/jest', '@types/node'] + : ['jest', 'jest-preset-angular', 'jest-environment-jsdom', '@types/jest', '@types/node'] + + await pnpmAdd(workspaceDir, log, jestPackages, ['-D', '--config.strict-peer-dependencies=false']) +} diff --git a/bin/utils/steps/updateTsConfigs.mjs b/bin/utils/steps/updateTsConfigs.mjs new file mode 100644 index 0000000..d5a26d2 --- /dev/null +++ b/bin/utils/steps/updateTsConfigs.mjs @@ -0,0 +1,101 @@ +import { updateJsonFile, joinPath, exists, readDir, readFile, writeFile, stat } from '../fs.mjs' +import { LIB_NAME } from '../constants.mjs' + +export async function updateTsConfigs(workspaceDir, version) { + const tsconfigFiles = [ + joinPath(workspaceDir, 'tsconfig.json'), + joinPath(workspaceDir, 'tsconfig.base.json'), + joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), + ] + + for (const file of tsconfigFiles) { + if (exists(file)) { + updateJsonFile(file, (json) => { + if (!json.compilerOptions) { + json.compilerOptions = {} + } + json.compilerOptions.types = ['node', 'jest'] + json.compilerOptions.esModuleInterop = true + json.compilerOptions.allowSyntheticDefaultImports = true + json.compilerOptions.skipLibCheck = true + }) + } + } + + const allSpecTsconfigs = [] + const findSpecConfigs = (dir) => { + readDir(dir).forEach((file) => { + const fullPath = joinPath(dir, file) + if (exists(fullPath) && stat(fullPath).isDirectory()) { + if (file !== 'node_modules') { + findSpecConfigs(fullPath) + } + } else if (file === 'tsconfig.spec.json') { + allSpecTsconfigs.push(fullPath) + } + }) + } + findSpecConfigs(workspaceDir) + + for (const file of allSpecTsconfigs) { + updateJsonFile(file, (json) => { + if (!json.compilerOptions) { + json.compilerOptions = {} + } + if (!json.compilerOptions.types) { + json.compilerOptions.types = [] + } + if (!json.compilerOptions.types.includes('node')) { + json.compilerOptions.types.push('node') + } + if (!json.compilerOptions.types.includes('jest')) { + json.compilerOptions.types.push('jest') + } + + json.compilerOptions.esModuleInterop = true + json.compilerOptions.allowSyntheticDefaultImports = true + json.compilerOptions.skipLibCheck = true + }) + } + + if (exists(joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'))) { + if (!exists(joinPath(workspaceDir, 'tsconfig.json')) && exists(joinPath(workspaceDir, 'tsconfig.base.json'))) { + updateJsonFile(joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), (json) => { + json.extends = '../../tsconfig.base.json' + }) + } + } + + const addNodeReference = (dir) => { + readDir(dir).forEach((file) => { + const fullPath = joinPath(dir, file) + if (exists(fullPath) && stat(fullPath).isDirectory()) { + addNodeReference(fullPath) + } else if (file.endsWith('.spec.ts')) { + let content = readFile(fullPath) + if (!content.startsWith('/// ')) { + content = '/// \n' + content + writeFile(fullPath, content) + } + } + }) + } + addNodeReference(joinPath(workspaceDir, 'projects', LIB_NAME)) + + if (parseInt(version) >= 21) { + const targetTsconfigs = [ + joinPath(workspaceDir, 'tsconfig.json'), + joinPath(workspaceDir, 'tsconfig.base.json'), + joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.lib.json'), + ] + for (const configPath of targetTsconfigs) { + if (exists(configPath)) { + updateJsonFile(configPath, (json) => { + if (json.compilerOptions) { + json.compilerOptions.moduleResolution = 'bundler' + } + }) + } + } + } +} diff --git a/package.json b/package.json index 08f87a3..15f59b4 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,12 @@ "build:ssr": "ng build && ng run fingerprintjs-pro-angular-demo:server", "prerender": "ng run fingerprintjs-pro-angular-demo:prerender", "generate:version": "node bin/generate-version.js", - "lint": "eslint --ext .js,.ts --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ext .js,.ts --ignore-path .gitignore --max-warnings 0 --fix .", + "lint": "eslint --ext .js,.mjs,.ts --ignore-path .gitignore --max-warnings 0 .", + "lint:fix": "eslint --ext .js,.mjs,.ts --ignore-path .gitignore --max-warnings 0 --fix .", "test:dts": "tsc -p tsconfig.test-dts.json", "test": "jest", "test:coverage": "jest --coverage", + "test:matrix": "node bin/test-matrix.mjs", "docs": "typedoc projects/fingerprintjs-pro-angular/src/public-api.ts --out docs", "changeset:publish": "HUSKY=0 pnpm build && changeset publish", "changeset:version": "changeset version" From 64e83d81c558b9e6b825e9e756f2f7970779f4a7 Mon Sep 17 00:00:00 2001 From: Orkun Date: Tue, 23 Jun 2026 13:01:37 +0300 Subject: [PATCH 04/13] refactor: node version and ci workflow adaptation --- .github/workflows/test-matrix.yml | 6 ------ .node-version | 2 +- package.json | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 1c05bd6..1149366 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -46,11 +46,5 @@ jobs: - name: Generate project version run: pnpm generate:version - - name: Setup environment variables - run: | - echo "CI=true" >> $GITHUB_ENV - echo "NG_CLI_ANALYTICS=false" >> $GITHUB_ENV - echo "LIB_NAME=fingerprintjs-pro-angular" >> $GITHUB_ENV - - name: Run tests for Angular ${{ matrix.angular-version }} run: pnpm run test:matrix -- --version=${{ matrix.angular-version }} diff --git a/.node-version b/.node-version index 2bd5a0a..3c03207 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22 +18 diff --git a/package.json b/package.json index 15f59b4..a0bcac5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fingerprintjs-pro-angular-demo", "version": "0.0.0", "engines": { - "node": ">=22.0.0" + "node": ">=18.0.0" }, "scripts": { "prepare": "husky install", From e7b9c0616be25d2d9b34955e352589048808d072 Mon Sep 17 00:00:00 2001 From: Orkun Date: Tue, 23 Jun 2026 13:30:59 +0300 Subject: [PATCH 05/13] refactor: ensure jest, build and dependencies for each major --- bin/test-matrix.mjs | 2 + bin/utils/commands/jestRun.mjs | 2 +- bin/utils/commands/ngBuild.mjs | 7 ++- bin/utils/steps/installDependencies.mjs | 80 +++++++++++++++++++++++-- bin/utils/steps/setupDemoApp.mjs | 65 ++++++++++++++++++++ 5 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 bin/utils/steps/setupDemoApp.mjs diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 5bc3578..490c666 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -9,6 +9,7 @@ import { jestRun } from './utils/commands/jestRun.mjs' import { copySourceFiles } from './utils/steps/copySourceFiles.mjs' import { updateTsConfigs } from './utils/steps/updateTsConfigs.mjs' import { installDependencies } from './utils/steps/installDependencies.mjs' +import { setupDemoApp } from './utils/steps/setupDemoApp.mjs' import { setupLogDir } from './utils/setupLogDir.mjs' import { logErrorSummary } from './utils/logErrorSummary.mjs' @@ -38,6 +39,7 @@ async function testVersion(version) { await ngNew(version, workspaceDir, log) await installDependencies(version, workspaceDir, log) + await setupDemoApp(workspaceDir, version, log) await copySourceFiles(workspaceDir) await updateTsConfigs(workspaceDir, version) await ngBuild(workspaceDir, log, LIB_NAME) diff --git a/bin/utils/commands/jestRun.mjs b/bin/utils/commands/jestRun.mjs index 99d0896..f2c0476 100644 --- a/bin/utils/commands/jestRun.mjs +++ b/bin/utils/commands/jestRun.mjs @@ -1,5 +1,5 @@ import { executeCommand } from '../command.mjs' export async function jestRun(workspaceDir, log) { - return executeCommand('pnpm', ['exec', 'jest'], { cwd: workspaceDir, env: { ...process.env } }, log) + return executeCommand('./node_modules/.bin/jest', [], { cwd: workspaceDir, env: { ...process.env } }, log) } diff --git a/bin/utils/commands/ngBuild.mjs b/bin/utils/commands/ngBuild.mjs index efe3f29..702aca7 100644 --- a/bin/utils/commands/ngBuild.mjs +++ b/bin/utils/commands/ngBuild.mjs @@ -1,5 +1,10 @@ import { executeCommand } from '../command.mjs' export async function ngBuild(workspaceDir, log, project) { - return executeCommand('pnpm', ['exec', 'ng', 'build', project], { cwd: workspaceDir, env: { ...process.env } }, log) + return executeCommand( + './node_modules/.bin/ng', + ['build', project], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) } diff --git a/bin/utils/steps/installDependencies.mjs b/bin/utils/steps/installDependencies.mjs index f06ab09..be82806 100644 --- a/bin/utils/steps/installDependencies.mjs +++ b/bin/utils/steps/installDependencies.mjs @@ -4,20 +4,88 @@ import { pnpmAdd } from '../commands/pnpmAdd.mjs' import { LIB_NAME } from '../constants.mjs' export async function installDependencies(version, workspaceDir, log) { + const majorVersion = parseInt(version) await pnpmInstall(workspaceDir, log) await ngGenerate(workspaceDir, log, 'library', LIB_NAME, ['--skip-install']) await pnpmInstall(workspaceDir, log, ['--config.strict-peer-dependencies=false', '--no-frozen-lockfile']) await pnpmAdd(workspaceDir, log, ['@fingerprint/agent'], ['--config.strict-peer-dependencies=false']) + let zoneVersion = 'latest' + if (majorVersion >= 21) { + zoneVersion = '~0.16.0' + } else if (majorVersion >= 19) { + zoneVersion = '~0.15.0' + } else if (majorVersion >= 17) { + zoneVersion = '~0.14.0' + } else if (majorVersion >= 16) { + zoneVersion = '~0.13.0' + } else if (majorVersion >= 15) { + zoneVersion = '~0.11.4' + } + await pnpmAdd( workspaceDir, log, - [`@angular/platform-browser-dynamic@${version}`, 'zone.js'], + [`@angular/platform-browser-dynamic@^${version}`, `zone.js@${zoneVersion}`], ['--config.strict-peer-dependencies=false'] ) - const jestPackages = - parseInt(version) >= 18 - ? ['jest@^29.5.0', 'jest-preset-angular@^14.1.0', 'jest-environment-jsdom@^29.5.0', '@types/jest', '@types/node'] - : ['jest', 'jest-preset-angular', 'jest-environment-jsdom', '@types/jest', '@types/node'] + let jestPackages = [] + const builderPackages = [`ng-packagr@^${version}`] + + if (majorVersion >= 21) { + jestPackages = [ + 'jest@^30.0.0', + 'jest-preset-angular@^17.0.0', + 'jest-environment-jsdom@^30.0.0', + '@types/jest@^29.5.0', + '@types/node', + ] + } else if (majorVersion >= 20) { + jestPackages = [ + 'jest@^29.7.0', + 'jest-preset-angular@^14.6.0', + 'jest-environment-jsdom@^29.7.0', + '@types/jest@^29.5.0', + '@types/node', + ] + } else if (majorVersion >= 19) { + jestPackages = [ + 'jest@^29.7.0', + 'jest-preset-angular@^14.6.0', + 'jest-environment-jsdom@^29.7.0', + '@types/jest@^29.5.0', + '@types/node', + ] + } else if (majorVersion >= 18) { + jestPackages = [ + 'jest@^29.5.0', + 'jest-preset-angular@^14.1.0', + 'jest-environment-jsdom@^29.5.0', + '@types/jest', + '@types/node', + ] + } else if (majorVersion >= 16) { + jestPackages = [ + 'jest@^29.0.0', + 'jest-preset-angular@^13.1.0', + 'jest-environment-jsdom@^29.0.0', + '@types/jest', + '@types/node', + ] + } else { + // Angular 15 + jestPackages = [ + 'jest@^28.0.0', + 'jest-preset-angular@^12.2.0', + 'jest-environment-jsdom@^28.0.0', + '@types/jest', + '@types/node', + ] + } - await pnpmAdd(workspaceDir, log, jestPackages, ['-D', '--config.strict-peer-dependencies=false']) + await pnpmAdd( + workspaceDir, + log, + [...jestPackages, ...builderPackages], + ['-D', '--config.strict-peer-dependencies=false'] + ) } diff --git a/bin/utils/steps/setupDemoApp.mjs b/bin/utils/steps/setupDemoApp.mjs new file mode 100644 index 0000000..c2f5b41 --- /dev/null +++ b/bin/utils/steps/setupDemoApp.mjs @@ -0,0 +1,65 @@ +import { ngGenerate } from '../commands/ngGenerate.mjs' +import { joinPath, exists, writeFile, readFile } from '../fs.mjs' + +export async function setupDemoApp(workspaceDir, version, log) { + const isLegacy = parseInt(version) === 15 + const options = ['--routing=false', '--style=css', '--skip-tests=true'] + + if (!isLegacy) { + options.push('--standalone=true') + } + + await ngGenerate(workspaceDir, log, 'application', 'demo', options) + + const appDir = joinPath(workspaceDir, 'projects', 'demo', 'src', 'app') + + if (isLegacy) { + const appModulePath = joinPath(appDir, 'app.module.ts') + if (exists(appModulePath)) { + let content = readFile(appModulePath) + content = content.replace( + "import { NgModule } from '@angular/core';", + "import { NgModule } from '@angular/core';\nimport { FingerprintModule } from '@fingerprint/angular';" + ) + content = content.replace( + 'imports: [', + `imports: [\n FingerprintModule.forRoot({ startOptions: { apiKey: 'test-key' } }),` + ) + writeFile(appModulePath, content) + } + } else { + const appConfigPath = joinPath(appDir, 'app.config.ts') + // In some versions it might be in src/app/app.config.ts, in others it might be differently structured + // Angular 17+ usually has app.config.ts if standalone + if (exists(appConfigPath)) { + let content = readFile(appConfigPath) + content = content.replace( + "import { ApplicationConfig } from '@angular/core';", + "import { ApplicationConfig } from '@angular/core';\nimport { provideFingerprint } from '@fingerprint/angular';" + ) + content = content.replace( + 'providers: [', + `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` + ) + writeFile(appConfigPath, content) + } else { + // Fallback or manual creation if it's Angular 16 standalone but didn't create app.config.ts + // Actually v16 might use main.ts for bootstrap if app.config doesn't exist + const mainTsPath = joinPath(workspaceDir, 'projects', 'demo', 'src', 'main.ts') + if (exists(mainTsPath)) { + let content = readFile(mainTsPath) + if (content.includes('bootstrapApplication')) { + content = content.replace( + "import { bootstrapApplication } from '@angular/platform-browser';", + "import { bootstrapApplication } from '@angular/platform-browser';\nimport { provideFingerprint } from '@fingerprint/angular';" + ) + content = content.replace( + 'providers: [', + `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` + ) + writeFile(mainTsPath, content) + } + } + } + } +} From 360c7c046cb79b7a936bc909a695c312f55a6d4d Mon Sep 17 00:00:00 2001 From: Orkun Date: Tue, 23 Jun 2026 15:52:10 +0300 Subject: [PATCH 06/13] refactor: if else blocks to map --- bin/test-matrix.mjs | 1 - bin/utils/steps/installDependencies.mjs | 81 +++++++++---------------- bin/utils/steps/setupDemoApp.mjs | 4 -- 3 files changed, 30 insertions(+), 56 deletions(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 490c666..86b6a6e 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -21,7 +21,6 @@ async function testVersion(version) { const logFile = joinPath(LOG_DIR, `angular-${version}.log`) const logStream = createWriteStream(logFile) - // Node version check for Angular 20+ if (parseInt(version) >= 20) { const nodeVersion = process.versions.node const [major, minor] = nodeVersion.split('.').map(Number) diff --git a/bin/utils/steps/installDependencies.mjs b/bin/utils/steps/installDependencies.mjs index be82806..9cdffeb 100644 --- a/bin/utils/steps/installDependencies.mjs +++ b/bin/utils/steps/installDependencies.mjs @@ -9,79 +9,58 @@ export async function installDependencies(version, workspaceDir, log) { await ngGenerate(workspaceDir, log, 'library', LIB_NAME, ['--skip-install']) await pnpmInstall(workspaceDir, log, ['--config.strict-peer-dependencies=false', '--no-frozen-lockfile']) await pnpmAdd(workspaceDir, log, ['@fingerprint/agent'], ['--config.strict-peer-dependencies=false']) - let zoneVersion = 'latest' - if (majorVersion >= 21) { - zoneVersion = '~0.16.0' - } else if (majorVersion >= 19) { - zoneVersion = '~0.15.0' - } else if (majorVersion >= 17) { - zoneVersion = '~0.14.0' - } else if (majorVersion >= 16) { - zoneVersion = '~0.13.0' - } else if (majorVersion >= 15) { - zoneVersion = '~0.11.4' + + const zoneVersionMap = { + 21: '~0.16.0', + 19: '~0.15.0', + 17: '~0.14.0', + 16: '~0.13.0', + 15: '~0.11.4', } + const zoneVersionKey = Object.keys(zoneVersionMap) + .map(Number) + .sort((a, b) => b - a) + .find((v) => majorVersion >= v) + + const zoneVersion = zoneVersionKey ? zoneVersionMap[zoneVersionKey] : 'latest' + await pnpmAdd( workspaceDir, log, [`@angular/platform-browser-dynamic@^${version}`, `zone.js@${zoneVersion}`], ['--config.strict-peer-dependencies=false'] ) - let jestPackages = [] + const builderPackages = [`ng-packagr@^${version}`] - if (majorVersion >= 21) { - jestPackages = [ + const jestPackagesMap = { + 21: [ 'jest@^30.0.0', 'jest-preset-angular@^17.0.0', 'jest-environment-jsdom@^30.0.0', '@types/jest@^29.5.0', '@types/node', - ] - } else if (majorVersion >= 20) { - jestPackages = [ - 'jest@^29.7.0', - 'jest-preset-angular@^14.6.0', - 'jest-environment-jsdom@^29.7.0', - '@types/jest@^29.5.0', - '@types/node', - ] - } else if (majorVersion >= 19) { - jestPackages = [ + ], + 19: [ 'jest@^29.7.0', 'jest-preset-angular@^14.6.0', 'jest-environment-jsdom@^29.7.0', '@types/jest@^29.5.0', '@types/node', - ] - } else if (majorVersion >= 18) { - jestPackages = [ - 'jest@^29.5.0', - 'jest-preset-angular@^14.1.0', - 'jest-environment-jsdom@^29.5.0', - '@types/jest', - '@types/node', - ] - } else if (majorVersion >= 16) { - jestPackages = [ - 'jest@^29.0.0', - 'jest-preset-angular@^13.1.0', - 'jest-environment-jsdom@^29.0.0', - '@types/jest', - '@types/node', - ] - } else { - // Angular 15 - jestPackages = [ - 'jest@^28.0.0', - 'jest-preset-angular@^12.2.0', - 'jest-environment-jsdom@^28.0.0', - '@types/jest', - '@types/node', - ] + ], + 18: ['jest@^29.5.0', 'jest-preset-angular@^14.1.0', 'jest-environment-jsdom@^29.5.0', '@types/jest', '@types/node'], + 16: ['jest@^29.0.0', 'jest-preset-angular@^13.1.0', 'jest-environment-jsdom@^29.0.0', '@types/jest', '@types/node'], + 0: ['jest@^28.0.0', 'jest-preset-angular@^12.2.0', 'jest-environment-jsdom@^28.0.0', '@types/jest', '@types/node'], } + const jestVersionKey = Object.keys(jestPackagesMap) + .map(Number) + .sort((a, b) => b - a) + .find((v) => majorVersion >= v) + + const jestPackages = jestPackagesMap[jestVersionKey] + await pnpmAdd( workspaceDir, log, diff --git a/bin/utils/steps/setupDemoApp.mjs b/bin/utils/steps/setupDemoApp.mjs index c2f5b41..b925f02 100644 --- a/bin/utils/steps/setupDemoApp.mjs +++ b/bin/utils/steps/setupDemoApp.mjs @@ -29,8 +29,6 @@ export async function setupDemoApp(workspaceDir, version, log) { } } else { const appConfigPath = joinPath(appDir, 'app.config.ts') - // In some versions it might be in src/app/app.config.ts, in others it might be differently structured - // Angular 17+ usually has app.config.ts if standalone if (exists(appConfigPath)) { let content = readFile(appConfigPath) content = content.replace( @@ -43,8 +41,6 @@ export async function setupDemoApp(workspaceDir, version, log) { ) writeFile(appConfigPath, content) } else { - // Fallback or manual creation if it's Angular 16 standalone but didn't create app.config.ts - // Actually v16 might use main.ts for bootstrap if app.config doesn't exist const mainTsPath = joinPath(workspaceDir, 'projects', 'demo', 'src', 'main.ts') if (exists(mainTsPath)) { let content = readFile(mainTsPath) From f0efde527effd0ae1a2f9cc904d4f72badebb797 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 10:12:45 +0300 Subject: [PATCH 07/13] refactor: remove redundant fs wrappers --- bin/test-matrix.mjs | 14 +++--- bin/utils/fs.mjs | 67 ++++------------------------- bin/utils/logErrorSummary.mjs | 4 +- bin/utils/setupLogDir.mjs | 11 ++--- bin/utils/steps/copySourceFiles.mjs | 48 +++++++++++---------- bin/utils/steps/setupDemoApp.mjs | 29 +++++++------ bin/utils/steps/updateTsConfigs.mjs | 44 ++++++++++--------- 7 files changed, 88 insertions(+), 129 deletions(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 86b6a6e..e414aae 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env node -import { joinPath, mkdtemp, createWriteStream, rm } from './utils/fs.mjs' +import fs from 'fs' +import path from 'path' +import os from 'os' import { PNPM_STORE_DIR, VERSIONS, LIB_NAME, LOG_DIR } from './utils/constants.mjs' import { ensurePnpm } from './utils/commands/ensurePnpm.mjs' import { ngNew } from './utils/commands/ngNew.mjs' @@ -18,8 +20,8 @@ process.env.NG_CLI_ANALYTICS = 'false' process.env.PNPM_STORE_DIR = PNPM_STORE_DIR async function testVersion(version) { - const logFile = joinPath(LOG_DIR, `angular-${version}.log`) - const logStream = createWriteStream(logFile) + const logFile = path.join(LOG_DIR, `angular-${version}.log`) + const logStream = fs.createWriteStream(logFile) if (parseInt(version) >= 20) { const nodeVersion = process.versions.node @@ -30,8 +32,8 @@ async function testVersion(version) { } } - const tempDir = mkdtemp('angular-test-') - const workspaceDir = joinPath(tempDir, 'test-workspace') + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'angular-test-')) + const workspaceDir = path.join(tempDir, 'test-workspace') try { const log = (data) => logStream.write(data) @@ -51,7 +53,7 @@ async function testVersion(version) { return 1 } finally { logStream.end() - rm(tempDir) + fs.rmSync(tempDir, { recursive: true, force: true }) } } diff --git a/bin/utils/fs.mjs b/bin/utils/fs.mjs index 8e73406..e1f5694 100644 --- a/bin/utils/fs.mjs +++ b/bin/utils/fs.mjs @@ -1,76 +1,27 @@ import fs from 'fs' import path from 'path' -import os from 'os' - -export function joinPath(...args) { - return path.join(...args) -} - -export function exists(path) { - return fs.existsSync(path) -} - -export function mkdir(path, options = { recursive: true }) { - return fs.mkdirSync(path, options) -} - -export function rm(path, options = { recursive: true, force: true }) { - return fs.rmSync(path, options) -} - -export function readDir(path) { - return fs.readdirSync(path) -} - -export function unlink(path) { - return fs.unlinkSync(path) -} - -export function stat(path) { - return fs.statSync(path) -} - -export function readFile(path, encoding = 'utf8') { - return fs.readFileSync(path, encoding) -} - -export function writeFile(path, data, encoding = 'utf8') { - return fs.writeFileSync(path, data, encoding) -} - -export function mkdtemp(prefix) { - return fs.mkdtempSync(path.join(os.tmpdir(), prefix)) -} - -export function createWriteStream(path) { - return fs.createWriteStream(path) -} - -export function copyFile(src, dest) { - return fs.copyFileSync(src, dest) -} export function updateJsonFile(filePath, updater) { - if (!exists(filePath)) { + if (!fs.existsSync(filePath)) { return } - const content = readFile(filePath) + const content = fs.readFileSync(filePath, 'utf8') const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1') const json = JSON.parse(cleanContent) updater(json) - writeFile(filePath, JSON.stringify(json, null, 2)) + fs.writeFileSync(filePath, JSON.stringify(json, null, 2), 'utf8') } export function copyRecursive(src, dest) { - const isDirectory = exists(src) && stat(src).isDirectory() + const isDirectory = fs.existsSync(src) && fs.statSync(src).isDirectory() if (isDirectory) { - if (!exists(dest)) { - mkdir(dest) + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }) } - readDir(src).forEach((childItemName) => { - copyRecursive(joinPath(src, childItemName), joinPath(dest, childItemName)) + fs.readdirSync(src).forEach((childItemName) => { + copyRecursive(path.join(src, childItemName), path.join(dest, childItemName)) }) } else { - copyFile(src, dest) + fs.copyFileSync(src, dest) } } diff --git a/bin/utils/logErrorSummary.mjs b/bin/utils/logErrorSummary.mjs index 8621d93..2eac8c8 100644 --- a/bin/utils/logErrorSummary.mjs +++ b/bin/utils/logErrorSummary.mjs @@ -1,4 +1,4 @@ -import { readFile } from './fs.mjs' +import fs from 'fs' export function logErrorSummary(logStream, logFile, err, version) { logStream.write(`\nError: ${err.message}\n`) @@ -6,7 +6,7 @@ export function logErrorSummary(logStream, logFile, err, version) { console.log(`Log: ${logFile}`) try { - const logContent = readFile(logFile) + const logContent = fs.readFileSync(logFile, 'utf8') const lines = logContent.split('\n') console.log(lines.slice(-15).join('\n')) } catch (e) { diff --git a/bin/utils/setupLogDir.mjs b/bin/utils/setupLogDir.mjs index ab9e58b..695edde 100644 --- a/bin/utils/setupLogDir.mjs +++ b/bin/utils/setupLogDir.mjs @@ -1,12 +1,13 @@ -import { exists, joinPath, mkdir, readDir, unlink } from './fs.mjs' +import fs from 'fs' +import path from 'path' import { LOG_DIR } from './constants.mjs' export function setupLogDir() { - if (!exists(LOG_DIR)) { - mkdir(LOG_DIR) + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }) } else { - readDir(LOG_DIR).forEach((file) => { - unlink(joinPath(LOG_DIR, file)) + fs.readdirSync(LOG_DIR).forEach((file) => { + fs.unlinkSync(path.join(LOG_DIR, file)) }) } } diff --git a/bin/utils/steps/copySourceFiles.mjs b/bin/utils/steps/copySourceFiles.mjs index 541b30e..66d347f 100644 --- a/bin/utils/steps/copySourceFiles.mjs +++ b/bin/utils/steps/copySourceFiles.mjs @@ -1,4 +1,6 @@ -import { copyRecursive, joinPath, exists, mkdir, copyFile, rm, writeFile } from '../fs.mjs' +import path from 'path' +import fs from 'fs' +import { copyRecursive } from '../fs.mjs' import { LIB_NAME, SOURCE_PROJECT_DIR, @@ -11,39 +13,39 @@ import { } from '../constants.mjs' export async function copySourceFiles(workspaceDir) { - const libDestDir = joinPath(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') - if (exists(libDestDir)) { - rm(libDestDir) + const libDestDir = path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') + if (fs.existsSync(libDestDir)) { + fs.rmSync(libDestDir, { recursive: true, force: true }) } - mkdir(libDestDir) + fs.mkdirSync(libDestDir, { recursive: true }) copyRecursive(SOURCE_LIB_DIR, libDestDir) - copyFile(SOURCE_PUBLIC_API, joinPath(workspaceDir, 'projects', LIB_NAME, 'src', 'public-api.ts')) + fs.copyFileSync(SOURCE_PUBLIC_API, path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'public-api.ts')) - if (exists(SOURCE_TEST_TS)) { - copyFile(SOURCE_TEST_TS, joinPath(workspaceDir, 'test.ts')) + if (fs.existsSync(SOURCE_TEST_TS)) { + fs.copyFileSync(SOURCE_TEST_TS, path.join(workspaceDir, 'test.ts')) } - if (exists(SOURCE_JEST_CONFIG)) { - copyFile(SOURCE_JEST_CONFIG, joinPath(workspaceDir, 'jest.config.js')) + if (fs.existsSync(SOURCE_JEST_CONFIG)) { + fs.copyFileSync(SOURCE_JEST_CONFIG, path.join(workspaceDir, 'jest.config.js')) } - if (exists(SOURCE_ROOT_TSCONFIG)) { - copyFile(SOURCE_ROOT_TSCONFIG, joinPath(workspaceDir, 'tsconfig.json')) + if (fs.existsSync(SOURCE_ROOT_TSCONFIG)) { + fs.copyFileSync(SOURCE_ROOT_TSCONFIG, path.join(workspaceDir, 'tsconfig.json')) } - if (exists(SOURCE_ROOT_TSCONFIG_SPEC)) { - copyFile(SOURCE_ROOT_TSCONFIG_SPEC, joinPath(workspaceDir, 'tsconfig.spec.json')) + if (fs.existsSync(SOURCE_ROOT_TSCONFIG_SPEC)) { + fs.copyFileSync(SOURCE_ROOT_TSCONFIG_SPEC, path.join(workspaceDir, 'tsconfig.spec.json')) } - if (!exists(joinPath(workspaceDir, 'tsconfig.spec.json'))) { - writeFile( - joinPath(workspaceDir, 'tsconfig.spec.json'), + if (!fs.existsSync(path.join(workspaceDir, 'tsconfig.spec.json'))) { + fs.writeFileSync( + path.join(workspaceDir, 'tsconfig.spec.json'), JSON.stringify({ extends: './tsconfig.json', compilerOptions: { types: ['jest', 'node'] } }) ) } - const projectDir = joinPath(workspaceDir, 'projects', LIB_NAME) - copyFile(joinPath(SOURCE_PROJECT_DIR, 'ng-package.json'), joinPath(projectDir, 'ng-package.json')) - copyFile(joinPath(SOURCE_PROJECT_DIR, 'package.json'), joinPath(projectDir, 'package.json')) - copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.lib.json'), joinPath(projectDir, 'tsconfig.lib.json')) - copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), joinPath(projectDir, 'tsconfig.lib.prod.json')) - copyFile(joinPath(SOURCE_PROJECT_DIR, 'tsconfig.spec.json'), joinPath(projectDir, 'tsconfig.spec.json')) + const projectDir = path.join(workspaceDir, 'projects', LIB_NAME) + fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'ng-package.json'), path.join(projectDir, 'ng-package.json')) + fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'package.json'), path.join(projectDir, 'package.json')) + fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.json'), path.join(projectDir, 'tsconfig.lib.json')) + fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), path.join(projectDir, 'tsconfig.lib.prod.json')) + fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.spec.json'), path.join(projectDir, 'tsconfig.spec.json')) } diff --git a/bin/utils/steps/setupDemoApp.mjs b/bin/utils/steps/setupDemoApp.mjs index b925f02..49f80d0 100644 --- a/bin/utils/steps/setupDemoApp.mjs +++ b/bin/utils/steps/setupDemoApp.mjs @@ -1,5 +1,6 @@ +import fs from 'fs' +import path from 'path' import { ngGenerate } from '../commands/ngGenerate.mjs' -import { joinPath, exists, writeFile, readFile } from '../fs.mjs' export async function setupDemoApp(workspaceDir, version, log) { const isLegacy = parseInt(version) === 15 @@ -11,12 +12,12 @@ export async function setupDemoApp(workspaceDir, version, log) { await ngGenerate(workspaceDir, log, 'application', 'demo', options) - const appDir = joinPath(workspaceDir, 'projects', 'demo', 'src', 'app') + const appDir = path.join(workspaceDir, 'projects', 'demo', 'src', 'app') if (isLegacy) { - const appModulePath = joinPath(appDir, 'app.module.ts') - if (exists(appModulePath)) { - let content = readFile(appModulePath) + const appModulePath = path.join(appDir, 'app.module.ts') + if (fs.existsSync(appModulePath)) { + let content = fs.readFileSync(appModulePath, 'utf8') content = content.replace( "import { NgModule } from '@angular/core';", "import { NgModule } from '@angular/core';\nimport { FingerprintModule } from '@fingerprint/angular';" @@ -25,12 +26,12 @@ export async function setupDemoApp(workspaceDir, version, log) { 'imports: [', `imports: [\n FingerprintModule.forRoot({ startOptions: { apiKey: 'test-key' } }),` ) - writeFile(appModulePath, content) + fs.writeFileSync(appModulePath, content, 'utf8') } } else { - const appConfigPath = joinPath(appDir, 'app.config.ts') - if (exists(appConfigPath)) { - let content = readFile(appConfigPath) + const appConfigPath = path.join(appDir, 'app.config.ts') + if (fs.existsSync(appConfigPath)) { + let content = fs.readFileSync(appConfigPath, 'utf8') content = content.replace( "import { ApplicationConfig } from '@angular/core';", "import { ApplicationConfig } from '@angular/core';\nimport { provideFingerprint } from '@fingerprint/angular';" @@ -39,11 +40,11 @@ export async function setupDemoApp(workspaceDir, version, log) { 'providers: [', `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` ) - writeFile(appConfigPath, content) + fs.writeFileSync(appConfigPath, content, 'utf8') } else { - const mainTsPath = joinPath(workspaceDir, 'projects', 'demo', 'src', 'main.ts') - if (exists(mainTsPath)) { - let content = readFile(mainTsPath) + const mainTsPath = path.join(workspaceDir, 'projects', 'demo', 'src', 'main.ts') + if (fs.existsSync(mainTsPath)) { + let content = fs.readFileSync(mainTsPath, 'utf8') if (content.includes('bootstrapApplication')) { content = content.replace( "import { bootstrapApplication } from '@angular/platform-browser';", @@ -53,7 +54,7 @@ export async function setupDemoApp(workspaceDir, version, log) { 'providers: [', `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` ) - writeFile(mainTsPath, content) + fs.writeFileSync(mainTsPath, content, 'utf8') } } } diff --git a/bin/utils/steps/updateTsConfigs.mjs b/bin/utils/steps/updateTsConfigs.mjs index d5a26d2..52c5dc8 100644 --- a/bin/utils/steps/updateTsConfigs.mjs +++ b/bin/utils/steps/updateTsConfigs.mjs @@ -1,15 +1,17 @@ -import { updateJsonFile, joinPath, exists, readDir, readFile, writeFile, stat } from '../fs.mjs' +import fs from 'fs' +import path from 'path' +import { updateJsonFile } from '../fs.mjs' import { LIB_NAME } from '../constants.mjs' export async function updateTsConfigs(workspaceDir, version) { const tsconfigFiles = [ - joinPath(workspaceDir, 'tsconfig.json'), - joinPath(workspaceDir, 'tsconfig.base.json'), - joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), + path.join(workspaceDir, 'tsconfig.json'), + path.join(workspaceDir, 'tsconfig.base.json'), + path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), ] for (const file of tsconfigFiles) { - if (exists(file)) { + if (fs.existsSync(file)) { updateJsonFile(file, (json) => { if (!json.compilerOptions) { json.compilerOptions = {} @@ -24,9 +26,9 @@ export async function updateTsConfigs(workspaceDir, version) { const allSpecTsconfigs = [] const findSpecConfigs = (dir) => { - readDir(dir).forEach((file) => { - const fullPath = joinPath(dir, file) - if (exists(fullPath) && stat(fullPath).isDirectory()) { + fs.readdirSync(dir).forEach((file) => { + const fullPath = path.join(dir, file) + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { if (file !== 'node_modules') { findSpecConfigs(fullPath) } @@ -58,38 +60,38 @@ export async function updateTsConfigs(workspaceDir, version) { }) } - if (exists(joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'))) { - if (!exists(joinPath(workspaceDir, 'tsconfig.json')) && exists(joinPath(workspaceDir, 'tsconfig.base.json'))) { - updateJsonFile(joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), (json) => { + if (fs.existsSync(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'))) { + if (!fs.existsSync(path.join(workspaceDir, 'tsconfig.json')) && fs.existsSync(path.join(workspaceDir, 'tsconfig.base.json'))) { + updateJsonFile(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), (json) => { json.extends = '../../tsconfig.base.json' }) } } const addNodeReference = (dir) => { - readDir(dir).forEach((file) => { - const fullPath = joinPath(dir, file) - if (exists(fullPath) && stat(fullPath).isDirectory()) { + fs.readdirSync(dir).forEach((file) => { + const fullPath = path.join(dir, file) + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { addNodeReference(fullPath) } else if (file.endsWith('.spec.ts')) { - let content = readFile(fullPath) + let content = fs.readFileSync(fullPath, 'utf8') if (!content.startsWith('/// ')) { content = '/// \n' + content - writeFile(fullPath, content) + fs.writeFileSync(fullPath, content, 'utf8') } } }) } - addNodeReference(joinPath(workspaceDir, 'projects', LIB_NAME)) + addNodeReference(path.join(workspaceDir, 'projects', LIB_NAME)) if (parseInt(version) >= 21) { const targetTsconfigs = [ - joinPath(workspaceDir, 'tsconfig.json'), - joinPath(workspaceDir, 'tsconfig.base.json'), - joinPath(workspaceDir, 'projects', LIB_NAME, 'tsconfig.lib.json'), + path.join(workspaceDir, 'tsconfig.json'), + path.join(workspaceDir, 'tsconfig.base.json'), + path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.lib.json'), ] for (const configPath of targetTsconfigs) { - if (exists(configPath)) { + if (fs.existsSync(configPath)) { updateJsonFile(configPath, (json) => { if (json.compilerOptions) { json.compilerOptions.moduleResolution = 'bundler' From 638125bab5c022cbcb185fa6c481639da999cafa Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 10:22:08 +0300 Subject: [PATCH 08/13] refactor: throw error if pnpm not installed --- bin/test-matrix.mjs | 7 ++++++- bin/utils/commands/ensurePnpm.mjs | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index e414aae..36ab2df 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -77,7 +77,12 @@ async function main() { console.log(`Logs: ${LOG_DIR}`) console.log('------------------------------------------------------------') - ensurePnpm() + try { + ensurePnpm() + } catch (e) { + console.error(e) + process.exit(1) + } const results = await Promise.all(versionsToTest.map((v) => testVersion(v))) const failed = results.some((code) => code !== 0) diff --git a/bin/utils/commands/ensurePnpm.mjs b/bin/utils/commands/ensurePnpm.mjs index d786f09..30fcef0 100644 --- a/bin/utils/commands/ensurePnpm.mjs +++ b/bin/utils/commands/ensurePnpm.mjs @@ -5,8 +5,7 @@ export function ensurePnpm() { try { exec('pnpm --version', { stdio: 'ignore' }) } catch (e) { - console.log('pnpm not found, installing...') - exec('npm install -g pnpm', { stdio: 'inherit' }) + throw new Error('pnpm not found') } exec(`pnpm config set store-dir "${PNPM_STORE_DIR}"`, { stdio: 'inherit' }) } From a995db21b96aada7137ea20c080277604907c3c8 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 10:22:46 +0300 Subject: [PATCH 09/13] refactor: remove CI from hardcoded env --- bin/test-matrix.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 36ab2df..7253895 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -15,7 +15,6 @@ import { setupDemoApp } from './utils/steps/setupDemoApp.mjs' import { setupLogDir } from './utils/setupLogDir.mjs' import { logErrorSummary } from './utils/logErrorSummary.mjs' -process.env.CI = 'true' process.env.NG_CLI_ANALYTICS = 'false' process.env.PNPM_STORE_DIR = PNPM_STORE_DIR From 3de9b6e5af9ab2db25077bb8554c522749380b26 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 10:23:46 +0300 Subject: [PATCH 10/13] refactor: remove redundant main wrapper function --- bin/test-matrix.mjs | 64 ++++++++++++++--------------- bin/utils/steps/copySourceFiles.mjs | 5 ++- bin/utils/steps/updateTsConfigs.mjs | 5 ++- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 7253895..d48620d 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -56,44 +56,40 @@ async function testVersion(version) { } } -async function main() { - setupLogDir() - - const args = process.argv.slice(2) - const versionArgs = [] - for (let i = 0; i < args.length; i++) { - if (args[i].startsWith('--version=')) { - versionArgs.push(args[i].split('=')[1]) - } else if (args[i] === '--version' && i + 1 < args.length) { - versionArgs.push(args[++i]) - } +setupLogDir() + +const args = process.argv.slice(2) +const versionArgs = [] +for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--version=')) { + versionArgs.push(args[i].split('=')[1]) + } else if (args[i] === '--version' && i + 1 < args.length) { + versionArgs.push(args[++i]) } +} - const versionsToTest = versionArgs.length > 0 ? versionArgs : VERSIONS +const versionsToTest = versionArgs.length > 0 ? versionArgs : VERSIONS - console.log(`Starting tests for Angular versions: ${versionsToTest.join(', ')}`) - console.log(`Using pnpm store at: ${PNPM_STORE_DIR}`) - console.log(`Logs: ${LOG_DIR}`) - console.log('------------------------------------------------------------') +console.log(`Starting tests for Angular versions: ${versionsToTest.join(', ')}`) +console.log(`Using pnpm store at: ${PNPM_STORE_DIR}`) +console.log(`Logs: ${LOG_DIR}`) +console.log('------------------------------------------------------------') - try { - ensurePnpm() - } catch (e) { - console.error(e) - process.exit(1) - } +try { + ensurePnpm() +} catch (e) { + console.error(e) + process.exit(1) +} - const results = await Promise.all(versionsToTest.map((v) => testVersion(v))) - const failed = results.some((code) => code !== 0) +const results = await Promise.all(versionsToTest.map((v) => testVersion(v))) +const failed = results.some((code) => code !== 0) - console.log('------------------------------------------------------------') - if (failed) { - console.log('CI Pipeline Failed') - process.exit(1) - } else { - console.log('Success: All Angular versions passed') - process.exit(0) - } +console.log('------------------------------------------------------------') +if (failed) { + console.log('CI Pipeline Failed') + process.exit(1) +} else { + console.log('Success: All Angular versions passed') + process.exit(0) } - -main() diff --git a/bin/utils/steps/copySourceFiles.mjs b/bin/utils/steps/copySourceFiles.mjs index 66d347f..8fad401 100644 --- a/bin/utils/steps/copySourceFiles.mjs +++ b/bin/utils/steps/copySourceFiles.mjs @@ -46,6 +46,9 @@ export async function copySourceFiles(workspaceDir) { fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'ng-package.json'), path.join(projectDir, 'ng-package.json')) fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'package.json'), path.join(projectDir, 'package.json')) fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.json'), path.join(projectDir, 'tsconfig.lib.json')) - fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), path.join(projectDir, 'tsconfig.lib.prod.json')) + fs.copyFileSync( + path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), + path.join(projectDir, 'tsconfig.lib.prod.json') + ) fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.spec.json'), path.join(projectDir, 'tsconfig.spec.json')) } diff --git a/bin/utils/steps/updateTsConfigs.mjs b/bin/utils/steps/updateTsConfigs.mjs index 52c5dc8..6ff0c9c 100644 --- a/bin/utils/steps/updateTsConfigs.mjs +++ b/bin/utils/steps/updateTsConfigs.mjs @@ -61,7 +61,10 @@ export async function updateTsConfigs(workspaceDir, version) { } if (fs.existsSync(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'))) { - if (!fs.existsSync(path.join(workspaceDir, 'tsconfig.json')) && fs.existsSync(path.join(workspaceDir, 'tsconfig.base.json'))) { + if ( + !fs.existsSync(path.join(workspaceDir, 'tsconfig.json')) && + fs.existsSync(path.join(workspaceDir, 'tsconfig.base.json')) + ) { updateJsonFile(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), (json) => { json.extends = '../../tsconfig.base.json' }) From f16e4d8b42d0f351b0a116bf046ddcd54e0732c8 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 15:21:30 +0300 Subject: [PATCH 11/13] refactor: simplify test matrix --- .github/workflows/test-matrix.yml | 41 +++-- bin/test-matrix.mjs | 202 ++++++++++++++++++------ bin/utils/angularMetadata.mjs | 9 ++ bin/utils/command.mjs | 22 --- bin/utils/commands/ensurePnpm.mjs | 11 -- bin/utils/commands/jestRun.mjs | 5 - bin/utils/commands/ngBuild.mjs | 10 -- bin/utils/commands/ngGenerate.mjs | 10 -- bin/utils/commands/ngNew.mjs | 23 --- bin/utils/commands/pnpmAdd.mjs | 6 - bin/utils/commands/pnpmInstall.mjs | 10 -- bin/utils/constants.mjs | 17 -- bin/utils/fs.mjs | 27 ---- bin/utils/helpers.mjs | 67 ++++++++ bin/utils/logErrorSummary.mjs | 15 -- bin/utils/setupLogDir.mjs | 13 -- bin/utils/steps/copySourceFiles.mjs | 54 ------- bin/utils/steps/installDependencies.mjs | 70 -------- bin/utils/steps/setupDemoApp.mjs | 62 -------- bin/utils/steps/updateTsConfigs.mjs | 106 ------------- 20 files changed, 252 insertions(+), 528 deletions(-) create mode 100644 bin/utils/angularMetadata.mjs delete mode 100644 bin/utils/command.mjs delete mode 100644 bin/utils/commands/ensurePnpm.mjs delete mode 100644 bin/utils/commands/jestRun.mjs delete mode 100644 bin/utils/commands/ngBuild.mjs delete mode 100644 bin/utils/commands/ngGenerate.mjs delete mode 100644 bin/utils/commands/ngNew.mjs delete mode 100644 bin/utils/commands/pnpmAdd.mjs delete mode 100644 bin/utils/commands/pnpmInstall.mjs delete mode 100644 bin/utils/constants.mjs delete mode 100644 bin/utils/fs.mjs create mode 100644 bin/utils/helpers.mjs delete mode 100644 bin/utils/logErrorSummary.mjs delete mode 100644 bin/utils/setupLogDir.mjs delete mode 100644 bin/utils/steps/copySourceFiles.mjs delete mode 100644 bin/utils/steps/installDependencies.mjs delete mode 100644 bin/utils/steps/setupDemoApp.mjs delete mode 100644 bin/utils/steps/updateTsConfigs.mjs diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 1149366..5199721 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -5,38 +5,37 @@ on: jobs: test-matrix: - name: Angular ${{ matrix.angular-version }} + name: Angular ${{ matrix.angular }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - angular-version: [15, 16, 17, 18, 19, 20, 21] include: - - angular-version: 15 - node-version: 18 - - angular-version: 16 - node-version: 18 - - angular-version: 17 - node-version: 18 - - angular-version: 18 - node-version: 20 - - angular-version: 19 - node-version: 20 - - angular-version: 20 - node-version: 20 - - angular-version: 21 - node-version: 22 + - angular: 15 + node: 18 + - angular: 16 + node: 18 + - angular: 17 + node: 18 + - angular: 18 + node: 20 + - angular: 19 + node: 20 + - angular: 20 + node: 20 + - angular: 21 + node: 22 steps: - uses: actions/checkout@v4 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ matrix.node }} - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 with: version: 8 @@ -46,5 +45,5 @@ jobs: - name: Generate project version run: pnpm generate:version - - name: Run tests for Angular ${{ matrix.angular-version }} - run: pnpm run test:matrix -- --version=${{ matrix.angular-version }} + - name: Run Matrix Check for Angular ${{ matrix.angular }} + run: pnpm run test:matrix -- --version=${{ matrix.angular }} diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index d48620d..867b552 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -3,51 +3,173 @@ import fs from 'fs' import path from 'path' import os from 'os' -import { PNPM_STORE_DIR, VERSIONS, LIB_NAME, LOG_DIR } from './utils/constants.mjs' -import { ensurePnpm } from './utils/commands/ensurePnpm.mjs' -import { ngNew } from './utils/commands/ngNew.mjs' -import { ngBuild } from './utils/commands/ngBuild.mjs' -import { jestRun } from './utils/commands/jestRun.mjs' -import { copySourceFiles } from './utils/steps/copySourceFiles.mjs' -import { updateTsConfigs } from './utils/steps/updateTsConfigs.mjs' -import { installDependencies } from './utils/steps/installDependencies.mjs' -import { setupDemoApp } from './utils/steps/setupDemoApp.mjs' -import { setupLogDir } from './utils/setupLogDir.mjs' -import { logErrorSummary } from './utils/logErrorSummary.mjs' +import { angularMetadata } from './utils/angularMetadata.mjs' +import { logErrorSummary, setupLogDir, executeCommand, updateJsonFile, copyRecursive } from './utils/helpers.mjs' + +const LIB_NAME = 'fingerprintjs-pro-angular' +const LOG_DIR = path.join(process.cwd(), 'test-logs') process.env.NG_CLI_ANALYTICS = 'false' -process.env.PNPM_STORE_DIR = PNPM_STORE_DIR async function testVersion(version) { + const meta = angularMetadata[version] + if (!meta) { + const errorMsg = `No metadata found for version ${version}` + console.error(errorMsg) + const logFile = path.join(LOG_DIR, `angular-${version}.log`) + fs.writeFileSync(logFile, errorMsg) + return 1 + } + const logFile = path.join(LOG_DIR, `angular-${version}.log`) const logStream = fs.createWriteStream(logFile) - - if (parseInt(version) >= 20) { - const nodeVersion = process.versions.node - const [major, minor] = nodeVersion.split('.').map(Number) - if (major < 20 || (major === 20 && minor < 19)) { - console.log(`Angular ${version}: Skipped (Node.js ${nodeVersion} < v20.19)`) - return 0 - } - } + const log = (data) => logStream.write(data) const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'angular-test-')) const workspaceDir = path.join(tempDir, 'test-workspace') try { - const log = (data) => logStream.write(data) + log(`Angular ${version}: Starting check...\n`) + console.log(`Angular ${version}: Starting check...`) + + // 1. Create Workspace + await executeCommand( + 'npx', + [ + '-y', + `@angular/cli@${version}`, + 'new', + 'test-workspace', + '--create-application=false', + '--skip-git', + '--skip-install', + '--defaults', + '--package-manager=pnpm', + ], + { cwd: tempDir, env: { ...process.env } }, + log + ) + + // Ensure npx worked and we have node_modules in the workspace after next steps + // Actually, step 1 just creates the folder structure. + // We need to install @angular/cli locally in the temp workspace to use it via ./node_modules/.bin/ng + // OR we use npx again for generate. + + // 2. Generate Library + await executeCommand( + 'npx', + ['-y', `@angular/cli@${version}`, 'generate', 'library', LIB_NAME, '--skip-install'], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + const angularVersionTag = `v${version}-lts` + + // 3. Install Peer Deps and Required Packages + const packagesToInstall = [ + `@angular/animations@${angularVersionTag}`, + `@angular/common@${angularVersionTag}`, + `@angular/compiler@${angularVersionTag}`, + `@angular/core@${angularVersionTag}`, + `@angular/forms@${angularVersionTag}`, + `@angular/platform-browser@${angularVersionTag}`, + `@angular/platform-browser-dynamic@${angularVersionTag}`, + `@angular/router@${angularVersionTag}`, + `zone.js@${meta.zone}`, + '@fingerprint/agent', + ] + + const jestVersion = meta.jest + const typesJestVersion = meta.typesJest || jestVersion + + const devPackagesToInstall = [ + `@angular/cli@${angularVersionTag}`, + `@angular/compiler-cli@${angularVersionTag}`, + `@angular-devkit/build-angular@${angularVersionTag}`, + `ng-packagr@^${version}`, + `typescript@${meta.typescript}`, + `jest-preset-angular@${meta.jpa}`, + `jest@${jestVersion}`, + `jest-environment-jsdom@${jestVersion}`, + `@types/jest@${typesJestVersion}`, + '@types/node', + ] - await ngNew(version, workspaceDir, log) - await installDependencies(version, workspaceDir, log) - await setupDemoApp(workspaceDir, version, log) - await copySourceFiles(workspaceDir) - await updateTsConfigs(workspaceDir, version) - await ngBuild(workspaceDir, log, LIB_NAME) - await jestRun(workspaceDir, log) + const commonOptions = ['--config.strict-peer-dependencies=false', '--no-lockfile', '--config.fund=false'] + + // We install everything in one go to avoid version mismatches and repeat installations + await executeCommand( + 'pnpm', + ['add', ...packagesToInstall, ...devPackagesToInstall, ...commonOptions], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + // 4. Copy Source Files + const libDestDir = path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') + fs.mkdirSync(libDestDir, { recursive: true }) + copyRecursive(path.join(process.cwd(), 'projects', LIB_NAME, 'src', 'lib'), libDestDir) + + fs.copyFileSync( + path.join(process.cwd(), 'projects', LIB_NAME, 'src', 'public-api.ts'), + path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'public-api.ts') + ) + + const rootFiles = ['jest.config.js', 'tsconfig.json', 'tsconfig.spec.json', 'test.ts'] + for (const file of rootFiles) { + const src = path.join(process.cwd(), file) + if (fs.existsSync(src)) { + fs.copyFileSync(src, path.join(workspaceDir, file)) + } + } + + const projectDir = path.join(workspaceDir, 'projects', LIB_NAME) + const projectFiles = [ + 'ng-package.json', + 'package.json', + 'tsconfig.lib.json', + 'tsconfig.lib.prod.json', + 'tsconfig.spec.json', + ] + for (const file of projectFiles) { + fs.copyFileSync(path.join(process.cwd(), 'projects', LIB_NAME, file), path.join(projectDir, file)) + } + + // 5. Update TS Configs for compatibility + const tsconfigFiles = [ + path.join(workspaceDir, 'tsconfig.json'), + path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), + ] + for (const file of tsconfigFiles) { + if (fs.existsSync(file)) { + updateJsonFile(file, (json) => { + if (!json.compilerOptions) { + json.compilerOptions = {} + } + json.compilerOptions.skipLibCheck = true + json.compilerOptions.esModuleInterop = true + if (parseInt(version) >= 21) { + json.compilerOptions.moduleResolution = 'bundler' + } + }) + } + } + + // 6. Build + await executeCommand( + './node_modules/.bin/ng', + ['build', LIB_NAME], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + // 7. Test + await executeCommand('./node_modules/.bin/jest', [], { cwd: workspaceDir, env: { ...process.env } }, log) console.log(`Angular ${version}: PASSED`) return 0 } catch (err) { + log(err.stack || err.message) logErrorSummary(logStream, logFile, err, version) return 1 } finally { @@ -56,7 +178,7 @@ async function testVersion(version) { } } -setupLogDir() +setupLogDir(LOG_DIR) const args = process.argv.slice(2) const versionArgs = [] @@ -68,28 +190,16 @@ for (let i = 0; i < args.length; i++) { } } -const versionsToTest = versionArgs.length > 0 ? versionArgs : VERSIONS +const versionsToTest = versionArgs.length > 0 ? versionArgs : Object.keys(angularMetadata) -console.log(`Starting tests for Angular versions: ${versionsToTest.join(', ')}`) -console.log(`Using pnpm store at: ${PNPM_STORE_DIR}`) -console.log(`Logs: ${LOG_DIR}`) -console.log('------------------------------------------------------------') - -try { - ensurePnpm() -} catch (e) { - console.error(e) - process.exit(1) -} +console.log(`Starting matrix tests for: ${versionsToTest.join(', ')}`) -const results = await Promise.all(versionsToTest.map((v) => testVersion(v))) +const results = await Promise.all(versionsToTest.map((version) => testVersion(version))) const failed = results.some((code) => code !== 0) -console.log('------------------------------------------------------------') if (failed) { - console.log('CI Pipeline Failed') process.exit(1) } else { - console.log('Success: All Angular versions passed') + console.log('Success: All versions passed') process.exit(0) } diff --git a/bin/utils/angularMetadata.mjs b/bin/utils/angularMetadata.mjs new file mode 100644 index 0000000..3c02013 --- /dev/null +++ b/bin/utils/angularMetadata.mjs @@ -0,0 +1,9 @@ +export const angularMetadata = { + 15: { node: 18, typescript: '~4.9.5', zone: '~0.11.4', jpa: '^13.1.4', jest: '^29.0.0' }, + 16: { node: 18, typescript: '~5.1.6', zone: '~0.13.3', jpa: '^13.1.4', jest: '^29.0.0' }, + 17: { node: 18, typescript: '~5.2.2', zone: '~0.14.4', jpa: '^14.1.1', jest: '^29.0.0' }, + 18: { node: 20, typescript: '~5.4.5', zone: '~0.14.4', jpa: '^14.1.1', jest: '^29.0.0' }, + 19: { node: 20, typescript: '~5.6.3', zone: '~0.15.0', jpa: '^14.4.2', jest: '^29.0.0' }, + 20: { node: 20, typescript: '~5.8.3', zone: '~0.15.0', jpa: '^15.0.0', jest: '^30.2.0', typesJest: '^30.0.0' }, + 21: { node: 22, typescript: '~5.9.2', zone: '~0.16.0', jpa: '^17.0.0', jest: '^30.2.0', typesJest: '^30.0.0' }, +} diff --git a/bin/utils/command.mjs b/bin/utils/command.mjs deleted file mode 100644 index 4c35ba4..0000000 --- a/bin/utils/command.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { spawn, execSync } from 'child_process' - -export function executeCommand(cmd, args, opts, log) { - return new Promise((resolve, reject) => { - const child = spawn(cmd, args, opts) - if (log) { - child.stdout.on('data', log) - child.stderr.on('data', log) - } - child.on('close', (code) => { - if (code === 0) { - resolve() - } else { - reject(new Error(`Command failed with code ${code}: ${cmd} ${args.join(' ')}`)) - } - }) - }) -} - -export function exec(cmd, options) { - return execSync(cmd, options) -} diff --git a/bin/utils/commands/ensurePnpm.mjs b/bin/utils/commands/ensurePnpm.mjs deleted file mode 100644 index 30fcef0..0000000 --- a/bin/utils/commands/ensurePnpm.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { exec } from '../command.mjs' -import { PNPM_STORE_DIR } from '../constants.mjs' - -export function ensurePnpm() { - try { - exec('pnpm --version', { stdio: 'ignore' }) - } catch (e) { - throw new Error('pnpm not found') - } - exec(`pnpm config set store-dir "${PNPM_STORE_DIR}"`, { stdio: 'inherit' }) -} diff --git a/bin/utils/commands/jestRun.mjs b/bin/utils/commands/jestRun.mjs deleted file mode 100644 index f2c0476..0000000 --- a/bin/utils/commands/jestRun.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function jestRun(workspaceDir, log) { - return executeCommand('./node_modules/.bin/jest', [], { cwd: workspaceDir, env: { ...process.env } }, log) -} diff --git a/bin/utils/commands/ngBuild.mjs b/bin/utils/commands/ngBuild.mjs deleted file mode 100644 index 702aca7..0000000 --- a/bin/utils/commands/ngBuild.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function ngBuild(workspaceDir, log, project) { - return executeCommand( - './node_modules/.bin/ng', - ['build', project], - { cwd: workspaceDir, env: { ...process.env } }, - log - ) -} diff --git a/bin/utils/commands/ngGenerate.mjs b/bin/utils/commands/ngGenerate.mjs deleted file mode 100644 index 4681c31..0000000 --- a/bin/utils/commands/ngGenerate.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function ngGenerate(workspaceDir, log, type, name, options = []) { - return executeCommand( - 'pnpm', - ['exec', 'ng', 'generate', type, name, ...options], - { cwd: workspaceDir, env: { ...process.env } }, - log - ) -} diff --git a/bin/utils/commands/ngNew.mjs b/bin/utils/commands/ngNew.mjs deleted file mode 100644 index 62a01c6..0000000 --- a/bin/utils/commands/ngNew.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function ngNew(version, workspaceDir, log) { - const parentDir = workspaceDir.split('/').slice(0, -1).join('/') - const workspaceName = workspaceDir.split('/').pop() - - return executeCommand( - 'npx', - [ - '-y', - `@angular/cli@${version}`, - 'new', - workspaceName, - '--create-application=false', - '--skip-git', - '--skip-install', - '--defaults', - '--package-manager=pnpm', - ], - { cwd: parentDir, env: { ...process.env } }, - log - ) -} diff --git a/bin/utils/commands/pnpmAdd.mjs b/bin/utils/commands/pnpmAdd.mjs deleted file mode 100644 index 33d8530..0000000 --- a/bin/utils/commands/pnpmAdd.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function pnpmAdd(workspaceDir, log, packages, options = []) { - const args = ['add', ...packages, '--config.fund=false', ...options] - return executeCommand('pnpm', args, { cwd: workspaceDir, env: { ...process.env } }, log) -} diff --git a/bin/utils/commands/pnpmInstall.mjs b/bin/utils/commands/pnpmInstall.mjs deleted file mode 100644 index 5ba9370..0000000 --- a/bin/utils/commands/pnpmInstall.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { executeCommand } from '../command.mjs' - -export async function pnpmInstall(workspaceDir, log, options = []) { - return executeCommand( - 'pnpm', - ['install', '--config.fund=false', ...options], - { cwd: workspaceDir, env: { ...process.env } }, - log - ) -} diff --git a/bin/utils/constants.mjs b/bin/utils/constants.mjs deleted file mode 100644 index be98747..0000000 --- a/bin/utils/constants.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import path from 'path' - -export const LIB_NAME = 'fingerprintjs-pro-angular' -export const VERSIONS = ['15', '16', '17', '18', '19', '20', '21'] - -export const ROOT_DIR = process.cwd() -export const PNPM_STORE_DIR = path.join(ROOT_DIR, '.pnpm-store') - -export const SOURCE_PROJECT_DIR = path.join(ROOT_DIR, 'projects', LIB_NAME) -export const SOURCE_LIB_DIR = path.join(SOURCE_PROJECT_DIR, 'src', 'lib') -export const SOURCE_PUBLIC_API = path.join(SOURCE_PROJECT_DIR, 'src', 'public-api.ts') -export const SOURCE_TEST_TS = path.join(ROOT_DIR, 'test.ts') -export const SOURCE_JEST_CONFIG = path.join(ROOT_DIR, 'jest.config.js') -export const SOURCE_ROOT_TSCONFIG = path.join(ROOT_DIR, 'tsconfig.json') -export const SOURCE_ROOT_TSCONFIG_SPEC = path.join(ROOT_DIR, 'tsconfig.spec.json') - -export const LOG_DIR = path.join(ROOT_DIR, 'test-logs') diff --git a/bin/utils/fs.mjs b/bin/utils/fs.mjs deleted file mode 100644 index e1f5694..0000000 --- a/bin/utils/fs.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs' -import path from 'path' - -export function updateJsonFile(filePath, updater) { - if (!fs.existsSync(filePath)) { - return - } - const content = fs.readFileSync(filePath, 'utf8') - const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1') - const json = JSON.parse(cleanContent) - updater(json) - fs.writeFileSync(filePath, JSON.stringify(json, null, 2), 'utf8') -} - -export function copyRecursive(src, dest) { - const isDirectory = fs.existsSync(src) && fs.statSync(src).isDirectory() - if (isDirectory) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true }) - } - fs.readdirSync(src).forEach((childItemName) => { - copyRecursive(path.join(src, childItemName), path.join(dest, childItemName)) - }) - } else { - fs.copyFileSync(src, dest) - } -} diff --git a/bin/utils/helpers.mjs b/bin/utils/helpers.mjs new file mode 100644 index 0000000..e2cdbf5 --- /dev/null +++ b/bin/utils/helpers.mjs @@ -0,0 +1,67 @@ +import fs from 'fs' +import path from 'path' +import { spawn } from 'child_process' + +export function setupLogDir(logDir) { + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) + } else { + fs.readdirSync(logDir).forEach((file) => { + fs.unlinkSync(path.join(logDir, file)) + }) + } +} + +export function logErrorSummary(logStream, logFile, err, version) { + console.log(`Angular ${version}: FAILED (see ${logFile})`) + + try { + const logContent = fs.readFileSync(logFile, 'utf8') + const lines = logContent.split('\n') + console.log(lines.slice(-15).join('\n')) + } catch (e) { + // ignore + } +} + +export function executeCommand(cmd, args, opts, log) { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, opts) + if (log) { + child.stdout.on('data', log) + child.stderr.on('data', log) + } + child.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`Command failed with code ${code}: ${cmd} ${args.join(' ')}`)) + } + }) + }) +} + +export function updateJsonFile(filePath, updater) { + if (!fs.existsSync(filePath)) { + return + } + const content = fs.readFileSync(filePath, 'utf8') + const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1') + const json = JSON.parse(cleanContent) + updater(json) + fs.writeFileSync(filePath, JSON.stringify(json, null, 2), 'utf8') +} + +export function copyRecursive(src, dest) { + const isDirectory = fs.existsSync(src) && fs.statSync(src).isDirectory() + if (isDirectory) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }) + } + fs.readdirSync(src).forEach((childItemName) => { + copyRecursive(path.join(src, childItemName), path.join(dest, childItemName)) + }) + } else { + fs.copyFileSync(src, dest) + } +} diff --git a/bin/utils/logErrorSummary.mjs b/bin/utils/logErrorSummary.mjs deleted file mode 100644 index 2eac8c8..0000000 --- a/bin/utils/logErrorSummary.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'fs' - -export function logErrorSummary(logStream, logFile, err, version) { - logStream.write(`\nError: ${err.message}\n`) - console.log(`Angular ${version}: FAILED`) - console.log(`Log: ${logFile}`) - - try { - const logContent = fs.readFileSync(logFile, 'utf8') - const lines = logContent.split('\n') - console.log(lines.slice(-15).join('\n')) - } catch (e) { - // ignore - } -} diff --git a/bin/utils/setupLogDir.mjs b/bin/utils/setupLogDir.mjs deleted file mode 100644 index 695edde..0000000 --- a/bin/utils/setupLogDir.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { LOG_DIR } from './constants.mjs' - -export function setupLogDir() { - if (!fs.existsSync(LOG_DIR)) { - fs.mkdirSync(LOG_DIR, { recursive: true }) - } else { - fs.readdirSync(LOG_DIR).forEach((file) => { - fs.unlinkSync(path.join(LOG_DIR, file)) - }) - } -} diff --git a/bin/utils/steps/copySourceFiles.mjs b/bin/utils/steps/copySourceFiles.mjs deleted file mode 100644 index 8fad401..0000000 --- a/bin/utils/steps/copySourceFiles.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import path from 'path' -import fs from 'fs' -import { copyRecursive } from '../fs.mjs' -import { - LIB_NAME, - SOURCE_PROJECT_DIR, - SOURCE_LIB_DIR, - SOURCE_PUBLIC_API, - SOURCE_TEST_TS, - SOURCE_JEST_CONFIG, - SOURCE_ROOT_TSCONFIG, - SOURCE_ROOT_TSCONFIG_SPEC, -} from '../constants.mjs' - -export async function copySourceFiles(workspaceDir) { - const libDestDir = path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') - if (fs.existsSync(libDestDir)) { - fs.rmSync(libDestDir, { recursive: true, force: true }) - } - fs.mkdirSync(libDestDir, { recursive: true }) - - copyRecursive(SOURCE_LIB_DIR, libDestDir) - fs.copyFileSync(SOURCE_PUBLIC_API, path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'public-api.ts')) - - if (fs.existsSync(SOURCE_TEST_TS)) { - fs.copyFileSync(SOURCE_TEST_TS, path.join(workspaceDir, 'test.ts')) - } - if (fs.existsSync(SOURCE_JEST_CONFIG)) { - fs.copyFileSync(SOURCE_JEST_CONFIG, path.join(workspaceDir, 'jest.config.js')) - } - if (fs.existsSync(SOURCE_ROOT_TSCONFIG)) { - fs.copyFileSync(SOURCE_ROOT_TSCONFIG, path.join(workspaceDir, 'tsconfig.json')) - } - if (fs.existsSync(SOURCE_ROOT_TSCONFIG_SPEC)) { - fs.copyFileSync(SOURCE_ROOT_TSCONFIG_SPEC, path.join(workspaceDir, 'tsconfig.spec.json')) - } - - if (!fs.existsSync(path.join(workspaceDir, 'tsconfig.spec.json'))) { - fs.writeFileSync( - path.join(workspaceDir, 'tsconfig.spec.json'), - JSON.stringify({ extends: './tsconfig.json', compilerOptions: { types: ['jest', 'node'] } }) - ) - } - - const projectDir = path.join(workspaceDir, 'projects', LIB_NAME) - fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'ng-package.json'), path.join(projectDir, 'ng-package.json')) - fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'package.json'), path.join(projectDir, 'package.json')) - fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.json'), path.join(projectDir, 'tsconfig.lib.json')) - fs.copyFileSync( - path.join(SOURCE_PROJECT_DIR, 'tsconfig.lib.prod.json'), - path.join(projectDir, 'tsconfig.lib.prod.json') - ) - fs.copyFileSync(path.join(SOURCE_PROJECT_DIR, 'tsconfig.spec.json'), path.join(projectDir, 'tsconfig.spec.json')) -} diff --git a/bin/utils/steps/installDependencies.mjs b/bin/utils/steps/installDependencies.mjs deleted file mode 100644 index 9cdffeb..0000000 --- a/bin/utils/steps/installDependencies.mjs +++ /dev/null @@ -1,70 +0,0 @@ -import { pnpmInstall } from '../commands/pnpmInstall.mjs' -import { ngGenerate } from '../commands/ngGenerate.mjs' -import { pnpmAdd } from '../commands/pnpmAdd.mjs' -import { LIB_NAME } from '../constants.mjs' - -export async function installDependencies(version, workspaceDir, log) { - const majorVersion = parseInt(version) - await pnpmInstall(workspaceDir, log) - await ngGenerate(workspaceDir, log, 'library', LIB_NAME, ['--skip-install']) - await pnpmInstall(workspaceDir, log, ['--config.strict-peer-dependencies=false', '--no-frozen-lockfile']) - await pnpmAdd(workspaceDir, log, ['@fingerprint/agent'], ['--config.strict-peer-dependencies=false']) - - const zoneVersionMap = { - 21: '~0.16.0', - 19: '~0.15.0', - 17: '~0.14.0', - 16: '~0.13.0', - 15: '~0.11.4', - } - - const zoneVersionKey = Object.keys(zoneVersionMap) - .map(Number) - .sort((a, b) => b - a) - .find((v) => majorVersion >= v) - - const zoneVersion = zoneVersionKey ? zoneVersionMap[zoneVersionKey] : 'latest' - - await pnpmAdd( - workspaceDir, - log, - [`@angular/platform-browser-dynamic@^${version}`, `zone.js@${zoneVersion}`], - ['--config.strict-peer-dependencies=false'] - ) - - const builderPackages = [`ng-packagr@^${version}`] - - const jestPackagesMap = { - 21: [ - 'jest@^30.0.0', - 'jest-preset-angular@^17.0.0', - 'jest-environment-jsdom@^30.0.0', - '@types/jest@^29.5.0', - '@types/node', - ], - 19: [ - 'jest@^29.7.0', - 'jest-preset-angular@^14.6.0', - 'jest-environment-jsdom@^29.7.0', - '@types/jest@^29.5.0', - '@types/node', - ], - 18: ['jest@^29.5.0', 'jest-preset-angular@^14.1.0', 'jest-environment-jsdom@^29.5.0', '@types/jest', '@types/node'], - 16: ['jest@^29.0.0', 'jest-preset-angular@^13.1.0', 'jest-environment-jsdom@^29.0.0', '@types/jest', '@types/node'], - 0: ['jest@^28.0.0', 'jest-preset-angular@^12.2.0', 'jest-environment-jsdom@^28.0.0', '@types/jest', '@types/node'], - } - - const jestVersionKey = Object.keys(jestPackagesMap) - .map(Number) - .sort((a, b) => b - a) - .find((v) => majorVersion >= v) - - const jestPackages = jestPackagesMap[jestVersionKey] - - await pnpmAdd( - workspaceDir, - log, - [...jestPackages, ...builderPackages], - ['-D', '--config.strict-peer-dependencies=false'] - ) -} diff --git a/bin/utils/steps/setupDemoApp.mjs b/bin/utils/steps/setupDemoApp.mjs deleted file mode 100644 index 49f80d0..0000000 --- a/bin/utils/steps/setupDemoApp.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { ngGenerate } from '../commands/ngGenerate.mjs' - -export async function setupDemoApp(workspaceDir, version, log) { - const isLegacy = parseInt(version) === 15 - const options = ['--routing=false', '--style=css', '--skip-tests=true'] - - if (!isLegacy) { - options.push('--standalone=true') - } - - await ngGenerate(workspaceDir, log, 'application', 'demo', options) - - const appDir = path.join(workspaceDir, 'projects', 'demo', 'src', 'app') - - if (isLegacy) { - const appModulePath = path.join(appDir, 'app.module.ts') - if (fs.existsSync(appModulePath)) { - let content = fs.readFileSync(appModulePath, 'utf8') - content = content.replace( - "import { NgModule } from '@angular/core';", - "import { NgModule } from '@angular/core';\nimport { FingerprintModule } from '@fingerprint/angular';" - ) - content = content.replace( - 'imports: [', - `imports: [\n FingerprintModule.forRoot({ startOptions: { apiKey: 'test-key' } }),` - ) - fs.writeFileSync(appModulePath, content, 'utf8') - } - } else { - const appConfigPath = path.join(appDir, 'app.config.ts') - if (fs.existsSync(appConfigPath)) { - let content = fs.readFileSync(appConfigPath, 'utf8') - content = content.replace( - "import { ApplicationConfig } from '@angular/core';", - "import { ApplicationConfig } from '@angular/core';\nimport { provideFingerprint } from '@fingerprint/angular';" - ) - content = content.replace( - 'providers: [', - `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` - ) - fs.writeFileSync(appConfigPath, content, 'utf8') - } else { - const mainTsPath = path.join(workspaceDir, 'projects', 'demo', 'src', 'main.ts') - if (fs.existsSync(mainTsPath)) { - let content = fs.readFileSync(mainTsPath, 'utf8') - if (content.includes('bootstrapApplication')) { - content = content.replace( - "import { bootstrapApplication } from '@angular/platform-browser';", - "import { bootstrapApplication } from '@angular/platform-browser';\nimport { provideFingerprint } from '@fingerprint/angular';" - ) - content = content.replace( - 'providers: [', - `providers: [\n provideFingerprint({ startOptions: { apiKey: 'test-key' } }),` - ) - fs.writeFileSync(mainTsPath, content, 'utf8') - } - } - } - } -} diff --git a/bin/utils/steps/updateTsConfigs.mjs b/bin/utils/steps/updateTsConfigs.mjs deleted file mode 100644 index 6ff0c9c..0000000 --- a/bin/utils/steps/updateTsConfigs.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { updateJsonFile } from '../fs.mjs' -import { LIB_NAME } from '../constants.mjs' - -export async function updateTsConfigs(workspaceDir, version) { - const tsconfigFiles = [ - path.join(workspaceDir, 'tsconfig.json'), - path.join(workspaceDir, 'tsconfig.base.json'), - path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), - ] - - for (const file of tsconfigFiles) { - if (fs.existsSync(file)) { - updateJsonFile(file, (json) => { - if (!json.compilerOptions) { - json.compilerOptions = {} - } - json.compilerOptions.types = ['node', 'jest'] - json.compilerOptions.esModuleInterop = true - json.compilerOptions.allowSyntheticDefaultImports = true - json.compilerOptions.skipLibCheck = true - }) - } - } - - const allSpecTsconfigs = [] - const findSpecConfigs = (dir) => { - fs.readdirSync(dir).forEach((file) => { - const fullPath = path.join(dir, file) - if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { - if (file !== 'node_modules') { - findSpecConfigs(fullPath) - } - } else if (file === 'tsconfig.spec.json') { - allSpecTsconfigs.push(fullPath) - } - }) - } - findSpecConfigs(workspaceDir) - - for (const file of allSpecTsconfigs) { - updateJsonFile(file, (json) => { - if (!json.compilerOptions) { - json.compilerOptions = {} - } - if (!json.compilerOptions.types) { - json.compilerOptions.types = [] - } - if (!json.compilerOptions.types.includes('node')) { - json.compilerOptions.types.push('node') - } - if (!json.compilerOptions.types.includes('jest')) { - json.compilerOptions.types.push('jest') - } - - json.compilerOptions.esModuleInterop = true - json.compilerOptions.allowSyntheticDefaultImports = true - json.compilerOptions.skipLibCheck = true - }) - } - - if (fs.existsSync(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'))) { - if ( - !fs.existsSync(path.join(workspaceDir, 'tsconfig.json')) && - fs.existsSync(path.join(workspaceDir, 'tsconfig.base.json')) - ) { - updateJsonFile(path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), (json) => { - json.extends = '../../tsconfig.base.json' - }) - } - } - - const addNodeReference = (dir) => { - fs.readdirSync(dir).forEach((file) => { - const fullPath = path.join(dir, file) - if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { - addNodeReference(fullPath) - } else if (file.endsWith('.spec.ts')) { - let content = fs.readFileSync(fullPath, 'utf8') - if (!content.startsWith('/// ')) { - content = '/// \n' + content - fs.writeFileSync(fullPath, content, 'utf8') - } - } - }) - } - addNodeReference(path.join(workspaceDir, 'projects', LIB_NAME)) - - if (parseInt(version) >= 21) { - const targetTsconfigs = [ - path.join(workspaceDir, 'tsconfig.json'), - path.join(workspaceDir, 'tsconfig.base.json'), - path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.lib.json'), - ] - for (const configPath of targetTsconfigs) { - if (fs.existsSync(configPath)) { - updateJsonFile(configPath, (json) => { - if (json.compilerOptions) { - json.compilerOptions.moduleResolution = 'bundler' - } - }) - } - } - } -} From bfd5faf31f3288cd3c7d8a38ce57944b50320f97 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 15:33:25 +0300 Subject: [PATCH 12/13] refactor: adjust angular tag logic and update pnpm version in CI workflow --- .github/workflows/test-matrix.yml | 2 +- bin/test-matrix.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 5199721..3c7741e 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -37,7 +37,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 8 + version: 9 - name: Install dependencies run: pnpm install diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 867b552..88c7c41 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -63,7 +63,7 @@ async function testVersion(version) { log ) - const angularVersionTag = `v${version}-lts` + const angularVersionTag = version > 15 ? `v${version}-lts` : `^${version}` // 3. Install Peer Deps and Required Packages const packagesToInstall = [ From be5df1eca0a24460f02b2eb42e4e5d26291a85e9 Mon Sep 17 00:00:00 2001 From: Orkun Date: Wed, 24 Jun 2026 15:42:22 +0300 Subject: [PATCH 13/13] refactor: remove redundant comments --- bin/test-matrix.mjs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs index 88c7c41..79742ac 100755 --- a/bin/test-matrix.mjs +++ b/bin/test-matrix.mjs @@ -32,7 +32,6 @@ async function testVersion(version) { log(`Angular ${version}: Starting check...\n`) console.log(`Angular ${version}: Starting check...`) - // 1. Create Workspace await executeCommand( 'npx', [ @@ -50,12 +49,6 @@ async function testVersion(version) { log ) - // Ensure npx worked and we have node_modules in the workspace after next steps - // Actually, step 1 just creates the folder structure. - // We need to install @angular/cli locally in the temp workspace to use it via ./node_modules/.bin/ng - // OR we use npx again for generate. - - // 2. Generate Library await executeCommand( 'npx', ['-y', `@angular/cli@${version}`, 'generate', 'library', LIB_NAME, '--skip-install'], @@ -65,7 +58,6 @@ async function testVersion(version) { const angularVersionTag = version > 15 ? `v${version}-lts` : `^${version}` - // 3. Install Peer Deps and Required Packages const packagesToInstall = [ `@angular/animations@${angularVersionTag}`, `@angular/common@${angularVersionTag}`, @@ -97,7 +89,6 @@ async function testVersion(version) { const commonOptions = ['--config.strict-peer-dependencies=false', '--no-lockfile', '--config.fund=false'] - // We install everything in one go to avoid version mismatches and repeat installations await executeCommand( 'pnpm', ['add', ...packagesToInstall, ...devPackagesToInstall, ...commonOptions], @@ -105,7 +96,6 @@ async function testVersion(version) { log ) - // 4. Copy Source Files const libDestDir = path.join(workspaceDir, 'projects', LIB_NAME, 'src', 'lib') fs.mkdirSync(libDestDir, { recursive: true }) copyRecursive(path.join(process.cwd(), 'projects', LIB_NAME, 'src', 'lib'), libDestDir) @@ -135,7 +125,6 @@ async function testVersion(version) { fs.copyFileSync(path.join(process.cwd(), 'projects', LIB_NAME, file), path.join(projectDir, file)) } - // 5. Update TS Configs for compatibility const tsconfigFiles = [ path.join(workspaceDir, 'tsconfig.json'), path.join(workspaceDir, 'projects', LIB_NAME, 'tsconfig.spec.json'), @@ -155,7 +144,6 @@ async function testVersion(version) { } } - // 6. Build await executeCommand( './node_modules/.bin/ng', ['build', LIB_NAME], @@ -163,7 +151,6 @@ async function testVersion(version) { log ) - // 7. Test await executeCommand('./node_modules/.bin/jest', [], { cwd: workspaceDir, env: { ...process.env } }, log) console.log(`Angular ${version}: PASSED`)