@teleporthq/teleport-plugin-next-workflows 0.43.66 → 0.43.67
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/__tests__/_helpers/bind-workflow-utils.ts +30 -0
- package/__tests__/_helpers/run-generated-middleware.ts +11 -2
- package/__tests__/admin-form-row-hydration.test.ts +55 -0
- package/__tests__/client-config-redaction.test.ts +53 -0
- package/__tests__/email-locale-module.test.ts +323 -0
- package/__tests__/email-locale-senders.test.ts +268 -0
- package/__tests__/locale-aware-redirects.test.ts +355 -0
- package/__tests__/navigation-go-to-page.test.ts +43 -0
- package/__tests__/node-handler-file-inline-map.test.ts +29 -3
- package/__tests__/resolve-config-localized-templates.test.ts +262 -0
- package/__tests__/sent-email-log-wiring.test.ts +2 -2
- package/dist/cjs/account-delete-route-generator.d.ts +20 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +16 -22
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +13 -2
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +2 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +42 -14
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +19 -0
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/email-locale/email-locale-code.d.ts +25 -0
- package/dist/cjs/email-locale/email-locale-code.d.ts.map +1 -0
- package/dist/cjs/email-locale/email-locale-code.js +228 -0
- package/dist/cjs/email-locale/email-locale-code.js.map +1 -0
- package/dist/cjs/email-locale/email-locale-scope.d.ts +46 -0
- package/dist/cjs/email-locale/email-locale-scope.d.ts.map +1 -0
- package/dist/cjs/email-locale/email-locale-scope.js +51 -0
- package/dist/cjs/email-locale/email-locale-scope.js.map +1 -0
- package/dist/cjs/email-locale/index.d.ts +11 -0
- package/dist/cjs/email-locale/index.d.ts.map +1 -0
- package/dist/cjs/email-locale/index.js +40 -0
- package/dist/cjs/email-locale/index.js.map +1 -0
- package/dist/cjs/executor-generator.d.ts +11 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +32 -3
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +14 -3
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +14 -2
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.d.ts.map +1 -1
- package/dist/cjs/invoice/index.js +4 -1
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.js +10 -3
- package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-login.js +3 -1
- package/dist/cjs/nodes/account/account-login.js.map +1 -1
- package/dist/cjs/nodes/account/account-logout.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-logout.js +4 -2
- package/dist/cjs/nodes/account/account-logout.js.map +1 -1
- package/dist/cjs/nodes/account/account-signup.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-signup.js +12 -2
- package/dist/cjs/nodes/account/account-signup.js.map +1 -1
- package/dist/cjs/nodes/account/account-social-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-social-login.js +4 -2
- package/dist/cjs/nodes/account/account-social-login.js.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.js +8 -0
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/ecommerce/ecommerce-generate-invoice.d.ts.map +1 -1
- package/dist/cjs/nodes/ecommerce/ecommerce-generate-invoice.js +6 -0
- package/dist/cjs/nodes/ecommerce/ecommerce-generate-invoice.js.map +1 -1
- package/dist/cjs/nodes/navigation/navigation-go-to-page.d.ts.map +1 -1
- package/dist/cjs/nodes/navigation/navigation-go-to-page.js +4 -1
- package/dist/cjs/nodes/navigation/navigation-go-to-page.js.map +1 -1
- package/dist/cjs/nodes/navigation/navigation-navigate-to-url.d.ts.map +1 -1
- package/dist/cjs/nodes/navigation/navigation-navigate-to-url.js +5 -2
- package/dist/cjs/nodes/navigation/navigation-navigate-to-url.js.map +1 -1
- package/dist/cjs/nodes/payment/payment-charge-user.d.ts.map +1 -1
- package/dist/cjs/nodes/payment/payment-charge-user.js +7 -2
- package/dist/cjs/nodes/payment/payment-charge-user.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +29 -2
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts +27 -0
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +43 -7
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.js +4 -3
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +17 -3
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/cjs/workflow-utils-alias.d.ts +21 -0
- package/dist/cjs/workflow-utils-alias.d.ts.map +1 -0
- package/dist/cjs/workflow-utils-alias.js +27 -0
- package/dist/cjs/workflow-utils-alias.js.map +1 -0
- package/dist/esm/account-delete-route-generator.d.ts +20 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +17 -23
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +13 -2
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +2 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +43 -15
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +19 -0
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/email-locale/email-locale-code.d.ts +25 -0
- package/dist/esm/email-locale/email-locale-code.d.ts.map +1 -0
- package/dist/esm/email-locale/email-locale-code.js +222 -0
- package/dist/esm/email-locale/email-locale-code.js.map +1 -0
- package/dist/esm/email-locale/email-locale-scope.d.ts +46 -0
- package/dist/esm/email-locale/email-locale-scope.d.ts.map +1 -0
- package/dist/esm/email-locale/email-locale-scope.js +47 -0
- package/dist/esm/email-locale/email-locale-scope.js.map +1 -0
- package/dist/esm/email-locale/index.d.ts +11 -0
- package/dist/esm/email-locale/index.d.ts.map +1 -0
- package/dist/esm/email-locale/index.js +28 -0
- package/dist/esm/email-locale/index.js.map +1 -0
- package/dist/esm/executor-generator.d.ts +11 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +32 -3
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +14 -3
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +14 -2
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.d.ts.map +1 -1
- package/dist/esm/invoice/index.js +4 -1
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.js +10 -3
- package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
- package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-login.js +3 -1
- package/dist/esm/nodes/account/account-login.js.map +1 -1
- package/dist/esm/nodes/account/account-logout.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-logout.js +4 -2
- package/dist/esm/nodes/account/account-logout.js.map +1 -1
- package/dist/esm/nodes/account/account-signup.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-signup.js +12 -2
- package/dist/esm/nodes/account/account-signup.js.map +1 -1
- package/dist/esm/nodes/account/account-social-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-social-login.js +4 -2
- package/dist/esm/nodes/account/account-social-login.js.map +1 -1
- package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-create-item.js +8 -0
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/ecommerce/ecommerce-generate-invoice.d.ts.map +1 -1
- package/dist/esm/nodes/ecommerce/ecommerce-generate-invoice.js +6 -0
- package/dist/esm/nodes/ecommerce/ecommerce-generate-invoice.js.map +1 -1
- package/dist/esm/nodes/navigation/navigation-go-to-page.d.ts.map +1 -1
- package/dist/esm/nodes/navigation/navigation-go-to-page.js +4 -1
- package/dist/esm/nodes/navigation/navigation-go-to-page.js.map +1 -1
- package/dist/esm/nodes/navigation/navigation-navigate-to-url.d.ts.map +1 -1
- package/dist/esm/nodes/navigation/navigation-navigate-to-url.js +5 -2
- package/dist/esm/nodes/navigation/navigation-navigate-to-url.js.map +1 -1
- package/dist/esm/nodes/payment/payment-charge-user.d.ts.map +1 -1
- package/dist/esm/nodes/payment/payment-charge-user.js +7 -2
- package/dist/esm/nodes/payment/payment-charge-user.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +29 -2
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts +27 -0
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +39 -6
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.js +4 -3
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +18 -4
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/dist/esm/workflow-utils-alias.d.ts +21 -0
- package/dist/esm/workflow-utils-alias.d.ts.map +1 -0
- package/dist/esm/workflow-utils-alias.js +21 -0
- package/dist/esm/workflow-utils-alias.js.map +1 -0
- package/package.json +4 -4
- package/src/account-delete-route-generator.ts +22 -4
- package/src/api-route-generator.ts +13 -2
- package/src/auth-generator.ts +48 -14
- package/src/data-api-route-generator.ts +19 -0
- package/src/email-locale/email-locale-code.ts +239 -0
- package/src/email-locale/email-locale-scope.ts +65 -0
- package/src/email-locale/index.ts +46 -0
- package/src/executor-generator.ts +48 -3
- package/src/index.ts +7 -0
- package/src/invoice/api-routes-code.ts +18 -3
- package/src/invoice/email-sender-code.ts +14 -2
- package/src/invoice/index.ts +4 -1
- package/src/nodes/account/account-delete-current.ts +10 -3
- package/src/nodes/account/account-login.ts +3 -1
- package/src/nodes/account/account-logout.ts +4 -2
- package/src/nodes/account/account-signup.ts +12 -2
- package/src/nodes/account/account-social-login.ts +7 -2
- package/src/nodes/data/data-create-item.ts +8 -0
- package/src/nodes/ecommerce/ecommerce-generate-invoice.ts +6 -0
- package/src/nodes/navigation/navigation-go-to-page.ts +5 -1
- package/src/nodes/navigation/navigation-navigate-to-url.ts +6 -2
- package/src/nodes/payment/payment-charge-user.ts +13 -2
- package/src/nodes/workflow-utils-global.d.ts +12 -0
- package/src/segment-splitter.ts +33 -2
- package/src/transactional-email-code.ts +51 -0
- package/src/workflow-component-plugin.ts +4 -3
- package/src/workflow-project-plugin.ts +21 -4
- package/src/workflow-utils-alias.ts +26 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EmailLocaleConfig, generateEmailLocaleModuleCode } from '../../src/email-locale'
|
|
2
|
+
import { WORKFLOW_UTILS_ALIAS } from '../../src/workflow-utils-alias'
|
|
3
|
+
|
|
4
|
+
// A node handler evaluated on its own has no generated file around it to bind
|
|
5
|
+
// `__workflowUtils` (see workflow-utils-alias.ts), so a test that RUNS such a
|
|
6
|
+
// handler binds it here — to the real generated locale helpers, so the
|
|
7
|
+
// behaviour under test is the one a generated project ships.
|
|
8
|
+
|
|
9
|
+
export const TEST_LOCALES: EmailLocaleConfig = { locales: ['en', 'es'], defaultLocale: 'en' }
|
|
10
|
+
|
|
11
|
+
export const loadEmailLocaleModule = (
|
|
12
|
+
config: EmailLocaleConfig = TEST_LOCALES
|
|
13
|
+
): Record<string, unknown> => {
|
|
14
|
+
const moduleShim = { exports: {} as Record<string, unknown> }
|
|
15
|
+
// eslint-disable-next-line no-new-func
|
|
16
|
+
new Function('module', 'exports', generateEmailLocaleModuleCode(config))(
|
|
17
|
+
moduleShim,
|
|
18
|
+
moduleShim.exports
|
|
19
|
+
)
|
|
20
|
+
return moduleShim.exports
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Binds the alias on the global scope; returns the function that unbinds it. */
|
|
24
|
+
export const bindWorkflowUtils = (config: EmailLocaleConfig = TEST_LOCALES): (() => void) => {
|
|
25
|
+
const globalRef = global as unknown as Record<string, unknown>
|
|
26
|
+
globalRef[WORKFLOW_UTILS_ALIAS] = loadEmailLocaleModule(config)
|
|
27
|
+
return () => {
|
|
28
|
+
delete globalRef[WORKFLOW_UTILS_ALIAS]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -29,6 +29,13 @@ export interface MiddlewareRequestOptions {
|
|
|
29
29
|
/** Session user the stubbed `/api/auth/session` returns; omit for a guest. */
|
|
30
30
|
sessionUser?: MiddlewareSessionUser | null
|
|
31
31
|
origin?: string
|
|
32
|
+
/**
|
|
33
|
+
* The request's language as Next.js reports it on `nextUrl` — the prefix
|
|
34
|
+
* is already stripped from `pathname`, so pass the bare route. Omit both for
|
|
35
|
+
* a project without internationalization.
|
|
36
|
+
*/
|
|
37
|
+
locale?: string
|
|
38
|
+
defaultLocale?: string
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
interface RawMiddlewareResult {
|
|
@@ -113,9 +120,11 @@ export const compileGeneratedMiddleware = (auth: UIDLAuthentication) => {
|
|
|
113
120
|
|
|
114
121
|
const getTokenStub = async (): Promise<null> => null
|
|
115
122
|
const middleware = factory(NEXT_RESPONSE_STUB, getTokenStub, fetchImpl)
|
|
123
|
+
const localePrefix =
|
|
124
|
+
options.locale && options.locale !== options.defaultLocale ? `/${options.locale}` : ''
|
|
116
125
|
const result = await middleware({
|
|
117
|
-
nextUrl: { pathname },
|
|
118
|
-
url: `${origin}${pathname}`,
|
|
126
|
+
nextUrl: { pathname, locale: options.locale, defaultLocale: options.defaultLocale },
|
|
127
|
+
url: `${origin}${localePrefix}${pathname}`,
|
|
119
128
|
cookies: {
|
|
120
129
|
get: (name: string) =>
|
|
121
130
|
sessionUser && name === 'next-auth.session-token' ? { name, value: 'stub' } : undefined,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
// The admin form hydration helpers are emitted verbatim into the generated
|
|
5
|
+
// page from a template literal in the plugin. Pull them out of the source and
|
|
6
|
+
// run them, so what is asserted is what the page runs.
|
|
7
|
+
const pluginSource = readFileSync(join(__dirname, '../src/workflow-component-plugin.ts'), 'utf8')
|
|
8
|
+
|
|
9
|
+
const extractHelper = (name: string): string => {
|
|
10
|
+
const match = pluginSource.match(new RegExp(`function ${name}\\([^)]*\\) \\{[\\s\\S]*?\\n\\}\\n`))
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(`helper ${name} not found in the plugin source`)
|
|
13
|
+
}
|
|
14
|
+
// Backticks are escaped inside the template literal.
|
|
15
|
+
return match[0].replace(/\\`/g, '`')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const helpers = new Function(
|
|
19
|
+
`${extractHelper('__normalizeTagsForFormInput')}\n${extractHelper('__normalizeAdminFormRow')}\n` +
|
|
20
|
+
'return { normalizeRow: __normalizeAdminFormRow, normalizeTags: __normalizeTagsForFormInput }'
|
|
21
|
+
)() as {
|
|
22
|
+
normalizeRow: (row: unknown, defaults: Record<string, unknown>) => Record<string, unknown>
|
|
23
|
+
normalizeTags: (value: unknown) => string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULTS = { name: '', tags: '', es_name: '', es_tags: '', zh_tags: '' }
|
|
27
|
+
|
|
28
|
+
describe('__normalizeAdminFormRow', () => {
|
|
29
|
+
it('keeps the per-language columns the form binds', () => {
|
|
30
|
+
const row = { id: 3, name: 'Lamp', es_name: 'Lámpara', color: 'red' }
|
|
31
|
+
expect(helpers.normalizeRow(row, DEFAULTS)).toEqual({
|
|
32
|
+
name: 'Lamp',
|
|
33
|
+
tags: '',
|
|
34
|
+
es_name: 'Lámpara',
|
|
35
|
+
es_tags: '',
|
|
36
|
+
zh_tags: '',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('reshapes a language copy of the tags like the tags themselves', () => {
|
|
41
|
+
const row = { tags: ['light', 'desk'], es_tags: '{"luz","escritorio"}', zh_tags: ['灯'] }
|
|
42
|
+
expect(helpers.normalizeRow(row, DEFAULTS)).toMatchObject({
|
|
43
|
+
tags: 'light, desk',
|
|
44
|
+
es_tags: 'luz, escritorio',
|
|
45
|
+
zh_tags: '灯',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('falls back to the default for a language the row has nothing for', () => {
|
|
50
|
+
expect(helpers.normalizeRow({ name: 'Lamp', es_name: null }, DEFAULTS)).toMatchObject({
|
|
51
|
+
es_name: '',
|
|
52
|
+
es_tags: '',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -222,3 +222,56 @@ describe('server API route still carries the SQL', () => {
|
|
|
222
222
|
expect(route).toContain(DATA_SOURCE_ID)
|
|
223
223
|
})
|
|
224
224
|
})
|
|
225
|
+
|
|
226
|
+
describe('route-backed account nodes: the email their route sends never ships to the browser', () => {
|
|
227
|
+
const { redactServerNodeConfig } = require('../src/segment-splitter')
|
|
228
|
+
|
|
229
|
+
const signupConfig = {
|
|
230
|
+
email: { type: 'workflowContext', nodeId: 'form', path: ['form', 'email'] },
|
|
231
|
+
password: { type: 'workflowContext', nodeId: 'form', path: ['form', 'password'] },
|
|
232
|
+
name: 'Jane',
|
|
233
|
+
company: 'Acme',
|
|
234
|
+
emailProvider: 'email-resend',
|
|
235
|
+
apiKey: { type: 'dynamic', content: { referenceType: 'secret', id: 'RESEND_APIKEY' } },
|
|
236
|
+
from: 'hello@example.com',
|
|
237
|
+
subject: 'Welcome {{userName}}',
|
|
238
|
+
body: '<p>SECRET_WELCOME_BODY</p>',
|
|
239
|
+
templateParams: [{ key: 'userName', value: 'Jane' }],
|
|
240
|
+
localizedTemplates: { es: { body: '<p>SECRET_WELCOME_BODY_ES</p>' } },
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it('keeps the credentials and the user columns of a signup, drops the welcome email', () => {
|
|
244
|
+
const redacted = redactServerNodeConfig(signupConfig, 'client', 'account-signup')
|
|
245
|
+
expect(redacted).toEqual({
|
|
246
|
+
email: signupConfig.email,
|
|
247
|
+
password: signupConfig.password,
|
|
248
|
+
name: 'Jane',
|
|
249
|
+
company: 'Acme',
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('keeps what the delete handler reads, drops the farewell email', () => {
|
|
254
|
+
const redacted = redactServerNodeConfig(
|
|
255
|
+
{
|
|
256
|
+
successMessage: 'Bye',
|
|
257
|
+
redirectTo: '/',
|
|
258
|
+
emailProvider: 'email-resend',
|
|
259
|
+
serverToken: 'x',
|
|
260
|
+
domain: 'mg.example.com',
|
|
261
|
+
deletedEmailPattern: 'deleted-{{userId}}@x.invalid',
|
|
262
|
+
subject: 'Goodbye',
|
|
263
|
+
body: '<p>SECRET_FAREWELL</p>',
|
|
264
|
+
localizedTemplates: { es: { body: '<p>SECRET_FAREWELL_ES</p>' } },
|
|
265
|
+
},
|
|
266
|
+
'client',
|
|
267
|
+
'account-delete-current'
|
|
268
|
+
)
|
|
269
|
+
expect(redacted).toEqual({ successMessage: 'Bye', redirectTo: '/' })
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('leaves every other client node config untouched', () => {
|
|
273
|
+
const config = { message: 'hi', subject: 'kept', body: 'kept' }
|
|
274
|
+
expect(redactServerNodeConfig(config, 'client', 'toast-show')).toBe(config)
|
|
275
|
+
expect(redactServerNodeConfig(config, 'client')).toBe(config)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateEmailLocaleModuleCode,
|
|
3
|
+
resolveEmailLocaleConfig,
|
|
4
|
+
EMAIL_LOCALE_HEADER,
|
|
5
|
+
} from '../src/email-locale'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shared locale module decides which language a customer email goes out
|
|
9
|
+
* in and which path a navigation lands on. It is EXECUTED here rather than
|
|
10
|
+
* grepped: every helper must be total (an unknown locale can never select a
|
|
11
|
+
* template that does not exist, nor send a visitor to a locale the app does
|
|
12
|
+
* not serve), and the precedence between the explicit header, the referring
|
|
13
|
+
* page and the Next.js cookie is what keeps a request without the header from
|
|
14
|
+
* silently falling back to the main language.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface EmailLocaleModule {
|
|
18
|
+
EMAIL_LOCALES: string[]
|
|
19
|
+
DEFAULT_EMAIL_LOCALE: string
|
|
20
|
+
normalizeEmailLocale: (value: unknown) => string | null
|
|
21
|
+
resolveRequestLocale: (req: unknown) => string
|
|
22
|
+
getClientLocale: () => string | null
|
|
23
|
+
localizeHref: (href: unknown, locale: unknown) => unknown
|
|
24
|
+
pickLocalizedTemplate: (
|
|
25
|
+
base: { subject?: string; body?: string },
|
|
26
|
+
localized: unknown,
|
|
27
|
+
locale: unknown
|
|
28
|
+
) => { subject?: string; body?: string; locale: string }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const loadModule = (config: { locales: string[]; defaultLocale: string }): EmailLocaleModule => {
|
|
32
|
+
const code = generateEmailLocaleModuleCode(config)
|
|
33
|
+
const moduleShim = { exports: {} as EmailLocaleModule }
|
|
34
|
+
// eslint-disable-next-line no-new-func
|
|
35
|
+
new Function('module', 'exports', 'require', code)(moduleShim, moduleShim.exports, () => ({}))
|
|
36
|
+
return moduleShim.exports
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const request = (headers: Record<string, string | string[]>) => ({ headers })
|
|
40
|
+
|
|
41
|
+
describe('resolveEmailLocaleConfig', () => {
|
|
42
|
+
it('reads the project languages and the main locale off the UIDL', () => {
|
|
43
|
+
const uidl = {
|
|
44
|
+
internationalization: {
|
|
45
|
+
main: { name: 'English', locale: 'en' },
|
|
46
|
+
languages: { en: 'English', es: 'Spanish' },
|
|
47
|
+
translations: {},
|
|
48
|
+
},
|
|
49
|
+
} as any
|
|
50
|
+
expect(resolveEmailLocaleConfig(uidl)).toEqual({ locales: ['en', 'es'], defaultLocale: 'en' })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('always lists the main locale, even when the language map omits it', () => {
|
|
54
|
+
const uidl = {
|
|
55
|
+
internationalization: {
|
|
56
|
+
main: { name: 'French', locale: 'fr' },
|
|
57
|
+
languages: { es: 'Spanish' },
|
|
58
|
+
translations: {},
|
|
59
|
+
},
|
|
60
|
+
} as any
|
|
61
|
+
expect(resolveEmailLocaleConfig(uidl)).toEqual({ locales: ['fr', 'es'], defaultLocale: 'fr' })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('has no locales at all for a project without internationalization', () => {
|
|
65
|
+
expect(resolveEmailLocaleConfig({} as any)).toEqual({ locales: [], defaultLocale: '' })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('email-locale module', () => {
|
|
70
|
+
const locales = loadModule({ locales: ['en', 'es', 'pt-BR'], defaultLocale: 'en' })
|
|
71
|
+
|
|
72
|
+
describe('normalizeEmailLocale', () => {
|
|
73
|
+
it('answers the canonical code, case-insensitively', () => {
|
|
74
|
+
expect(locales.normalizeEmailLocale('es')).toBe('es')
|
|
75
|
+
expect(locales.normalizeEmailLocale(' ES ')).toBe('es')
|
|
76
|
+
expect(locales.normalizeEmailLocale('pt-br')).toBe('pt-BR')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('rejects anything that is not a project locale', () => {
|
|
80
|
+
expect(locales.normalizeEmailLocale('de')).toBeNull()
|
|
81
|
+
expect(locales.normalizeEmailLocale('')).toBeNull()
|
|
82
|
+
expect(locales.normalizeEmailLocale(null)).toBeNull()
|
|
83
|
+
expect(locales.normalizeEmailLocale(42)).toBeNull()
|
|
84
|
+
expect(locales.normalizeEmailLocale({ locale: 'es' })).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('resolveRequestLocale', () => {
|
|
89
|
+
it('prefers the explicit header the client runtime sends', () => {
|
|
90
|
+
expect(
|
|
91
|
+
locales.resolveRequestLocale(
|
|
92
|
+
request({
|
|
93
|
+
[EMAIL_LOCALE_HEADER]: 'es',
|
|
94
|
+
cookie: 'NEXT_LOCALE=pt-BR',
|
|
95
|
+
referer: 'https://shop.example/pt-BR/checkout',
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
).toBe('es')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('takes the first value of a repeated header', () => {
|
|
102
|
+
expect(locales.resolveRequestLocale(request({ [EMAIL_LOCALE_HEADER]: ['es', 'en'] }))).toBe(
|
|
103
|
+
'es'
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('falls back to the locale prefix of the referring page', () => {
|
|
108
|
+
expect(
|
|
109
|
+
locales.resolveRequestLocale(request({ referer: 'https://shop.example/es/cart' }))
|
|
110
|
+
).toBe('es')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('trusts the page the request came from over a stored locale cookie', () => {
|
|
114
|
+
expect(
|
|
115
|
+
locales.resolveRequestLocale(
|
|
116
|
+
request({ cookie: 'session=abc; NEXT_LOCALE=pt-BR; other=1', referer: 'https://x/es/' })
|
|
117
|
+
)
|
|
118
|
+
).toBe('es')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('falls back to the Next.js locale cookie when nothing names the page', () => {
|
|
122
|
+
expect(
|
|
123
|
+
locales.resolveRequestLocale(request({ cookie: 'session=abc; NEXT_LOCALE=pt-BR; other=1' }))
|
|
124
|
+
).toBe('pt-BR')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('reads an unprefixed referring page as the default language', () => {
|
|
128
|
+
expect(locales.resolveRequestLocale(request({ referer: 'https://shop.example/cart' }))).toBe(
|
|
129
|
+
'en'
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('ignores values that are not project locales and lands on the default', () => {
|
|
134
|
+
expect(
|
|
135
|
+
locales.resolveRequestLocale(
|
|
136
|
+
request({
|
|
137
|
+
[EMAIL_LOCALE_HEADER]: 'de',
|
|
138
|
+
cookie: 'NEXT_LOCALE=fr',
|
|
139
|
+
referer: 'not a url',
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
).toBe('en')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('answers the default language for a request without headers', () => {
|
|
146
|
+
expect(locales.resolveRequestLocale(null)).toBe('en')
|
|
147
|
+
expect(locales.resolveRequestLocale({})).toBe('en')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('pickLocalizedTemplate', () => {
|
|
152
|
+
const base = { subject: 'Hello {{name}}', body: '<p>Hi {{name}}</p>' }
|
|
153
|
+
const localized = {
|
|
154
|
+
es: { subject: 'Hola {{name}}', body: '<p>Hola {{name}}</p>' },
|
|
155
|
+
'pt-BR': { body: '<p>Olá {{name}}</p>' },
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it('returns the copy of the requested language', () => {
|
|
159
|
+
expect(locales.pickLocalizedTemplate(base, localized, 'es')).toEqual({
|
|
160
|
+
subject: 'Hola {{name}}',
|
|
161
|
+
body: '<p>Hola {{name}}</p>',
|
|
162
|
+
locale: 'es',
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('keeps the base subject when the language has no subject of its own', () => {
|
|
167
|
+
expect(locales.pickLocalizedTemplate(base, localized, 'pt-br')).toEqual({
|
|
168
|
+
subject: 'Hello {{name}}',
|
|
169
|
+
body: '<p>Olá {{name}}</p>',
|
|
170
|
+
locale: 'pt-BR',
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('returns the base copy for the default language', () => {
|
|
175
|
+
expect(locales.pickLocalizedTemplate(base, localized, 'en')).toEqual({
|
|
176
|
+
...base,
|
|
177
|
+
locale: 'en',
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns the base copy for a language without a copy, an unknown one, or none', () => {
|
|
182
|
+
const expected = { ...base, locale: 'en' }
|
|
183
|
+
expect(locales.pickLocalizedTemplate(base, { es: localized.es }, 'pt-BR')).toEqual(expected)
|
|
184
|
+
expect(locales.pickLocalizedTemplate(base, localized, 'de')).toEqual(expected)
|
|
185
|
+
expect(locales.pickLocalizedTemplate(base, localized, undefined)).toEqual(expected)
|
|
186
|
+
expect(locales.pickLocalizedTemplate(base, null, 'es')).toEqual(expected)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('never selects a copy with an empty body', () => {
|
|
190
|
+
expect(
|
|
191
|
+
locales.pickLocalizedTemplate(base, { es: { subject: 'Hola', body: '' } }, 'es')
|
|
192
|
+
).toEqual({ ...base, locale: 'en' })
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('getClientLocale', () => {
|
|
197
|
+
const globalRef = global as unknown as Record<string, unknown>
|
|
198
|
+
|
|
199
|
+
const withBrowser = <T>(browser: Record<string, unknown>, run: () => T): T => {
|
|
200
|
+
globalRef.window = browser.window
|
|
201
|
+
globalRef.document = browser.document
|
|
202
|
+
try {
|
|
203
|
+
return run()
|
|
204
|
+
} finally {
|
|
205
|
+
delete globalRef.window
|
|
206
|
+
delete globalRef.document
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
it('is inert outside a browser', () => {
|
|
211
|
+
expect(locales.getClientLocale()).toBeNull()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('reads the router live, so a client-side language switch is not lost', () => {
|
|
215
|
+
// After `router.push(..., { locale: 'es' })` Next.js updates the router
|
|
216
|
+
// and <html lang>, but the page data keeps the locale the document was
|
|
217
|
+
// served in.
|
|
218
|
+
expect(
|
|
219
|
+
withBrowser(
|
|
220
|
+
{
|
|
221
|
+
window: { next: { router: { locale: 'es' } }, __NEXT_DATA__: { locale: 'en' } },
|
|
222
|
+
document: { documentElement: { lang: 'es' } },
|
|
223
|
+
},
|
|
224
|
+
() => locales.getClientLocale()
|
|
225
|
+
)
|
|
226
|
+
).toBe('es')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('falls back to <html lang>, then to the page data', () => {
|
|
230
|
+
expect(
|
|
231
|
+
withBrowser(
|
|
232
|
+
{
|
|
233
|
+
window: { __NEXT_DATA__: { locale: 'en' } },
|
|
234
|
+
document: { documentElement: { lang: 'es' } },
|
|
235
|
+
},
|
|
236
|
+
() => locales.getClientLocale()
|
|
237
|
+
)
|
|
238
|
+
).toBe('es')
|
|
239
|
+
expect(
|
|
240
|
+
withBrowser({ window: { __NEXT_DATA__: { locale: 'pt-br' } }, document: {} }, () =>
|
|
241
|
+
locales.getClientLocale()
|
|
242
|
+
)
|
|
243
|
+
).toBe('pt-BR')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('ignores a candidate that is not a project locale', () => {
|
|
247
|
+
expect(
|
|
248
|
+
withBrowser(
|
|
249
|
+
{
|
|
250
|
+
window: { next: { router: { locale: 'de' } } },
|
|
251
|
+
document: { documentElement: { lang: 'x' } },
|
|
252
|
+
},
|
|
253
|
+
() => locales.getClientLocale()
|
|
254
|
+
)
|
|
255
|
+
).toBeNull()
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('localizeHref', () => {
|
|
260
|
+
it('prefixes a site-relative page path with a non-default locale', () => {
|
|
261
|
+
expect(locales.localizeHref('/', 'es')).toBe('/es')
|
|
262
|
+
expect(locales.localizeHref('/sign-in', 'es')).toBe('/es/sign-in')
|
|
263
|
+
expect(locales.localizeHref('/orders/ORD-42?payment=success', 'pt-br')).toBe(
|
|
264
|
+
'/pt-BR/orders/ORD-42?payment=success'
|
|
265
|
+
)
|
|
266
|
+
expect(locales.localizeHref('/?ref=1#top', 'es')).toBe('/es?ref=1#top')
|
|
267
|
+
expect(locales.localizeHref('/products/', 'es')).toBe('/es/products/')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('keeps the bare path for the default language, an unknown locale and no locale', () => {
|
|
271
|
+
expect(locales.localizeHref('/sign-in', 'en')).toBe('/sign-in')
|
|
272
|
+
expect(locales.localizeHref('/sign-in', 'de')).toBe('/sign-in')
|
|
273
|
+
expect(locales.localizeHref('/sign-in', undefined)).toBe('/sign-in')
|
|
274
|
+
expect(locales.localizeHref('/sign-in', null)).toBe('/sign-in')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('never touches a path that already names a locale', () => {
|
|
278
|
+
expect(locales.localizeHref('/es/cart', 'es')).toBe('/es/cart')
|
|
279
|
+
expect(locales.localizeHref('/es', 'es')).toBe('/es')
|
|
280
|
+
expect(locales.localizeHref('/en/cart', 'es')).toBe('/en/cart')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('never touches an API route, a Next.js internal path or a file', () => {
|
|
284
|
+
expect(locales.localizeHref('/api/invoices/download?id=1', 'es')).toBe(
|
|
285
|
+
'/api/invoices/download?id=1'
|
|
286
|
+
)
|
|
287
|
+
expect(locales.localizeHref('/api', 'es')).toBe('/api')
|
|
288
|
+
expect(locales.localizeHref('/_next/static/x.js', 'es')).toBe('/_next/static/x.js')
|
|
289
|
+
expect(locales.localizeHref('/brochure.pdf', 'es')).toBe('/brochure.pdf')
|
|
290
|
+
expect(locales.localizeHref('/sitemap.xml?v=2', 'es')).toBe('/sitemap.xml?v=2')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('never touches anything that is not a site-relative path', () => {
|
|
294
|
+
expect(locales.localizeHref('https://shop.example/cart', 'es')).toBe(
|
|
295
|
+
'https://shop.example/cart'
|
|
296
|
+
)
|
|
297
|
+
expect(locales.localizeHref('//cdn.example/x', 'es')).toBe('//cdn.example/x')
|
|
298
|
+
expect(locales.localizeHref('#top', 'es')).toBe('#top')
|
|
299
|
+
expect(locales.localizeHref('mailto:a@b.c', 'es')).toBe('mailto:a@b.c')
|
|
300
|
+
expect(locales.localizeHref('products', 'es')).toBe('products')
|
|
301
|
+
expect(locales.localizeHref('', 'es')).toBe('')
|
|
302
|
+
expect(locales.localizeHref(undefined, 'es')).toBeUndefined()
|
|
303
|
+
expect(locales.localizeHref(42, 'es')).toBe(42)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('email-locale module without internationalization', () => {
|
|
309
|
+
const none = loadModule({ locales: [], defaultLocale: '' })
|
|
310
|
+
|
|
311
|
+
it('recognises no locale and keeps the single template', () => {
|
|
312
|
+
expect(none.normalizeEmailLocale('en')).toBeNull()
|
|
313
|
+
expect(none.resolveRequestLocale(request({ [EMAIL_LOCALE_HEADER]: 'en' }))).toBe('')
|
|
314
|
+
expect(
|
|
315
|
+
none.pickLocalizedTemplate({ subject: 'S', body: 'B' }, { en: { body: 'X' } }, 'en')
|
|
316
|
+
).toEqual({ subject: 'S', body: 'B', locale: '' })
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('never prefixes a path', () => {
|
|
320
|
+
expect(none.localizeHref('/sign-in', 'en')).toBe('/sign-in')
|
|
321
|
+
expect(none.localizeHref('/', 'es')).toBe('/')
|
|
322
|
+
})
|
|
323
|
+
})
|