@nextsparkjs/core 0.1.0-beta.149 → 0.1.0-beta.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/components/dashboard/block-editor/block-picker.d.ts +6 -1
  2. package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
  3. package/dist/components/dashboard/block-editor/block-picker.js +34 -12
  4. package/dist/components/dashboard/block-editor/block-preview-canvas.d.ts +2 -1
  5. package/dist/components/dashboard/block-editor/block-preview-canvas.d.ts.map +1 -1
  6. package/dist/components/dashboard/block-editor/block-preview-canvas.js +5 -0
  7. package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
  8. package/dist/components/dashboard/block-editor/builder-editor-view.js +41 -13
  9. package/dist/components/dashboard/block-editor/floating-block-toolbar.d.ts +2 -1
  10. package/dist/components/dashboard/block-editor/floating-block-toolbar.d.ts.map +1 -1
  11. package/dist/components/dashboard/block-editor/floating-block-toolbar.js +17 -1
  12. package/dist/components/dashboard/block-editor/tree-view-node.d.ts +4 -1
  13. package/dist/components/dashboard/block-editor/tree-view-node.d.ts.map +1 -1
  14. package/dist/components/dashboard/block-editor/tree-view-node.js +52 -1
  15. package/dist/components/dashboard/block-editor/tree-view.d.ts +4 -1
  16. package/dist/components/dashboard/block-editor/tree-view.d.ts.map +1 -1
  17. package/dist/components/dashboard/block-editor/tree-view.js +6 -0
  18. package/dist/components/entities/EntityTable.d.ts +1 -1
  19. package/dist/components/entities/EntityTable.d.ts.map +1 -1
  20. package/dist/components/entities/EntityTable.js +14 -1
  21. package/dist/components/entities/entity-table.types.d.ts +2 -0
  22. package/dist/components/entities/entity-table.types.d.ts.map +1 -1
  23. package/dist/components/entities/wrappers/EntityListWrapper.d.ts.map +1 -1
  24. package/dist/components/entities/wrappers/EntityListWrapper.js +19 -1
  25. package/dist/emails/otp-verification.d.ts +9 -0
  26. package/dist/emails/otp-verification.d.ts.map +1 -0
  27. package/dist/emails/otp-verification.js +72 -0
  28. package/dist/emails/reset-password.d.ts +9 -0
  29. package/dist/emails/reset-password.d.ts.map +1 -0
  30. package/dist/emails/reset-password.js +95 -0
  31. package/dist/emails/team-invitation.d.ts +9 -0
  32. package/dist/emails/team-invitation.d.ts.map +1 -0
  33. package/dist/emails/team-invitation.js +93 -0
  34. package/dist/emails/verify-email.d.ts +13 -0
  35. package/dist/emails/verify-email.d.ts.map +1 -0
  36. package/dist/emails/verify-email.js +84 -0
  37. package/dist/lib/api/entities.d.ts +6 -1
  38. package/dist/lib/api/entities.d.ts.map +1 -1
  39. package/dist/lib/api/entities.js +23 -2
  40. package/dist/lib/auth.d.ts.map +1 -1
  41. package/dist/lib/auth.js +12 -7
  42. package/dist/lib/blocks/clipboard.d.ts +11 -0
  43. package/dist/lib/blocks/clipboard.d.ts.map +1 -0
  44. package/dist/lib/blocks/clipboard.js +30 -0
  45. package/dist/lib/email/index.d.ts +1 -0
  46. package/dist/lib/email/index.d.ts.map +1 -1
  47. package/dist/lib/email/index.js +12 -0
  48. package/dist/lib/email/send.d.ts +15 -0
  49. package/dist/lib/email/send.d.ts.map +1 -0
  50. package/dist/lib/email/send.js +11 -0
  51. package/dist/lib/email/templates.d.ts +42 -29
  52. package/dist/lib/email/templates.d.ts.map +1 -1
  53. package/dist/lib/email/templates.js +8 -303
  54. package/dist/lib/email/types.d.ts +32 -0
  55. package/dist/lib/email/types.d.ts.map +1 -1
  56. package/dist/lib/services/subscription.service.d.ts +2 -2
  57. package/dist/lib/services/subscription.service.d.ts.map +1 -1
  58. package/dist/lib/services/subscription.service.js +6 -6
  59. package/dist/messages/de/email.json +58 -0
  60. package/dist/messages/en/admin.json +6 -2
  61. package/dist/messages/en/email.json +58 -0
  62. package/dist/messages/en/index.d.ts +4 -0
  63. package/dist/messages/en/index.d.ts.map +1 -1
  64. package/dist/messages/es/admin.json +6 -2
  65. package/dist/messages/es/email.json +58 -0
  66. package/dist/messages/es/index.d.ts +4 -0
  67. package/dist/messages/es/index.d.ts.map +1 -1
  68. package/dist/messages/fr/email.json +58 -0
  69. package/dist/messages/it/email.json +58 -0
  70. package/dist/messages/pt/email.json +58 -0
  71. package/dist/styles/classes.json +3 -2
  72. package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +9 -7
  73. package/dist/templates/contents/themes/starter/emails/_README.md +69 -0
  74. package/dist/templates/contents/themes/starter/emails/verify-email.ts +34 -0
  75. package/package.json +6 -2
  76. package/scripts/build/registry/discovery/emails.mjs +146 -0
  77. package/scripts/build/registry/generators/email-registry.mjs +94 -0
  78. package/scripts/build/registry.mjs +8 -4
  79. package/templates/app/api/v1/teams/[teamId]/members/route.ts +9 -7
  80. package/templates/contents/themes/starter/emails/_README.md +69 -0
  81. package/templates/contents/themes/starter/emails/verify-email.ts +34 -0
  82. package/tests/jest/__mocks__/@nextsparkjs/registries/email-registry.ts +41 -0
  83. package/tests/jest/__mocks__/next-intl-server.js +55 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextsparkjs/core",
