├── .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 | [](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 |
--------------------------------------------------------------------------------