diff --git a/.eslintignore b/.eslintignore index a0a3ac6..b5d9e67 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,3 @@ dist/ -config/ coverage/ node_modules/ -lib/ -es/ -umd/ diff --git a/.eslintrc b/.eslintrc index 83ec572..61193cb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,20 +1,87 @@ { - "env": {}, - "extends": ["airbnb", "plugin:jest/recommended"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "extends": [ + "airbnb", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:testing-library/react" + ], "globals": { - "document": true + "page": true, + "document": true, + "vi": true }, - "parser": "@babel/eslint-parser", - "plugins": ["babel", "jest", "react", "react-hooks"], + "plugins": [ + "react", + "react-hooks", + "testing-library" + ], "rules": { + "import/no-unresolved": [ + 2, { "ignore": ["test-utils"] } + ], "import/prefer-default-export": "off", - "import/no-extraneous-dependencies": "off", - "no-console": "warn", + "no-console": "off", + "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], + "no-unused-vars": "off", + "no-undef": "off", + "no-restricted-syntax": ["warn", "WithStatement"], + "no-restricted-globals": ["error"], + "eqeqeq": ["warn", "smart"], + "no-use-before-define": [ + "warn", + { + "functions": false, + "classes": false, + "variables": false + }, + ], + "no-mixed-operators": [ + "warn", + { + "groups": [ + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"], + ], + "allowSamePrecedence": false, + }, + ], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], - "react/jsx-fragments": "off", - "react/jsx-props-no-spreading": "off", + "no-underscore-dangle": "off", "react/prefer-stateless-function": "off", + "react/jsx-props-no-spreading": "off", "react/function-component-definition": "off", - "react/require-default-props": "off" + "default-param-last": "off", + "arrow-parens": "off", + "import/no-anonymous-default-export": "off", + "import/no-extraneous-dependencies": "off", + "max-len": ["error", { + "code": 120, + "ignoreComments": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreRegExpLiterals": true + }], + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "react/require-default-props": [2, { + "functions": "defaultArguments" + }], + "react-hooks/exhaustive-deps": "error", + "testing-library/render-result-naming-convention": "off", + "testing-library/no-render-in-lifecycle": [ + "error", + { + "allowTestingFrameworkSetupHook": "beforeEach" + } + ] } } diff --git a/README.md b/README.md index 220948f..8eaab8c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # mirador-dl-plugin -[![Travis][build-badge]][build] [![npm package][npm-badge]][npm] [![Coveralls][coveralls-badge]][coveralls] @@ -10,10 +9,6 @@ ![mirador-download-options](https://user-images.githubusercontent.com/5402927/87057857-3d056e80-c1bc-11ea-8860-7662208c19fa.png) - -[build-badge]: https://img.shields.io/travis/projectmirador/mirador-dl-plugin/main.png?style=flat-square -[build]: https://travis-ci.org/projectmirador/mirador-dl-plugin - [npm-badge]: https://img.shields.io/npm/v/mirador-dl-plugin.png?style=flat-square [npm]: https://www.npmjs.org/package/mirador-dl-plugin diff --git a/__tests__/CanvasDownloadLinks.test.js b/__tests__/CanvasDownloadLinks.test.js index d436d20..e0696f7 100644 --- a/__tests__/CanvasDownloadLinks.test.js +++ b/__tests__/CanvasDownloadLinks.test.js @@ -44,7 +44,7 @@ describe('CanvasDownloadLinks', () => { let currentBoundsSpy; beforeEach(() => { - currentBoundsSpy = jest.spyOn(CanvasDownloadLinks.prototype, 'currentBounds'); + currentBoundsSpy = vi.spyOn(CanvasDownloadLinks.prototype, 'currentBounds'); }); afterEach(() => { @@ -197,8 +197,8 @@ describe('CanvasDownloadLinks', () => { describe('For Images Less Than 1000px Wide', () => { it('does not render a smaller version link if image is under 1000px wide', () => { - canvas.getWidth = () => 999; - createWrapper({ canvas }); + const smallCanvas = { ...canvas, getWidth: () => 999 }; + createWrapper({ canvas: smallCanvas }); const links = screen.getAllByRole('link'); expect(links).toHaveLength(2); // Should only show full-size version and link to PDF. diff --git a/__tests__/MiradorDownloadDialog.test.js b/__tests__/MiradorDownloadDialog.test.js index 85eb6ac..7bc5796 100644 --- a/__tests__/MiradorDownloadDialog.test.js +++ b/__tests__/MiradorDownloadDialog.test.js @@ -49,7 +49,7 @@ describe('Dialog', () => { }); it('calls the closeDialog function when the close button is clicked', async () => { - const closeDialog = jest.fn(); + const closeDialog = vi.fn(); createWrapper({ closeDialog }); const closeButton = await screen.findByText(/mirador-dl-plugin\.close/); fireEvent.click(closeButton); diff --git a/__tests__/miradorDownloadPlugin.test.js b/__tests__/miradorDownloadPlugin.test.js index b691986..ab79377 100644 --- a/__tests__/miradorDownloadPlugin.test.js +++ b/__tests__/miradorDownloadPlugin.test.js @@ -28,8 +28,8 @@ describe('miradorDownloadPlugin', () => { describe('MenuItem', () => { it('triggers both openDownloadDialog and handleClose when "Download" is clicked', async () => { - const handleClose = jest.fn(); - const openDownloadDialog = jest.fn(); + const handleClose = vi.fn(); + const openDownloadDialog = vi.fn(); createWrapper({ handleClose, openDownloadDialog }); const openDownloadDialogButton = await screen.findByText(/mirador-dl-plugin\.download/); fireEvent.click(openDownloadDialogButton); diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index d2c9247..0000000 --- a/babel.config.js +++ /dev/null @@ -1,93 +0,0 @@ -const moduleFormatMap = { - cjs: 'commonjs', - es: false, -}; - -module.exports = (api) => ({ - presets: [ - api.env('test') && [ - '@babel/preset-env', - { - modules: 'commonjs', - targets: { - node: 'current', - }, - }, - ], - (api.env('production') || api.env('development')) && [ - '@babel/preset-env', - { - corejs: 3, - exclude: ['transform-typeof-symbol'], - forceAllTransforms: true, - modules: moduleFormatMap[process.env.MODULE_FORMAT] || false, - useBuiltIns: 'entry', - }, - ], - [ - '@babel/preset-react', - { - development: api.env('development') || api.env('test'), - runtime: 'automatic', - useBuiltIns: true, - }, - ], - ].filter(Boolean), - plugins: [ - 'babel-plugin-macros', - '@babel/plugin-transform-destructuring', - [ - '@babel/plugin-proposal-class-properties', - { - loose: true, - }, - ], - ['@babel/plugin-proposal-private-property-in-object', { loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - [ - '@babel/plugin-proposal-object-rest-spread', - { - useBuiltIns: true, - }, - ], - [ - '@babel/plugin-transform-runtime', - { - corejs: false, - helpers: false, // Needed to support IE/Edge - regenerator: true, - }, - ], - [ - '@babel/plugin-transform-regenerator', - { - async: false, - }, - ], - ['transform-react-remove-prop-types', - { - ignoreFilenames: ['node_modules'], - removeImport: true, - }, - ], - [ - '@emotion', - { - importMap: { - '@mui/system': { - styled: { - canonicalImport: ['@emotion/styled', 'default'], - styledBaseImport: ['@mui/system', 'styled'], - }, - }, - '@mui/material/styles': { - styled: { - canonicalImport: ['@emotion/styled', 'default'], - styledBaseImport: ['@mui/material/styles', 'styled'], - }, - }, - }, - }, - ], - ].filter(Boolean), -}); diff --git a/demo/src/index.html b/demo/src/index.html index 4f8170c..51c3c81 100644 --- a/demo/src/index.html +++ b/demo/src/index.html @@ -6,6 +6,6 @@
- + diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 3cd8f65..0000000 --- a/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -// For a detailed explanation regarding each configuration property, visit: -// https://jestjs.io/docs/en/configuration.html - -module.exports = { - // Automatically clear mock calls and instances between every test - clearMocks: true, - - // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', - setupFilesAfterEnv: [ - '/setupJest.js', - ], - testEnvironment: 'jsdom', - // Ignore Mirador/Manifesto/dnd libs code from jest transforms - transformIgnorePatterns: [ - '/node_modules/(?!(mirador|manifesto.js|react-dnd|dnd-core|@react-dnd|dnd-multi-backend|rdndmb-html5-to-touch|react-mosaic-component2|lodash-es))', - ], - testPathIgnorePatterns: [ - '/__tests__/test-utils.js', - ], -}; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..28c4b40 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,9 @@ +# This is the configuration file for the netlify preview deploys +# See https://www.netlify.com/docs/netlify-toml-reference/ for more + +[build] + publish = "dist/" + +[[redirects]] + from = "/" + to = "/demo/src/" diff --git a/package.json b/package.json index 488b686..9a8dbc2 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,25 @@ { "name": "mirador-dl-plugin", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "mirador-dl-plugin React component", - "main": "lib/index.js", - "module": "es/index.js", + "main": "./dist/mirador-dl-plugin.js", + "module": "./dist/mirador-dl-pugin.es.js", "files": [ - "css", - "es", - "lib", - "umd" + "dist" ], + "exports": { + "./src": "./src/index.js" + }, "scripts": { - "build": "npm run build:umd && npm run build:demo", - "build:demo": "NODE_ENV=development webpack --mode=development", - "build:umd": "NODE_ENV=production webpack --mode=production", - "build:es": "mkdir -p es && cp -r src/* es && NODE_ENV=production MODULE_FORMAT=es npx babel es -d es", - "build:cjs": "mkdir -p lib && cp -r src/* lib && NODE_ENV=production MODULE_FORMAT=cjs npx babel lib -d lib", - "clean": "rm -rf ./umd && rm -rf ./es && rm -rf ./lib && rm -rf ./demo/dist", - "lint": "eslint ./src ./__tests__", - "prepublishOnly": "npm run clean && npm run build:es && npm run build:cjs && npm run build", - "start": "NODE_ENV=development webpack serve --open", - "test": "npm run lint && jest", - "test:coverage": "jest --coverage", - "test:watch": "jest --watch" + "build": "vite build --config vite.config.js", + "clean": "rm -rf ./dist", + "lint": "npx eslint ./", + "prepublishOnly": "npm run build", + "start": "vite", + "test": "npm run lint && npx vitest run", + "test:coverage": "npm run lint && npx vitest run --coverage" }, "dependencies": { - "@emotion/babel-plugin": "^11.11.0", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6" @@ -39,48 +33,31 @@ "react-i18next": "^13.0.0 || ^14.0.0 || ^15.0.0" }, "devDependencies": { - "@babel/cli": "^7.17.6", - "@babel/core": "^7.17.7", - "@babel/eslint-parser": "^7.5.4", - "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-regenerator": "^7.16.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", "@mui/material": "^5.x", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", - "@testing-library/dom": "^9.2.0", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.1.5", - "@testing-library/react": "^14.1.0", + "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", - "babel-eslint": "^10.0.3", - "babel-jest": "^24.9.0", - "babel-loader": "^9.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^2.1.8", + "@vitest/ui": "^2.1.4", "eslint": "^8.11.0", "eslint-config-airbnb": "^19.0.4", - "eslint-plugin-babel": "^5.3.0", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^27.1.5", + "eslint-plugin-jest-dom": "^5.1.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.3.0", - "html-loader": "^5.0.0", - "html-webpack-plugin": "^5.6.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.4.3", - "jest-puppeteer": "^9.0.2", + "eslint-plugin-testing-library": "^6.2.0", + "glob": "^10.3.0", + "happy-dom": "^15.11.7", "lodash": "^4.17.15", "mirador": "^4.0.0-alpha.5", "react": "^18.0.0", "react-dom": "^18.0.0", - "terser-webpack-plugin": "^5.3.1", - "webpack": "^5.70.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.7.4" + "vite": "^6.0.0", + "vitest": "^2.1.4", + "vitest-fetch-mock": "^0.4.2" }, "author": "", "homepage": "", diff --git a/setupJest.js b/setupTest.js similarity index 55% rename from setupJest.js rename to setupTest.js index 7b0828b..a7623d7 100644 --- a/setupJest.js +++ b/setupTest.js @@ -1 +1,2 @@ import '@testing-library/jest-dom'; +import { vi } from 'vitest'; diff --git a/src/CanvasDownloadLinks.js b/src/CanvasDownloadLinks.js index 7c01e3c..9ef38fc 100644 --- a/src/CanvasDownloadLinks.js +++ b/src/CanvasDownloadLinks.js @@ -183,7 +183,7 @@ export default class CanvasDownloadLinks extends Component { const { canvas, canvasLabel } = this.props; return ( - + <> {canvasLabel} @@ -210,7 +210,7 @@ export default class CanvasDownloadLinks extends Component { ))} - + ); } } diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c6cc182 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,86 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import fs from 'fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'url'; +import { globSync } from 'glob'; +import pkg from './package.json'; + +/** +* Vite configuration +*/ +export default defineConfig({ + ...( + process.env.NETLIFY ? { + build: { + rollupOptions: { + external: ['__tests__/*', '__mocks__/*'], + input: Object.fromEntries( + globSync('./demo/src/*.html').map((file) => [ + // This remove `src/` as well as the file extension from each + // file, so e.g. src/nested/foo.js becomes nested/foo + path.relative( + 'demo/src/', + file.slice(0, file.length - path.extname(file).length), + ), + // This expands the relative paths to absolute paths, so e.g. + // src/nested/foo becomes /project/src/nested/foo.js + fileURLToPath(new URL(file, import.meta.url)), + ]), + ), + }, + sourcemap: true, + }, + } : { + build: { + lib: { + entry: './src/index.js', + fileName: (format) => (format === 'umd' ? 'mirador-dl-plugin.js' : 'mirador-dl-plugin.es.js'), + formats: ['es', 'umd'], + name: 'MiradorDlPlugin', + }, + rollupOptions: { + external: [...Object.keys(pkg.peerDependencies || {}), '__tests__/*', '__mocks__/*'], + output: { + assetFileNames: 'mirador-dl-plugin.[ext]', + }, + }, + sourcemap: true, + }, + } + ), + esbuild: { + exclude: [], + // Matches .js and .jsx in __tests__ and .jsx in src + include: [/__tests__\/.*\.(js|jsx)$/, /src\/.*\.jsx?$/], + loader: 'jsx', + }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'load-js-files-as-jsx', + // TODO: rename all our files to .jsx ... + /** */ + setup(build) { + build.onLoad({ filter: /(src|__tests__)\/.*\.js$/ }, async (args) => ({ + contents: await fs.readFile(args.path, 'utf8'), + loader: 'jsx', + })); + }, + }, + ], + }, + include: ['@emotion/styled'], + }, + plugins: [react()], + resolve: { + alias: { + '@tests/': fileURLToPath(new URL('./__tests__', import.meta.url)), + }, + }, + server: { + open: '/demo/src/index.html', + port: '4444', + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fe74466 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'url'; +import fs from 'fs/promises'; + +export default defineConfig({ + esbuild: { + exclude: [], + include: /(src|__tests__)\/.*\.jsx?$/, + loader: 'jsx', + }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'load-js-files-as-jsx', + /** */ + setup(build) { + build.onLoad({ filter: /(src|__tests__)\/.*\.js$/ }, async (args) => ({ + contents: await fs.readFile(args.path, 'utf8'), + loader: 'jsx', + })); + }, + }, + ], + }, + }, + plugins: [react()], + resolve: { + alias: { + '@tests': fileURLToPath(new URL('./__tests__', import.meta.url)), + }, + }, + test: { + coverage: { + all: true, + enabled: true, + }, + environment: 'happy-dom', + exclude: ['node_modules'], + globals: true, + include: ['**/*.test.js', '**/*.test.jsx'], + sequence: { + shuffle: true, + }, + setupFiles: ['./setupTest.js'], + }, +}); diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index db72c48..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,98 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const webpack = require('webpack'); -const TerserPlugin = require('terser-webpack-plugin'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -/** */ -const baseConfig = (mode) => ({ - entry: ['./src/index.js'], - module: { - rules: [ - { - include: path.resolve(fs.realpathSync(process.cwd()), '.'), // CRL - loader: require.resolve('babel-loader'), - options: { - // Save disk space when time isn't as important - cacheCompression: true, - cacheDirectory: true, - compact: true, - envName: mode, - }, - test: /\.(js|mjs|jsx)$/, - }, - { - test: /\.html$/, - loader: 'html-loader', - }, - ], - }, - optimization: { - minimizer: [ - new TerserPlugin({ - extractComments: true, - }), - ], - }, - output: { - filename: 'mirador-dl-plugin.js', - hashFunction: 'md5', - library: 'MiradorDownloadPlugin', - libraryExport: 'default', - libraryTarget: 'umd', - path: path.join(__dirname, 'umd'), - }, - plugins: [ - new webpack.IgnorePlugin({ - resourceRegExp: /@blueprintjs\/(core|icons)/, // ignore optional UI framework dependencies - }), - ], - resolve: { - fallback: { url: false }, - extensions: ['.js'], - }, -}); - -module.exports = (env, options) => { - const isProduction = options.mode === 'production'; - const config = baseConfig(options.mode); - - if (isProduction) { - return { - ...config, - devtool: 'source-map', - mode: 'production', - plugins: [ - ...(config.plugins || []), - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1, - }), - ], - }; - } - - return { - ...config, - output: { - filename: 'demo.js', - path: path.join(__dirname, 'demo/dist'), - publicPath: '/', - }, - devServer: { - hot: true, - port: 4444, - static: [ - './demo/dist/', - ], - }, - devtool: 'eval-source-map', - mode: 'development', - entry: ['./demo/src/index.js'], - plugins: [ - ...(config.plugins || []), - new HtmlWebpackPlugin({ template: path.join(__dirname, 'demo/src/index.html') }), - new ReactRefreshWebpackPlugin(), - ], - }; -};