├── .npmrc ├── playground ├── nuxt-layer │ ├── .npmrc │ ├── .nuxtrc │ ├── app.vue │ ├── tsconfig.json │ ├── .playground │ │ ├── nuxt.config.ts │ │ └── app.config.ts │ ├── .eslintrc.cjs │ ├── nuxt.config.ts │ ├── components │ │ └── HelloWorld.vue │ ├── .editorconfig │ ├── .gitignore │ ├── app.config.ts │ ├── package.json │ ├── README.md │ └── emails │ │ └── TestEmail.vue ├── tsconfig.json ├── server │ ├── tsconfig.json │ └── api │ │ └── test.get.ts ├── app.vue ├── tailwind.config.js ├── package.json └── nuxt.config.ts ├── client ├── tsconfig.json ├── server │ ├── tsconfig.json │ └── api │ │ ├── render │ │ └── [file].post.ts │ │ └── emails.get.ts ├── app.config.ts ├── pages │ ├── index.vue │ └── email │ │ └── [file].vue ├── util │ └── copy-text-to-clipboard.ts ├── plugins │ └── components.client.ts ├── tailwind.config.ts ├── types │ ├── settings.ts │ └── email.ts ├── composables │ ├── useWindow.ts │ ├── shiki.ts │ ├── useTool.ts │ └── useEmail.ts ├── app.vue ├── layouts │ └── default.vue ├── components │ ├── TopNav.vue │ ├── HeaderButtons.vue │ ├── SearchButton.vue │ ├── Navigation.vue │ ├── SendEmail.vue │ ├── Settings.vue │ ├── CommandPalette.vue │ ├── EmptyState.vue │ ├── EmailPreview.vue │ └── CodeContainer.vue ├── emails │ ├── code-components.vue │ ├── koala-welcome.vue │ ├── Components │ │ └── markdown-email.vue │ ├── github-access-token.vue │ ├── vercel-invite-user.vue │ ├── stripe-welcome.vue │ ├── twitch-reset-password.vue │ └── yelp-recent-login.vue ├── package.json ├── nuxt.config.ts └── public │ └── icon.svg ├── src ├── runtime │ ├── server │ │ ├── nitro │ │ │ ├── index.ts │ │ │ └── useCompiler.ts │ │ └── api │ │ │ ├── render │ │ │ └── [file].post.ts │ │ │ └── emails.get.ts │ └── types │ │ ├── settings.ts │ │ └── email.ts └── module.ts ├── pnpm-workspace.yaml ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── ci.yml ├── eslint.config.js ├── tsconfig.json ├── README.md ├── .editorconfig ├── scripts ├── release.sh ├── release-edge.sh └── bump-edge.ts ├── .gitignore ├── LICENSE ├── .vscode └── settings.json └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /playground/nuxt-layer/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /playground/nuxt-layer/.nuxtrc: -------------------------------------------------------------------------------- 1 | typescript.includeWorkspace = true 2 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/nuxt-layer/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/runtime/server/nitro/index.ts: -------------------------------------------------------------------------------- 1 | export { useCompiler } from './useCompiler' 2 | -------------------------------------------------------------------------------- /client/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - client 3 | - playground 4 | - layer-test 5 | -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vue-email 4 | -------------------------------------------------------------------------------- /playground/nuxt-layer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/nuxt-layer/.playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: ['..'], 3 | }) 4 | -------------------------------------------------------------------------------- /playground/nuxt-layer/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@nuxt/eslint-config'], 4 | } 5 | -------------------------------------------------------------------------------- /client/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: 'emerald', 4 | gray: 'neutral', 5 | }, 6 | }) 7 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /playground/nuxt-layer/.playground/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | myLayer: { 3 | name: 'My amazing Nuxt layer (overwritten)', 4 | }, 5 | }) 6 | -------------------------------------------------------------------------------- /playground/nuxt-layer/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | }) 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | rules: { 5 | 'node/prefer-global/process': 'off', 6 | 'no-eval': 'off', 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "strictNullChecks": false, 5 | "noImplicitAny": false 6 | }, 7 | "exclude": ["client", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | we just merged a new project rewrite, please do check the docs and the updated logic https://vuemail.net/ 2 | 3 | Currently there's no need for a nuxt module, as we are not adding anything specific to nuxt. 4 | -------------------------------------------------------------------------------- /client/util/copy-text-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | export async function copyTextToClipboard(text: string) { 2 | try { 3 | await navigator.clipboard.writeText(text) 4 | } 5 | catch { 6 | throw new Error('Not able to copy') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /playground/nuxt-layer/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /playground/nuxt-layer/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /client/plugins/components.client.ts: -------------------------------------------------------------------------------- 1 | import { Pane, Splitpanes } from 'splitpanes' 2 | 3 | import 'splitpanes/dist/splitpanes.css' 4 | 5 | export default defineNuxtPlugin((plugin) => { 6 | plugin.vueApp.component('Splitpanes', Splitpanes) 7 | plugin.vueApp.component('Pane', Pane) 8 | }) 9 | -------------------------------------------------------------------------------- /playground/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: 'cyan', 8 | secondary: '#ca8a04', 9 | }, 10 | }, 11 | }, 12 | plugins: [], 13 | } 14 | -------------------------------------------------------------------------------- /playground/nuxt-layer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .nuxt 4 | nuxt.d.ts 5 | .output 6 | .data 7 | .env 8 | package-lock.json 9 | framework 10 | dist 11 | .DS_Store 12 | 13 | # Yarn 14 | .yarn/cache 15 | .yarn/*state* 16 | 17 | # Local History 18 | .history 19 | 20 | # VSCode 21 | .vscode/ 22 | -------------------------------------------------------------------------------- /playground/nuxt-layer/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | myLayer: { 3 | name: 'Hello from Nuxt layer', 4 | }, 5 | }) 6 | 7 | declare module '@nuxt/schema' { 8 | interface AppConfigInput { 9 | myLayer?: { 10 | /** Project name */ 11 | name?: string 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import defaultTheme from 'tailwindcss/defaultTheme' 3 | 4 | export default >{ 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | sans: ['Inter', 'Inter fallback', ...defaultTheme.fontFamily.sans], 9 | }, 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /client/types/settings.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | horizontalSplit: boolean 3 | email: string 4 | } 5 | 6 | export interface PreviewModes { 7 | id: 'both' | 'code' | 'iframe' 8 | label: string 9 | icon: string 10 | } 11 | 12 | export interface editorCodes { 13 | id: 'all' | 'html' | 'txt' | 'vue' 14 | label: string 15 | icon: string 16 | } 17 | -------------------------------------------------------------------------------- /src/runtime/types/settings.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | horizontalSplit: boolean 3 | email: string 4 | } 5 | 6 | export interface PreviewModes { 7 | id: 'both' | 'code' | 'iframe' 8 | label: string 9 | icon: string 10 | } 11 | 12 | export interface editorCodes { 13 | id: 'all' | 'html' | 'txt' | 'vue' 14 | label: string 15 | icon: string 16 | } 17 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-module-playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxt preview", 10 | "analyze": "nuxt analyze" 11 | }, 12 | "devDependencies": { 13 | "@nuxtjs/tailwindcss": "^6.11.2", 14 | "nuxt": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Restore all git changes 4 | git restore -s@ -SW -- . 5 | 6 | # Update token 7 | if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then 8 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 9 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 10 | echo "always-auth=true" >> ~/.npmrc 11 | npm whoami 12 | fi 13 | 14 | # Release package 15 | echo "Publishing vue-email nuxt" 16 | npm publish -q --access public 17 | -------------------------------------------------------------------------------- /playground/server/api/test.get.ts: -------------------------------------------------------------------------------- 1 | import { useCompiler } from '#vue-email' 2 | 3 | export default defineEventHandler(async () => { 4 | try { 5 | const template = await useCompiler('TestEmail.vue', { 6 | props: { 7 | username: 'Flowko', 8 | }, 9 | }).catch((error) => { 10 | console.error(error) 11 | }) 12 | 13 | if (!template) 14 | return null 15 | 16 | return template.html 17 | } 18 | catch (error) { 19 | console.error(error) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /scripts/release-edge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Restore all git changes 4 | git restore -s@ -SW -- . 5 | 6 | # Bump versions to edge 7 | pnpm jiti ./scripts/bump-edge 8 | 9 | # Update token 10 | if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then 11 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 12 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 13 | echo "always-auth=true" >> ~/.npmrc 14 | npm whoami 15 | fi 16 | 17 | # Release package 18 | echo "Publishing vue-email nuxt" 19 | npm publish -q --access public 20 | -------------------------------------------------------------------------------- /client/composables/useWindow.ts: -------------------------------------------------------------------------------- 1 | import { withBase } from 'ufo' 2 | 3 | export default function (): { 4 | hostname: Ref 5 | host: Ref 6 | } { 7 | const hostname = useState('hostname', () => '') 8 | const host = useState('host', () => '') 9 | 10 | onMounted(() => { 11 | if (import.meta.client) { 12 | hostname.value = window.location.host 13 | host.value = withBase('/', `${window.location.protocol}//${hostname.value}`) 14 | } 15 | }) 16 | 17 | return { 18 | hostname, 19 | host, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playground/nuxt-layer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-nuxt-layer", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "main": "./nuxt.config.ts", 6 | "scripts": { 7 | "dev": "nuxi dev .playground", 8 | "build": "nuxt build .playground", 9 | "generate": "nuxt generate .playground", 10 | "preview": "nuxt preview .playground", 11 | "lint": "eslint .", 12 | "postinstall": "nuxt prepare .playground" 13 | }, 14 | "devDependencies": { 15 | "@nuxt/eslint-config": "^0.1.1", 16 | "eslint": "^8.28.0", 17 | "nuxt": "^3.6.2", 18 | "typescript": "^4.9.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/composables/shiki.ts: -------------------------------------------------------------------------------- 1 | import type { Highlighter } from 'shiki' 2 | import { getHighlighter } from 'shiki' 3 | import { ref } from 'vue' 4 | 5 | export const shiki = ref() 6 | 7 | // TODO: Only loading when needed 8 | getHighlighter({ 9 | themes: ['vitesse-dark'], 10 | langs: ['html', 'txt', 'md', 'vue', 'vue-html'], 11 | }).then((i) => { 12 | shiki.value = i 13 | }) 14 | 15 | export function highlight(code: string, lang: string) { 16 | if (!shiki.value) 17 | return code 18 | return shiki.value.codeToHtml(code, { 19 | lang, 20 | theme: 'vitesse-dark', 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /client/app.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /client/types/email.ts: -------------------------------------------------------------------------------- 1 | export interface Email { 2 | label: string 3 | filename: string 4 | content: string 5 | icon: string 6 | size: number 7 | created: Date 8 | modified: Date 9 | props: { 10 | label: string 11 | value: any 12 | type: string 13 | description?: string 14 | }[] 15 | } 16 | 17 | export interface Directory { 18 | label: string 19 | email: Email 20 | children: Email[] 21 | } 22 | 23 | export type ActiveView = 'desktop' | 'mobile' | 'source' 24 | export type ActiveLang = 'html' | 'txt' | 'vue' 25 | 26 | export interface Template { 27 | vue: string 28 | html: string 29 | txt: string 30 | } 31 | 32 | export interface MarkupProps { 33 | language: ActiveLang 34 | content: string 35 | } 36 | -------------------------------------------------------------------------------- /src/runtime/types/email.ts: -------------------------------------------------------------------------------- 1 | export interface Email { 2 | label: string 3 | filename: string 4 | content: string 5 | icon: string 6 | size: number 7 | created: Date 8 | modified: Date 9 | props: { 10 | label: string 11 | value: any 12 | type: string 13 | description?: string 14 | }[] 15 | } 16 | 17 | export interface Directory { 18 | label: string 19 | email: Email 20 | children: Email[] 21 | } 22 | 23 | export type ActiveView = 'desktop' | 'mobile' | 'source' 24 | export type ActiveLang = 'html' | 'txt' | 'vue' 25 | 26 | export interface Template { 27 | vue: string 28 | html: string 29 | txt: string 30 | } 31 | 32 | export interface MarkupProps { 33 | language: ActiveLang 34 | content: string 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .data 23 | .vercel_build_output 24 | .build-* 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | -------------------------------------------------------------------------------- /client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /client/components/TopNav.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /scripts/bump-edge.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { execSync } from 'node:child_process' 4 | 5 | async function loadPackage(dir: string) { 6 | const pkgPath = resolve(dir, 'package.json') 7 | 8 | const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}')) 9 | 10 | const save = () => fsp.writeFile(pkgPath, `${JSON.stringify(data, null, 2)}\n`) 11 | 12 | return { 13 | dir, 14 | data, 15 | save, 16 | } 17 | } 18 | 19 | async function main() { 20 | const pkg = await loadPackage(process.cwd()) 21 | 22 | const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim() 23 | 24 | const date = Math.round(Date.now() / (1000 * 60)) 25 | 26 | pkg.data.name = `${pkg.data.name}-edge` 27 | 28 | pkg.data.version = `${pkg.data.version}-${date}.${commit}` 29 | 30 | pkg.save() 31 | } 32 | 33 | main().catch((err) => { 34 | console.error(err) 35 | process.exit(1) 36 | }) 37 | -------------------------------------------------------------------------------- /client/emails/code-components.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: write 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | 23 | - name: Set node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: lts/* 27 | 28 | - name: Setup 29 | run: npm i -g @antfu/ni 30 | 31 | - name: Install 32 | run: nci 33 | 34 | - name: Lint 35 | run: nr lint 36 | 37 | - name: Build 38 | run: nr build 39 | 40 | - name: Release 41 | run: | 42 | chmod +x ./scripts/release.sh 43 | ./scripts/release.sh 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} 46 | 47 | - run: npx changelogithub 48 | env: 49 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 50 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: ['../src/module', '@nuxtjs/tailwindcss'], 3 | extends: ['./nuxt-layer'], 4 | vueEmail: { 5 | baseUrl: 'https://vue-email-demo.vercel.app/', 6 | i18n: { 7 | defaultLocale: 'fr', 8 | translations: { 9 | en: { 10 | title: 'Welcome to Vue Email', 11 | subtitle: 12 | 'A Vue.js component for generating beautiful emails using MJML', 13 | button: 'Get Started', 14 | }, 15 | fr: { 16 | title: 'Bienvenue sur Vue Email', 17 | subtitle: 18 | 'Un composant Vue.js pour générer de beaux emails en utilisant MJML', 19 | button: 'Commencer', 20 | }, 21 | }, 22 | }, 23 | autoImport: false, 24 | // tailwind: { 25 | // theme: { 26 | // extend: { 27 | // colors: { 28 | // primary: "#ea580c", 29 | // secondary: "#ca8a04", 30 | // }, 31 | // }, 32 | // }, 33 | // }, 34 | }, 35 | devtools: { enabled: true }, 36 | }) 37 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt generate", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "devDependencies": { 11 | "@iconify-json/fluent": "latest", 12 | "@iconify-json/heroicons": "latest", 13 | "@iconify-json/ph": "latest", 14 | "@iconify-json/simple-icons": "latest", 15 | "@iconify-json/twemoji": "latest", 16 | "@nuxt/devtools": "latest", 17 | "@nuxt/ui": "^2.12.3", 18 | "@nuxtjs/fontaine": "^0.4.1", 19 | "@nuxtjs/google-fonts": "^3.1.3", 20 | "@types/html-to-text": "^9.0.4", 21 | "@types/pretty": "^2.0.3", 22 | "@types/splitpanes": "^2.2.6", 23 | "@vueuse/core": "^10.7.2", 24 | "@vueuse/nuxt": "^10.7.2", 25 | "destr": "^2.0.2", 26 | "html-to-text": "^9.0.5", 27 | "json-editor-vue": "^0.12.0", 28 | "json5": "^2.2.3", 29 | "nuxt": "^3.9.3", 30 | "pretty": "^2.0.0", 31 | "scule": "^1.2.0", 32 | "shiki": "^1.0.0-beta.3", 33 | "splitpanes": "^3.1.5", 34 | "vue-component-meta": "^1.8.27" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'pathe' 2 | import { defineNuxtConfig } from 'nuxt/config' 3 | import vueEmailModule from '../src/module' 4 | 5 | export default defineNuxtConfig({ 6 | ssr: false, 7 | devtools: { enabled: true, componentInspector: false, viteInspect: false }, 8 | modules: [vueEmailModule, '@nuxt/ui', '@nuxtjs/fontaine', '@nuxtjs/google-fonts', '@vueuse/nuxt'], 9 | app: { 10 | baseURL: process.env.NODE_ENV === 'development' ? undefined : '/__vue_email__/client', 11 | }, 12 | nitro: { 13 | output: { 14 | publicDir: resolve(__dirname, '../dist/client'), 15 | }, 16 | }, 17 | ui: { 18 | global: true, 19 | icons: ['heroicons', 'simple-icons', 'ph', 'twemoji', 'fluent'], 20 | }, 21 | googleFonts: { 22 | families: { 23 | Inter: [400, 500, 600, 700], 24 | }, 25 | }, 26 | tailwindcss: { 27 | exposeConfig: true, 28 | viewer: false, 29 | }, 30 | colorMode: { 31 | preference: 'dark', 32 | fallback: 'dark', 33 | }, 34 | ignore: ['emails/**/*'], 35 | vueEmail: { 36 | playground: false, 37 | baseUrl: 'https://vue-email-demo.vercel.app/', 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vue Email 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: read 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v2 23 | 24 | - name: Set node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: lts/* 28 | 29 | - name: Setup 30 | run: npm i -g @antfu/ni 31 | 32 | - name: Install 33 | run: nci 34 | 35 | - uses: dorny/paths-filter@v2 36 | id: changes 37 | with: 38 | filters: | 39 | src: 40 | - 'src/**' 41 | - 'package.json' 42 | - 'pnpm-lock.yaml' 43 | 44 | - name: Lint 45 | run: nr lint 46 | 47 | - name: Build 48 | run: nr build 49 | 50 | - name: Relase Edge 51 | run: | 52 | chmod +x ./scripts/release-edge.sh 53 | ./scripts/release-edge.sh 54 | env: 55 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "format/*", "severity": "off" }, 19 | { "rule": "*-indent", "severity": "off" }, 20 | { "rule": "*-spacing", "severity": "off" }, 21 | { "rule": "*-spaces", "severity": "off" }, 22 | { "rule": "*-order", "severity": "off" }, 23 | { "rule": "*-dangle", "severity": "off" }, 24 | { "rule": "*-newline", "severity": "off" }, 25 | { "rule": "*quotes", "severity": "off" }, 26 | { "rule": "*semi", "severity": "off" } 27 | ], 28 | 29 | // Enable eslint for all supported languages 30 | "eslint.validate": [ 31 | "javascript", 32 | "javascriptreact", 33 | "typescript", 34 | "typescriptreact", 35 | "vue", 36 | "html", 37 | "markdown", 38 | "json", 39 | "jsonc", 40 | "yaml", 41 | "toml" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /client/components/HeaderButtons.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /src/runtime/server/nitro/useCompiler.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '@vue-email/compiler' 2 | import { templateRender } from '@vue-email/compiler' 3 | import type { ModuleOptions } from '../../../module' 4 | import { useRuntimeConfig, useStorage } from '#imports' 5 | 6 | const storageKey = 'assets:emails' 7 | 8 | export async function useCompiler( 9 | filename: string, 10 | data?: RenderOptions, 11 | verbose = false, 12 | ) { 13 | const vueEmailOptions = useRuntimeConfig().public.vueEmail as ModuleOptions 14 | let source = await useStorage(storageKey).getItem(filename) 15 | if (source instanceof Uint8Array) 16 | source = new TextDecoder().decode(source) 17 | const keys = await useStorage(storageKey).getKeys() 18 | const components: { 19 | name: string 20 | source: string 21 | }[] = [] 22 | for (const key of keys) { 23 | let value = await useStorage(storageKey).getItem(key) 24 | if (value instanceof Uint8Array) 25 | value = new TextDecoder().decode(value) 26 | 27 | if (value && key.endsWith('.vue')) { 28 | components.push({ 29 | name: key, 30 | source: value as string, 31 | }) 32 | } 33 | } 34 | 35 | if (!source) 36 | throw new Error(`Template ${filename} not found`) 37 | 38 | const template = await templateRender( 39 | filename, 40 | { source: source as string, components }, 41 | data, 42 | { 43 | verbose, 44 | options: { 45 | baseUrl: vueEmailOptions?.baseUrl, 46 | i18n: vueEmailOptions?.i18n, 47 | tailwind: vueEmailOptions?.tailwind, 48 | }, 49 | }, 50 | ) 51 | 52 | return template 53 | } 54 | -------------------------------------------------------------------------------- /client/components/SearchButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /client/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 54 | -------------------------------------------------------------------------------- /playground/nuxt-layer/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Layer Starter 2 | 3 | Create Nuxt extendable layer with this GitHub template. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | pnpm install 11 | ``` 12 | 13 | ## Working on your theme 14 | 15 | Your theme is at the root of this repository, it is exactly like a regular Nuxt project, except you can publish it on NPM. 16 | 17 | The `.playground` directory should help you on trying your theme during development. 18 | 19 | Running `pnpm dev` will prepare and boot `.playground` directory, which imports your theme itself. 20 | 21 | ## Distributing your theme 22 | 23 | Your Nuxt layer is shaped exactly the same as any other Nuxt project, except you can publish it on NPM. 24 | 25 | To do so, you only have to check if `files` in `package.json` are valid, then run: 26 | 27 | ```bash 28 | npm publish --access public 29 | ``` 30 | 31 | Once done, your users will only have to run: 32 | 33 | ```bash 34 | npm install --save your-theme 35 | ``` 36 | 37 | Then add the dependency to their `extends` in `nuxt.config`: 38 | 39 | ```ts 40 | defineNuxtConfig({ 41 | extends: 'your-theme' 42 | }) 43 | ``` 44 | 45 | ## Development Server 46 | 47 | Start the development server on http://localhost:3000 48 | 49 | ```bash 50 | pnpm dev 51 | ``` 52 | 53 | ## Production 54 | 55 | Build the application for production: 56 | 57 | ```bash 58 | pnpm build 59 | ``` 60 | 61 | Or statically generate it with: 62 | 63 | ```bash 64 | pnpm generate 65 | ``` 66 | 67 | Locally preview production build: 68 | 69 | ```bash 70 | pnpm preview 71 | ``` 72 | 73 | Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 74 | -------------------------------------------------------------------------------- /client/server/api/render/[file].post.ts: -------------------------------------------------------------------------------- 1 | import { destr } from 'destr' 2 | import { useCompiler } from '#vue-email' 3 | import { createError, defineEventHandler, readBody } from '#imports' 4 | 5 | export default defineEventHandler(async (event: any) => { 6 | try { 7 | const file = event.context.params && event.context.params.file ? event.context.params.file : null 8 | const body = await readBody(event) 9 | 10 | let props: any = null 11 | if (body && body.props) { 12 | props = body.props.reduce((acc: Record, prop: any) => { 13 | if (prop.type === 'string') 14 | acc[prop.label] = destr(prop.value) || '' 15 | 16 | if (prop.type === 'number') 17 | acc[prop.label] = destr(prop.value) || 0 18 | 19 | if (prop.type === 'boolean') 20 | acc[prop.label] = destr(prop.value) || false 21 | 22 | if (prop.type === 'object') 23 | acc[prop.label] = destr(prop.value) || {} 24 | 25 | if (prop.type === 'array') 26 | acc[prop.label] = destr(prop.value) || [] 27 | 28 | if (prop.type === 'date') 29 | acc[prop.label] = new Date(prop.value) || new Date() 30 | 31 | return acc 32 | }, {}) 33 | } 34 | 35 | // TODO: pass props to template 36 | const template = await useCompiler(file, { 37 | props, 38 | }) 39 | 40 | if (!template) { 41 | throw createError({ 42 | statusCode: 404, 43 | statusMessage: 'Not Found', 44 | }) 45 | } 46 | 47 | return template 48 | } 49 | catch (error) { 50 | console.error(error) 51 | 52 | throw createError({ 53 | statusCode: 500, 54 | statusMessage: 'Internal Server Error', 55 | }) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /src/runtime/server/api/render/[file].post.ts: -------------------------------------------------------------------------------- 1 | import { destr } from 'destr' 2 | import { useCompiler } from '#vue-email' 3 | import { createError, defineEventHandler, readBody } from '#imports' 4 | 5 | export default defineEventHandler(async (event: any) => { 6 | try { 7 | const file = event.context.params && event.context.params.file ? event.context.params.file : null 8 | const body = await readBody(event) 9 | 10 | let props: any = null 11 | if (body && body.props) { 12 | props = body.props.reduce((acc: Record, prop: any) => { 13 | if (prop.type === 'string') 14 | acc[prop.label] = destr(prop.value) || '' 15 | 16 | if (prop.type === 'number') 17 | acc[prop.label] = destr(prop.value) || 0 18 | 19 | if (prop.type === 'boolean') 20 | acc[prop.label] = destr(prop.value) || false 21 | 22 | if (prop.type === 'object') 23 | acc[prop.label] = destr(prop.value) || {} 24 | 25 | if (prop.type === 'array') 26 | acc[prop.label] = destr(prop.value) || [] 27 | 28 | if (prop.type === 'date') 29 | acc[prop.label] = new Date(prop.value) || new Date() 30 | 31 | return acc 32 | }, {}) 33 | } 34 | 35 | // TODO: pass props to template 36 | const template = await useCompiler(file, { 37 | props, 38 | }) 39 | 40 | if (!template) { 41 | throw createError({ 42 | statusCode: 404, 43 | statusMessage: 'Not Found', 44 | }) 45 | } 46 | 47 | return template 48 | } 49 | catch (error) { 50 | console.error(error) 51 | 52 | throw createError({ 53 | statusCode: 500, 54 | statusMessage: 'Internal Server Error', 55 | }) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /client/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 18 | 19 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /client/pages/email/[file].vue: -------------------------------------------------------------------------------- 1 | 19 | 20 |