From 025a6aae01d970c1ac9c3ee3fa45993a724892a8 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 19:49:51 +0200 Subject: [PATCH 01/14] feat(esm): Add vitest plugins and config to packages/testing (take 2) --- packages/storybook/src/plugins/mock-router.ts | 1 + packages/testing/src/api/mockContext.ts | 46 ++ .../src/api/vitest/CedarApiVitestEnv.ts | 59 +++ packages/testing/src/api/vitest/index.ts | 3 + .../src/api/vitest/vite-plugin-auto-import.ts | 36 ++ .../vite-plugin-cedar-vitest-api-config.ts | 35 ++ .../vitest/vite-plugin-track-db-imports.ts | 33 ++ .../src/api/vitest/vitest-api.setup.ts | 413 ++++++++++++++++++ packages/testing/src/web/MockRouter.tsx | 12 +- packages/testing/src/web/vitest/index.ts | 3 + .../src/web/vitest/vite-plugin-auto-import.ts | 25 ++ ...-plugin-cedarjs-router-import-transform.ts | 24 + ...ite-plugin-create-auth-import-transform.ts | 26 ++ packages/testing/tsconfig.cjs.json | 10 +- 14 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 packages/testing/src/api/mockContext.ts create mode 100644 packages/testing/src/api/vitest/CedarApiVitestEnv.ts create mode 100644 packages/testing/src/api/vitest/index.ts create mode 100644 packages/testing/src/api/vitest/vite-plugin-auto-import.ts create mode 100644 packages/testing/src/api/vitest/vite-plugin-cedar-vitest-api-config.ts create mode 100644 packages/testing/src/api/vitest/vite-plugin-track-db-imports.ts create mode 100644 packages/testing/src/api/vitest/vitest-api.setup.ts create mode 100644 packages/testing/src/web/vitest/index.ts create mode 100644 packages/testing/src/web/vitest/vite-plugin-auto-import.ts create mode 100644 packages/testing/src/web/vitest/vite-plugin-cedarjs-router-import-transform.ts create mode 100644 packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts diff --git a/packages/storybook/src/plugins/mock-router.ts b/packages/storybook/src/plugins/mock-router.ts index 1043006947..60f4c67bf5 100644 --- a/packages/storybook/src/plugins/mock-router.ts +++ b/packages/storybook/src/plugins/mock-router.ts @@ -8,6 +8,7 @@ export function mockRouter(): PluginOption { if (id.includes('src')) { code = code.replace( "'@cedarjs/router'", + // TODO(storybook): Use the mock router from @cedarjs/testing instead "'storybook-framework-cedarjs/dist/mocks/MockRouter'", ) } diff --git a/packages/testing/src/api/mockContext.ts b/packages/testing/src/api/mockContext.ts new file mode 100644 index 0000000000..54343cd4a6 --- /dev/null +++ b/packages/testing/src/api/mockContext.ts @@ -0,0 +1,46 @@ +// TODO: See if we can use `GlobalContext` instead of `any` here to more +// closely match the production context. +// https://github.com/cedarjs/cedar/pull/355#discussion_r2264851576 +const mockContextStore = new Map() +const mockContext = new Proxy( + {}, + { + get: (_target, prop) => { + // Handle toJSON() calls, i.e. JSON.stringify(context) + if (prop === 'toJSON') { + return () => mockContextStore.get('context') + } + + const ctx = mockContextStore.get('context') + + if (!ctx) { + return undefined + } + + return ctx[prop] + }, + set: (_target, prop, value) => { + const ctx = mockContextStore.get('context') + + if (!ctx) { + return false + } + + ctx[prop] = value + + return true + }, + }, +) + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface GlobalContext extends Record {} + +export const context = mockContext + +export const setContext = (newContext: GlobalContext): GlobalContext => { + mockContextStore.set('context', newContext) + // TODO: See if this should be `newContext` instead + // https://github.com/cedarjs/cedar/pull/355#discussion_r2264851567 + return mockContext +} diff --git a/packages/testing/src/api/vitest/CedarApiVitestEnv.ts b/packages/testing/src/api/vitest/CedarApiVitestEnv.ts new file mode 100644 index 0000000000..c933d0286f --- /dev/null +++ b/packages/testing/src/api/vitest/CedarApiVitestEnv.ts @@ -0,0 +1,59 @@ +import { getSchema } from '@prisma/internals' +import 'dotenv-defaults/config.js' +import execa from 'execa' +import type { Environment } from 'vitest/environments' + +import { getPaths } from '@cedarjs/project-config' + +import { getDefaultDb, checkAndReplaceDirectUrl } from '../directUrlHelpers.js' + +const CedarApiVitestEnvironment: Environment = { + name: 'cedar-api', + transformMode: 'ssr', + + async setup() { + if (process.env.SKIP_DB_PUSH === '1') { + return { + teardown() {}, + } + } + + const cedarPaths = getPaths() + const defaultDb = getDefaultDb(cedarPaths.base) + + process.env.DATABASE_URL = process.env.TEST_DATABASE_URL || defaultDb + + // NOTE: This is a workaround to get the directUrl from the schema + // Instead of using the schema, we can use the config file + // const prismaConfig = await getConfig(rwjsPaths.api.dbSchema) + // and then check for the prismaConfig.datasources[0].directUrl + const prismaSchema = (await getSchema(cedarPaths.api.dbSchema)).toString() + + const directUrlEnvVar = checkAndReplaceDirectUrl(prismaSchema, defaultDb) + + const command = + process.env.TEST_DATABASE_STRATEGY === 'reset' + ? ['prisma', 'migrate', 'reset', '--force', '--skip-seed'] + : ['prisma', 'db', 'push', '--force-reset', '--accept-data-loss'] + + const directUrlDefinition = directUrlEnvVar + ? { [directUrlEnvVar]: process.env[directUrlEnvVar] } + : {} + + execa.sync(`yarn rw`, command, { + cwd: cedarPaths.api.base, + stdio: 'inherit', + shell: true, + env: { + DATABASE_URL: process.env.DATABASE_URL, + ...directUrlDefinition, + }, + }) + + return { + teardown() {}, + } + }, +} + +export default CedarApiVitestEnvironment diff --git a/packages/testing/src/api/vitest/index.ts b/packages/testing/src/api/vitest/index.ts new file mode 100644 index 0000000000..95a6a2063f --- /dev/null +++ b/packages/testing/src/api/vitest/index.ts @@ -0,0 +1,3 @@ +export { autoImportsPlugin } from './vite-plugin-auto-import.js' +export { cedarVitestApiConfigPlugin } from './vite-plugin-cedar-vitest-api-config.js' +export { trackDbImportsPlugin } from './vite-plugin-track-db-imports.js' diff --git a/packages/testing/src/api/vitest/vite-plugin-auto-import.ts b/packages/testing/src/api/vitest/vite-plugin-auto-import.ts new file mode 100644 index 0000000000..2817e33b9c --- /dev/null +++ b/packages/testing/src/api/vitest/vite-plugin-auto-import.ts @@ -0,0 +1,36 @@ +import autoImport from 'unplugin-auto-import/vite' + +export function autoImportsPlugin() { + return autoImport({ + // targets to transform + include: [ + /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx + ], + + // global imports to register + imports: [ + // import { mockContext, mockHttpEvent, mockSignedWebhook } from '@cedarjs/testing/api'; + { + '@cedarjs/testing/api': [ + 'mockContext', + 'mockHttpEvent', + 'mockSignedWebhook', + ], + }, + // import { gql } from 'graphql-tag' + { + 'graphql-tag': ['gql'], + }, + // import { context } from '@cedarjs/context' + { + '@cedarjs/context': ['context'], + }, + ], + + // We provide our mocking types elsewhere and so don't need this plugin to + // generate them. + // TODO: Maybe we should have it at least generate the types for the gql + // import? (Or do we already provide that some other way?) + dts: false, + }) +} diff --git a/packages/testing/src/api/vitest/vite-plugin-cedar-vitest-api-config.ts b/packages/testing/src/api/vitest/vite-plugin-cedar-vitest-api-config.ts new file mode 100644 index 0000000000..d8f102d930 --- /dev/null +++ b/packages/testing/src/api/vitest/vite-plugin-cedar-vitest-api-config.ts @@ -0,0 +1,35 @@ +import path from 'node:path' + +import type { Plugin } from 'vite' + +import { getEnvVarDefinitions, getPaths } from '@cedarjs/project-config' + +export function cedarVitestApiConfigPlugin(): Plugin { + return { + name: 'cedar-vitest-plugin', + config: () => { + return { + define: getEnvVarDefinitions(), + ssr: { + noExternal: ['@cedarjs/testing'], + }, + resolve: { + alias: { + src: getPaths().api.src, + }, + }, + test: { + environment: path.join(import.meta.dirname, 'CedarApiVitestEnv.js'), + // fileParallelism: false, + // fileParallelism doesn't work with vitest projects (which is what + // we're using in the root vitest.config.ts). As a workaround we set + // poolOptions instead, which also shouldn't work, but was suggested + // by Vitest team member AriPerkkio (Hiroshi's answer didn't work). + // https://github.com/vitest-dev/vitest/discussions/7416 + poolOptions: { forks: { singleFork: true } }, + setupFiles: [path.join(import.meta.dirname, 'vitest-api.setup.js')], + }, + } + }, + } +} diff --git a/packages/testing/src/api/vitest/vite-plugin-track-db-imports.ts b/packages/testing/src/api/vitest/vite-plugin-track-db-imports.ts new file mode 100644 index 0000000000..fa32f7f127 --- /dev/null +++ b/packages/testing/src/api/vitest/vite-plugin-track-db-imports.ts @@ -0,0 +1,33 @@ +import type { Plugin } from 'vite' + +export function trackDbImportsPlugin(): Plugin { + return { + name: 'db-import-tracker', + transform(code, id) { + // This regex and code content check could potentially match other files. + // It's very unlikely, but it is possible. For now this is good enough + if ( + id.match(/\/api\/src\/lib\/db\.(js|ts)$/) && + code.includes('PrismaClient') + ) { + // Inserting the code last (instead of at the top) works nicer with + // sourcemaps + return ( + code + + ` + ;if (typeof globalThis !== "undefined") { + globalThis.__cedarjs_db_imported__ = true; + } else { + throw new Error( + "vite-plugin-track-db-imports: globalThis is undefined. " + + "This is an error with CedarJS" + ); + } + ` + ) + } + + return code + }, + } +} diff --git a/packages/testing/src/api/vitest/vitest-api.setup.ts b/packages/testing/src/api/vitest/vitest-api.setup.ts new file mode 100644 index 0000000000..cada8e3d20 --- /dev/null +++ b/packages/testing/src/api/vitest/vitest-api.setup.ts @@ -0,0 +1,413 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { afterAll, beforeEach, it, describe, vi, beforeAll } from 'vitest' + +import { getPaths } from '@cedarjs/project-config' +import { defineScenario } from '@cedarjs/testing/api' +import type { DefineScenario } from '@cedarjs/testing/api' + +// Attempt to emulate the request context isolation behavior +// This is a little more complicated than it would necessarily need to be +// but we're following the same pattern as in `@cedarjs/context` +const mockContextStore = vi.hoisted(() => new Map()) +const mockContext = vi.hoisted( + () => + new Proxy( + {}, + { + get: (_target, prop) => { + // Handle toJSON() calls, i.e. JSON.stringify(context) + if (prop === 'toJSON') { + return () => mockContextStore.get('context') + } + + return mockContextStore.get('context')[prop] + }, + set: (_target, prop, value) => { + const ctx = mockContextStore.get('context') + ctx[prop] = value + + return true + }, + }, + ), +) + +vi.mock('@cedarjs/context', () => { + return { + context: mockContext, + setContext: (newContext: unknown) => { + mockContextStore.set('context', newContext) + }, + } +}) + +beforeEach(() => { + mockContextStore.set('context', {}) +}) + +declare global { + // eslint-disable-next-line no-var + var mockCurrentUser: (currentUser: Record | null) => void +} + +globalThis.mockCurrentUser = (currentUser: Record | null) => { + mockContextStore.set('context', { currentUser }) +} + +// ==================================== +// Scenario support +// ==================================== + +declare global { + // eslint-disable-next-line no-var + var defineScenario: DefineScenario + // eslint-disable-next-line no-var + var __cedarjs_db_imported__: boolean +} + +globalThis.defineScenario = defineScenario + +const cedarPaths = getPaths() + +// Error codes thrown by [MySQL, SQLite, Postgres] when foreign key constraint +// fails on DELETE +const FOREIGN_KEY_ERRORS = [1451, 1811, 23503] +const TEARDOWN_CACHE_PATH = path.join( + cedarPaths.generated.base, + 'scenarioTeardown.json', +) +const DEFAULT_SCENARIO = 'standard' +let teardownOrder: (string | null)[] = [] +let originalTeardownOrder: string[] = [] + +type It = typeof it | typeof it.only +type Describe = typeof describe | typeof describe.only +type TestFunc = (scenarioData: any) => any +type DescribeBlock = (getScenario: () => any) => any + +/** + * Wraps "it" or "test", to seed and teardown the scenario after each test + * This one passes scenario data to the test function + */ +function buildScenario(itFunc: It) { + return ( + ...args: + | [scenarioName: string, testName: string, testFunc: TestFunc] + | [testName: string, testFunc: TestFunc] + ) => { + let scenarioName: string + let testName: string + let testFunc: TestFunc + + if (args.length === 3) { + ;[scenarioName, testName, testFunc] = args + } else if (args.length === 2) { + scenarioName = DEFAULT_SCENARIO + ;[testName, testFunc] = args + } else { + throw new Error('scenario() requires 2 or 3 arguments') + } + + return itFunc(testName, async (ctx) => { + const testPath = ctx.task.file.filepath + const { scenario } = await loadScenarios(testPath, scenarioName) + + const scenarioData = await seedScenario(scenario) + try { + const result = await testFunc(scenarioData) + + return result + } finally { + // Make sure to cleanup, even if test fails + await teardown() + } + }) + } +} + +/** + * This creates a describe() block that will seed the scenario ONCE before all tests in the block + * Note that you need to use the getScenario() function to get the data. + */ +function buildDescribeScenario(describeFunc: Describe) { + return ( + ...args: [string, string, DescribeBlock] | [string, DescribeBlock] + ) => { + let scenarioName: string + let describeBlockName: string + let describeBlock: DescribeBlock + + if (args.length === 3) { + ;[scenarioName, describeBlockName, describeBlock] = args + } else if (args.length === 2) { + scenarioName = DEFAULT_SCENARIO + ;[describeBlockName, describeBlock] = args + } else { + throw new Error('describeScenario() requires 2 or 3 arguments') + } + + return describeFunc(describeBlockName, () => { + let scenarioData: Record + + beforeAll(async (ctx) => { + const testPath = ctx.file.filepath + const { scenario } = await loadScenarios(testPath, scenarioName) + scenarioData = await seedScenario(scenario) + }) + + afterAll(async () => { + await teardown() + }) + + const getScenario = () => scenarioData + + describeBlock(getScenario) + }) + } +} + +async function configureTeardown() { + if (!wasDbImported()) { + return + } + + const { getDMMF, getSchema } = await import('@prisma/internals') + + // @NOTE prisma utils are available in cli lib/schemaHelpers + // But avoid importing them, to prevent memory leaks in jest + const datamodel = await getSchema(cedarPaths.api.dbSchema) + const schema = await getDMMF({ datamodel }) + const schemaModels = schema.datamodel.models.map((m) => { + return m.dbName || m.name + }) + + // check if pre-defined delete order already exists and if so, use it to start + if (fs.existsSync(TEARDOWN_CACHE_PATH)) { + teardownOrder = JSON.parse(fs.readFileSync(TEARDOWN_CACHE_PATH).toString()) + } + + // check the number of models in case we've added or removed any models since + // cache was built + if (teardownOrder.length !== schemaModels.length) { + teardownOrder = schemaModels + } + + // keep a copy of the original order to compare against + originalTeardownOrder = deepCopy(teardownOrder) +} + +beforeAll(async () => { + await configureTeardown() +}) + +afterAll(() => { + // afterAll runs after all the tests in a single test file, and then for the + // next test file the code that's injected in src/lib/db.ts in the user's + // project will set this to `true` again if it's imported + globalThis.__cedarjs_db_imported__ = false +}) + +async function teardown() { + if (!wasDbImported()) { + return + } + + const quoteStyle = await getQuoteStyle() + const projectDb = await getProjectDb() + + for (const modelName of teardownOrder) { + try { + const query = `DELETE FROM ${quoteStyle}${modelName}${quoteStyle}` + await projectDb.$executeRawUnsafe(query) + } catch (e) { + console.error('teardown error\n', e) + const match = isErrorWithMessage(e) && e.message.match(/Code: `(\d+)`/) + + if (match && FOREIGN_KEY_ERRORS.includes(parseInt(match[1]))) { + const index = teardownOrder.indexOf(modelName) + teardownOrder[index] = null + teardownOrder.push(modelName) + } else { + throw e + } + } + } + + // remove nulls + teardownOrder = teardownOrder.filter((val) => val) + + // if the order of delete changed, write out the cached file again + if (!isIdenticalArray(teardownOrder, originalTeardownOrder)) { + originalTeardownOrder = deepCopy(teardownOrder) + fs.writeFileSync(TEARDOWN_CACHE_PATH, JSON.stringify(teardownOrder)) + } +} + +const seedScenario = async (scenario: Record) => { + if (scenario) { + const scenarios: Record = {} + + const projectDb = await getProjectDb() + + for (const [model, namedFixtures] of Object.entries(scenario)) { + scenarios[model] = {} + + for (const [name, createArgs] of Object.entries(namedFixtures)) { + if (typeof createArgs === 'function') { + scenarios[model][name] = await projectDb[model].create( + createArgs(scenarios), + ) + } else { + scenarios[model][name] = await projectDb[model].create(createArgs) + } + } + } + + return scenarios + } else { + return {} + } +} + +async function loadScenarios(testPath: string, scenarioName: string) { + const testFileDir = path.parse(testPath) + // e.g. ['comments', 'test'] or ['signup', 'state', 'machine', 'test'] + const testFileNameParts = testFileDir.name.split('.') + const testFilePath = `${testFileDir.dir}/${testFileNameParts + .slice(0, testFileNameParts.length - 1) + .join('.')}.scenarios` + let allScenarios: Record | undefined + let scenario: any + + try { + allScenarios = await import(testFilePath) + } catch (e) { + // ignore error if scenario file not found, otherwise re-throw + if (isErrorWithCode(e)) { + if (e instanceof Error) { + throw e + } else { + console.error('unexpected error type', e) + // eslint-disable-next-line + throw e + } + } + } + + if (allScenarios) { + if (allScenarios[scenarioName]) { + scenario = allScenarios[scenarioName] + } else { + throw new Error( + `UndefinedScenario: There is no scenario named "${scenarioName}" in ` + + `${testFilePath}.{js,ts}`, + ) + } + } + return { scenario } +} + +/** + * All these hooks run in the VM/Context that the test runs in since we're using + * "setupAfterEnv". + * There's a new context for each test-suite i.e. each test file + * + * Doing this means if the db isn't used in the current test context, no need to + * do any of the teardown logic - allowing simple tests to run faster + * At the same time, if the db is used, disconnecting it in this context + * prevents connection limit errors. + * Just disconnecting db in jest-preset is not enough, because the Prisma client + * is created in a different context. + */ +const wasDbImported = () => { + return Boolean(globalThis.__cedarjs_db_imported__) +} + +let quoteStyle: string +// determine what kind of quotes are needed around table names in raw SQL +async function getQuoteStyle() { + const { getConfig: getPrismaConfig, getSchema } = await import( + '@prisma/internals' + ) + + // @NOTE prisma utils are available in cli lib/schemaHelpers + // But avoid importing them, to prevent memory leaks in jest + const datamodel = await getSchema(cedarPaths.api.dbSchema) + + if (!quoteStyle) { + const config = await getPrismaConfig({ + datamodel, + }) + + switch (config.datasources?.[0]?.provider) { + case 'mysql': + quoteStyle = '`' + break + default: + quoteStyle = '"' + } + } + + return quoteStyle +} + +async function getProjectDb() { + const libDb = await import(`${cedarPaths.api.lib}/db`) + + return libDb.db +} + +function isIdenticalArray(a: unknown[], b: unknown[]) { + return JSON.stringify(a) === JSON.stringify(b) +} + +function deepCopy(obj: unknown[]) { + return JSON.parse(JSON.stringify(obj)) +} + +function isErrorWithMessage(e: unknown): e is { message: string } { + return ( + !!e && + typeof e === 'object' && + 'message' in e && + typeof e.message === 'string' + ) +} + +function isErrorWithCode(e: unknown): e is { code: string } { + return ( + !!e && typeof e === 'object' && 'code' in e && typeof e.code === 'string' + ) +} + +// These types are still in `global.d.ts` to work with Jest +// +// interface GlobalScenario { +// (...args: [string, string, TestFunc] | [string, TestFunc]): ReturnType +// only?: ( +// ...args: [string, string, TestFunc] | [string, TestFunc] +// ) => ReturnType +// } + +// interface DescribeScenario { +// ( +// ...args: [string, string, DescribeBlock] | [string, DescribeBlock] +// ): ReturnType +// only?: ( +// ...args: [string, string, DescribeBlock] | [string, DescribeBlock] +// ) => ReturnType +// } + +// declare global { +// // eslint-disable-next-line no-var +// var scenario: GlobalScenario +// // eslint-disable-next-line no-var +// var describeScenario: DescribeScenario +// } + +globalThis.scenario = buildScenario(it) +globalThis.scenario.only = buildScenario(it.only) +globalThis.describeScenario = buildDescribeScenario(describe) +globalThis.describeScenario.only = buildDescribeScenario(describe.only) diff --git a/packages/testing/src/web/MockRouter.tsx b/packages/testing/src/web/MockRouter.tsx index 0130c21be3..5eeed8fd31 100644 --- a/packages/testing/src/web/MockRouter.tsx +++ b/packages/testing/src/web/MockRouter.tsx @@ -1,3 +1,11 @@ +// For Vitest we use a plugin to swap out imports from `@cedarjs/router` to this +// file. @see ../vitest/cedarJsRoutesImportTransformPlugin.ts +// +// For Jest we overwrite the default `Router` export using jest-preset. So every +// import of @cedarjs/router will import this Router instead +// +// It's therefore important to reexport everything that we *don't* want to mock. + import type React from 'react' import { flattenAll } from '@cedarjs/router/dist/react-util' @@ -7,14 +15,12 @@ import { flattenAll } from '@cedarjs/router/dist/react-util' import { isValidRoute } from '@cedarjs/router/dist/route-validators' import type { RouterProps } from '@cedarjs/router/dist/router' import { replaceParams } from '@cedarjs/router/dist/util' + export * from '@cedarjs/router/dist/index' export const routes: { [routeName: string]: () => string } = {} /** - * We overwrite the default `Router` export (see jest-preset). So every import - * of @cedarjs/router will import this Router instead - * * This router populates the `routes.()` utility object. */ export const Router: React.FC = ({ children }) => { diff --git a/packages/testing/src/web/vitest/index.ts b/packages/testing/src/web/vitest/index.ts new file mode 100644 index 0000000000..62f593972b --- /dev/null +++ b/packages/testing/src/web/vitest/index.ts @@ -0,0 +1,3 @@ +export { cedarJsRouterImportTransformPlugin } from './vite-plugin-cedarjs-router-import-transform.js' +export { createAuthImportTransformPlugin } from './vite-plugin-create-auth-import-transform.js' +export { autoImportsPlugin } from './vite-plugin-auto-import.js' diff --git a/packages/testing/src/web/vitest/vite-plugin-auto-import.ts b/packages/testing/src/web/vitest/vite-plugin-auto-import.ts new file mode 100644 index 0000000000..36797a1929 --- /dev/null +++ b/packages/testing/src/web/vitest/vite-plugin-auto-import.ts @@ -0,0 +1,25 @@ +import autoImport from 'unplugin-auto-import/vite' + +export function autoImportsPlugin() { + return autoImport({ + // targets to transform + include: [ + /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx + ], + + // global imports to register + imports: [ + { + '@cedarjs/testing/web': [ + 'mockGraphQLQuery', + 'mockGraphQLMutation', + 'mockCurrentUser', + ], + }, + ], + + // We provide our mocking types elsewhere and so don't need this plugin to + // generate them. + dts: false, + }) +} diff --git a/packages/testing/src/web/vitest/vite-plugin-cedarjs-router-import-transform.ts b/packages/testing/src/web/vitest/vite-plugin-cedarjs-router-import-transform.ts new file mode 100644 index 0000000000..b123aa5049 --- /dev/null +++ b/packages/testing/src/web/vitest/vite-plugin-cedarjs-router-import-transform.ts @@ -0,0 +1,24 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore - Silence the TS error on this line for CJS builds +import type { PluginOption } from 'vite' + +/** + * Replace `@cedarjs/router` imports with imports of + * `@cedarjs/testing/web/MockRouter.js` instead + */ +export function cedarJsRouterImportTransformPlugin(): PluginOption { + return { + name: 'cedarjs-router-import-transform', + enforce: 'pre', + transform(code: string, id: string) { + if (id.includes('/web/src')) { + code = code.replace( + /['"]@cedarjs\/router['"]/, + "'@cedarjs/testing/web/MockRouter.js'", + ) + } + + return code + }, + } +} diff --git a/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts b/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts new file mode 100644 index 0000000000..97220dcb2d --- /dev/null +++ b/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts @@ -0,0 +1,26 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore - Silence the TS error on this line for CJS builds +import type { PluginOption } from 'vite' + +export function createAuthImportTransformPlugin(): PluginOption { + return { + name: 'create-auth-import-transform', + enforce: 'pre', + transform(code: string, id: string) { + if (id.includes('web/src/auth')) { + // Remove any existing import of `createAuth` without affecting anything else. + // This regex defines 4 capture groups, where the second is `createAuth` and + // the third is an (optional) comma for subsequent named imports — we want to remove those two. + code = code.replace( + /(^\s*import\s*{[^}]*?)(\bcreateAuth\b)(,?)([^}]*})/m, + '$1$4', + ) + // Add import to mocked `createAuth` at the top of the file. + code = + "import { createAuthentication as createAuth } from '@cedarjs/testing/auth'\n" + + code + } + return code + }, + } +} diff --git a/packages/testing/tsconfig.cjs.json b/packages/testing/tsconfig.cjs.json index a660cecf11..233539bd2c 100644 --- a/packages/testing/tsconfig.cjs.json +++ b/packages/testing/tsconfig.cjs.json @@ -3,5 +3,13 @@ "compilerOptions": { "outDir": "dist/cjs", "tsBuildInfoFile": "./tsconfig.cjs.tsbuildinfo" - } + }, + "exclude": [ + "dist", + "node_modules", + "**/__tests__", + "**/__mocks__", + "**/*.test.*", + "src/api/vitest" + ] } From 5199f72604fe0f5463dc2512f1f80bdb2d8237a0 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 20:03:45 +0200 Subject: [PATCH 02/14] vite-plugin-create-auth-import-transform id check --- .../vitest/vite-plugin-create-auth-import-transform.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts b/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts index 97220dcb2d..819394a54f 100644 --- a/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts +++ b/packages/testing/src/web/vitest/vite-plugin-create-auth-import-transform.ts @@ -7,10 +7,12 @@ export function createAuthImportTransformPlugin(): PluginOption { name: 'create-auth-import-transform', enforce: 'pre', transform(code: string, id: string) { - if (id.includes('web/src/auth')) { - // Remove any existing import of `createAuth` without affecting anything else. - // This regex defines 4 capture groups, where the second is `createAuth` and - // the third is an (optional) comma for subsequent named imports — we want to remove those two. + if (id.endsWith('/web/src/auth.ts') || id.endsWith('/web/src/auth.js')) { + // Remove any existing import of `createAuth` without affecting anything + // else. + // This regex defines 4 capture groups, where the second is `createAuth` + // and the third is an (optional) comma for subsequent named imports — + // we want to remove those two. code = code.replace( /(^\s*import\s*{[^}]*?)(\bcreateAuth\b)(,?)([^}]*})/m, '$1$4', From b714b1ac341fcc34d35167473961fc62753218c6 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 20:21:00 +0200 Subject: [PATCH 03/14] bring more changes over from #355 --- packages/testing/build.mts | 18 ++++++++++++++++-- packages/testing/package.json | 4 ++++ packages/testing/src/web/MockProviders.tsx | 7 ++----- packages/vite/tsconfig.build.json | 1 + packages/vite/tsconfig.json | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index 6427862176..3c73a4184b 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -1,6 +1,6 @@ import fs from 'node:fs' -import { buildCjs, buildEsm } from '@cedarjs/framework-tools' +import { build, buildEsm, defaultBuildOptions } from '@cedarjs/framework-tools' import { generateTypesCjs, generateTypesEsm, @@ -10,7 +10,21 @@ import { await buildEsm() await generateTypesEsm() -await buildCjs() +await build({ + buildOptions: { + ...defaultBuildOptions, + tsconfig: 'tsconfig.cjs.json', + outdir: 'dist/cjs', + logOverride: { + // This feels a bit dangerous, I wish I could do this with a comment + // inside the file where I need this for greater control, but I haven't + // found a way to do that yet. + // This is to silence the CJS warning for `import.meta.glob` and + // `import.meta.dirname` + 'empty-import-meta': 'silent', + }, + }, +}) await generateTypesCjs() await insertCommonJsPackageJson({ buildFileUrl: import.meta.url, diff --git a/packages/testing/package.json b/packages/testing/package.json index f66eeae16a..43d5a5792e 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -63,6 +63,10 @@ "default": "./dist/cjs/api/index.js" } }, + "./api/vitest": { + "types": "./dist/api/vitest/index.d.ts", + "default": "./dist/api/vitest/index.js" + }, "./cache": { "import": { "types": "./dist/cache/index.d.ts", diff --git a/packages/testing/src/web/MockProviders.tsx b/packages/testing/src/web/MockProviders.tsx index e480b43b94..bdc89a417f 100644 --- a/packages/testing/src/web/MockProviders.tsx +++ b/packages/testing/src/web/MockProviders.tsx @@ -1,7 +1,3 @@ -/** - * NOTE: This module should not contain any nodejs functionality, - * because it's also used by Storybook in the browser. - */ import React from 'react' import { LocationProvider } from '@cedarjs/router' @@ -30,7 +26,8 @@ try { UserRoutes = () => <> } -// TODO(pc): see if there are props we want to allow to be passed into our mock provider (e.g. AuthProviderProps) +// TODO(pc): see if there are props we want to allow to be passed into our mock +// provider (e.g. AuthProviderProps) export const MockProviders: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => { diff --git a/packages/vite/tsconfig.build.json b/packages/vite/tsconfig.build.json index c5ad0e00c5..5380b45b53 100644 --- a/packages/vite/tsconfig.build.json +++ b/packages/vite/tsconfig.build.json @@ -13,6 +13,7 @@ { "path": "../project-config" }, { "path": "../router/tsconfig.build.json" }, { "path": "../server-store" }, + { "path": "../testing/tsconfig.build.json" }, { "path": "../web/tsconfig.build.json" } ] } diff --git a/packages/vite/tsconfig.json b/packages/vite/tsconfig.json index 4f59899bb5..0b25aa9ec7 100644 --- a/packages/vite/tsconfig.json +++ b/packages/vite/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../project-config" }, { "path": "../router/tsconfig.build.json" }, { "path": "../server-store" }, + { "path": "../testing/tsconfig.build.json" }, { "path": "../web/tsconfig.build.json" } ] } From 0c17224b834f4af555343147f26d9f2565c7b85c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 20:39:24 +0200 Subject: [PATCH 04/14] bring more changes over from #355 - II --- packages/testing/package.json | 4 ++++ packages/vite/src/lib/getMergedConfig.ts | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/testing/package.json b/packages/testing/package.json index 43d5a5792e..4ca77b2b2d 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -86,6 +86,10 @@ "types": "./dist/cjs/web/index.d.ts", "default": "./dist/cjs/web/index.js" } + }, + "./web/vitest": { + "types": "./dist/web/vitest/index.d.ts", + "default": "./dist/web/vitest/index.js" } }, "files": [ diff --git a/packages/vite/src/lib/getMergedConfig.ts b/packages/vite/src/lib/getMergedConfig.ts index 8110f6e67d..6f319af527 100644 --- a/packages/vite/src/lib/getMergedConfig.ts +++ b/packages/vite/src/lib/getMergedConfig.ts @@ -1,8 +1,8 @@ import path from 'node:path' import type { InputOption } from 'rollup' -import type { ConfigEnv, UserConfig } from 'vite' import { mergeConfig } from 'vite' +import type { ConfigEnv, ViteUserConfig } from 'vitest/config' import type { Config, Paths } from '@cedarjs/project-config' import { @@ -19,7 +19,7 @@ import { * build */ export function getMergedConfig(rwConfig: Config, rwPaths: Paths) { - return (userConfig: UserConfig, env: ConfigEnv): UserConfig => { + return (userConfig: ViteUserConfig, env: ConfigEnv): ViteUserConfig => { let apiHost = process.env.REDWOOD_API_HOST apiHost ??= rwConfig.api.host apiHost ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '[::]' @@ -35,7 +35,7 @@ export function getMergedConfig(rwConfig: Config, rwPaths: Paths) { apiPort = rwConfig.api.port } - const defaultRwViteConfig: UserConfig = { + const defaultRwViteConfig: ViteUserConfig = { root: rwPaths.web.src, // @MARK: when we have these aliases, the warnings from the FE server go // away BUT, if you have imports like this: @@ -145,6 +145,15 @@ export function getMergedConfig(rwConfig: Config, rwPaths: Paths) { }, }, }, + ssr: { + // `@cedarjs/testing` is not externalized in order to support + // `import.meta.glob`, which we use in one of the files in the package + noExternal: env.mode === 'test' ? ['@cedarjs/testing'] : [], + }, + test: { + globals: false, + environment: 'jsdom', + }, } return mergeConfig(defaultRwViteConfig, userConfig) From 91b28ba785224b5e232d321c8fa78ef9aacadf3c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 21:01:11 +0200 Subject: [PATCH 05/14] bring more changes over from #355 - III --- packages/vite/src/index.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index f4d25123f0..60a7dce7af 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -3,6 +3,11 @@ import type { PluginOption } from 'vite' import { getWebSideDefaultBabelConfig } from '@cedarjs/babel-config' import { getConfig } from '@cedarjs/project-config' +import { + autoImportsPlugin, + cedarJsRouterImportTransformPlugin, + createAuthImportTransformPlugin, +} from '@cedarjs/testing/web/vitest' import { cedarCellTransform } from './plugins/vite-plugin-cedar-cell.js' import { cedarEntryInjectionPlugin } from './plugins/vite-plugin-cedar-entry-injection.js' @@ -24,10 +29,14 @@ export { cedarTransformJsAsJsx } from './plugins/vite-plugin-jsx-loader.js' export { cedarMergedConfig } from './plugins/vite-plugin-merged-config.js' export { cedarSwapApolloProvider } from './plugins/vite-plugin-swap-apollo-provider.js' +type PluginOptions = { + mode?: string | undefined +} + /** * Pre-configured vite plugin, with required config for CedarJS apps. */ -export function cedar(): PluginOption[] { +export function cedar({ mode }: PluginOptions = {}): PluginOption[] { const rwConfig = getConfig() const rscEnabled = rwConfig.experimental?.rsc?.enabled @@ -51,6 +60,9 @@ export function cedar(): PluginOption[] { } return [ + mode === 'test' && cedarJsRouterImportTransformPlugin(), + mode === 'test' && createAuthImportTransformPlugin(), + mode === 'test' && autoImportsPlugin(), cedarNodePolyfills(), cedarHtmlEnvPlugin(), cedarEntryInjectionPlugin(), From 044743ca64d93d3ccf3b36f881298239217ef66f Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 9 Aug 2025 21:14:26 +0200 Subject: [PATCH 06/14] bring more changes over from #355 - IV --- packages/testing/package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/testing/package.json b/packages/testing/package.json index 4ca77b2b2d..b90f9e9af2 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -87,6 +87,16 @@ "default": "./dist/cjs/web/index.js" } }, + "./web/MockRouter.js": { + "import": { + "types": "./dist/web/MockRouter.d.ts", + "default": "./dist/web/MockRouter.js" + }, + "require": { + "types": "./dist/cjs/web/MockRouter.d.ts", + "default": "./dist/cjs/web/MockRouter.js" + } + }, "./web/vitest": { "types": "./dist/web/vitest/index.d.ts", "default": "./dist/web/vitest/index.js" From 65f861db8f42aec49c85034e4a7cddc1707613b2 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 00:48:28 +0200 Subject: [PATCH 07/14] fix link to import tranform plugin --- packages/testing/src/web/MockRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing/src/web/MockRouter.tsx b/packages/testing/src/web/MockRouter.tsx index 5eeed8fd31..06e34083ac 100644 --- a/packages/testing/src/web/MockRouter.tsx +++ b/packages/testing/src/web/MockRouter.tsx @@ -1,5 +1,5 @@ // For Vitest we use a plugin to swap out imports from `@cedarjs/router` to this -// file. @see ../vitest/cedarJsRoutesImportTransformPlugin.ts +// file. @see ./vitest/vite-plugin-cedarjs-router-import-transform.ts // // For Jest we overwrite the default `Router` export using jest-preset. So every // import of @cedarjs/router will import this Router instead From 852e917d663766d94f4065c49f64ad0f0df05649 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 01:03:15 +0200 Subject: [PATCH 08/14] add globRoutesImporter --- .../testing/src/web/globRoutesImporter.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/testing/src/web/globRoutesImporter.ts diff --git a/packages/testing/src/web/globRoutesImporter.ts b/packages/testing/src/web/globRoutesImporter.ts new file mode 100644 index 0000000000..6598a872fb --- /dev/null +++ b/packages/testing/src/web/globRoutesImporter.ts @@ -0,0 +1,52 @@ +/// + +// TODO(storybook): Use this for Storybook as well + +// We're building this file with esbuild, which doesn't understand the vite- +// specific `import.meta.glob` feature. So it'll just leave it as is, which is +// actually exactly what we want. We can't evaluate the glob import when +// building the framework, because at that time we have no idea what user +// project this will be used in or what routes it will have. So instead I tell +// vite to process this file when building the user's project. I do that by +// including @cedarjs/testing in `noExternal` in the default vite config (see +// lib/getMergedConfig.ts in the vite package) + +// We want to find the user's Routes file in web/src/Routes.{tsx,jsx} +// When running tests from the root of the user's project, vite will see the +// path as `/src/Routes.tsx`. When running the tests from the web/ directory, +// vite will see the path as `/Routes.tsx` +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore - Silence the TS error on this line for CJS builds +const defaultExports = import.meta.glob( + ['/src/Routes.{tsx,jsx}', '/Routes.{tsx,jsx}'], + { + import: 'default', + eager: true, + }, +) +const routesFileName = Object.keys(defaultExports)[0] + +if (!routesFileName) { + throw new Error('@cedarjs/testing: No routes found') +} + +const routesFunction = defaultExports[routesFileName] + +if (typeof routesFunction !== 'function') { + throw new Error( + '@cedarjs/testing: Routes file does not export a React component', + ) +} + +/** + * All the routes the user has defined + * + * We render this in the `` component to populate the `routes` + * import from `@cedarjs/router` to make sure code like + * `Home` works in tests + * + * The final piece to this puzzle is to realize that the user's Routes file + * imports `@cedarjs/router`, which we replace to import from '@cedarjs/testing' + * instead using a vite plugin that we only run for vitest and storybook + */ +export const UserRoutes = routesFunction as React.FC From cae4abd5536a7d423e4f36c7469552b2d8abc6fa Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 01:25:39 +0200 Subject: [PATCH 09/14] debug testing build --- packages/testing/build.mts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index 3c73a4184b..b586579a5f 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import os from 'node:os' import { build, buildEsm, defaultBuildOptions } from '@cedarjs/framework-tools' import { @@ -85,3 +86,27 @@ fs.writeFileSync( configJestWebJestSetupPath, webJestSetupFile.replaceAll('await import', 'require'), ) + +// ./src/web/globRoutesImporter.ts contains `import.meta.glob`. This is not +// supported in CJS. And for CJS we don't really use this, but it does get +// imported and executed, so we need to mock it. esbuild will just make +// `import.meta` be an empty object, but that's not quite enough for what we +// need here, so I extend it a bit more. +const globRoutesImporterBuildPath = './dist/cjs/web/globRoutesImporter.js' +const globRoutesImporterFile = fs.readFileSync( + globRoutesImporterBuildPath, + 'utf-8', +) + +console.log('globRoutesImporterFile', globRoutesImporterFile) +if (os.platform() === 'win32') { + throw new Error('Windows is not supported') +} + +fs.writeFileSync( + globRoutesImporterBuildPath, + globRoutesImporterFile.replaceAll( + 'const import_meta = {};', + 'const import_meta = { glob: () => ({ "routes.tsx": () => null }) };', + ), +) From bdec4c2a9d07155af387e0f7575eaad74499066a Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 01:32:31 +0200 Subject: [PATCH 10/14] debug testing build - log after write --- packages/testing/build.mts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index b586579a5f..77b7a8a86a 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -98,11 +98,6 @@ const globRoutesImporterFile = fs.readFileSync( 'utf-8', ) -console.log('globRoutesImporterFile', globRoutesImporterFile) -if (os.platform() === 'win32') { - throw new Error('Windows is not supported') -} - fs.writeFileSync( globRoutesImporterBuildPath, globRoutesImporterFile.replaceAll( @@ -110,3 +105,8 @@ fs.writeFileSync( 'const import_meta = { glob: () => ({ "routes.tsx": () => null }) };', ), ) + +console.log( + 'globRoutesImporterFile', + fs.readFileSync(globRoutesImporterBuildPath, 'utf-8'), +) From cf0f346dba5481d8114b402008a60f8378a8607e Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 01:36:37 +0200 Subject: [PATCH 11/14] remove unused import --- packages/testing/build.mts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index 77b7a8a86a..d78bf5626a 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -1,5 +1,4 @@ import fs from 'node:fs' -import os from 'node:os' import { build, buildEsm, defaultBuildOptions } from '@cedarjs/framework-tools' import { From c9c9f566e9174c49ec09bd9e3b050a0c00ed0c05 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 01:56:43 +0200 Subject: [PATCH 12/14] Import globRoutesImporter in MockProvider --- packages/testing/src/web/MockProviders.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/testing/src/web/MockProviders.tsx b/packages/testing/src/web/MockProviders.tsx index bdc89a417f..fdff96138a 100644 --- a/packages/testing/src/web/MockProviders.tsx +++ b/packages/testing/src/web/MockProviders.tsx @@ -4,6 +4,7 @@ import { LocationProvider } from '@cedarjs/router' import { RedwoodProvider } from '@cedarjs/web' import { RedwoodApolloProvider } from '@cedarjs/web/apollo' +import { UserRoutes as VitestUserRoutes } from './globRoutesImporter.js' import { useAuth } from './mockAuth.js' import { MockParamsProvider } from './MockParamsProvider.js' @@ -35,6 +36,7 @@ export const MockProviders: React.FunctionComponent<{ + {children} From c052ec5adfcc7db48486ac2c27914f6b5413b80c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 02:02:36 +0200 Subject: [PATCH 13/14] log MockProviders --- packages/testing/build.mts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index d78bf5626a..0d28a2f1ef 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -109,3 +109,8 @@ console.log( 'globRoutesImporterFile', fs.readFileSync(globRoutesImporterBuildPath, 'utf-8'), ) +console.log() +console.log( + 'MockProviders', + fs.readFileSync('./dist/cjs/web/MockProviders.js', 'utf-8'), +) From 39975620f8785c1b5556220d9227d7b04ec1ef38 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 10 Aug 2025 02:15:59 +0200 Subject: [PATCH 14/14] remove debug logs --- packages/testing/build.mts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/testing/build.mts b/packages/testing/build.mts index 0d28a2f1ef..2681168291 100644 --- a/packages/testing/build.mts +++ b/packages/testing/build.mts @@ -104,13 +104,3 @@ fs.writeFileSync( 'const import_meta = { glob: () => ({ "routes.tsx": () => null }) };', ), ) - -console.log( - 'globRoutesImporterFile', - fs.readFileSync(globRoutesImporterBuildPath, 'utf-8'), -) -console.log() -console.log( - 'MockProviders', - fs.readFileSync('./dist/cjs/web/MockProviders.js', 'utf-8'), -)