('template')
- const props = useState<{
- label: string
- value: any
- type: string
- description?: string
- }[]>('props')
const { host } = useWindow()
@@ -51,7 +25,7 @@ export function useEmail() {
emails.value = data.value
}
- const renderEmail = async () => {
+ const renderEmail = async (props?: Email['props']) => {
if (!email.value)
return null
@@ -59,7 +33,7 @@ export function useEmail() {
method: 'POST',
baseURL: host.value,
body: {
- props: props.value,
+ props,
},
})
@@ -78,37 +52,6 @@ export function useEmail() {
if (found) {
email.value = found
- try {
- if (found.props) {
- props.value = found.props.map((prop) => {
- const value = removeQuotes(prop.default) || ''
- const destructuredType = prop.type.split('|').map((type) => {
- if (type === 'string')
- return 'string'
-
- if (type === 'number')
- return 'number'
-
- if (type === 'boolean')
- return 'boolean'
-
- if (type === 'object')
- return 'object'
-
- return 'string'
- })
-
- return {
- label: upperFirst(prop.name),
- type: destructuredType[0],
- value,
- }
- })
- }
- }
- catch (error) {
- console.error(error)
- }
await renderEmail()
}
@@ -171,7 +114,6 @@ export function useEmail() {
sending,
refresh,
template,
- props,
getEmail,
sendTestEmail,
renderEmail,
diff --git a/client/emails/github-access-token.vue b/client/emails/github-access-token.vue
index 0ee2520..f8ebc97 100644
--- a/client/emails/github-access-token.vue
+++ b/client/emails/github-access-token.vue
@@ -6,6 +6,31 @@ defineProps({
type: String,
default: 'John Doe',
},
+ string: {
+ type: String,
+ },
+ number: {
+ type: Number,
+ default: 0,
+ },
+ boolean: {
+ type: Boolean,
+ default: true,
+ },
+ array: {
+ type: Array,
+ default: () => [
+ {
+ key: 'value',
+ },
+ ],
+ },
+ object: {
+ type: Object,
+ default: () => ({
+ key: 'value',
+ }),
+ },
})
const main = {
@@ -74,6 +99,14 @@ const footer = {
@{{ username }}, a personal access was created on your account.
+
+ {{ string }}
+ {{ number }}
+ {{ boolean }}
+ {{ array }}
+ {{ object }}
+
+
Hey {{ username }}!
diff --git a/client/package.json b/client/package.json
index f952f05..bef0932 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,7 +22,10 @@
"@types/splitpanes": "^2.2.6",
"@vueuse/core": "^10.7.2",
"@vueuse/nuxt": "^10.7.2",
+ "destr": "^2.0.2",
"html-to-text": "^9.0.5",
+ "json-editor-vue": "^0.12.0",
+ "json5": "^2.2.3",
"nuxt": "^3.9.3",
"pretty": "^2.0.0",
"scule": "^1.2.0",
diff --git a/client/server/api/emails.get.ts b/client/server/api/emails.get.ts
index 060e24a..92f5953 100644
--- a/client/server/api/emails.get.ts
+++ b/client/server/api/emails.get.ts
@@ -1,6 +1,8 @@
import path from 'node:path'
-import { kebabCase, pascalCase } from 'scule'
+import { kebabCase, pascalCase, upperFirst } from 'scule'
import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
+import { destr } from 'destr'
+import JSON5 from 'json5'
import type { Email } from '~/types/email'
import { createError, defineEventHandler, useStorage } from '#imports'
@@ -82,6 +84,58 @@ export default defineEventHandler(async () => {
return 0
})
emailProps = emailProps.map(stripeTypeScriptInternalTypesSchema)
+ const destructuredProps = emailProps.map((prop) => {
+ const destructuredType = prop.type.split('|').map((type) => {
+ type = type.trim()
+ const value = prop.default
+
+ if (type === 'string') {
+ return {
+ type: 'string',
+ value: destr(value) ?? '',
+ }
+ }
+
+ if (type === 'number') {
+ return {
+ type: 'number',
+ value: destr(value) || 0,
+ }
+ }
+
+ if (type === 'boolean') {
+ return {
+ type: 'boolean',
+ value: destr(value) || false,
+ }
+ }
+
+ if (type === 'object' || type.includes('Record') || type.includes('Record<')) {
+ return {
+ type: 'object',
+ value: value ? JSON5.parse(value) : {},
+ }
+ }
+
+ if (type === 'array' || type.includes('[]') || type.includes('Array') || type.includes('Array<')) {
+ return {
+ type: 'array',
+ value: value ? JSON5.parse(value) : [],
+ }
+ }
+
+ return {
+ type: 'string',
+ value: value ?? '',
+ }
+ })
+
+ return {
+ label: prop.name,
+ type: destructuredType[0].type,
+ value: destructuredType[0].value,
+ }
+ })
const content = (await useStorage('assets:emails').getItem(
email,
@@ -99,7 +153,7 @@ export default defineEventHandler(async () => {
size: emailData.size,
created: emailData.birthtime,
modified: emailData.mtime,
- props: emailProps,
+ props: destructuredProps,
}
}),
)
@@ -114,6 +168,8 @@ export default defineEventHandler(async () => {
return emails
}
catch (error) {
+ console.error(error)
+
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
diff --git a/client/server/api/render/[file].post.ts b/client/server/api/render/[file].post.ts
index b4d17fe..7a72350 100644
--- a/client/server/api/render/[file].post.ts
+++ b/client/server/api/render/[file].post.ts
@@ -1,3 +1,4 @@
+import { destr } from 'destr'
import { useCompiler } from '#vue-email'
import { createError, defineEventHandler } from '#imports'
@@ -9,7 +10,21 @@ export default defineEventHandler(async (event: any) => {
let props: any = null
if (body && body.props) {
props = body.props.reduce((acc: Record, prop: any) => {
- acc[prop.label.toLowerCase()] = prop.value
+ if (prop.type === 'string')
+ acc[prop.label] = destr(prop.value) || ''
+
+ if (prop.type === 'number')
+ acc[prop.label] = destr(prop.value) || 0
+
+ if (prop.type === 'boolean')
+ acc[prop.label] = destr(prop.value) || false
+
+ if (prop.type === 'object')
+ acc[prop.label] = destr(prop.value) || {}
+
+ if (prop.type === 'array')
+ acc[prop.label] = destr(prop.value) || []
+
return acc
}, {})
}
diff --git a/client/types/email.ts b/client/types/email.ts
index 19a4cd5..da4dbac 100644
--- a/client/types/email.ts
+++ b/client/types/email.ts
@@ -8,7 +8,12 @@ export interface Email {
size: number
created: Date
modified: Date
- props: PropertyMeta[]
+ props: {
+ label: string
+ value: any
+ type: string
+ description?: string
+ }[]
}
export interface Directory {
diff --git a/package.json b/package.json
index de6982e..017dd2b 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,8 @@
"@nuxt/kit": "^3.10.0",
"@vue-email/compiler": "npm:@vue-email/compiler-edge@0.8.9-28446863.0aab8eb",
"defu": "^6.1.4",
+ "destr": "^2.0.2",
+ "json5": "^2.2.3",
"sirv": "^2.0.4",
"vue-component-meta": "^1.8.27",
"vue-email": "npm:vue-email-edge@0.8.7-28446842.1f6e4a0"
@@ -64,6 +66,7 @@
"@nuxt/test-utils": "^3.11.0",
"@types/node": "^20.11.10",
"bumpp": "^9.3.0",
+ "destr": "^2.0.2",
"eslint": "^8.56.0",
"jiti": "^1.21.0",
"nuxt": "^3.10.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 253b200..6addff8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,12 @@ importers:
defu:
specifier: ^6.1.4
version: 6.1.4
+ destr:
+ specifier: ^2.0.2
+ version: 2.0.2
+ json5:
+ specifier: ^2.2.3
+ version: 2.2.3
sirv:
specifier: ^2.0.4
version: 2.0.4
@@ -105,9 +111,18 @@ importers:
'@vueuse/nuxt':
specifier: ^10.7.2
version: 10.7.2(nuxt@3.9.3)(rollup@3.29.4)(vue@3.4.15)
+ destr:
+ specifier: ^2.0.2
+ version: 2.0.2
html-to-text:
specifier: ^9.0.5
version: 9.0.5
+ json-editor-vue:
+ specifier: ^0.12.0
+ version: 0.12.0(@lezer/common@1.2.1)(vue@3.4.15)
+ json5:
+ specifier: ^2.2.3
+ version: 2.2.3
nuxt:
specifier: ^3.9.3
version: 3.9.3(@types/node@20.11.10)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.12)
@@ -590,6 +605,75 @@ packages:
mime: 3.0.0
dev: true
+ /@codemirror/autocomplete@6.12.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.2.1):
+ resolution: {integrity: sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==}
+ peerDependencies:
+ '@codemirror/language': ^6.0.0
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+ '@lezer/common': ^1.0.0
+ dependencies:
+ '@codemirror/language': 6.10.1
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ '@lezer/common': 1.2.1
+ dev: true
+
+ /@codemirror/commands@6.3.3:
+ resolution: {integrity: sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==}
+ dependencies:
+ '@codemirror/language': 6.10.1
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ '@lezer/common': 1.2.1
+ dev: true
+
+ /@codemirror/lang-json@6.0.1:
+ resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==}
+ dependencies:
+ '@codemirror/language': 6.10.1
+ '@lezer/json': 1.0.2
+ dev: true
+
+ /@codemirror/language@6.10.1:
+ resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==}
+ dependencies:
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ '@lezer/common': 1.2.1
+ '@lezer/highlight': 1.2.0
+ '@lezer/lr': 1.4.0
+ style-mod: 4.1.0
+ dev: true
+
+ /@codemirror/lint@6.5.0:
+ resolution: {integrity: sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==}
+ dependencies:
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ crelt: 1.0.6
+ dev: true
+
+ /@codemirror/search@6.5.5:
+ resolution: {integrity: sha512-PIEN3Ke1buPod2EHbJsoQwlbpkz30qGZKcnmH1eihq9+bPQx8gelauUwLYaY4vBOuBAuEhmpDLii4rj/uO0yMA==}
+ dependencies:
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ crelt: 1.0.6
+ dev: true
+
+ /@codemirror/state@6.4.0:
+ resolution: {integrity: sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==}
+ dev: true
+
+ /@codemirror/view@6.23.1:
+ resolution: {integrity: sha512-J2Xnn5lFYT1ZN/5ewEoMBCmLlL71lZ3mBdb7cUEuHhX2ESoSrNEucpsDXpX22EuTGm9LOgC9v4Z0wx+Ez8QmGA==}
+ dependencies:
+ '@codemirror/state': 6.4.0
+ style-mod: 4.1.0
+ w3c-keyname: 2.2.8
+ dev: true
+
/@csstools/cascade-layer-name-parser@1.0.7(@csstools/css-parser-algorithms@2.5.0)(@csstools/css-tokenizer@2.2.3):
resolution: {integrity: sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==}
engines: {node: ^14 || ^16 || >=18}
@@ -1130,6 +1214,28 @@ packages:
engines: {node: '>=14'}
dev: true
+ /@fortawesome/fontawesome-common-types@6.5.1:
+ resolution: {integrity: sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: true
+
+ /@fortawesome/free-regular-svg-icons@6.5.1:
+ resolution: {integrity: sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.5.1
+ dev: true
+
+ /@fortawesome/free-solid-svg-icons@6.5.1:
+ resolution: {integrity: sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.5.1
+ dev: true
+
/@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.1):
resolution: {integrity: sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==}
engines: {node: '>=10'}
@@ -1340,6 +1446,30 @@ packages:
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
dev: true
+ /@lezer/common@1.2.1:
+ resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==}
+ dev: true
+
+ /@lezer/highlight@1.2.0:
+ resolution: {integrity: sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==}
+ dependencies:
+ '@lezer/common': 1.2.1
+ dev: true
+
+ /@lezer/json@1.0.2:
+ resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==}
+ dependencies:
+ '@lezer/common': 1.2.1
+ '@lezer/highlight': 1.2.0
+ '@lezer/lr': 1.4.0
+ dev: true
+
+ /@lezer/lr@1.4.0:
+ resolution: {integrity: sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==}
+ dependencies:
+ '@lezer/common': 1.2.1
+ dev: true
+
/@mapbox/node-pre-gyp@1.0.11:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
@@ -2246,6 +2376,18 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: true
+ /@replit/codemirror-indentation-markers@6.5.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1):
+ resolution: {integrity: sha512-5RgeuQ6erfROi1EVI2X7G4UR+KByjb07jhYMynvpvlrV22JlnARifmKMGEUKy0pKcxBNfwbFqoUlTYHPgyZNlg==}
+ peerDependencies:
+ '@codemirror/language': ^6.0.0
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+ dependencies:
+ '@codemirror/language': 6.10.1
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ dev: true
+
/@rollup/plugin-alias@5.1.0(rollup@3.29.4):
resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
engines: {node: '>=14.0.0'}
@@ -2653,6 +2795,10 @@ packages:
resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==}
engines: {node: '>=18'}
+ /@sphinxxxx/color-conversion@2.2.2:
+ resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==}
+ dev: true
+
/@stylistic/eslint-plugin-js@1.5.4(eslint@8.56.0):
resolution: {integrity: sha512-3ctWb3NvJNV1MsrZN91cYp2EGInLPSoZKphXIbIRx/zjZxKwLDr9z4LMOWtqjq14li/OgqUUcMq5pj8fgbLoTw==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -3620,6 +3766,15 @@ packages:
uri-js: 4.4.1
dev: true
+ /ajv@8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: true
+
/ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -3720,6 +3875,12 @@ packages:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
+ /aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+ dependencies:
+ dequal: 2.0.3
+ dev: true
+
/array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@@ -3800,6 +3961,12 @@ packages:
postcss-value-parser: 4.2.0
dev: true
+ /axobject-query@4.0.0:
+ resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
+ dependencies:
+ dequal: 2.0.3
+ dev: true
+
/b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
dev: true
@@ -4132,6 +4299,28 @@ packages:
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
dev: true
+ /code-red@1.0.4:
+ resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@types/estree': 1.0.5
+ acorn: 8.11.3
+ estree-walker: 3.0.3
+ periscopic: 3.1.0
+ dev: true
+
+ /codemirror-wrapped-line-indent@1.0.3(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1):
+ resolution: {integrity: sha512-1MWPgyxcDcpGpqmBlraoQyIgbZMAmppj/e/9+gpqug68Gli+BtSLE3GLxGoRoRK5n5sFp8RH0xAQL5i7jOo2qQ==}
+ peerDependencies:
+ '@codemirror/language': ^6.9.0
+ '@codemirror/state': ^6.2.1
+ '@codemirror/view': ^6.17.1
+ dependencies:
+ '@codemirror/language': 6.10.1
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ dev: true
+
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@@ -4302,6 +4491,10 @@ packages:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
+ /crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+ dev: true
+
/cross-fetch@3.1.8:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
dependencies:
@@ -4561,6 +4754,11 @@ packages:
engines: {node: '>= 0.8'}
dev: true
+ /dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+ dev: true
+
/destr@2.0.2:
resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==}
@@ -6172,6 +6370,14 @@ packages:
resolution: {integrity: sha512-ZBGjl0ZMEMeOC3Ns0wUF/5UdUmr3qQhBSCniT0LxOgGGIRHiNFOkMtIHB7EOznRU47V2AxPgiVP+s+0/UCU0Hg==}
dev: true
+ /immutable-json-patch@6.0.1:
+ resolution: {integrity: sha512-BHL/cXMjwFZlTOffiWNdY8ZTvNyYLrutCnWxrcKPHr5FqpAb6vsO6WWSPnVSys3+DruFN6lhHJJPHi8uELQL5g==}
+ dev: true
+
+ /immutable@4.3.5:
+ resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
+ dev: true
+
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -6389,6 +6595,12 @@ packages:
'@types/estree': 1.0.5
dev: true
+ /is-reference@3.0.2:
+ resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
+ dependencies:
+ '@types/estree': 1.0.5
+ dev: true
+
/is-ssh@1.4.0:
resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==}
dependencies:
@@ -6468,6 +6680,11 @@ packages:
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
hasBin: true
+ /jmespath@0.16.0:
+ resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==}
+ engines: {node: '>= 0.6.0'}
+ dev: true
+
/js-beautify@1.14.11:
resolution: {integrity: sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==}
engines: {node: '>=14'}
@@ -6554,6 +6771,23 @@ packages:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
dev: true
+ /json-editor-vue@0.12.0(@lezer/common@1.2.1)(vue@3.4.15):
+ resolution: {integrity: sha512-VOsWy2EiAUY+iD5zdKssG/lBVVTNbDEgkUWnq4FJbOVNuVXwdRdX76zWvtbhh6MSjlFVCxZc/7V0VEayZbcGeQ==}
+ requiresBuild: true
+ peerDependencies:
+ '@vue/composition-api': '>=1'
+ vue: 2||3
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ dependencies:
+ vanilla-jsoneditor: 0.21.4(@lezer/common@1.2.1)
+ vue: 3.4.15(typescript@5.3.3)
+ vue-demi: 0.14.6(vue@3.4.15)
+ transitivePeerDependencies:
+ - '@lezer/common'
+ dev: true
+
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: true
@@ -6567,6 +6801,14 @@ packages:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
+ /json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: true
+
+ /json-source-map@0.6.1:
+ resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==}
+ dev: true
+
/json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
@@ -6602,6 +6844,11 @@ packages:
engines: {'0': node >= 0.2.0}
dev: true
+ /jsonrepair@3.5.1:
+ resolution: {integrity: sha512-F0VxiEj1j7m1OAVUVy6fFYk5s8tthF61J7tjYtEACw1DeNQqKmZF6dPddduxc7Tc5IrLqKTdLAwUNTmrqqg+hw==}
+ hasBin: true
+ dev: true
+
/keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'}
@@ -6774,6 +7021,10 @@ packages:
mlly: 1.5.0
pkg-types: 1.0.3
+ /locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+ dev: true
+
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -6788,6 +7039,10 @@ packages:
p-locate: 5.0.0
dev: true
+ /lodash-es@4.17.21:
+ resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+ dev: true
+
/lodash._reinterpolate@3.0.0:
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
dev: true
@@ -6953,6 +7208,10 @@ packages:
engines: {node: '>= 0.6'}
dev: true
+ /memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+ dev: true
+
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -8083,6 +8342,14 @@ packages:
/perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+ /periscopic@3.1.0:
+ resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
+ dependencies:
+ '@types/estree': 1.0.5
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+ dev: true
+
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -8750,6 +9017,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
@@ -8910,6 +9182,16 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
requiresBuild: true
+ /sass@1.70.0:
+ resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ chokidar: 3.5.3
+ immutable: 4.3.5
+ source-map-js: 1.0.2
+ dev: true
+
/saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@@ -9291,6 +9573,10 @@ packages:
js-tokens: 8.0.2
dev: true
+ /style-mod@4.1.0:
+ resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==}
+ dev: true
+
/stylehacks@6.0.2(postcss@8.4.33):
resolution: {integrity: sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==}
engines: {node: ^14 || ^16 || >=18.0}
@@ -9337,6 +9623,26 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ /svelte@4.2.9:
+ resolution: {integrity: sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==}
+ engines: {node: '>=16'}
+ dependencies:
+ '@ampproject/remapping': 2.2.1
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/trace-mapping': 0.3.22
+ '@types/estree': 1.0.5
+ acorn: 8.11.3
+ aria-query: 5.3.0
+ axobject-query: 4.0.0
+ code-red: 1.0.4
+ css-tree: 2.3.1
+ estree-walker: 3.0.3
+ is-reference: 3.0.2
+ locate-character: 3.0.0
+ magic-string: 0.30.5
+ periscopic: 3.1.0
+ dev: true
+
/svg-tags@1.0.0:
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
dev: true
@@ -9979,6 +10285,44 @@ packages:
builtins: 5.0.1
dev: true
+ /vanilla-jsoneditor@0.21.4(@lezer/common@1.2.1):
+ resolution: {integrity: sha512-uhvF7IZbd/QM6yPznZ4IxF/FbOj3T85euc0jerjD65uExVV9qDihEpNg7hjaazj1njtRkao83aBRJZdaGCA/Sw==}
+ dependencies:
+ '@codemirror/autocomplete': 6.12.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.2.1)
+ '@codemirror/commands': 6.3.3
+ '@codemirror/lang-json': 6.0.1
+ '@codemirror/language': 6.10.1
+ '@codemirror/lint': 6.5.0
+ '@codemirror/search': 6.5.5
+ '@codemirror/state': 6.4.0
+ '@codemirror/view': 6.23.1
+ '@fortawesome/free-regular-svg-icons': 6.5.1
+ '@fortawesome/free-solid-svg-icons': 6.5.1
+ '@lezer/highlight': 1.2.0
+ '@replit/codemirror-indentation-markers': 6.5.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)
+ ajv: 8.12.0
+ codemirror-wrapped-line-indent: 1.0.3(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)
+ diff-sequences: 29.6.3
+ immutable-json-patch: 6.0.1
+ jmespath: 0.16.0
+ json-source-map: 0.6.1
+ jsonrepair: 3.5.1
+ lodash-es: 4.17.21
+ memoize-one: 6.0.0
+ natural-compare-lite: 1.4.0
+ sass: 1.70.0
+ svelte: 4.2.9
+ vanilla-picker: 2.12.2
+ transitivePeerDependencies:
+ - '@lezer/common'
+ dev: true
+
+ /vanilla-picker@2.12.2:
+ resolution: {integrity: sha512-dk0gNeNL9fQFGd1VEhNDQfFlbCqAiksRh1H2tVPlavkH88n/a/y30rXi9PPKrYPTK5kEfPO4xcldt4ts/1wIAg==}
+ dependencies:
+ '@sphinxxxx/color-conversion': 2.2.2
+ dev: true
+
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -10520,6 +10864,10 @@ packages:
'@vue/shared': 3.4.15
typescript: 5.3.3
+ /w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+ dev: true
+
/w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
diff --git a/src/runtime/server/api/emails.get.ts b/src/runtime/server/api/emails.get.ts
index fd39e66..d6dfd1e 100644
--- a/src/runtime/server/api/emails.get.ts
+++ b/src/runtime/server/api/emails.get.ts
@@ -1,25 +1,159 @@
-import { kebabCase, pascalCase } from 'scule'
+import path from 'node:path'
+import { kebabCase, pascalCase, upperFirst } from 'scule'
+import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
+import { destr } from 'destr'
+import JSON5 from 'json5'
import type { Email } from '../../types/email'
import { createError, defineEventHandler, useStorage } from '#imports'
+const rootDir = process.cwd()
+const checker = createComponentMetaCheckerByJsonConfig(
+ rootDir,
+ {
+ extends: `${rootDir}/tsconfig.json`,
+ skipLibCheck: true,
+ include: ['emails/**/*'],
+ exclude: [],
+ },
+ {
+ forceUseTs: true,
+ printer: { newLine: 1 },
+ },
+)
+
+function stripeTypeScriptInternalTypesSchema(type: any): any {
+ if (!type)
+ return type
+
+ if (type.declarations && type.declarations.find((d: any) => d.file.includes('node_modules/typescript')))
+ return false
+
+ if (Array.isArray(type))
+ return type.map((sch: any) => stripeTypeScriptInternalTypesSchema(sch)).filter(r => r !== false)
+
+ if (Array.isArray(type.schema)) {
+ return {
+ ...type,
+ schema: type.schema.map((sch: any) => stripeTypeScriptInternalTypesSchema(sch)).filter((r: any) => r !== false),
+ }
+ }
+ if (!type.schema || typeof type.schema !== 'object')
+ return type
+
+ const schema: any = {}
+ Object.keys(type.schema).forEach((sch) => {
+ const res = stripeTypeScriptInternalTypesSchema(type.schema[sch])
+ if (res !== false)
+ schema[sch] = res
+ })
+ return {
+ ...type,
+ schema,
+ }
+}
+
export default defineEventHandler(async () => {
try {
const nitroEmails = await useStorage('assets:emails').getKeys()
const emails: Email[] = await Promise.all(
- nitroEmails.map(async (email: string) => {
- const data = JSON.stringify(await useStorage('assets:emails').getMeta(email))
+ nitroEmails.map(async (email) => {
+ const data = JSON.stringify(
+ await useStorage('assets:emails').getMeta(email),
+ )
const emailData = JSON.parse(data)
- const content = (await useStorage('assets:emails').getItem(email)) as string
+ const emailPath = path.join(
+ rootDir,
+ 'emails',
+ email.replaceAll(':', '/'),
+ )
+ const { props } = checker.getComponentMeta(emailPath)
+ let emailProps = (props).filter(prop => !prop.global).sort((a, b) => {
+ if (!a.required && b.required)
+ return 1
+
+ if (a.required && !b.required)
+ return -1
+
+ if (a.type === 'boolean' && b.type !== 'boolean')
+ return 1
+
+ if (a.type !== 'boolean' && b.type === 'boolean')
+ return -1
+
+ return 0
+ })
+ emailProps = emailProps.map(stripeTypeScriptInternalTypesSchema)
+ const destructuredProps = emailProps.map((prop) => {
+ const destructuredType = prop.type.split('|').map((type) => {
+ type = type.trim()
+ const value = prop.default
+
+ if (type === 'string') {
+ return {
+ type: 'string',
+ value: destr(value) ?? '',
+ }
+ }
+
+ if (type === 'number') {
+ return {
+ type: 'number',
+ value: destr(value) || 0,
+ }
+ }
+
+ if (type === 'boolean') {
+ return {
+ type: 'boolean',
+ value: destr(value) || false,
+ }
+ }
+
+ if (type === 'object' || type.includes('Record') || type.includes('Record<')) {
+ return {
+ type: 'object',
+ value: value ? JSON5.parse(value) : {},
+ }
+ }
+
+ if (type === 'array' || type.includes('[]') || type.includes('Array') || type.includes('Array<')) {
+ return {
+ type: 'array',
+ value: value ? JSON5.parse(value) : [],
+ }
+ }
+
+ return {
+ type: 'string',
+ value: value ?? '',
+ }
+ })
+
+ return {
+ label: prop.name,
+ type: destructuredType[0].type,
+ value: destructuredType[0].value,
+ }
+ })
+
+ const content = (await useStorage('assets:emails').getItem(
+ email,
+ )) as string
return {
- label: pascalCase(kebabCase(email.replace('.vue', '').replace(':', '_')).split('-').join(' ')),
+ label: pascalCase(
+ kebabCase(email.replace('.vue', '').replace(':', '_'))
+ .split('-')
+ .join(' '),
+ ),
filename: email,
content,
icon: 'i-heroicons-envelope',
size: emailData.size,
created: emailData.birthtime,
modified: emailData.mtime,
+ props: destructuredProps,
}
}),
)
@@ -34,6 +168,8 @@ export default defineEventHandler(async () => {
return emails
}
catch (error) {
+ console.error(error)
+
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
diff --git a/src/runtime/server/api/render/[file].get.ts b/src/runtime/server/api/render/[file].get.ts
index 796661b..7a72350 100644
--- a/src/runtime/server/api/render/[file].get.ts
+++ b/src/runtime/server/api/render/[file].get.ts
@@ -1,12 +1,38 @@
+import { destr } from 'destr'
import { useCompiler } from '#vue-email'
import { createError, defineEventHandler } from '#imports'
export default defineEventHandler(async (event: any) => {
try {
const file = event.context.params && event.context.params.file ? event.context.params.file : null
+ const body = await readBody(event)
+
+ let props: any = null
+ if (body && body.props) {
+ props = body.props.reduce((acc: Record, prop: any) => {
+ if (prop.type === 'string')
+ acc[prop.label] = destr(prop.value) || ''
+
+ if (prop.type === 'number')
+ acc[prop.label] = destr(prop.value) || 0
+
+ if (prop.type === 'boolean')
+ acc[prop.label] = destr(prop.value) || false
+
+ if (prop.type === 'object')
+ acc[prop.label] = destr(prop.value) || {}
+
+ if (prop.type === 'array')
+ acc[prop.label] = destr(prop.value) || []
+
+ return acc
+ }, {})
+ }
// TODO: pass props to template
- const template = await useCompiler(file)
+ const template = await useCompiler(file, {
+ props,
+ })
if (!template) {
throw createError({
diff --git a/src/runtime/types/email.ts b/src/runtime/types/email.ts
index d5da2fb..02a61c1 100644
--- a/src/runtime/types/email.ts
+++ b/src/runtime/types/email.ts
@@ -6,6 +6,12 @@ export interface Email {
size: number
created: Date
modified: Date
+ props: {
+ label: string
+ value: any
+ type: string
+ description?: string
+ }[]
}
export interface Directory {