diff --git a/.changeset/rich-dryers-lay.md b/.changeset/rich-dryers-lay.md new file mode 100644 index 0000000000..7bdd0c49d4 --- /dev/null +++ b/.changeset/rich-dryers-lay.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +Watch directories targeted by dynamic `import()` template literals so changes to runtime-resolved files trigger preview reloads. diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts index 54e55d623c..4ee9e0af27 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { createDependencyGraph, type DependencyGraph, + isUnderDirectory, } from './create-dependency-graph.js'; const testingDiretctory = path.join( @@ -162,6 +163,7 @@ import {} from './general-importing-file'; toAbsolute('file-b.ts'), toAbsolute('general-importing-file.ts'), ], + dynamicDependencyDirectories: [], moduleDependencies: [], } satisfies DependencyGraph[number]); expect(dependencyGraph[toAbsolute('file-a.ts')]?.dependentPaths).toContain( @@ -192,6 +194,7 @@ import {} from './file-b'; path: pathToTemporaryFile, dependentPaths: [], dependencyPaths: [toAbsolute('file-a.ts'), toAbsolute('file-b.ts')], + dynamicDependencyDirectories: [], moduleDependencies: [], } satisfies DependencyGraph[number]); expect(dependencyGraph[toAbsolute('file-a.ts')]?.dependentPaths).toContain( @@ -224,3 +227,121 @@ import {} from './file-b'; ).not.toContain(pathToTemporaryFile); }); }); + +describe('createDependencyGraph() with dynamic imports', () => { + const fixtureDirectory = path.join( + import.meta.dirname, + './test/dynamic-import-graph', + ); + + const aliasedFixtureDirectory = path.join( + import.meta.dirname, + './test/aliased-import-graph/src', + ); + + const aliasedFallbackFixtureDirectory = path.join( + import.meta.dirname, + './test/aliased-fallback-graph/src', + ); + + const collectDynamicDependencyDirectories = (graph: DependencyGraph) => { + const directories = new Set(); + for (const module of Object.values(graph)) { + for (const directory of module.dynamicDependencyDirectories) { + directories.add(directory); + } + } + return [...directories]; + }; + + it('exposes the directory of a template-literal `import()` as a dynamic dependency and resolves runtime files to the importing module', async () => { + const [graph, , { resolveDependentsOf }] = + await createDependencyGraph(fixtureDirectory); + + const messagesDirectory = path.join(fixtureDirectory, 'messages'); + const templatePath = path.join(fixtureDirectory, 'template.ts'); + + expect(collectDynamicDependencyDirectories(graph)).toEqual([ + messagesDirectory, + ]); + + expect( + resolveDependentsOf(path.join(messagesDirectory, 'en', 'common.json')), + ).toEqual([templatePath]); + }); + + it('falls through to a later alias candidate when the first does not exist on disk', async () => { + const [graph, , { resolveDependentsOf }] = await createDependencyGraph( + aliasedFallbackFixtureDirectory, + ); + + const localesDirectory = path.join( + path.dirname(aliasedFallbackFixtureDirectory), + 'lib', + 'locales', + ); + const templatePath = path.join( + aliasedFallbackFixtureDirectory, + 'template.ts', + ); + + expect(collectDynamicDependencyDirectories(graph)).toEqual([ + localesDirectory, + ]); + + expect( + resolveDependentsOf(path.join(localesDirectory, 'de', 'common.json')), + ).toEqual([templatePath]); + }); + + it('resolves tsconfig path aliases in dynamic import template literals', async () => { + const [graph, , { resolveDependentsOf }] = await createDependencyGraph( + aliasedFixtureDirectory, + ); + + const localesDirectory = path.join(aliasedFixtureDirectory, 'locales'); + const templatePath = path.join(aliasedFixtureDirectory, 'template.ts'); + + expect(collectDynamicDependencyDirectories(graph)).toEqual([ + localesDirectory, + ]); + + expect( + resolveDependentsOf(path.join(localesDirectory, 'tr', 'common.json')), + ).toEqual([templatePath]); + }); +}); + +describe('isUnderDirectory()', () => { + it('matches a nested file', () => { + expect( + isUnderDirectory( + path.resolve('/proj/messages/en.json'), + path.resolve('/proj/messages'), + ), + ).toBe(true); + }); + + it('matches the directory itself', () => { + expect( + isUnderDirectory( + path.resolve('/proj/messages'), + path.resolve('/proj/messages'), + ), + ).toBe(true); + }); + + it('does not match a sibling sharing a prefix', () => { + expect( + isUnderDirectory( + path.resolve('/proj/messages-extra/x'), + path.resolve('/proj/messages'), + ), + ).toBe(false); + }); + + it('handles root directory paths without producing a double separator', () => { + expect(isUnderDirectory(path.resolve('/foo/bar'), path.sep)).toBe(true); + expect(isUnderDirectory(path.sep, path.sep)).toBe(true); + }); +}); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts index d61b885bdc..f5d180fca8 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts @@ -2,7 +2,10 @@ import { existsSync, promises as fs, statSync } from 'node:fs'; import path from 'node:path'; import type { EventName } from 'chokidar/handler.js'; import { getImportedModules } from './get-imported-modules.js'; -import { resolvePathAliases } from './resolve-path-aliases.js'; +import { + resolveAliasedDirectoryPrefix, + resolvePathAliases, +} from './resolve-path-aliases.js'; interface Module { path: string; @@ -10,6 +13,8 @@ interface Module { dependencyPaths: string[]; dependentPaths: string[]; + dynamicDependencyDirectories: string[]; + moduleDependencies: string[]; } @@ -70,6 +75,54 @@ const checkFileExtensionsUntilItExists = ( } }; +export const isUnderDirectory = (filePath: string, directoryPath: string) => { + if (filePath === directoryPath) return true; + const prefix = directoryPath.endsWith(path.sep) + ? directoryPath + : directoryPath + path.sep; + return filePath.startsWith(prefix); +}; + +const resolveDynamicImportDirectory = ( + prefix: string, + filePath: string, +): string | undefined => { + const moduleDirectory = path.dirname(filePath); + const normalizedPrefix = path.normalize(prefix); + const endsWithSeparator = normalizedPrefix.endsWith(path.sep); + const trimmed = endsWithSeparator + ? normalizedPrefix.slice(0, -path.sep.length) + : normalizedPrefix; + + let resolvedPrefix: string; + const isRelative = prefix.startsWith('.') || path.isAbsolute(prefix); + if (isRelative) { + resolvedPrefix = path.resolve(moduleDirectory, normalizedPrefix); + } else { + if (trimmed.length === 0) return undefined; + const aliased = resolveAliasedDirectoryPrefix(trimmed, moduleDirectory); + if (aliased === undefined) { + return undefined; + } + resolvedPrefix = aliased; + } + + const directory = endsWithSeparator + ? resolvedPrefix + : path.dirname(resolvedPrefix); + + if (isUnderDirectory(moduleDirectory, directory)) return undefined; + + if (!existsSync(directory)) return undefined; + try { + if (!statSync(directory).isDirectory()) return undefined; + } catch (_) { + return undefined; + } + + return directory; +}; + /** * Creates a stateful dependency graph that is structured in a way that you can get * the dependents of a module from its path. @@ -88,6 +141,7 @@ export const createDependencyGraph = async (directory: string) => { path, dependencyPaths: [], dependentPaths: [], + dynamicDependencyDirectories: [], moduleDependencies: [], }, ]), @@ -95,8 +149,11 @@ export const createDependencyGraph = async (directory: string) => { const getDependencyPaths = async (filePath: string) => { const contents = await fs.readFile(filePath, 'utf8'); + const imports = isJavascriptModule(filePath) + ? getImportedModules(contents) + : { staticImports: [], dynamicImportPrefixes: [] }; const importedPaths = isJavascriptModule(filePath) - ? resolvePathAliases(getImportedModules(contents), path.dirname(filePath)) + ? resolvePathAliases(imports.staticImports, path.dirname(filePath)) : []; const importedPathsRelativeToDirectory = importedPaths.map( (dependencyPath) => { @@ -115,7 +172,7 @@ export const createDependencyGraph = async (directory: string) => { /* path.resolve resolves paths differently from what imports on javascript do. - So if we wouldn't do this, for an email at "/path/to/email.tsx" with a dependency path of "./other-email" + So if we wouldn't do this, for an email at "/path/to/email.tsx" with a dependency path of "./other-email" would end up going into /path/to/email.tsx/other-email instead of /path/to/other-email which is the one the import is meant to go to */ @@ -183,9 +240,18 @@ export const createDependencyGraph = async (directory: string) => { dependencyPath.startsWith('.') || path.isAbsolute(dependencyPath), ); + const dynamicDependencyDirectories = Array.from( + new Set( + imports.dynamicImportPrefixes + .map((prefix) => resolveDynamicImportDirectory(prefix, filePath)) + .filter((d): d is string => typeof d === 'string'), + ), + ); + return { dependencyPaths: nonNodeModuleImportPathsRelativeToDirectory, moduleDependencies, + dynamicDependencyDirectories, }; }; @@ -195,14 +261,20 @@ export const createDependencyGraph = async (directory: string) => { path: moduleFilePath, dependencyPaths: [], dependentPaths: [], + dynamicDependencyDirectories: [], moduleDependencies: [], }; } - const { moduleDependencies, dependencyPaths: newDependencyPaths } = - await getDependencyPaths(moduleFilePath); + const { + moduleDependencies, + dependencyPaths: newDependencyPaths, + dynamicDependencyDirectories: newDynamicDependencyDirectories, + } = await getDependencyPaths(moduleFilePath); graph[moduleFilePath].moduleDependencies = moduleDependencies; + graph[moduleFilePath].dynamicDependencyDirectories = + newDynamicDependencyDirectories; // we go through these to remove the ones that don't exist anymore for (const dependencyPath of graph[moduleFilePath].dependencyPaths) { @@ -309,6 +381,11 @@ export const createDependencyGraph = async (directory: string) => { /** * Resolves all modules that depend on the specified module, directly or indirectly. * + * If the path doesn't correspond to a graph node (e.g. a JSON file + * loaded via dynamic `import(\`...\`)`), modules whose dynamic-import + * directories contain the path are treated as direct dependents and + * their own dependents are resolved transitively. + * * @param pathToModule - The path to the module whose dependents we want to find * @returns An array of paths to all modules that depend on the specified module */ @@ -318,6 +395,19 @@ export const createDependencyGraph = async (directory: string) => { const dependentPaths = new Set(); const stack: string[] = [pathToModule]; + for (const module of Object.values(graph)) { + for (const directory of module.dynamicDependencyDirectories) { + if ( + isUnderDirectory(pathToModule, directory) && + module.path !== pathToModule && + !dependentPaths.has(module.path) + ) { + dependentPaths.add(module.path); + stack.push(module.path); + } + } + } + while (stack.length > 0) { const currentPath = stack.pop()!; const moduleEntry = graph[currentPath]; diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts index 5ad03d1191..06fab100ef 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts @@ -10,23 +10,22 @@ describe('getImportedModules()', () => { it('works with this test file', async () => { const contents = await fs.readFile(import.meta.filename, 'utf8'); - expect(getImportedModules(contents)).toEqual([ - 'node:fs', - './get-imported-modules.js', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: ['node:fs', './get-imported-modules.js'], + dynamicImportPrefixes: [], + }); }); it('works with direct exports', () => { const contents = `export * from './component-a'; - export { ComponentB } from './component-b'; + export { ComponentB } from './component-b'; import { ComponentC } from './component-c'; export { ComponentC }`; - expect(getImportedModules(contents)).toEqual([ - './component-a', - './component-b', - './component-c', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: ['./component-a', './component-b', './component-c'], + dynamicImportPrefixes: [], + }); }); it('works with regular imports and double quotes', () => { @@ -51,12 +50,15 @@ import { Component } from '../../my-component'; import * as React from "react"; `; - expect(getImportedModules(contents)).toEqual([ - '@react-email/components', - '@react-email/tailwind', - '../../my-component', - 'react', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: [ + '@react-email/components', + '@react-email/tailwind', + '../../my-component', + 'react', + ], + dynamicImportPrefixes: [], + }); }); it('works with regular imports and single quotes', () => { @@ -81,12 +83,15 @@ import { Component } from '../../my-component'; import * as React from 'react'; `; - expect(getImportedModules(contents)).toEqual([ - 'react-email', - '@react-email/tailwind', - '../../my-component', - 'react', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: [ + 'react-email', + '@react-email/tailwind', + '../../my-component', + 'react', + ], + dynamicImportPrefixes: [], + }); }); it('works with commonjs require with double quotes', () => { @@ -111,12 +116,15 @@ const { Component } = require("../../my-component"); const React = require("react"); `; - expect(getImportedModules(contents)).toEqual([ - '@react-email/components', - '@react-email/tailwind', - '../../my-component', - 'react', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: [ + '@react-email/components', + '@react-email/tailwind', + '../../my-component', + 'react', + ], + dynamicImportPrefixes: [], + }); }); it('works with commonjs require with single quotes', () => { @@ -141,11 +149,53 @@ const { Component } = require('../../my-component'); const React = require('react'); `; - expect(getImportedModules(contents)).toEqual([ - '@react-email/components', - '@react-email/tailwind', - '../../my-component', - 'react', - ]); + expect(getImportedModules(contents)).toEqual({ + staticImports: [ + '@react-email/components', + '@react-email/tailwind', + '../../my-component', + 'react', + ], + dynamicImportPrefixes: [], + }); + }); + + it('treats dynamic import() with a string literal as a static import', () => { + const contents = `const mod = await import('./my-module.json');`; + expect(getImportedModules(contents)).toEqual({ + staticImports: ['./my-module.json'], + dynamicImportPrefixes: [], + }); + }); + + it('captures the leading static prefix of dynamic import() template literals', () => { + const contents = ` + i18next.use( + resourcesToBackend( + (lng, ns) => import(\`./messages/\${lng}/\${ns}.json\`), + ), + ); + `; + expect(getImportedModules(contents)).toEqual({ + staticImports: [], + dynamicImportPrefixes: ['./messages/'], + }); + }); + + it('ignores dynamic import() template literals that start with an interpolation', () => { + // biome-ignore lint/suspicious/noTemplateCurlyInString: the `${base}` here is intentionally raw source text, not a template-string interpolation. + const contents = 'const m = await import(`${base}/file.json`);'; + expect(getImportedModules(contents)).toEqual({ + staticImports: [], + dynamicImportPrefixes: [], + }); + }); + + it('treats a dynamic import() template literal with no interpolation as a static import', () => { + const contents = 'const m = await import(`./my-module.json`);'; + expect(getImportedModules(contents)).toEqual({ + staticImports: ['./my-module.json'], + dynamicImportPrefixes: [], + }); }); }); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts index cd7b6ff3c7..77eee631ae 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts @@ -8,8 +8,14 @@ const traverse = : // @ts-expect-error we keep this check here so that this still works with the dev:preview script's use of tsx traverseModule.default; -export const getImportedModules = (contents: string) => { - const importedPaths: string[] = []; +export interface ImportedModules { + staticImports: string[]; + dynamicImportPrefixes: string[]; +} + +export const getImportedModules = (contents: string): ImportedModules => { + const staticImports: string[] = []; + const dynamicImportPrefixes: string[] = []; const parsedContents = parse(contents, { sourceType: 'unambiguous', strictMode: false, @@ -19,30 +25,54 @@ export const getImportedModules = (contents: string) => { traverse(parsedContents, { ImportDeclaration({ node }) { - importedPaths.push(node.source.value); + staticImports.push(node.source.value); }, ExportAllDeclaration({ node }) { - importedPaths.push(node.source.value); + staticImports.push(node.source.value); }, ExportNamedDeclaration({ node }) { if (node.source) { - importedPaths.push(node.source.value); + staticImports.push(node.source.value); } }, TSExternalModuleReference({ node }) { - importedPaths.push(node.expression.value); + staticImports.push(node.expression.value); }, CallExpression({ node }) { if ('name' in node.callee && node.callee.name === 'require') { if (node.arguments.length === 1) { const importPathNode = node.arguments[0]!; if (importPathNode!.type === 'StringLiteral') { - importedPaths.push(importPathNode.value); + staticImports.push(importPathNode.value); + } + } + return; + } + + if (node.callee.type === 'Import' && node.arguments.length === 1) { + const argument = node.arguments[0]!; + if (argument.type === 'StringLiteral') { + staticImports.push(argument.value); + return; + } + if (argument.type === 'TemplateLiteral' && argument.quasis.length > 0) { + if (argument.expressions.length === 0) { + const onlyQuasi = argument.quasis[0]!; + const staticPath = onlyQuasi.value.cooked ?? onlyQuasi.value.raw; + if (staticPath.length > 0) { + staticImports.push(staticPath); + } + return; + } + const firstQuasi = argument.quasis[0]!; + const leadingStatic = firstQuasi.value.cooked ?? firstQuasi.value.raw; + if (leadingStatic.length > 0) { + dynamicImportPrefixes.push(leadingStatic); } } } }, }); - return importedPaths; + return { staticImports, dynamicImportPrefixes }; }; diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts index c7a42aa71f..9eb0ab0022 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts @@ -1,3 +1,4 @@ +import { statSync } from 'node:fs'; import path from 'node:path'; import { createMatchPath, loadConfig } from 'tsconfig-paths'; @@ -30,3 +31,33 @@ export const resolvePathAliases = ( return importPaths; }; + +const isExistingDirectory = (candidate: string): boolean => { + try { + return statSync(candidate).isDirectory(); + } catch (_) { + return false; + } +}; + +export const resolveAliasedDirectoryPrefix = ( + prefix: string, + projectPath: string, +): string | undefined => { + const configLoadResult = loadConfig(projectPath); + if (configLoadResult.resultType !== 'success') return undefined; + + const matchPath = createMatchPath( + configLoadResult.absoluteBaseUrl, + configLoadResult.paths, + ); + const resolved = matchPath(prefix, undefined, isExistingDirectory, [ + '.tsx', + '.ts', + '.js', + '.jsx', + '.cjs', + '.mjs', + ]); + return resolved ?? undefined; +}; diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts index d6fb95e2bc..04f776e6ef 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts @@ -66,6 +66,23 @@ export const setupHotreloading = async ( watcher.add(p); } + // Directories targeted by dynamic `import(\`./prefix/${expr}\`)` calls. + // These files are resolved at runtime so they never appear in the static + // dependency graph; we still want their changes to refresh the preview. + const getDynamicDependencyDirectories = () => { + const directories = new Set(); + for (const module of Object.values(dependencyGraph)) { + for (const directory of module.dynamicDependencyDirectories) { + directories.add(directory); + } + } + return [...directories]; + }; + let dynamicDependencyDirectories = getDynamicDependencyDirectories(); + for (const directory of dynamicDependencyDirectories) { + watcher.add(directory); + } + const exit = async () => { await watcher.close(); }; @@ -101,6 +118,19 @@ export const setupHotreloading = async ( } filesOutsideEmailsDirectory = newFilesOutsideEmailsDirectory; + const newDynamicDependencyDirectories = getDynamicDependencyDirectories(); + for (const directory of dynamicDependencyDirectories) { + if (!newDynamicDependencyDirectories.includes(directory)) { + watcher.unwatch(directory); + } + } + for (const directory of newDynamicDependencyDirectories) { + if (!dynamicDependencyDirectories.includes(directory)) { + watcher.add(directory); + } + } + dynamicDependencyDirectories = newDynamicDependencyDirectories; + changes.push({ event, filename: relativePathToChangeTarget, @@ -114,6 +144,7 @@ export const setupHotreloading = async ( filename: path.relative(absolutePathToEmailsDirectory, dependentPath), }); } + reload(); }); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/lib/locales/de/common.json b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/lib/locales/de/common.json new file mode 100644 index 0000000000..1dbb38d415 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/lib/locales/de/common.json @@ -0,0 +1 @@ +{ "hello": "Hallo" } diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/src/template.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/src/template.ts new file mode 100644 index 0000000000..d959eeff93 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/src/template.ts @@ -0,0 +1,5 @@ +/** biome-ignore-all lint/correctness/noUnusedVariables: used in testing */ + +export const loadMessages = (lng: string, ns: string) => + // @ts-expect-error -- alias resolution provided by the test's tsconfig + import(`@/locales/${lng}/${ns}.json`); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/tsconfig.json b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/tsconfig.json new file mode 100644 index 0000000000..5bab72776e --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-fallback-graph/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*", "lib/*"] + } + } +} diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/locales/tr/common.json b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/locales/tr/common.json new file mode 100644 index 0000000000..1bd2f42112 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/locales/tr/common.json @@ -0,0 +1 @@ +{ "hello": "merhaba" } diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/template.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/template.ts new file mode 100644 index 0000000000..d959eeff93 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/src/template.ts @@ -0,0 +1,5 @@ +/** biome-ignore-all lint/correctness/noUnusedVariables: used in testing */ + +export const loadMessages = (lng: string, ns: string) => + // @ts-expect-error -- alias resolution provided by the test's tsconfig + import(`@/locales/${lng}/${ns}.json`); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/tsconfig.json b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/tsconfig.json new file mode 100644 index 0000000000..2c8ee2bb01 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/aliased-import-graph/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/messages/en/common.json b/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/messages/en/common.json new file mode 100644 index 0000000000..c21310b9a7 --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/messages/en/common.json @@ -0,0 +1 @@ +{ "hello": "Hello" } diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/template.ts b/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/template.ts new file mode 100644 index 0000000000..567cc45e4a --- /dev/null +++ b/packages/react-email/src/cli/utils/preview/hot-reloading/test/dynamic-import-graph/template.ts @@ -0,0 +1,4 @@ +/** biome-ignore-all lint/correctness/noUnusedVariables: used in testing */ + +export const loadMessages = (lng: string, ns: string) => + import(`./messages/${lng}/${ns}.json`);