From 00e9c58c242d77fc5ce7966ce6e289b0a7e2cf0e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 20 May 2022 19:09:37 +0200 Subject: [PATCH] feat: improve module formats (cjs, esm, iife) (#1247) * chore: adopt "tsup" * chore: copy worker with tsup * chore: bundle iife correctly, fix call frames * chore: inject "setTimeout" for node build * chore: log worker checksum * chore(postinstall): use early return over process.exit --- .eslintignore | 3 + .gitignore | 4 - CONTRIBUTING.md | 2 - config/constants.js | 4 +- config/copyServiceWorker.ts | 11 +- config/plugins/esbuild/workerScriptPlugin.ts | 82 +++ .../getChecksum.js | 24 - .../rollup-integrity-check-plugin/index.js | 49 -- config/polyfills-node.ts | 8 + config/scripts/postinstall.js | 57 +- native/package.json | 5 +- node/package.json | 5 +- package.json | 29 +- rollup.config.ts | 199 ------- src/node/createSetupServer.ts | 1 + src/utils/internal/getCallFrame.ts | 5 +- src/utils/internal/requestIntegrityCheck.ts | 2 +- test/msw-api/distribution/iife.test.ts | 4 +- test/msw-api/distribution/umd.mocks.ts | 9 - test/msw-api/distribution/umd.test.ts | 18 - .../request/body/body-form-data.node.test.ts | 2 +- test/tsconfig.json | 1 + test/typings/index.test-d.ts | 3 +- tsup.config.ts | 75 +++ yarn.lock | 534 +++++++++--------- 25 files changed, 515 insertions(+), 621 deletions(-) create mode 100644 config/plugins/esbuild/workerScriptPlugin.ts delete mode 100644 config/plugins/rollup-integrity-check-plugin/getChecksum.js delete mode 100644 config/plugins/rollup-integrity-check-plugin/index.js create mode 100644 config/polyfills-node.ts delete mode 100644 rollup.config.ts delete mode 100644 test/msw-api/distribution/umd.mocks.ts delete mode 100644 test/msw-api/distribution/umd.test.ts create mode 100644 tsup.config.ts diff --git a/.eslintignore b/.eslintignore index ed8bf3d62..2d0562177 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,4 @@ +lib +node +native test/typings \ No newline at end of file diff --git a/.gitignore b/.gitignore index 80589def8..6e582dda2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,6 @@ __* .DS_* node_modules /lib -/native/**/* -/node/**/* -!/node/package.json -!/native/package.json tmp *-error.log ./package-lock.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 722e73f2d..5a406d3f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -244,8 +244,6 @@ Build the library with the following command: $ yarn build ``` -> Learn more about the build in the [Rollup configuration file](../rollup.config.ts). - [yarn-url]: https://classic.yarnpkg.com/en/ [jest-url]: https://jestjs.io [page-with-url]: https://github.com/kettanaito/page-with diff --git a/config/constants.js b/config/constants.js index 342560588..e38940945 100644 --- a/config/constants.js +++ b/config/constants.js @@ -1,5 +1,4 @@ const path = require('path') -const packageJson = require('../package.json') const SERVICE_WORKER_SOURCE_PATH = path.resolve( __dirname, @@ -9,8 +8,7 @@ const SERVICE_WORKER_SOURCE_PATH = path.resolve( const SERVICE_WORKER_BUILD_PATH = path.resolve( __dirname, - '../', - path.dirname(packageJson.module), + '../lib', path.basename(SERVICE_WORKER_SOURCE_PATH), ) diff --git a/config/copyServiceWorker.ts b/config/copyServiceWorker.ts index 22a309901..b447f298e 100644 --- a/config/copyServiceWorker.ts +++ b/config/copyServiceWorker.ts @@ -1,6 +1,6 @@ import * as fs from 'fs' import * as path from 'path' -import * as chalk from 'chalk' +import chalk from 'chalk' import { until } from '@open-draft/until' /** @@ -30,7 +30,14 @@ export default async function copyServiceWorker( await fs.promises.mkdir(destFileDirectory, { recursive: true }) } - const nextFileContent = fileContent.replace('', checksum) + const packageJson = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'), + ) + + const nextFileContent = fileContent + .replace('', checksum) + .replace('', packageJson.version) + const [writeFileError] = await until(() => fs.promises.writeFile(destFilePath, nextFileContent), ) diff --git a/config/plugins/esbuild/workerScriptPlugin.ts b/config/plugins/esbuild/workerScriptPlugin.ts new file mode 100644 index 000000000..f2c5885b7 --- /dev/null +++ b/config/plugins/esbuild/workerScriptPlugin.ts @@ -0,0 +1,82 @@ +import path from 'path' +import fs from 'fs-extra' +import crypto from 'crypto' +import minify from 'babel-minify' +import { invariant } from 'outvariant' +import type { Plugin } from 'esbuild' +import copyServiceWorker from '../../copyServiceWorker' + +function getChecksum(contents: string): string { + const { code } = minify( + contents, + {}, + { + // @ts-ignore "babel-minify" has no type definitions. + comments: false, + }, + ) + + return crypto.createHash('md5').update(code, 'utf8').digest('hex') +} + +let hasRunAlready = false + +export function workerScriptPlugin(): Plugin { + return { + name: 'workerScriptPlugin', + async setup(build) { + const workerSourcePath = path.resolve( + process.cwd(), + './src/mockServiceWorker.js', + ) + const workerOutputPath = path.resolve( + process.cwd(), + './lib/mockServiceWorker.js', + ) + + invariant( + workerSourcePath, + 'Failed to locate the worker script source file', + ) + invariant( + workerOutputPath, + 'Failed to locate the worker script output file', + ) + + // Generate the checksum from the worker script's contents. + const workerContents = await fs.readFile(workerSourcePath, 'utf8') + const checksum = getChecksum(workerContents) + + // Inject the global "SERVICE_WORKER_CHECKSUM" variable + // for runtime worker integrity check. + build.initialOptions.define = { + SERVICE_WORKER_CHECKSUM: JSON.stringify(checksum), + } + + // Prevent from copying the worker script multiple times. + // esbuild will execute this plugin for *each* format. + if (hasRunAlready) { + return + } + + hasRunAlready = true + + build.onLoad({ filter: /mockServiceWorker\.js$/ }, async () => { + return { + // Prevent the worker script from being transpiled. + // But, generally, the worker script is not in the entrypoints. + contents: '', + } + }) + + build.onEnd(() => { + console.log('worker script checksum:', checksum) + + // Copy the worker script on the next tick. + setTimeout(async () => { + await copyServiceWorker(workerSourcePath, workerOutputPath, checksum) + }, 100) + }) + }, + } +} diff --git a/config/plugins/rollup-integrity-check-plugin/getChecksum.js b/config/plugins/rollup-integrity-check-plugin/getChecksum.js deleted file mode 100644 index a73ca7fc7..000000000 --- a/config/plugins/rollup-integrity-check-plugin/getChecksum.js +++ /dev/null @@ -1,24 +0,0 @@ -const fs = require('fs') -const chalk = require('chalk') -const crypto = require('crypto') -const minify = require('babel-minify') - -/** - * Returns an MD5 checksum for minified and normalized Service Worker file. - */ -module.exports = function getChecksum(sourceFilePath) { - const fileContent = fs.readFileSync(sourceFilePath, 'utf8') - const { code } = minify( - fileContent, - {}, - { - comments: false, - }, - ) - - const checksum = crypto.createHash('md5').update(code, 'utf8').digest('hex') - - console.log('Generated checksum: %s', chalk.magenta(checksum)) - - return checksum -} diff --git a/config/plugins/rollup-integrity-check-plugin/index.js b/config/plugins/rollup-integrity-check-plugin/index.js deleted file mode 100644 index 432da3c7d..000000000 --- a/config/plugins/rollup-integrity-check-plugin/index.js +++ /dev/null @@ -1,49 +0,0 @@ -const fs = require('fs') -const path = require('path') -const chalk = require('chalk') -const replace = require('@rollup/plugin-replace') -const getChecksum = require('./getChecksum') -const packageJson = require('../../../package.json') - -function injectChecksum(checksum) { - return { - SERVICE_WORKER_CHECKSUM: JSON.stringify(checksum), - } -} - -module.exports = function integrityCheckPlugin(options) { - const { - input, - output, - checksumPlaceholder, - packageVersionPlaceholder, - } = options - - return { - name: 'integrity-check', - transform(...args) { - this.addWatchFile(input) - return replace(injectChecksum(this.checksum)).transform(...args) - }, - buildStart() { - if (!fs.existsSync(input)) { - this.error(`Failed to locate the Service Worker file at: ${input}`) - } - - console.log('Signing the Service Worker at:\n%s', chalk.cyan(input)) - - this.checksum = getChecksum(input) - - const workerContent = fs.readFileSync(input, 'utf8') - const publicWorkerContent = workerContent - .replace(checksumPlaceholder, this.checksum) - .replace(packageVersionPlaceholder, packageJson.version) - - this.emitFile({ - type: 'asset', - fileName: path.basename(output), - source: publicWorkerContent, - }) - }, - } -} diff --git a/config/polyfills-node.ts b/config/polyfills-node.ts new file mode 100644 index 000000000..71a403cc1 --- /dev/null +++ b/config/polyfills-node.ts @@ -0,0 +1,8 @@ +import { setTimeout as nodeSetTimeout } from 'timers' + +// Polyfill the global "setTimeout" so MSW could be used +// with "jest.useFakeTimers()". MSW response handling +// is wrapped in "setTimeout", and without this polyfill +// you'd have to manually advance the timers for the response +// to finally resolve. +export const setTimeout = nodeSetTimeout diff --git a/config/scripts/postinstall.js b/config/scripts/postinstall.js index 6bf321d6d..cc852bfd9 100755 --- a/config/scripts/postinstall.js +++ b/config/scripts/postinstall.js @@ -7,35 +7,42 @@ const { execSync } = require('child_process') // NPM stores the parent project directory in the "INIT_CWD" env variable. const parentPackageCwd = process.env.INIT_CWD -// 1. Check if "package.json" has "msw.workerDirectory" property set. -const packageJson = JSON.parse( - fs.readFileSync(path.resolve(parentPackageCwd, 'package.json'), 'utf8'), -) - -if (!packageJson.msw || !packageJson.msw.workerDirectory) { - return -} +function postinstall() { + // 1. Check if "package.json" has "msw.workerDirectory" property set. + const packageJson = JSON.parse( + fs.readFileSync(path.resolve(parentPackageCwd, 'package.json'), 'utf8'), + ) -// 2. Check if the worker directory is an existing path. -const { workerDirectory } = packageJson.msw -const absoluteWorkerDirectory = path.resolve(parentPackageCwd, workerDirectory) + if (!packageJson.msw || !packageJson.msw.workerDirectory) { + return + } -if (!fs.existsSync(absoluteWorkerDirectory)) { - return console.error( - `[MSW] Failed to automatically update the worker script at "%s": given path does not exist.`, + // 2. Check if the worker directory is an existing path. + const { workerDirectory } = packageJson.msw + const absoluteWorkerDirectory = path.resolve( + parentPackageCwd, workerDirectory, ) -} -// 3. Update the worker script. -const cliExecutable = path.resolve(process.cwd(), 'cli/index.js') + if (!fs.existsSync(absoluteWorkerDirectory)) { + return console.error( + `[MSW] Failed to automatically update the worker script at "%s": given path does not exist.`, + workerDirectory, + ) + } -try { - execSync(`node ${cliExecutable} init ${absoluteWorkerDirectory}`, { - cwd: parentPackageCwd, - }) -} catch (error) { - console.error( - `[MSW] Failed to automatically update the worker script:\n${error}`, - ) + // 3. Update the worker script. + const cliExecutable = path.resolve(process.cwd(), 'cli/index.js') + + try { + execSync(`node ${cliExecutable} init ${absoluteWorkerDirectory}`, { + cwd: parentPackageCwd, + }) + } catch (error) { + console.error( + `[MSW] Failed to automatically update the worker script:\n${error}`, + ) + } } + +postinstall() diff --git a/native/package.json b/native/package.json index 853e27df0..bb12dd1d1 100644 --- a/native/package.json +++ b/native/package.json @@ -1,4 +1,5 @@ { - "main": "./lib/index.js", - "types": "../lib/types/native" + "main": "../lib/native/index.js", + "module": "../lib/native/index.mjs", + "typings": "../lib/native/index.d.ts" } diff --git a/node/package.json b/node/package.json index 415b2d544..3d1591115 100644 --- a/node/package.json +++ b/node/package.json @@ -1,4 +1,5 @@ { - "main": "./lib/index.js", - "types": "../lib/types/node" + "main": "../lib/node/index.js", + "module": "../lib/node/index.mjs", + "typings": "../lib/node/index.d.ts" } diff --git a/package.json b/package.json index a681b15e9..9b23bcaf1 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "msw", "version": "0.40.2", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", - "main": "lib/umd/index.js", - "module": "lib/esm/index.js", - "types": "lib/types/index.d.ts", + "main": "./lib/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/index.d.ts", "bin": { "msw": "cli/index.js" }, @@ -12,17 +12,15 @@ "node": ">=14" }, "scripts": { - "start": "cross-env NODE_ENV=development rollup -c rollup.config.ts -w", - "clean": "rimraf lib {native,node}/lib", + "start": "tsup --watch", + "clean": "rimraf ./lib", "lint": "eslint \"{cli,config,src,test}/**/*.ts\"", - "prebuild": "yarn clean && yarn lint", - "build": "cross-env NODE_ENV=production rollup -c rollup.config.ts", - "postbuild": "yarn test:ts", + "build": "cross-env NODE_ENV=production tsup", "test": "yarn test:unit && yarn test:integration", "test:unit": "cross-env BABEL_ENV=test jest --maxWorkers=3", - "test:integration": "jest --config=test/jest.config.js --maxWorkers=1", - "test:smoke": "config/scripts/smoke.sh", - "test:ts": "yarn tsc -p test/typings/tsconfig.json", + "test:integration": "jest --config=./test/jest.config.js --maxWorkers=1", + "test:smoke": "./config/scripts/smoke.sh", + "test:ts": "yarn tsc -p ./test/typings/tsconfig.json", "prepare": "yarn simple-git-hooks init", "prepack": "yarn build", "release": "release publish", @@ -96,11 +94,6 @@ "@commitlint/config-conventional": "^16.0.0", "@open-draft/test-server": "^0.2.3", "@ossjs/release": "^0.3.0", - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-inject": "^4.0.4", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.1.3", - "@rollup/plugin-replace": "^3.1.0", "@types/fs-extra": "^9.0.13", "@types/jest": "26", "@types/json-bigint": "^1.0.1", @@ -128,13 +121,11 @@ "prettier": "^2.3.2", "regenerator-runtime": "^0.13.9", "rimraf": "^3.0.2", - "rollup": "^2.67.2", - "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.31.2", "simple-git-hooks": "^2.7.0", "ts-jest": "26", "ts-loader": "^9.2.6", "ts-node": "^10.1.0", + "tsup": "^5.12.8", "typescript": "^4.6.4", "url-loader": "^4.1.1", "webpack": "^5.68.0", diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index fdc39a64a..000000000 --- a/rollup.config.ts +++ /dev/null @@ -1,199 +0,0 @@ -import * as path from 'path' -import resolve from '@rollup/plugin-node-resolve' -import commonjs from '@rollup/plugin-commonjs' -import inject from '@rollup/plugin-inject' -import typescript from 'rollup-plugin-typescript2' -import json from '@rollup/plugin-json' -import replace from '@rollup/plugin-replace' -import { terser } from 'rollup-plugin-terser' -import packageJson from './package.json' - -const integrityCheck = require('./config/plugins/rollup-integrity-check-plugin') -const { - SERVICE_WORKER_SOURCE_PATH, - SERVICE_WORKER_BUILD_PATH, -} = require('./config/constants') - -const extensions = ['.js', '.ts'] - -const integrityPluginOptions = { - checksumPlaceholder: '', - packageVersionPlaceholder: '', - input: SERVICE_WORKER_SOURCE_PATH, - output: SERVICE_WORKER_BUILD_PATH, -} - -/** - * Exclude @mswjs/interceptors and @mswjw/cookies - * (and any relative paths under these packages). - * @see https://github.com/mswjs/interceptors/issues/52 - */ -const ecosystemDependencies = /^@mswjs\/(interceptors|cookies)/ - -/** - * Configuration for the ESM build. - */ -const buildEsm = { - input: [ - // Split modules so they can be tree-shaken. - 'src/index.ts', - 'src/rest.ts', - 'src/graphql.ts', - 'src/context/index.ts', - ], - external: ['debug', ecosystemDependencies], - output: { - format: 'esm', - entryFileNames: '[name].js', - chunkFileNames: '[name]-deps.js', - dir: path.dirname(packageJson.module), - }, - plugins: [ - json(), - resolve({ - preferBuiltins: false, - mainFields: ['module', 'main', 'jsnext:main'], - extensions, - }), - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify('development'), - }), - integrityCheck(integrityPluginOptions), - typescript({ - useTsconfigDeclarationDir: true, - }), - commonjs(), - ], -} - -/** - * Configuration for the UMD build. - */ -const buildUmd = { - input: 'src/index.ts', - output: { - format: 'umd', - file: packageJson.main, - name: 'MockServiceWorker', - esModule: false, - }, - plugins: [ - json(), - replace({ - preventAssignment: false, - 'process.env.NODE_ENV': JSON.stringify('development'), - }), - resolve({ - browser: true, - preferBuiltins: false, - mainFields: ['browser', 'main', 'module'], - extensions, - }), - integrityCheck(integrityPluginOptions), - typescript({ - useTsconfigDeclarationDir: true, - }), - commonjs(), - ], -} - -/** - * Configuration for the Node.js (CJS) build. - */ -const buildNode = { - input: 'src/node/index.ts', - external: [ - 'http', - 'https', - 'util', - 'events', - 'tty', - 'os', - 'timers', - ecosystemDependencies, - ], - output: { - format: 'cjs', - file: 'node/lib/index.js', - }, - plugins: [ - json(), - resolve({ - browser: false, - preferBuiltins: true, - extensions, - }), - typescript({ - useTsconfigDeclarationDir: true, - tsconfigOverride: { - outDir: './node/lib', - }, - }), - inject({ - setTimeout: ['timers', 'setTimeout'], - }), - commonjs(), - ], -} - -/** - * Configuration for the React Native (CJS) build. - */ -const buildNative = { - input: 'src/native/index.ts', - external: ['chalk', 'util', 'events', ecosystemDependencies], - output: { - file: 'native/lib/index.js', - format: 'cjs', - }, - plugins: [ - json(), - resolve({ - browser: false, - preferBuiltins: true, - extensions, - }), - typescript({ - useTsconfigDeclarationDir: true, - tsconfigOverride: { - outDir: './native/lib', - }, - }), - commonjs(), - ], -} - -/** - * Configuration for the IIFE build. - */ -const buildIife = { - input: 'src/index.ts', - output: { - file: 'lib/iife/index.js', - name: 'MockServiceWorker', - format: 'iife', - esModule: false, - }, - plugins: [ - json(), - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - resolve({ - browser: true, - preferBuiltins: false, - mainFields: ['browser', 'module', 'main', 'jsnext:main'], - extensions, - }), - integrityCheck(integrityPluginOptions), - typescript({ - useTsconfigDeclarationDir: true, - }), - commonjs(), - terser(), - ], -} - -export default [buildNode, buildNative, buildEsm, buildUmd, buildIife] diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts index d358c257a..a7a7fe172 100644 --- a/src/node/createSetupServer.ts +++ b/src/node/createSetupServer.ts @@ -119,6 +119,7 @@ export function createSetupServer(...interceptors: Interceptor[]) { printHandlers() { currentHandlers.forEach((handler) => { const { header, callFrame } = handler.info + const pragma = handler.info.hasOwnProperty('operationType') ? '[graphql]' : '[rest]' diff --git a/src/utils/internal/getCallFrame.ts b/src/utils/internal/getCallFrame.ts index cf21626cc..d1c6c49e7 100644 --- a/src/utils/internal/getCallFrame.ts +++ b/src/utils/internal/getCallFrame.ts @@ -1,3 +1,6 @@ +// Ignore the source files traces for local testing. +const SOURCE_FRAME = /\/msw\/src\/(.+)/ + const BUILD_FRAME = /(node_modules)?[\/\\]lib[\/\\](umd|esm|iief|cjs)[\/\\]|^[^\/\\]*$/ @@ -17,7 +20,7 @@ export function getCallFrame(error: Error) { // Get the first frame that doesn't reference the library's internal trace. // Assume that frame is the invocation frame. const declarationFrame = frames.find((frame) => { - return !BUILD_FRAME.test(frame) + return !(SOURCE_FRAME.test(frame) || BUILD_FRAME.test(frame)) }) if (!declarationFrame) { diff --git a/src/utils/internal/requestIntegrityCheck.ts b/src/utils/internal/requestIntegrityCheck.ts index 4e62b8c7c..10f1fb112 100644 --- a/src/utils/internal/requestIntegrityCheck.ts +++ b/src/utils/internal/requestIntegrityCheck.ts @@ -12,7 +12,7 @@ export async function requestIntegrityCheck( ) // Compare the response from the Service Worker and the - // global variable set by Rollup during the build. + // global variable set during the build. if (actualChecksum !== SERVICE_WORKER_CHECKSUM) { throw new Error( `Currently active Service Worker (${actualChecksum}) is behind the latest published one (${SERVICE_WORKER_CHECKSUM}).`, diff --git a/test/msw-api/distribution/iife.test.ts b/test/msw-api/distribution/iife.test.ts index f5546c105..98704d9b0 100644 --- a/test/msw-api/distribution/iife.test.ts +++ b/test/msw-api/distribution/iife.test.ts @@ -1,7 +1,7 @@ import * as path from 'path' import { pageWith } from 'page-with' -it('supports the usage of the IIFE bundle in a