@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.
- package/dist/components/dashboard/block-editor/block-picker.d.ts +6 -1
- package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/block-picker.js +34 -12
- package/dist/components/dashboard/block-editor/block-preview-canvas.d.ts +2 -1
- package/dist/components/dashboard/block-editor/block-preview-canvas.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/block-preview-canvas.js +5 -0
- package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/builder-editor-view.js +41 -13
- package/dist/components/dashboard/block-editor/floating-block-toolbar.d.ts +2 -1
- package/dist/components/dashboard/block-editor/floating-block-toolbar.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/floating-block-toolbar.js +17 -1
- package/dist/components/dashboard/block-editor/tree-view-node.d.ts +4 -1
- package/dist/components/dashboard/block-editor/tree-view-node.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/tree-view-node.js +52 -1
- package/dist/components/dashboard/block-editor/tree-view.d.ts +4 -1
- package/dist/components/dashboard/block-editor/tree-view.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/tree-view.js +6 -0
- package/dist/components/entities/EntityTable.d.ts +1 -1
- package/dist/components/entities/EntityTable.d.ts.map +1 -1
- package/dist/components/entities/EntityTable.js +14 -1
- package/dist/components/entities/entity-table.types.d.ts +2 -0
- package/dist/components/entities/entity-table.types.d.ts.map +1 -1
- package/dist/components/entities/wrappers/EntityListWrapper.d.ts.map +1 -1
- package/dist/components/entities/wrappers/EntityListWrapper.js +19 -1
- package/dist/emails/otp-verification.d.ts +9 -0
- package/dist/emails/otp-verification.d.ts.map +1 -0
- package/dist/emails/otp-verification.js +72 -0
- package/dist/emails/reset-password.d.ts +9 -0
- package/dist/emails/reset-password.d.ts.map +1 -0
- package/dist/emails/reset-password.js +95 -0
- package/dist/emails/team-invitation.d.ts +9 -0
- package/dist/emails/team-invitation.d.ts.map +1 -0
- package/dist/emails/team-invitation.js +93 -0
- package/dist/emails/verify-email.d.ts +13 -0
- package/dist/emails/verify-email.d.ts.map +1 -0
- package/dist/emails/verify-email.js +84 -0
- package/dist/lib/api/entities.d.ts +6 -1
- package/dist/lib/api/entities.d.ts.map +1 -1
- package/dist/lib/api/entities.js +23 -2
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +12 -7
- package/dist/lib/blocks/clipboard.d.ts +11 -0
- package/dist/lib/blocks/clipboard.d.ts.map +1 -0
- package/dist/lib/blocks/clipboard.js +30 -0
- package/dist/lib/email/index.d.ts +1 -0
- package/dist/lib/email/index.d.ts.map +1 -1
- package/dist/lib/email/index.js +12 -0
- package/dist/lib/email/send.d.ts +15 -0
- package/dist/lib/email/send.d.ts.map +1 -0
- package/dist/lib/email/send.js +11 -0
- package/dist/lib/email/templates.d.ts +42 -29
- package/dist/lib/email/templates.d.ts.map +1 -1
- package/dist/lib/email/templates.js +8 -303
- package/dist/lib/email/types.d.ts +32 -0
- package/dist/lib/email/types.d.ts.map +1 -1
- package/dist/lib/services/subscription.service.d.ts +2 -2
- package/dist/lib/services/subscription.service.d.ts.map +1 -1
- package/dist/lib/services/subscription.service.js +6 -6
- package/dist/messages/de/email.json +58 -0
- package/dist/messages/en/admin.json +6 -2
- package/dist/messages/en/email.json +58 -0
- package/dist/messages/en/index.d.ts +4 -0
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/es/admin.json +6 -2
- package/dist/messages/es/email.json +58 -0
- package/dist/messages/es/index.d.ts +4 -0
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/fr/email.json +58 -0
- package/dist/messages/it/email.json +58 -0
- package/dist/messages/pt/email.json +58 -0
- package/dist/styles/classes.json +3 -2
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +9 -7
- package/dist/templates/contents/themes/starter/emails/_README.md +69 -0
- package/dist/templates/contents/themes/starter/emails/verify-email.ts +34 -0
- package/package.json +6 -2
- package/scripts/build/registry/discovery/emails.mjs +146 -0
- package/scripts/build/registry/generators/email-registry.mjs +94 -0
- package/scripts/build/registry.mjs +8 -4
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +9 -7
- package/templates/contents/themes/starter/emails/_README.md +69 -0
- package/templates/contents/themes/starter/emails/verify-email.ts +34 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/email-registry.ts +41 -0
- 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.
|
|
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.
|
|
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 {
|
|
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 =
|
|
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
|
+
}
|