3
- "version": "0.1.0-beta.149",
3
+ "version": "0.1.0-beta.150",
4
4
  "description": "NextSpark - The complete SaaS framework for Next.js",
5
5
  "license": "MIT",
6
6
  "author": "NextSpark <hello@nextspark.dev>",
@@ -188,6 +188,10 @@
188
188
  "types": "./dist/lib/*.d.ts",
189
189
  "import": "./dist/lib/*.js"
190
190
  },
191
+ "./emails/*": {
192
+ "types": "./dist/emails/*.d.ts",
193
+ "import": "./dist/emails/*.js"
194
+ },
191
195
  "./hooks": {
192
196
  "types": "./dist/hooks/index.d.ts",
193
197
  "import": "./dist/hooks/index.js"
@@ -463,7 +467,7 @@
463
467
  "tailwind-merge": "^3.3.1",
464
468
  "uuid": "^13.0.0",
465
469
  "zod": "^4.1.5",
466
- "@nextsparkjs/testing": "0.1.0-beta.149"
470
+ "@nextsparkjs/testing": "0.1.0-beta.150"
467
471
  },
468
472
  "scripts": {
469
473
  "postinstall": "node scripts/postinstall.mjs || true",
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Email Discovery
3
+ *
4
+ * Discovers email template files from core defaults and the active theme.
5
+ * Theme files override core defaults at the same slug; new theme slugs are
6
+ * additive (themes can ship templates that core doesn't know about).
7
+ *
8
+ * @module core/scripts/build/registry/discovery/emails
9
+ */
10
+
11
+ import { readdir } from 'fs/promises'
12
+ import { join } from 'path'
13
+
14
+ import { CONFIG as DEFAULT_CONFIG } from '../config.mjs'
15
+ import { log, verbose } from '../../../utils/index.mjs'
16
+
17
+ /**
18
+ * @typedef {Object} DiscoveredEmail
19
+ * @property {string} slug - Filename without extension (e.g. 'verify-email')
20
+ * @property {'core' | 'theme'} source - Where the file lives
21
+ * @property {string} importPath - Module specifier the generator will emit
22
+ * @property {string} importName - Sanitized identifier safe to use in generated TS
23
+ */
24
+
25
+ /**
26
+ * Convert a file slug to a JavaScript identifier safe for use as an import name.
27
+ * - "verify-email" → "verifyEmail"
28
+ * - "team-invite" → "teamInvite"
29
+ * - "purchase.v2" → "purchaseV2"
30
+ *
31
+ * Keeps existing camelCase intact (e.g. "verifyEmail" → "verifyEmail").
32
+ *
33
+ * @param {string} slug
34
+ * @returns {string}
35
+ */
36
+ function slugToIdentifier(slug) {
37
+ return slug
38
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
39
+ .replace(/^([0-9])/, '_$1')
40
+ }
41
+
42
+ /**
43
+ * Walk a directory and return its email entries (one per .ts/.tsx file at the
44
+ * top level — emails are not nested).
45
+ *
46
+ * @param {string} dir
47
+ * @returns {Promise<Array<{ slug: string, fileName: string }>>}
48
+ */
49
+ async function listEmailFiles(dir) {
50
+ let entries
51
+ try {
52
+ entries = await readdir(dir, { withFileTypes: true })
53
+ } catch {
54
+ // Directory doesn't exist — that's fine, no emails here.
55
+ return []
56
+ }
57
+
58
+ const out = []
59
+ for (const entry of entries) {
60
+ if (!entry.isFile()) continue
61
+ if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue
62
+ // Skip barrel files and underscore-prefixed (README/notes)
63
+ if (entry.name === 'index.ts' || entry.name === 'index.tsx') continue
64
+ if (entry.name.startsWith('_')) continue
65
+ if (entry.name.endsWith('.d.ts')) continue
66
+ if (entry.name.endsWith('.test.ts') || entry.name.endsWith('.test.tsx')) continue
67
+
68
+ const slug = entry.name.replace(/\.(tsx|ts)$/, '')
69
+ out.push({ slug, fileName: entry.name })
70
+ }
71
+ return out
72
+ }
73
+
74
+ /**
75
+ * Discover all email templates: core defaults first, then active theme overrides
76
+ * and additions. Theme entries take precedence per slug.
77
+ *
78
+ * @param {object} config - Configuration object from getConfig() (defaults to DEFAULT_CONFIG)
79
+ * @returns {Promise<DiscoveredEmail[]>}
80
+ */
81
+ export async function discoverEmails(config = DEFAULT_CONFIG) {
82
+ log('Discovering email templates...', 'info')
83
+
84
+ // Resolve core default emails dir.
85
+ // - Monorepo mode: packages/core/src/emails/ (uncompiled .ts)
86
+ // - NPM mode: node_modules/@nextsparkjs/core/dist/emails/ (compiled .js)
87
+ // The build-time import alias is the same shape in both cases — Next.js
88
+ // resolves '@nextsparkjs/core/emails/<slug>' via the package's exports field
89
+ // in npm mode, and '@/core/emails/<slug>' via tsconfig paths in monorepo mode.
90
+ const coreEmailsDir = config.isNpmMode
91
+ ? join(config.coreDir, 'dist', 'emails')
92
+ : join(config.coreDir, 'src', 'emails')
93
+
94
+ const coreImportBase = config.isNpmMode
95
+ ? '@nextsparkjs/core/emails'
96
+ : '@/core/emails'
97
+
98
+ /** @type {Map<string, DiscoveredEmail>} */
99
+ const merged = new Map()
100
+
101
+ // Core defaults
102
+ const coreFiles = await listEmailFiles(coreEmailsDir)
103
+ if (coreFiles.length > 0) {
104
+ verbose(`Found ${coreFiles.length} core email template(s) in: ${coreEmailsDir}`)
105
+ for (const { slug } of coreFiles) {
106
+ merged.set(slug, {
107
+ slug,
108
+ source: 'core',
109
+ importPath: `${coreImportBase}/${slug}`,
110
+ importName: `${slugToIdentifier(slug)}_core`,
111
+ })
112
+ }
113
+ } else {
114
+ verbose(`No core email templates found in: ${coreEmailsDir}`)
115
+ }
116
+
117
+ // Theme overrides + additions.
118
+ if (config.activeTheme && config.themesDir) {
119
+ const themeEmailsDir = join(config.themesDir, config.activeTheme, 'emails')
120
+ const themeFiles = await listEmailFiles(themeEmailsDir)
121
+
122
+ if (themeFiles.length > 0) {
123
+ verbose(`Found ${themeFiles.length} theme email override(s) in: ${themeEmailsDir}`)
124
+ for (const { slug } of themeFiles) {
125
+ merged.set(slug, {
126
+ slug,
127
+ source: 'theme',
128
+ importPath: `@/contents/themes/${config.activeTheme}/emails/${slug}`,
129
+ importName: `${slugToIdentifier(slug)}_theme`,
130
+ })
131
+ }
132
+ }
133
+ } else if (!config.activeTheme) {
134
+ verbose('No active theme — only core email templates will be registered.')
135
+ }
136
+
137
+ const result = Array.from(merged.values()).sort((a, b) => a.slug.localeCompare(b.slug))
138
+
139
+ log(
140
+ `Discovered ${result.length} email template(s): ` +
141
+ result.map(e => `${e.slug}(${e.source})`).join(', '),
142
+ 'info',
143
+ )
144
+
145
+ return result
146
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Email Registry Generator
3
+ *
4
+ * Generates email-registry.ts — a static, frozen map from slug to template
5
+ * function, where theme overrides win over core defaults.
6
+ *
7
+ * The generated file is intentionally NOT annotated with `Record<string, …>`.
8
+ * Instead it uses `as const` so TypeScript infers per-slug types from each
9
+ * imported function's signature — same trick `entity-registry.mjs` already
10
+ * uses. This means consumer-defined slugs (in `themes/<theme>/emails/`) get
11
+ * full per-slug type inference for free, with zero `declare module` boilerplate.
12
+ *
13
+ * @module core/scripts/build/registry/generators/email-registry
14
+ */
15
+
16
+ import { join } from 'path'
17
+ import { convertCorePath } from '../config.mjs'
18
+
19
+ /**
20
+ * @param {Array<{slug: string, source: 'core'|'theme', importPath: string, importName: string}>} emails
21
+ * @param {object} config - Configuration object from getConfig()
22
+ * @returns {string} Generated TypeScript content
23
+ */
24
+ export function generateEmailRegistry(emails, config) {
25
+ const outputFilePath = join(config.outputDir, 'email-registry.ts')
26
+
27
+ // Resolve core import paths through convertCorePath so they work in both
28
+ // monorepo and npm modes. Theme paths are project-local so don't need it.
29
+ const importLines = emails
30
+ .map(email => {
31
+ const importPath =
32
+ email.source === 'core'
33
+ ? convertCorePath(email.importPath, outputFilePath, config)
34
+ : email.importPath
35
+ return `import ${email.importName} from '${importPath}'`
36
+ })
37
+ .join('\n')
38
+
39
+ // Map literal entries — quoted slug keys (some slugs are kebab-case which
40
+ // is not a valid identifier without quoting).
41
+ const registryEntries = emails
42
+ .map(email => ` '${email.slug}': ${email.importName},`)
43
+ .join('\n')
44
+
45
+ // Metadata for diagnostics / DX
46
+ const metadataEntries = emails
47
+ .map(
48
+ email =>
49
+ ` '${email.slug}': { source: '${email.source}' as const, overridden: ${email.source === 'theme'} },`,
50
+ )
51
+ .join('\n')
52
+
53
+ const generatedAt = new Date().toISOString()
54
+ const sourcesLine = emails.length === 0
55
+ ? '(no email templates discovered)'
56
+ : emails.map(e => `${e.slug}(${e.source})`).join(', ')
57
+
58
+ return `/**
59
+ * Auto-generated Email Registry
60
+ *
61
+ * Generated at: ${generatedAt}
62
+ * Templates discovered: ${emails.length}
63
+ * Sources: ${sourcesLine}
64
+ *
65
+ * DO NOT EDIT - This file is auto-generated by scripts/build/registry.mjs.
66
+ * Override behaviour: drop a file at \`themes/<active-theme>/emails/<slug>.ts\`
67
+ * and rebuild — the theme version wins over the core default at the same slug,
68
+ * and entirely new slugs from a theme are additive.
69
+ *
70
+ * Type safety: this file emits a literal object with \`as const\` so TypeScript
71
+ * infers per-slug data types from each imported function's signature.
72
+ * Do NOT add a \`Record<string, …>\` annotation — that would erase the per-slug
73
+ * inference and consumer-defined slugs would lose type safety.
74
+ */
75
+
76
+ ${importLines}
77
+
78
+ export const EMAIL_REGISTRY = {
79
+ ${registryEntries}
80
+ } as const
81
+
82
+ export type EmailSlug = keyof typeof EMAIL_REGISTRY
83
+
84
+ export const EMAIL_REGISTRY_METADATA = {
85
+ ${metadataEntries}
86
+ } as const
87
+
88
+ export const EMAIL_REGISTRY_INFO = {
89
+ totalTemplates: ${emails.length},
90
+ generatedAt: '${generatedAt}',
91
+ slugs: [${emails.map(e => `'${e.slug}'`).join(', ')}] as const,
92
+ } as const
93
+ `
94
+ }
@@ -51,6 +51,7 @@ import { discoverPlugins } from './registry/discovery/plugins.mjs'
51
51
  import { discoverThemes } from './registry/discovery/themes.mjs'
52
52
  import { discoverMiddlewares } from './registry/discovery/middlewares.mjs'
53
53
  import { discoverTemplates } from './registry/discovery/templates.mjs'
54
+ import { discoverEmails } from './registry/discovery/emails.mjs'
54
55
  import { discoverBlocks } from './registry/discovery/blocks.mjs'
55
56
  import { discoverCoreRoutes } from './registry/discovery/core-routes.mjs'
56
57
  import { discoverApiPresets } from './registry/discovery/api-presets.mjs'
@@ -60,6 +61,7 @@ import { generateEntityRegistry, generateEntityRegistryClient } from './registry
60
61
  import { generateEntityTypes } from './registry/generators/entity-types.mjs'
61
62
  import { generateThemeRegistry } from './registry/generators/theme-registry.mjs'
62
63
  import { generateTemplateRegistry, generateTemplateRegistryClient } from './registry/generators/template-registry.mjs'
64
+ import { generateEmailRegistry } from './registry/generators/email-registry.mjs'
63
65
  import { generateBlockRegistry } from './registry/generators/block-registry.mjs'
64
66
  import { generateMiddlewareRegistry } from './registry/generators/middleware-registry.mjs'
65
67
  import { generateRouteHandlersRegistry } from './registry/generators/route-handlers.mjs'
@@ -87,7 +89,7 @@ import { syncAppGlobalsCss } from './theme.mjs'
87
89
 
88
90
  // ==================== Registry File Generation ====================
89
91
 
90
- async function generateRegistryFiles(CONFIG, plugins, entities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData) {
92
+ async function generateRegistryFiles(CONFIG, plugins, entities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData, emails) {
91
93
  log('Generating registry files...', 'build')
92
94
 
93
95
  try {
@@ -109,6 +111,7 @@ async function generateRegistryFiles(CONFIG, plugins, entities, themes, template
109
111
  { name: 'translation-registry.ts', content: generateTranslationRegistry(themes, CONFIG) },
110
112
  { name: 'template-registry.ts', content: generateTemplateRegistry(templates, CONFIG) },
111
113
  { name: 'template-registry.client.ts', content: templateRegistryClientContent },
114
+ { name: 'email-registry.ts', content: generateEmailRegistry(emails, CONFIG) },
112
115
  { name: 'block-registry.ts', content: generateBlockRegistry(blocks, CONFIG) },
113
116
  { name: 'billing-registry.ts', content: await generateBillingRegistry(CONFIG.activeTheme, CONFIG.contentsDir, CONFIG) },
114
117
  { name: 'middleware-registry.ts', content: generateMiddlewareRegistry(middlewares, CONFIG) },
@@ -188,7 +191,7 @@ export async function buildRegistries(projectRoot = null) {
188
191
  await discoverParentChildRelations(CONFIG)
189
192
 
190
193
  // Discover all content types in parallel (pass CONFIG to each)
191
- const [plugins, coreEntities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData] = await Promise.all([
194
+ const [plugins, coreEntities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData, emails] = await Promise.all([
192
195
  discoverPlugins(CONFIG),
193
196
  discoverCoreEntities(CONFIG),
194
197
  discoverThemes(CONFIG),
@@ -197,7 +200,8 @@ export async function buildRegistries(projectRoot = null) {
197
200
  discoverBlocks(CONFIG),
198
201
  discoverPermissionsConfig(CONFIG),
199
202
  discoverCoreRoutes(CONFIG),
200
- discoverApiPresets(CONFIG)
203
+ discoverApiPresets(CONFIG),
204
+ discoverEmails(CONFIG)
201
205
  ])
202
206
 
203
207
  // Aggregate all entities with proper priority: plugin < core < theme
@@ -256,7 +260,7 @@ export async function buildRegistries(projectRoot = null) {
256
260
 
257
261
  // Hoist plugin dependencies to root workspace for proper resolution
258
262
  // Generate all registry files (use aggregated entities for entity registry + blocks)
259
- await generateRegistryFiles(CONFIG, plugins, allEntities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData)
263
+ await generateRegistryFiles(CONFIG, plugins, allEntities, themes, templates, middlewares, blocks, permissionsConfig, coreRoutes, apiPresetsData, emails)
260
264
 
261
265
  // Generate missing pages for templates that don't have core app pages
262
266
  await generateMissingPages(templates, CONFIG)
@@ -16,7 +16,8 @@ import { inviteMemberSchema, memberListQuerySchema } from '@nextsparkjs/core/lib
16
16
  import { TeamMemberService, MembershipService } from '@nextsparkjs/core/lib/services'
17
17
  import type { TeamMember, TeamInvitation, TeamRole, Team } from '@nextsparkjs/core/lib/teams/types'
18
18
  import { EmailFactory } from '@nextsparkjs/core/lib/email/factory'
19
- import { createTeamInvitationEmail } from '@nextsparkjs/core/lib/email/templates'
19
+ import { sendTeamInvitationEmail } from '@nextsparkjs/core/lib/email/send'
20
+ import { I18N_CONFIG } from '@nextsparkjs/core/lib/config'
20
21
 
21
22
  // Role hierarchy for invite validation (higher number = more power)
22
23
  const ROLE_HIERARCHY: Record<TeamRole, number> = {
@@ -310,14 +311,15 @@ export const POST = withRateLimitTier(withApiLogging(
310
311
  try {
311
312
  const emailProvider = EmailFactory.getInstance()
312
313
  const inviterName = authResult.user!.email // Use email as inviter name
313
- const emailContent = createTeamInvitationEmail(
314
- validatedData.email,
314
+ const emailContent = await sendTeamInvitationEmail({
315
+ inviteeEmail: validatedData.email,
315
316
  inviterName,
316
- team.name,
317
- validatedData.role,
317
+ teamName: team.name,
318
+ role: validatedData.role,
318
319
  acceptUrl,
319
- '7 days'
320
- )
320
+ expiresIn: '7 days',
321
+ appName: process.env.NEXT_PUBLIC_APP_NAME || 'Your App',
322
+ }, I18N_CONFIG.defaultLocale)
321
323
 
322
324
  await emailProvider.send({
323
325
  to: validatedData.email,
@@ -0,0 +1,69 @@
1
+ # Email templates
2
+
3
+ Drop a `.ts` file in this directory to override or add transactional email
4
+ templates for this theme. The build-time email registry picks files here
5
+ over the core defaults at the same slug, and any new slugs are additive.
6
+
7
+ ## How discovery works
8
+
9
+ At build time, `pnpm dev` and `pnpm build` regenerate
10
+ `.nextspark/registries/email-registry.ts`. The registry first scans core's
11
+ defaults, then walks `themes/<NEXT_PUBLIC_ACTIVE_THEME>/emails/`. Theme files
12
+ with the same filename as a core default override the core file. Theme files
13
+ with new filenames are added to the registry as new slugs.
14
+
15
+ ## File contract
16
+
17
+ Each file must have a default export with this shape:
18
+
19
+ ```ts
20
+ export default async function <name>(
21
+ data: <YourDataShape>,
22
+ locale?: string,
23
+ ): Promise<{ subject: string; html: string }>
24
+ ```
25
+
26
+ `locale` is the BCP47 code (e.g. `'en'`, `'es'`); use it with
27
+ `getTranslations({ locale, namespace: 'email.<slug>' })` from `next-intl/server`
28
+ when you want translated copy. Core's defaults already do this for the four
29
+ slugs below.
30
+
31
+ ## Core slugs you can override
32
+
33
+ | Slug | Data type | Sent on |
34
+ |---------------------|----------------------------|-------------------------------------------|
35
+ | `verify-email` | `VerificationEmailData` | Email/password signup (when enabled) |
36
+ | `reset-password` | `PasswordResetEmailData` | Password reset request |
37
+ | `otp-verification` | `OtpVerificationEmailData` | OTP-based sign-in / verification |
38
+ | `team-invitation` | `TeamInvitationEmailData` | Adding a member to a team |
39
+
40
+ Import the data types from `@nextsparkjs/core/lib/email/types`.
41
+
42
+ ## Adding new slugs
43
+
44
+ A theme can ship slugs core doesn't know about (e.g. `welcome.ts`,
45
+ `weekly-digest.ts`, `purchase-confirmation.ts`). After rebuild, they become
46
+ available as `EMAIL_REGISTRY['<slug>']` from
47
+ `@nextsparkjs/registries/email-registry` with full per-slug TypeScript
48
+ inference of the data argument — no `declare module` boilerplate.
49
+
50
+ ## Falling back to core defaults
51
+
52
+ Delete a file from this directory to revert that slug to core's default on
53
+ the next build.
54
+
55
+ ## Convenience helpers for the four core slugs
56
+
57
+ If you don't override, call sites in your project can keep using the typed
58
+ helpers exposed by core:
59
+
60
+ ```ts
61
+ import {
62
+ sendVerifyEmail,
63
+ sendResetPasswordEmail,
64
+ sendOtpVerificationEmail,
65
+ sendTeamInvitationEmail,
66
+ } from '@nextsparkjs/core/lib/email/send'
67
+ ```
68
+
69
+ These wrap `EMAIL_REGISTRY['<slug>']` and pick up your overrides automatically.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Theme override: Verify Email.
3
+ *
4
+ * Drop your branded HTML and copy here to customize the verification email.
5
+ * The build-time email registry in `@nextsparkjs/registries/email-registry`
6
+ * will pick this file over the core default at the same slug.
7
+ *
8
+ * The other three core slugs you can override or add to:
9
+ * - reset-password.ts
10
+ * - otp-verification.ts
11
+ * - team-invitation.ts
12
+ *
13
+ * You can also add brand-new slugs here (e.g. `welcome.ts`,
14
+ * `purchase-confirmation.ts`). They're picked up automatically by the
15
+ * registry generator and become available as `EMAIL_REGISTRY['<slug>']` —
16
+ * with full per-slug TypeScript inference of the data argument.
17
+ *
18
+ * Out of the box this file simply delegates to core so the project keeps
19
+ * working before you brand it. Replace the body with your own HTML when
20
+ * you're ready, or delete this file entirely to fall back to the core default.
21
+ */
22
+
23
+ import coreVerifyEmail from '@nextsparkjs/core/emails/verify-email'
24
+ import type {
25
+ EmailContent,
26
+ VerificationEmailData,
27
+ } from '@nextsparkjs/core/lib/email/types'
28
+
29
+ export default async function verifyEmail(
30
+ data: VerificationEmailData,
31
+ locale?: string,
32
+ ): Promise<EmailContent> {
33
+ return coreVerifyEmail(data, locale)
34
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Test mock for `@nextsparkjs/registries/email-registry`.
3
+ *
4
+ * Mirrors what the build-time generator (`generators/email-registry.mjs`)
5
+ * emits in production, but constructed by hand so unit tests don't depend
6
+ * on running the real registry build before each test run. Imports the
7
+ * actual core default templates so end-to-end tests for
8
+ * `lib/email/send` and the deprecated `lib/email/templates` adapters
9
+ * exercise real output.
10
+ *
11
+ * Tests that need to spy on the registry should use `jest.mock(
12
+ * '@nextsparkjs/registries/email-registry', ...)` to fully replace this
13
+ * module — that takes precedence over the file mapping.
14
+ */
15
+
16
+ import verifyEmail from '../../../../../src/emails/verify-email'
17
+ import resetPassword from '../../../../../src/emails/reset-password'
18
+ import otpVerification from '../../../../../src/emails/otp-verification'
19
+ import teamInvitation from '../../../../../src/emails/team-invitation'
20
+
21
+ export const EMAIL_REGISTRY = {
22
+ 'verify-email': verifyEmail,
23
+ 'reset-password': resetPassword,
24
+ 'otp-verification': otpVerification,
25
+ 'team-invitation': teamInvitation,
26
+ } as const
27
+
28
+ export type EmailSlug = keyof typeof EMAIL_REGISTRY
29
+
30
+ export const EMAIL_REGISTRY_METADATA = {
31
+ 'verify-email': { source: 'core' as const, overridden: false },
32
+ 'reset-password': { source: 'core' as const, overridden: false },
33
+ 'otp-verification': { source: 'core' as const, overridden: false },
34
+ 'team-invitation': { source: 'core' as const, overridden: false },
35
+ } as const
36
+
37
+ export const EMAIL_REGISTRY_INFO = {
38
+ totalTemplates: 4,
39
+ generatedAt: 'test-mock',
40
+ slugs: ['verify-email', 'reset-password', 'otp-verification', 'team-invitation'] as const,
41
+ } as const
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Jest mock for `next-intl/server`'s `getTranslations`.
3
+ *
4
+ * Loads the corresponding `messages/<locale>/<root>.json` file directly and
5
+ * resolves keys inside `namespace` (which can be dotted, e.g. `email.verifyEmail`).
6
+ * Supports ICU-style `{var}` placeholders in values — replaced naively, no
7
+ * pluralization. Sufficient for email template snapshot tests.
8
+ */
9
+
10
+ const path = require('path')
11
+ const fs = require('fs')
12
+
13
+ function loadNamespace(locale, namespace) {
14
+ // namespace is dotted: "email.verifyEmail" → root file "email", key path "verifyEmail"
15
+ const [rootFile, ...rest] = namespace.split('.')
16
+ const filePath = path.join(
17
+ __dirname,
18
+ '../../../src/messages',
19
+ locale,
20
+ `${rootFile}.json`,
21
+ )
22
+ const raw = fs.readFileSync(filePath, 'utf8')
23
+ let obj = JSON.parse(raw)
24
+ for (const segment of rest) {
25
+ obj = obj[segment]
26
+ if (!obj) throw new Error(`next-intl mock: missing segment "${segment}" in ${filePath}`)
27
+ }
28
+ return obj
29
+ }
30
+
31
+ function format(template, params) {
32
+ if (!params) return template
33
+ return template.replace(/\{(\w+)\}/g, (_, key) =>
34
+ params[key] === undefined ? `{${key}}` : String(params[key]),
35
+ )
36
+ }
37
+
38
+ async function getTranslations(opts) {
39
+ const locale = opts?.locale || 'en'
40
+ const namespace = opts?.namespace || ''
41
+ const ns = loadNamespace(locale, namespace)
42
+
43
+ return function t(key, params) {
44
+ const value = ns[key]
45
+ if (value === undefined) {
46
+ throw new Error(`next-intl mock: missing key "${key}" in namespace "${namespace}"`)
47
+ }
48
+ return format(value, params)
49
+ }
50
+ }
51
+
52
+ module.exports = {
53
+ getTranslations,
54
+ __esModule: true,
55
+ }