├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── README.md ├── __tests__ ├── .eslintrc.json ├── fixtures │ └── templates │ │ ├── .gitignore │ │ ├── SimpleTemplate.svelte │ │ └── WithStyles.svelte └── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { "targets": { "node": 10 } }], 4 | "@babel/typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/node:10 6 | 7 | git-login: &git-login 8 | name: Setting up git user 9 | command: git config --global user.email ci@ls-age.com && git config --global user.name "ls-age CI" 10 | 11 | npm-login: &npm-login 12 | name: Logging in to npm 13 | command: echo "$NPM_TOKEN" > ~/.npmrc 14 | 15 | jobs: 16 | install: 17 | <<: *defaults 18 | steps: 19 | - checkout 20 | - restore_cache: 21 | keys: 22 | - v2-npm-deps-{{ checksum "package-lock.json" }} 23 | - v2-npm-deps 24 | - run: 25 | name: Installing npm dependencies 26 | command: npm ci 27 | - save_cache: 28 | key: v2-npm-deps-{{ checksum "package-lock.json" }} 29 | paths: 30 | - ~/.npm 31 | - ~/.cache 32 | - persist_to_workspace: 33 | root: . 34 | paths: 35 | - . 36 | 37 | build: 38 | <<: *defaults 39 | steps: 40 | - attach_workspace: 41 | at: . 42 | - run: 43 | name: Bundle module 44 | command: npm run compile 45 | - run: 46 | name: Generate type definitions 47 | command: npm run types 48 | when: always 49 | - persist_to_workspace: 50 | root: . 51 | paths: 52 | - out 53 | - __tests__/fixtures/templates 54 | 55 | lint: 56 | <<: *defaults 57 | steps: 58 | - attach_workspace: 59 | at: . 60 | - run: 61 | name: Run Prettier 62 | command: npm run format -- --check 63 | - run: 64 | name: Run ESLint 65 | command: npm run lint -- --format junit --output-file ~/reports/eslint.xml 66 | when: always 67 | - store_test_results: 68 | path: ~/reports 69 | - store_artifacts: 70 | path: ~/reports 71 | 72 | test: 73 | <<: *defaults 74 | steps: 75 | - attach_workspace: 76 | at: . 77 | - run: 78 | name: Run tests 79 | command: npm run test -- --ci --runInBand --reporters=default --reporters=jest-junit 80 | environment: 81 | JEST_JUNIT_OUTPUT: ~/reports/jest.xml 82 | - store_test_results: 83 | path: ~/reports 84 | - store_artifacts: 85 | path: ~/reports 86 | 87 | deploy: 88 | <<: *defaults 89 | steps: 90 | - checkout 91 | - add_ssh_keys 92 | - attach_workspace: 93 | at: . 94 | - run: 95 | <<: *git-login 96 | - run: 97 | <<: *npm-login 98 | - run: 99 | name: Deploying changes 100 | command: npx @ls-age/bump-version release --gh-token $RELEASE_GITHUB_TOKEN 101 | 102 | workflows: 103 | version: 2 104 | 105 | build-test-deploy: 106 | jobs: 107 | - install 108 | - build: 109 | requires: 110 | - install 111 | - lint: 112 | requires: 113 | - build 114 | - test: 115 | requires: 116 | - build 117 | - deploy: 118 | requires: 119 | - build 120 | - lint 121 | - test 122 | filters: 123 | branches: 124 | only: 125 | - master 126 | - beta 127 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __tests__/fixtures/templates 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "@ls-age", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:import/typescript", 7 | "prettier", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "rules": { 11 | "compat/compat": "off", 12 | "@typescript-eslint/explicit-function-return-type": [2, { "allowExpressions": true }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.1.1 (2019-06-17) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * Release to npm ([69c20cb](https://github.com/ls-age/svelte-mail/commits/69c20cb)) 8 | 9 | 10 | 11 | 12 | 13 | # 0.1.0 (2019-06-15) 14 | 15 | 16 | ### Features 17 | 18 | * Initial release ([#4](https://github.com/ls-age/svelte-mail/issues/4)) ([341fef6](https://github.com/ls-age/svelte-mail/commits/341fef6)) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-mail 2 | 3 | > Renders [Svelte](https://svelte.dev) components for emails. Inlines styles and renders additional plain text version. 4 | 5 | [![CircleCI](https://circleci.com/gh/ls-age/svelte-mail/tree/master.svg?style=svg)](https://circleci.com/gh/ls-age/svelte-mail/tree/master) 6 | 7 | ## Installation 8 | 9 | Run `npm i --save svelte-mail`. 10 | 11 | ## Usage 12 | 13 | Simply pass a Svelte component and some options: 14 | 15 | *`./components/Mail.svelte`* 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 | Hello, {user} 23 | ``` 24 | 25 | *`./sendMail.js`* 26 | ```javascript 27 | import { renderMail } from 'svelte-mail'; 28 | import Mail from './components/Mail.svelte'; 29 | 30 | async function sendMail() { 31 | const { html, text } = await renderMail(Mail, { data: { user: 'World' } }); 32 | 33 | /* 34 | `html` contains the rendered html string: 35 | "Hello, World" 36 | 37 | `text` contains the rendered plain text message: 38 | "Hello, World" 39 | */ 40 | 41 | // TODO: Send mail, e.g. using nodemailer... 42 | } 43 | 44 | sendMail() 45 | .catch(console.error); 46 | ``` 47 | 48 | **Note: The mail component must be compiled for server side rendering.** 49 | 50 | ## Options 51 | 52 | Internally, this module uses [juice](https://www.npmjs.com/package/juice) to inline styles and [html-to-text](https://www.npmjs.com/package/html-to-text) to render plain text messages. All options passed to the *renderMail* function will be passed to them: 53 | 54 | ```javascript 55 | renderMail(Mail, { 56 | data: {}, 57 | // add any juice options here, e.g.: 58 | extraCss: 'strong { text-decoration: underline }', 59 | // add any html-to-text options here, e.g.: 60 | uppercaseHeadings: false, 61 | }); 62 | ``` 63 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@ls-age", "plugin:jest/recommended", "prettier"], 3 | "plugins": ["jest"], 4 | "env": { 5 | "jest": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/templates/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !*.svelte 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/templates/SimpleTemplate.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello, {user} 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/templates/WithStyles.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | Hello, {user} 9 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { renderMail } from '../src/index'; 2 | import SimpleTemplate from './fixtures/templates/SimpleTemplate'; 3 | import TemplateWithStyles from './fixtures/templates/WithStyles'; 4 | 5 | test('renders templates', async () => { 6 | const { html, text } = await renderMail(SimpleTemplate, { data: { user: 'Username' } }); 7 | 8 | expect(html).toEqual('Hello, Username'); 9 | expect(text).toBe('Hello, Username'); 10 | }); 11 | 12 | test('inlines styles', async () => { 13 | const { html } = await renderMail(TemplateWithStyles, { data: { user: 'Username' } }); 14 | 15 | expect(html).toMatch( 16 | /^Hello, Username<\/strong>$/ 17 | ); 18 | }); 19 | 20 | test('passes options to juice', async () => { 21 | const { html } = await renderMail(SimpleTemplate, { 22 | data: { user: 'Username' }, 23 | extraCss: 'strong { text-decoration: underline; }', 24 | }); 25 | 26 | expect(html).toEqual('Hello, Username'); 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-mail", 3 | "version": "0.1.1", 4 | "description": "Renders Svelte components for emails: Inlines styles and renders additional plain text version", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ls-age/svelte-mail.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/ls-age/svelte-mail/issues" 12 | }, 13 | "homepage": "https://github.com/ls-age/svelte-mail#readme", 14 | "author": "Lukas Hechenberger ", 15 | "keywords": [ 16 | "svelte", 17 | "email", 18 | "mail", 19 | "inline" 20 | ], 21 | "main": "out/index.js", 22 | "module": "out/es/index.js", 23 | "scripts": { 24 | "compile": "rollup -c", 25 | "format": "prettier \"**/*.{js,ts,json,yml}\" \"!package-lock.json\" \"!out/**/*\" \"!__tests__/fixtures/templates/*.js\"", 26 | "lint": "eslint rollup.config.js src __tests__ --ext .js,.ts", 27 | "test": "jest --testPathIgnorePatterns=__tests__/fixtures/templates", 28 | "types": "tsc --emitDeclarationOnly" 29 | }, 30 | "types": "out/types/index.d.ts", 31 | "dependencies": { 32 | "html-to-text": "5.1.1", 33 | "juice": "5.2.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "7.16.12", 37 | "@babel/preset-env": "7.16.11", 38 | "@babel/preset-typescript": "7.16.7", 39 | "@ls-age/bump-version": "0.2.1", 40 | "@ls-age/eslint-config": "0.9.0", 41 | "@types/html-to-text": "1.4.31", 42 | "@typescript-eslint/eslint-plugin": "2.34.0", 43 | "@typescript-eslint/parser": "2.34.0", 44 | "eslint": "5.16.0", 45 | "eslint-config-prettier": "7.2.0", 46 | "eslint-plugin-jest": "24.4.0", 47 | "jest": "27.4.7", 48 | "jest-junit": "13.0.0", 49 | "prettier": "2.5.1", 50 | "rollup": "2.66.0", 51 | "rollup-plugin-babel": "4.4.0", 52 | "rollup-plugin-node-resolve": "5.2.0", 53 | "rollup-plugin-svelte": "6.1.1", 54 | "svelte": "3.46.2", 55 | "typescript": "4.5.5" 56 | }, 57 | "renovate": { 58 | "extends": [ 59 | "@ls-age:automergeDev" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import svelte from 'rollup-plugin-svelte'; 5 | import { dependencies } from './package.json'; 6 | 7 | const extensions = ['.mjs', '.js', '.json', '.ts']; 8 | 9 | export default [ 10 | { 11 | input: ['./src/index.ts'], 12 | external: builtinModules.concat(Object.keys(dependencies)), 13 | plugins: [resolve({ extensions }), babel({ extensions, include: 'src/**/*' })], 14 | output: [ 15 | { 16 | format: 'cjs', 17 | dir: 'out', 18 | sourcemap: true, 19 | }, 20 | { 21 | format: 'es', 22 | dir: 'out/es', 23 | sourcemap: true, 24 | }, 25 | ], 26 | }, 27 | { 28 | input: [ 29 | './__tests__/fixtures/templates/SimpleTemplate.svelte', 30 | './__tests__/fixtures/templates/WithStyles.svelte', 31 | ], 32 | plugins: [ 33 | resolve({ extensions }), 34 | svelte({ 35 | dev: true, 36 | generate: 'ssr', 37 | }), 38 | ], 39 | output: { 40 | format: 'es', 41 | dir: '__tests__/fixtures/templates', 42 | sourcemap: true, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | import { juiceResources, Options as JuiceOptions } from 'juice'; 4 | import { fromString as getPlainText } from 'html-to-text'; 5 | 6 | interface SvelteSSRComponent { 7 | render(data: {}): { html: string; css: { code: string }; head: string }; 8 | } 9 | 10 | export async function renderMail( 11 | Component: SvelteSSRComponent, 12 | { data = {}, ...options }: { data?: {} } & JuiceOptions & HtmlToTextOptions = {} 13 | ): Promise<{ 14 | html: string; 15 | text: string; 16 | }> { 17 | const { html: rawHtml, css, head } = Component.render(data); 18 | 19 | if (head) { 20 | // eslint-disable-next-line no-console 21 | console.error('Rendering a document head is not supported'); 22 | } 23 | 24 | const html: string = await new Promise((resolve, reject) => { 25 | juiceResources( 26 | `${css.code ? `` : ''}${rawHtml}`, 27 | options, 28 | (err, result) => (err ? reject(err) : resolve(result)) 29 | ); 30 | }); 31 | 32 | return { 33 | html, 34 | text: getPlainText(html, { ignoreImage: true, ...options }), 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "out/types", 5 | "skipLibCheck": true, 6 | "lib": ["es2018"] 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | --------------------------------------------------------------------------------