diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60ab9aba..d0688c52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,15 +7,36 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20.x - - name: Install dependencies - uses: borales/actions-yarn@v4 + node-version: 24.x + cache: yarn + - run: corepack enable + - run: yarn install --frozen-lockfile + - run: make verify + + react-compatibility: + runs-on: ubuntu-latest + strategy: + matrix: + react: [18, 19] + + steps: + - uses: actions/checkout@v7 + - name: Use Node.js + uses: actions/setup-node@v7 with: - cmd: install # will run `yarn install` command - - run: make lint - - run: make test - - run: make clean build + node-version: 24.x + cache: yarn + - run: corepack enable + - run: yarn install --frozen-lockfile + - name: Install React compatibility version + run: >- + yarn add --dev --ignore-scripts --no-lockfile + react@${{ matrix.react }} + react-dom@${{ matrix.react }} + @types/react@${{ matrix.react }} + @types/react-dom@${{ matrix.react }} + - run: make typecheck test diff --git a/Makefile b/Makefile index 05954341..e3946357 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,12 @@ NODE = node --harmony BABEL = ./node_modules/.bin/babel MOCHA = node --harmony node_modules/.bin/mocha --reporter spec --require test/setup.js --compilers js:babel-core/register ESLINT = node_modules/.bin/eslint -JEST = node_modules/.bin/jest +JEST = node_modules/.bin/jest --no-watchman ROLLUP = node_modules/.bin/rollup +TSC = node_modules/.bin/tsc PRETTIER = node_modules/.bin/prettier STORYBOOK_TO_PAGES = node_modules/.bin/storybook-to-ghpages -NPM_ENV_VARS = npm_config_registry=https://registry.npmjs.org -NPM = $(NPM_ENV_VARS) npm -XYZ = $(NPM_ENV_VARS) node_modules/.bin/xyz --repo git@github.com:plaid/react-plaid-link.git STORYBOOK = node_modules/.bin/start-storybook -RELEASE_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) SRC_FILES = $(shell find src -name '*.js|*.tsx|*.ts' | sort) @@ -45,6 +42,16 @@ test-watch: export BABEL_ENV=testing test-watch: @$(JEST) --watch + +.PHONY: typecheck +typecheck: + @$(TSC) --noEmit + + +.PHONY: verify +verify: lint typecheck test build + @$(MAKE) check-import + .PHONY: lint-fix lint-fix: @$(ESLINT) --fix '{src,examples}/**/*.{ts,tsx,js,jsx}' @@ -75,6 +82,6 @@ storybook-deploy: $(STORYBOOK_TO_PAGES) -.PHONY: release-major release-minor release-patch release-premajor release-preminor release-prepatch release-prerelease -release-major release-minor release-patch release-premajor release-preminor release-prepatch release-prerelease: build - @$(XYZ) --increment $(@:release-%=%) --branch $(RELEASE_BRANCH) --prerelease-label beta +.PHONY: release +release: verify + @./scripts/release diff --git a/PUBLISH.md b/PUBLISH.md index 73d58738..3a912c83 100644 --- a/PUBLISH.md +++ b/PUBLISH.md @@ -1,55 +1,29 @@ ## Publishing -**We use the `xyz` NPM package to simplify releases. This library uses a feature that is available in Bash 4 or beyond, but latest versions of macOS -ship with an older version of Bash. If you run into issues with `xyz`, use `brew install bash` to install a newer verison of Bash.** +Releases are prepared on a branch and published from the merge commit on +`master`. -If you run into any `npm` package versioning issues, make sure you run `make setup` rather than calling `npm install` directly. +1. Create a release branch from the latest `master`. +2. Move the `Unreleased` changelog entries under the new version heading. +3. Update `package.json` without creating a tag: -1. Create a branch to stage the release, for example `release-3.4.5` (Read [semver](https://semver.org/) to determine what type of version bump to use.) + ```bash + yarn version --new-version --no-git-tag-version + ``` -``` -git checkout -b release-3.4.5 -git fetch -git reset --hard origin/master -``` +4. Open and merge the release PR after CI passes. +5. Check out the updated `master`, confirm it matches `origin/master`, and + authenticate with npm. +6. Run: -2. Run `make storybook` and verify that there are no regressions with Link. + ```bash + make release + ``` -3. Update `CHANGELOG.md` with changes that will be included in the release, commit and push to the release branch. To help confirm the changes -that will be included in the release, consider using GitHub's compare feature: https://github.com/plaid/react-plaid-link/compare/v3.4.0...master -(replace `v3.4.0` with the last published version). +The release command runs lint, type checking, tests, and all builds before +publishing. It publishes the version already recorded in `package.json`, then +creates and pushes the annotated `v` tag. -4. When ready, publish the new version from the branch before merging - -``` -make release-(patch|minor|major) -``` - -5. Merge the branch - -#### **If the `make` command errors out with an authentication issue** - -If `make release-*` from above errors out with an authentication issue, -you may need to `npm login` or otherwise manually set up your `~/.npmrc` file to -make sure you have access to publish to npm. - -#### (dangerous) Manually publish an already-built package to npm - -If you've set up your npm authentication as described above, but are still seeing authentication issues when running `make release-*`, you can -directly publish a **built** package to the npm registry using the manual commands below. You should **only** publish the package manually using these steps -if the build steps of the `make release-*` command were **successful**. If the build steps did not succeeed, running these commands could publish an unbuilt -version of the package, which would lead to a broken release. - -1. Publish already-built package to npm manually - -```bash -npm --registry=https://registry.npmjs.com publish -``` - -2. Push tags manually - -```bash -git push --follow-tags -``` - -Also, under [github releases page](https://github.com/plaid/react-plaid-link/releases), draft a new release corresponding to the tag of the latest version. +If npm publication succeeds but tagging fails, rerun `make release`. The +release script detects the published npm version and resumes with the missing +tag instead of attempting to republish. diff --git a/package.json b/package.json index 8a293caf..8851516f 100644 --- a/package.json +++ b/package.json @@ -65,18 +65,17 @@ "@storybook/preset-typescript": "^2.1.0", "@storybook/react": "^5.3.17", "@storybook/storybook-deployer": "^2.8.3", - "@testing-library/react": "^12.1.4", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", "@types/jest": "^27.4.1", - "@types/react": "^17.0.9", - "@types/react-dom": "^17.0.6", - "@typescript-eslint/eslint-plugin": "^2.24.0", - "@typescript-eslint/parser": "^2.24.0", - "@wessberg/rollup-plugin-ts": "^1.2.21", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", "babel-loader": "^8.0.6", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-typescript-to-proptypes": "^1.3.2", - "eslint": "6.6.0", + "eslint": "7.32.0", "eslint-config-airbnb": "18.0.1", "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.18.2", @@ -89,19 +88,20 @@ "jest": "^27.5.1", "mocha": "2.3.x", "prettier": "^1.19.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "19.2.8", + "react-dom": "19.2.8", "react-tools": "0.13.x", - "rollup": "^1.27.0", + "rollup": "2.80.0", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-terser": "^5.1.2", + "rollup-plugin-typescript2": "0.37.0", "sinon": "1.17.x", "ts-loader": "^6.2.1", - "typescript": "^3.8.3", - "xyz": "^4.0.x" + "tslib": "^2.8.1", + "typescript": "5.9.3" }, "tags": [ "react", diff --git a/rollup.config.js b/rollup.config.js index 28491059..e624d6da 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'; import resolve from 'rollup-plugin-node-resolve'; import { terser } from 'rollup-plugin-terser'; import replace from 'rollup-plugin-replace'; -import ts from '@wessberg/rollup-plugin-ts'; +import typescript from 'rollup-plugin-typescript2'; import pkg from './package.json'; export default [ @@ -15,8 +15,8 @@ export default [ { file: pkg.module, format: 'es' }, ], plugins: [ - ts(), resolve(), + typescript({ clean: true, tsconfig: 'tsconfig.build.json' }), babel({ extensions: ['.ts', '.js', '.tsx', '.jsx'], }), @@ -38,8 +38,11 @@ export default [ }, ], plugins: [ - ts(), resolve(), + typescript({ + tsconfig: 'tsconfig.build.json', + tsconfigOverride: { compilerOptions: { declaration: false } }, + }), babel({ extensions: ['.ts', '.js', '.tsx', '.jsx'], }), @@ -61,8 +64,11 @@ export default [ }, ], plugins: [ - ts(), resolve(), + typescript({ + tsconfig: 'tsconfig.build.json', + tsconfigOverride: { compilerOptions: { declaration: false } }, + }), babel({ extensions: ['.ts', '.js', '.tsx', '.jsx'], }), diff --git a/scripts/release b/scripts/release new file mode 100755 index 00000000..6df3c40f --- /dev/null +++ b/scripts/release @@ -0,0 +1,81 @@ +#!/bin/sh + +set -eu + +registry=https://registry.npmjs.org + +abort() { + printf '%s\n' "$1" >&2 + exit 1 +} + +branch=$(git rev-parse --abbrev-ref HEAD) +[ "$branch" = master ] || abort "Releases must run from master." +[ -z "$(git status --porcelain)" ] || abort "The working tree must be clean." + +git fetch origin master --tags + +head_commit=$(git rev-parse HEAD) +remote_commit=$(git rev-parse origin/master) +[ "$head_commit" = "$remote_commit" ] || + abort "Local master must match origin/master." + +name=$(node -p "require('./package.json').name") +version=$(node -p "require('./package.json').version") +tag="v$version" + +grep -F "## $version" CHANGELOG.md >/dev/null || + abort "CHANGELOG.md must contain a section for $version." + +published=false +if published_version=$(npm view "$name@$version" version --registry="$registry" 2>/dev/null); then + [ "$published_version" = "$version" ] || + abort "Unexpected npm version response: $published_version" + published=true +elif ! npm view "$name" version --registry="$registry" >/dev/null 2>&1; then + abort "Unable to query $name on npm." +fi + +tag_exists=false +if tag_commit=$(git rev-parse -q --verify "refs/tags/$tag^{commit}"); then + [ "$tag_commit" = "$head_commit" ] || + abort "$tag already points to a different commit." + tag_exists=true +fi + +remote_tag_exists=false +if [ -n "$(git ls-remote --tags origin "refs/tags/$tag")" ]; then + remote_tag_exists=true +fi + +if [ "$published" = true ] && [ "$remote_tag_exists" = true ]; then + printf '%s\n' "$name@$version is already published and tagged." + exit 0 +fi + +printf 'Release %s@%s from %s? Type "release" to continue: ' \ + "$name" "$version" "$head_commit" +read -r confirmation +[ "$confirmation" = release ] || abort "Release cancelled." + +if [ "$published" = false ]; then + case "$version" in + *-*) + prerelease=${version#*-} + dist_tag=${prerelease%%.*} + npm publish --registry="$registry" --tag "$dist_tag" + ;; + *) + npm publish --registry="$registry" + ;; + esac +else + printf '%s\n' "$name@$version is already on npm; continuing with the tag." +fi + +if [ "$tag_exists" = false ]; then + git tag --annotate "$tag" --message "Version $version" +fi + +git push origin "refs/tags/$tag" +printf 'Released %s@%s and %s.\n' "$name" "$version" "$tag" diff --git a/src/PlaidEmbeddedLink.tsx b/src/PlaidEmbeddedLink.tsx index 40696d4d..b2054ad8 100644 --- a/src/PlaidEmbeddedLink.tsx +++ b/src/PlaidEmbeddedLink.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useMemo } from 'react'; +import React from 'react'; import useScript from './react-script-hook'; import { PLAID_LINK_STABLE_URL } from './constants'; @@ -7,6 +7,8 @@ import { PlaidLinkOptionsWithLinkToken, } from './types'; +const { useEffect, useMemo, useRef } = React; + export const PlaidEmbeddedLink = (props: PlaidEmbeddedLinkPropTypes) => { const { style, diff --git a/src/react-script-hook/index.test.tsx b/src/react-script-hook/index.test.tsx index 9b748e1d..4ee99958 100644 --- a/src/react-script-hook/index.test.tsx +++ b/src/react-script-hook/index.test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, renderHook, waitFor } from '@testing-library/react'; import useScript, { scripts } from './'; @@ -65,7 +65,7 @@ describe('useScript', () => { }); expect(document.querySelectorAll('script').length).toBe(1); - handle.rerender(); + handle.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); }); @@ -81,11 +81,11 @@ describe('useScript', () => { }); expect(document.querySelectorAll('script').length).toBe(1); - handle2.rerender(); + handle2.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); - handle1.rerender(); + handle1.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); - handle2.rerender(); + handle2.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); }); @@ -104,7 +104,7 @@ describe('useScript', () => { ); expect(document.querySelectorAll('script').length).toBe(1); - handle.rerender(); + handle.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); }); @@ -246,7 +246,7 @@ describe('useScript', () => { expect(document.querySelectorAll('script').length).toBe(1); expect(handle.result.current).toStrictEqual([false, null]); - handle.rerender(); + handle.rerender(props); expect(document.querySelectorAll('script').length).toBe(1); expect(handle.result.current).toStrictEqual([false, null]); }); @@ -266,7 +266,7 @@ describe('useScript', () => { }); expect(document.querySelectorAll('script').length).toBe(2); - handle.rerender(); + handle.rerender(props); expect(document.querySelectorAll('script').length).toBe(2); }); @@ -287,7 +287,7 @@ describe('useScript', () => { expect(document.querySelectorAll('script').length).toBe(0); const props = { src: null }; - const { result, rerender, waitFor } = renderHook((p) => useScript(p), { + const { result, rerender } = renderHook((p) => useScript(p), { initialProps: props, }); diff --git a/src/react-script-hook/index.tsx b/src/react-script-hook/index.tsx index 55254da7..408ed7b1 100644 --- a/src/react-script-hook/index.tsx +++ b/src/react-script-hook/index.tsx @@ -2,7 +2,7 @@ // We originally started with patch-package, but with React 19 we also needed // to update its dependency versions, so (given the size) we just forked it. -import { useState, useEffect } from 'react'; +import React from 'react'; interface ScriptProps { src: HTMLScriptElement['src'] | null; @@ -23,6 +23,7 @@ type ScriptStatusMap = { // Previously loading/loaded scripts and their current status export const scripts: ScriptStatusMap = {}; +const { useEffect, useState } = React; // Check for existing