diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index df93a3b..24c41de 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,6 +42,10 @@ jobs: smoke: runs-on: ubuntu-latest timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + react: [18, 19] defaults: run: working-directory: ./package @@ -64,10 +68,11 @@ jobs: - run: npm run smoke env: CI: 'true' + REACT_VERSION: ${{ matrix.react }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: ${{ !cancelled() }} with: - name: playwright-report-smoke + name: playwright-report-smoke-react${{ matrix.react }} path: package/playwright-report-smoke/ retention-days: 14 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index f806824..1fa8f77 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -44,6 +44,10 @@ jobs: smoke: runs-on: ubuntu-latest timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + react: [18, 19] defaults: run: working-directory: ./package @@ -66,11 +70,12 @@ jobs: - run: npm run smoke env: CI: 'true' + REACT_VERSION: ${{ matrix.react }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: ${{ !cancelled() }} with: - name: release-playwright-report-smoke + name: release-playwright-report-smoke-react${{ matrix.react }} path: package/playwright-report-smoke/ retention-days: 14 diff --git a/README.md b/README.md index a0bf677..3d8774d 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,15 @@ $ npm i react-qrcode-reader | react-qrcode-reader version | React version | | --------------------------- | ------------- | -| 3.x.x | 19.x | +| 3.1.x | 19.x, 18.x | +| 3.0.x | 19.x | | 2.x.x | 18.x, 17.x | Please ensure your React version matches the supported version of the package. +Since 3.1.0 the package no longer ships `@types/react` as a runtime dependency, so its +types resolve against the `@types/react` in your own project. + # how to use ## 1. with onRead diff --git a/package/package-lock.json b/package/package-lock.json index 5d29187..3b431c5 100644 --- a/package/package-lock.json +++ b/package/package-lock.json @@ -1,20 +1,16 @@ { "name": "react-qrcode-reader", - "version": "3.0.2", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-qrcode-reader", - "version": "3.0.2", + "version": "3.1.0", "license": "MIT", "dependencies": { - "@types/node": "^24.5.2", - "@types/react": "^19.1.6", - "@types/react-dom": "^19.1.6", "jsqr": "^1.4.0", - "react-webcam": "^7.0.1", - "typescript": "^5.4.5" + "react-webcam": "^7.0.1" }, "devDependencies": { "@biomejs/biome": "^2.4.15", @@ -25,6 +21,9 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.1.4", "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.2.0", "jest": "^30.1.3", "jest-environment-jsdom": "^30.1.2", @@ -36,14 +35,15 @@ "ts-jest": "^29.0.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", + "typescript": "^5.4.5", "vite": "^8.0.16" }, "engines": { "node": ">=24" }, "peerDependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -2486,6 +2486,7 @@ "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -2495,6 +2496,7 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2504,6 +2506,7 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3511,6 +3514,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -7040,6 +7044,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7067,6 +7072,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/package/package.json b/package/package.json index 7aea584..8318b30 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-qrcode-reader", - "version": "3.0.3", + "version": "3.1.0", "author": { "name": "ysuzuki19", "url": "https://ysuzuki19.github.io" @@ -43,16 +43,12 @@ "prepublishOnly": "cp ../README.md . && cp ../LICENSE . && cp ../MIGRATION.md . && npm run build" }, "peerDependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "dependencies": { - "@types/node": "^24.5.2", - "@types/react": "^19.1.6", - "@types/react-dom": "^19.1.6", "jsqr": "^1.4.0", - "react-webcam": "^7.0.1", - "typescript": "^5.4.5" + "react-webcam": "^7.0.1" }, "devDependencies": { "@biomejs/biome": "^2.4.15", @@ -63,6 +59,9 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.1.4", "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.2.0", "jest": "^30.1.3", "jest-environment-jsdom": "^30.1.2", @@ -74,6 +73,7 @@ "ts-jest": "^29.0.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", + "typescript": "^5.4.5", "vite": "^8.0.16" }, "browserslist": { diff --git a/package/rollup.config.ts b/package/rollup.config.ts index d353506..c3bfcd0 100644 --- a/package/rollup.config.ts +++ b/package/rollup.config.ts @@ -19,7 +19,11 @@ const dts_config = { const plugins_for_build = [ resolve(), commonjs(), - typescript({ compilerOptions: { outDir: 'dist' } }), + // The classic runtime keeps the emitted code's only React dependency on + // `react` itself. `react-jsx` would emit `react/jsx-runtime`, which has no + // UMD global and would therefore have to be inlined into the UMD bundle -- + // pinning the published artifact to one React major. + typescript({ compilerOptions: { outDir: 'dist', jsx: 'react' } }), babel({ babelHelpers: 'bundled', extensions: ['.ts'], @@ -27,7 +31,11 @@ const plugins_for_build = [ }), ]; -const external = ['react', 'react-dom']; +const external = (id) => + id === 'react' || + id.startsWith('react/') || + id === 'react-dom' || + id.startsWith('react-dom/'); const es_config = { input: entry, @@ -48,7 +56,7 @@ const umd_config = { exports: 'named', indent: false, globals: { - react: 'react', + react: 'React', }, }, external, diff --git a/package/scripts/smoke.mjs b/package/scripts/smoke.mjs index 9ff61ca..68c4400 100644 --- a/package/scripts/smoke.mjs +++ b/package/scripts/smoke.mjs @@ -2,9 +2,19 @@ // Build → pack → install the tarball into smoke/ → run the smoke Playwright // suite. Mirrors what `npm publish` would produce so that the test exercises // the actual distributed artifact, not the source tree. +// +// REACT_VERSION selects which React major the consumer app runs on; it must be +// one of the majors declared in the package's peerDependencies. import { spawnSync } from 'node:child_process'; -import { copyFileSync, existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs'; +import { + copyFileSync, + existsSync, + readFileSync, + readdirSync, + rmSync, + unlinkSync, +} from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -13,6 +23,28 @@ const packageDir = resolve(here, '..'); const repoRoot = resolve(packageDir, '..'); const smokeDir = resolve(packageDir, 'smoke'); +// smoke/package-lock.json pins the default major, so that leg is fully +// deterministic. Other majors are overridden with --no-save, which keeps the +// committed lockfile authoritative instead of forking it per major. +const DEFAULT_REACT_MAJOR = '19'; +const REACT_OVERLAYS = { + 18: [ + 'react@^18.3.1', + 'react-dom@^18.3.1', + '@types/react@^18.3.12', + '@types/react-dom@^18.3.1', + ], +}; + +const reactMajor = process.env.REACT_VERSION ?? DEFAULT_REACT_MAJOR; +if (reactMajor !== DEFAULT_REACT_MAJOR && !REACT_OVERLAYS[reactMajor]) { + console.error( + `REACT_VERSION=${reactMajor} is not supported; expected one of ` + + `${[DEFAULT_REACT_MAJOR, ...Object.keys(REACT_OVERLAYS)].join(', ')}`, + ); + process.exit(1); +} + function run(cmd, args, opts = {}) { const res = spawnSync(cmd, args, { stdio: 'inherit', ...opts }); if (res.status !== 0) { @@ -58,15 +90,20 @@ const tgzPath = resolve(packageDir, tgz); console.log(`packed: ${tgz}`); // 5. Re-install smoke/ base deps from the committed lockfile (deterministic), -// then overlay the freshly packed tarball. `--no-save` keeps both -// package.json and package-lock.json untouched, so the working tree stays -// clean across runs. +// then overlay the freshly packed tarball (and the React override, if any). +// `--no-save` keeps both package.json and package-lock.json untouched, so the +// working tree stays clean across runs. step('install smoke/ base deps (npm ci)'); rmSync(resolve(smokeDir, 'node_modules'), { recursive: true, force: true }); run('npm', ['ci', '--no-audit', '--no-fund'], { cwd: smokeDir }); -step(`overlay tarball: ${tgz}`); -run('npm', ['install', '--no-save', '--no-audit', '--no-fund', tgzPath], { +// The tarball and the React override must go in as ONE install: npm reconciles +// the whole tree against smoke/package.json on every install, so a React +// override applied in a separate, earlier step would be reverted to the pinned +// default by the tarball install. +const overlay = [tgzPath, ...(REACT_OVERLAYS[reactMajor] ?? [])]; +step(`overlay tarball (${tgz}) + React ${reactMajor}`); +run('npm', ['install', '--no-save', '--no-audit', '--no-fund', ...overlay], { cwd: smokeDir, }); @@ -77,9 +114,20 @@ if (!existsSync(installed)) { process.exit(1); } +// Sanity: the consumer app must actually be on the requested major, otherwise +// a silently-reverted override would let the matrix report a false pass. +const reactPkg = resolve(smokeDir, 'node_modules/react/package.json'); +const installedReact = JSON.parse(readFileSync(reactPkg, 'utf8')).version; +if (installedReact.split('.')[0] !== reactMajor) { + console.error(`expected React ${reactMajor}.x in smoke/, got ${installedReact}`); + process.exit(1); +} +console.log(`smoke/ react: ${installedReact}`); + // 6. Type-check the consumer fixture against the built artifact's .d.ts. -// This must run AFTER the overlay so @types/react (transitive via the -// tarball's `dependencies`) and dist/index.d.ts are present. +// This must run AFTER the overlay so dist/index.d.ts is present. It resolves +// `import React from 'react'` inside the .d.ts against smoke/'s own +// @types/react, which is what pins the types to the React major under test. step('typecheck smoke/ against dist/index.d.ts'); run('npm', ['run', 'typecheck'], { cwd: smokeDir }); @@ -93,4 +141,4 @@ run('npx', ['playwright', 'test', '--config=playwright.smoke.config.ts'], { cwd: packageDir, }); -console.log('\nsmoke: all checks passed'); +console.log(`\nsmoke: all checks passed (React ${installedReact})`); diff --git a/package/smoke/package-lock.json b/package/smoke/package-lock.json index e7b2a11..631de55 100644 --- a/package/smoke/package-lock.json +++ b/package/smoke/package-lock.json @@ -12,6 +12,8 @@ "react-dom": "^19.1.0" }, "devDependencies": { + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.0.3", "typescript": "^5.4.5", "vite": "^8.0.16" @@ -667,6 +669,26 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -763,6 +785,13 @@ "dev": true, "license": "MIT" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package/smoke/package.json b/package/smoke/package.json index 4eaa975..62ba742 100644 --- a/package/smoke/package.json +++ b/package/smoke/package.json @@ -12,6 +12,8 @@ "react-dom": "^19.1.0" }, "devDependencies": { + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^5.0.3", "typescript": "^5.4.5", "vite": "^8.0.16"