From b5e0f3d6ff7939b69ca31f6c58cec59560ab11a9 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Fri, 18 Oct 2024 12:12:44 -0400 Subject: [PATCH] fix(vite): set `ssr.noExternal` even if not in project package.json (#404) --- src/__tests__/utils.js | 2 + src/__tests__/vite-plugin.test.js | 227 ++++++++++++++++++++++++++++++ src/vite.js | 48 ++++++- vite.config.js | 1 + 4 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/vite-plugin.test.js diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 68be33c..f637d6f 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -4,6 +4,8 @@ export const IS_JSDOM = window.navigator.userAgent.includes('jsdom') export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js +export const IS_JEST = Boolean(process.env.JEST_WORKER_ID) + export const IS_SVELTE_5 = SVELTE_VERSION >= '5' export const MODE_LEGACY = 'legacy' diff --git a/src/__tests__/vite-plugin.test.js b/src/__tests__/vite-plugin.test.js new file mode 100644 index 0000000..9232f65 --- /dev/null +++ b/src/__tests__/vite-plugin.test.js @@ -0,0 +1,227 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { svelteTesting } from '../vite.js' +import { IS_JEST } from './utils.js' + +describe.skipIf(IS_JEST)('vite plugin', () => { + beforeEach(() => { + vi.stubEnv('VITEST', '1') + }) + + test('does not modify config if disabled', () => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: false, + }) + + const result = {} + subject.config(result) + + expect(result).toEqual({}) + }) + + test('does not modify config if not Vitest', () => { + vi.stubEnv('VITEST', '') + + const subject = svelteTesting() + + const result = {} + subject.config(result) + + expect(result).toEqual({}) + }) + + test.each([ + { + config: () => ({ resolve: { conditions: ['node'] } }), + expectedConditions: ['browser', 'node'], + }, + { + config: () => ({ resolve: { conditions: ['svelte', 'node'] } }), + expectedConditions: ['svelte', 'browser', 'node'], + }, + ])( + 'adds browser condition if necessary', + ({ config, expectedConditions }) => { + const subject = svelteTesting({ + resolveBrowser: true, + autoCleanup: false, + noExternal: false, + }) + + const result = config() + subject.config(result) + + expect(result).toEqual({ + resolve: { + conditions: expectedConditions, + }, + }) + } + ) + + test.each([ + { + config: () => ({}), + expectedConditions: [], + }, + { + config: () => ({ resolve: { conditions: [] } }), + expectedConditions: [], + }, + { + config: () => ({ resolve: { conditions: ['svelte'] } }), + expectedConditions: ['svelte'], + }, + ])( + 'skips browser condition if possible', + ({ config, expectedConditions }) => { + const subject = svelteTesting({ + resolveBrowser: true, + autoCleanup: false, + noExternal: false, + }) + + const result = config() + subject.config(result) + + expect(result).toEqual({ + resolve: { + conditions: expectedConditions, + }, + }) + } + ) + + test.each([ + { + config: () => ({}), + expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], + }, + { + config: () => ({ test: { setupFiles: [] } }), + expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], + }, + { + config: () => ({ test: { setupFiles: 'other-file.js' } }), + expectedSetupFiles: [ + 'other-file.js', + expect.stringMatching(/src\/vitest.js$/u), + ], + }, + ])('adds cleanup', ({ config, expectedSetupFiles }) => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: true, + noExternal: false, + }) + + const result = config() + subject.config(result) + + expect(result).toEqual({ + test: { + setupFiles: expectedSetupFiles, + }, + }) + }) + + test('skips cleanup in global mode', () => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: true, + noExternal: false, + }) + + const result = { test: { globals: true } } + subject.config(result) + + expect(result).toEqual({ + test: { + globals: true, + }, + }) + }) + + test.each([ + { + config: () => ({ ssr: { noExternal: [] } }), + expectedNoExternal: ['@testing-library/svelte'], + }, + { + config: () => ({}), + expectedNoExternal: ['@testing-library/svelte'], + }, + { + config: () => ({ ssr: { noExternal: 'other-file.js' } }), + expectedNoExternal: ['other-file.js', '@testing-library/svelte'], + }, + { + config: () => ({ ssr: { noExternal: /other/u } }), + expectedNoExternal: [/other/u, '@testing-library/svelte'], + }, + ])('adds noExternal rule', ({ config, expectedNoExternal }) => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) + + const result = config() + subject.config(result) + + expect(result).toEqual({ + ssr: { + noExternal: expectedNoExternal, + }, + }) + }) + + test.each([ + { + config: () => ({ ssr: { noExternal: true } }), + expectedNoExternal: true, + }, + { + config: () => ({ ssr: { noExternal: '@testing-library/svelte' } }), + expectedNoExternal: '@testing-library/svelte', + }, + { + config: () => ({ ssr: { noExternal: /svelte/u } }), + expectedNoExternal: /svelte/u, + }, + ])('skips noExternal if able', ({ config, expectedNoExternal }) => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) + + const result = config() + subject.config(result) + + expect(result).toEqual({ + ssr: { + noExternal: expectedNoExternal, + }, + }) + }) + + test('bails on noExternal if input is unexpected', () => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) + + const result = { ssr: { noExternal: false } } + subject.config(result) + + expect(result).toEqual({ + ssr: { + noExternal: false, + }, + }) + }) +}) diff --git a/src/vite.js b/src/vite.js index 0062b89..1ad712f 100644 --- a/src/vite.js +++ b/src/vite.js @@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url' * Ensures Svelte is imported correctly in tests * and that the DOM is cleaned up after each test. * - * @param {{resolveBrowser?: boolean, autoCleanup?: boolean}} options + * @param {{resolveBrowser?: boolean, autoCleanup?: boolean, noExternal?: boolean}} options * @returns {import('vite').Plugin} */ export const svelteTesting = ({ resolveBrowser = true, autoCleanup = true, + noExternal = true, } = {}) => ({ name: 'vite-plugin-svelte-testing-library', config: (config) => { @@ -27,6 +28,10 @@ export const svelteTesting = ({ if (autoCleanup) { addAutoCleanup(config) } + + if (noExternal) { + addNoExternal(config) + } }, }) @@ -64,6 +69,10 @@ const addAutoCleanup = (config) => { const test = config.test ?? {} let setupFiles = test.setupFiles ?? [] + if (test.globals) { + return + } + if (typeof setupFiles === 'string') { setupFiles = [setupFiles] } @@ -73,3 +82,40 @@ const addAutoCleanup = (config) => { test.setupFiles = setupFiles config.test = test } + +/** + * Add `@testing-library/svelte` to Vite's noExternal rules, if not present. + * + * This ensures `@testing-library/svelte` is processed by `@sveltejs/vite-plugin-svelte` + * in certain monorepo setups. + */ +const addNoExternal = (config) => { + const ssr = config.ssr ?? {} + let noExternal = ssr.noExternal ?? [] + + if (noExternal === true) { + return + } + + if (typeof noExternal === 'string' || noExternal instanceof RegExp) { + noExternal = [noExternal] + } + + if (!Array.isArray(noExternal)) { + return + } + + for (const rule of noExternal) { + if (typeof rule === 'string' && rule === '@testing-library/svelte') { + return + } + + if (rule instanceof RegExp && rule.test('@testing-library/svelte')) { + return + } + } + + noExternal.push('@testing-library/svelte') + ssr.noExternal = noExternal + config.ssr = ssr +} diff --git a/vite.config.js b/vite.config.js index 76baf61..1ddeea5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,6 +11,7 @@ export default defineConfig({ setupFiles: ['./src/__tests__/_vitest-setup.js'], mockReset: true, unstubGlobals: true, + unstubEnvs: true, coverage: { provider: 'v8', include: ['src/**/*'],