diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml new file mode 100644 index 0000000..3c7741e --- /dev/null +++ b/.github/workflows/test-matrix.yml @@ -0,0 +1,49 @@ +name: Test Angular Matrix + +on: + pull_request: + +jobs: + test-matrix: + name: Angular ${{ matrix.angular }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - 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 }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install + + - name: Generate project version + run: pnpm generate:version + + - name: Run Matrix Check for Angular ${{ matrix.angular }} + run: pnpm run test:matrix -- --version=${{ matrix.angular }} diff --git a/.gitignore b/.gitignore index 61d149b..9c48f85 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /tmp /out-tsc /bazel-out +.pnpm-store projects/*/dist projects/*/out-tsc @@ -12,6 +13,7 @@ projects/*/out-tsc 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..3c03207 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18 diff --git a/bin/test-matrix.mjs b/bin/test-matrix.mjs new file mode 100755 index 0000000..79742ac --- /dev/null +++ b/bin/test-matrix.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import os from 'os' +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' + +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) + const log = (data) => logStream.write(data) + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'angular-test-')) + const workspaceDir = path.join(tempDir, 'test-workspace') + + try { + log(`Angular ${version}: Starting check...\n`) + console.log(`Angular ${version}: Starting check...`) + + 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 + ) + + await executeCommand( + 'npx', + ['-y', `@angular/cli@${version}`, 'generate', 'library', LIB_NAME, '--skip-install'], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + const angularVersionTag = version > 15 ? `v${version}-lts` : `^${version}` + + 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', + ] + + const commonOptions = ['--config.strict-peer-dependencies=false', '--no-lockfile', '--config.fund=false'] + + await executeCommand( + 'pnpm', + ['add', ...packagesToInstall, ...devPackagesToInstall, ...commonOptions], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + 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)) + } + + 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' + } + }) + } + } + + await executeCommand( + './node_modules/.bin/ng', + ['build', LIB_NAME], + { cwd: workspaceDir, env: { ...process.env } }, + log + ) + + 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 { + logStream.end() + fs.rmSync(tempDir, { recursive: true, force: true }) + } +} + +setupLogDir(LOG_DIR) + +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 : Object.keys(angularMetadata) + +console.log(`Starting matrix tests for: ${versionsToTest.join(', ')}`) + +const results = await Promise.all(versionsToTest.map((version) => testVersion(version))) +const failed = results.some((code) => code !== 0) + +if (failed) { + process.exit(1) +} else { + 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/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/package.json b/package.json index bd277e4..a0bcac5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "fingerprintjs-pro-angular-demo", "version": "0.0.0", + "engines": { + "node": ">=18.0.0" + }, "scripts": { "prepare": "husky install", "ng": "ng", @@ -13,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"