astro-html 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Banner.astro ADDED
@@ -0,0 +1,69 @@
1
+ ---
2
+ const props = Astro.props;
3
+ ---
4
+
5
+ <html lang="de">
6
+ <head>
7
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
8
+
9
+ <meta name="ad.orientation" content="portrait,landscape">
10
+ <meta name="ad.size" content={`width=${props.width},height=${props.height}`}>
11
+
12
+ <title>{props.title}</title>
13
+
14
+ <style>
15
+ body {
16
+ margin: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ }
20
+ .banner {
21
+ display: block;
22
+ overflow: hidden;
23
+ cursor: pointer;
24
+ text-decoration: none;
25
+ }
26
+ </style>
27
+
28
+ <slot name="head" />
29
+ </head>
30
+
31
+ <body>
32
+ <a href="javascript:window.open(window.clickTag)" class={['banner', props.class].join(" ")} id="clicktag" target="_blank">
33
+ <slot />
34
+ </a>
35
+
36
+ <script is:inline type="application/javascript">
37
+ var clickTag = "";
38
+ var getUriParams = (function () {
39
+ var query_string = {};
40
+ var query = window.location.search.substring(1);
41
+ var parmsArray = query.split("&");
42
+ if (parmsArray.length <= 0) return query_string;
43
+ for (var i = 0; i < parmsArray.length; i++) {
44
+ var pair = parmsArray[i].split("=");
45
+ var val = decodeURIComponent(pair[1]);
46
+ if (val != "" && pair[0] != "") query_string[pair[0]] = val;
47
+ }
48
+ return query_string;
49
+ })();
50
+
51
+ const element = document.getElementById("clicktag");
52
+ const href = getUriParams.clicktag || getUriParams.clickTag || getUriParams.clicktag1;
53
+ const target = getUriParams.target || getUriParams.target1;
54
+
55
+ // google
56
+ if (href) {
57
+ window.clickTag = href;
58
+ }
59
+
60
+ // default
61
+ if (element && href) {
62
+ element.setAttribute("href", href);
63
+ }
64
+ if (element && target) {
65
+ element.setAttribute("target", target);
66
+ }
67
+ </script>
68
+ </body>
69
+ </html>
package/Mail.astro ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ import { Head, Html } from "@react-email/components";
3
+ ---
4
+
5
+ <Fragment
6
+ set:html={`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">`}
7
+ />
8
+ <Html>
9
+ <Head />
10
+
11
+ <slot />
12
+ </Html>
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # astro-html
2
+
3
+ Integration for Astro to Generate HTML emails with React ([react-email](https://github.com/resend/react-email)).
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm i astro-html
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import { defineConfig } from "astro/config";
15
+ import email from "astro-html";
16
+
17
+ // https://astro.build/config
18
+ export default defineConfig({
19
+ integrations: [
20
+ email({
21
+ filename: "[name].html",
22
+ }),
23
+ ],
24
+ });
25
+ ```
26
+
27
+ ### Development
28
+
29
+ ```sh
30
+ npx astro dev
31
+ ```
32
+
33
+ Development preview with live reload.
34
+
35
+ ### Production
36
+
37
+ ```sh
38
+ npx astro build
39
+ ```
40
+
41
+ This will generate HTML files for each `.astro` file in the `src/pages` directory.
42
+
43
+ ## Example
44
+
45
+ ```astro
46
+ ---
47
+ // Important! Without partial astro will render the default Doctype.
48
+ export const partial = true;
49
+
50
+ import * as React from "react";
51
+ import { Body, Container, Heading, Link, Text } from "@react-email/components";
52
+ import Mail from "astro-html/Mail.astro";
53
+
54
+ const fontFamily =
55
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif";
56
+
57
+ const container: React.CSSProperties = {
58
+ paddingLeft: "45px",
59
+ paddingRight: "45px",
60
+ margin: "0 auto",
61
+ maxWidth: "640px",
62
+ };
63
+ ---
64
+
65
+ <Mail>
66
+ <Body
67
+ style={{
68
+ fontFamily,
69
+ backgroundColor: "#ffffff",
70
+ margin: "0",
71
+ }}
72
+ >
73
+ <Container style={container}>
74
+ <Heading>My Email</Heading>
75
+ <Text>Lorem Ipsum</Text>
76
+
77
+ <Link href="https://example.com">Click me</Link>
78
+ </Container>
79
+ </Body>
80
+ </Mail>
81
+
82
+ ```
package/index.astro ADDED
@@ -0,0 +1,522 @@
1
+ ---
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ let failed = false;
6
+
7
+ async function findTemplates(
8
+ basePath: string,
9
+ relativePath: string = "",
10
+ currentDepth: number = 0,
11
+ maxDepth: number = 2,
12
+ ): Promise<any[]> {
13
+ if (currentDepth > maxDepth) {
14
+ return [];
15
+ }
16
+
17
+ const fullPath = path.resolve(basePath, relativePath);
18
+ const items = fs.readdirSync(fullPath);
19
+ const results: any[] = [];
20
+
21
+ for (const item of items) {
22
+ const itemPath = path.resolve(fullPath, item);
23
+ const stat = fs.statSync(itemPath);
24
+
25
+ try {
26
+ if (stat.isDirectory()) {
27
+ const subResults = await findTemplates(
28
+ basePath,
29
+ path.join(relativePath, item),
30
+ currentDepth + 1,
31
+ maxDepth,
32
+ );
33
+ results.push(...subResults);
34
+ } else {
35
+ if (!itemPath.endsWith(".astro")) {
36
+ continue;
37
+ }
38
+
39
+ const astroPageRaw = { default: fs.readFileSync(itemPath, "utf-8") };
40
+ const frontMatterMatch = astroPageRaw.default?.match(
41
+ /^---\s*([\s\S]*?)\s*---/,
42
+ );
43
+
44
+ // Extract only export statements from frontmatter
45
+ let exportStatements = "";
46
+ if (frontMatterMatch) {
47
+ const frontMatter = frontMatterMatch[1];
48
+ const exportMatches = frontMatter.match(
49
+ /export\s+[\s\S]*?(?=export\s+|$)/g,
50
+ );
51
+ if (exportMatches) {
52
+ exportStatements = exportMatches.join("\n");
53
+ }
54
+ }
55
+
56
+ const frontMatterToProcess = exportStatements || "";
57
+
58
+ let exprts = {};
59
+
60
+ // TODO: This limits us to not use relative file imports. Why cant we import astro file here at build time.
61
+ if (frontMatterMatch) {
62
+ const frontMatterModule = await import(
63
+ `data:text/javascript,${encodeURIComponent(frontMatterToProcess)}`
64
+ );
65
+ exprts = frontMatterModule;
66
+ }
67
+
68
+ const templateName = (
69
+ relativePath ? path.join(relativePath, item) : item
70
+ ).replace(".astro", "");
71
+
72
+ // Check if getStaticPaths exists and generate multiple pages if so
73
+ if (
74
+ exprts.getStaticPaths &&
75
+ typeof exprts.getStaticPaths === "function"
76
+ ) {
77
+ const staticPaths = await exprts.getStaticPaths();
78
+
79
+ for (const pathData of staticPaths) {
80
+ let pageName = templateName;
81
+ for (const key in pathData.params) {
82
+ pageName = pageName.replace(`[${key}]`, pathData.params[key]);
83
+ }
84
+
85
+ results.push({
86
+ name: pageName,
87
+ config: exprts.config,
88
+ params: pathData.params,
89
+ });
90
+ }
91
+ } else {
92
+ results.push({
93
+ name: templateName,
94
+ config: exprts.config,
95
+ });
96
+ }
97
+ }
98
+ } catch (err) {
99
+ console.error(err);
100
+ failed = true;
101
+ }
102
+ }
103
+
104
+ return results;
105
+ }
106
+
107
+ const templates = await findTemplates("src/pages", "", 0, 2);
108
+
109
+ if (failed) {
110
+ throw new Error("Failed to build pages");
111
+ }
112
+ ---
113
+
114
+ <html>
115
+ <head>
116
+ <title>Astro Email</title>
117
+
118
+ <Fragment
119
+ set:html={`
120
+ <script id="data" type="application/json">${JSON.stringify(
121
+ {
122
+ pages: templates,
123
+ },
124
+ null,
125
+ " "
126
+ )}</script>
127
+ `}
128
+ />
129
+ </head>
130
+
131
+ <body>
132
+ <div class="layout">
133
+ <div class="sidebar">
134
+ <div class="sidebar-header">
135
+ <h1>Templates</h1>
136
+ <button id="download-assets" class="download-button" data-has-assets="true">
137
+ Download All Assets
138
+ </button>
139
+ </div>
140
+ {
141
+ templates.map((template, index) => {
142
+ const path = template?.name.replace(".astro", "");
143
+ return (
144
+ <div class="button" data-template={path}>
145
+ <a href={`#${path}`}>
146
+ <span class="template-number">{String(index + 1).padStart(2, '0')}</span>
147
+ <span class="template-name">{path}</span>
148
+ </a>
149
+ <a target="_blank" href={path} class="template-link">
150
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" style="margin-bottom:-2px;" viewBox="0 0 15 15">
151
+ <defs>
152
+ <clipPath id="clip-path">
153
+ <rect id="Rectangle_1687" data-name="Rectangle 1687" width="15" height="15" transform="translate(11726 152)" fill="none"/>
154
+ </clipPath>
155
+ </defs>
156
+ <g id="Mask_Group_3" data-name="Mask Group 3" transform="translate(-11726 -152)" clip-path="url(#clip-path)">
157
+ <g id="Group_422" data-name="Group 422" fill="currentColor">
158
+ <path id="Path_4182" data-name="Path 4182" d="M14.6,14H7.4A2.4,2.4,0,0,1,5,11.6V4.4A2.4,2.4,0,0,1,7.4,2h2.8V3.2H7.4A1.2,1.2,0,0,0,6.2,4.4v7.2a1.2,1.2,0,0,0,1.2,1.2h7.2a1.2,1.2,0,0,0,1.2-1.2V9H17v2.6A2.4,2.4,0,0,1,14.6,14Z" transform="translate(11722 152)"/>
159
+ <path id="Path_4181" data-name="Path 4181" d="M3.385,9.591V1.927L.963,4.349a.564.564,0,0,1-.8-.8L3.54.176a.564.564,0,0,1,.684-.1h0l.01.006,0,0,.007,0,.006,0,0,0L4.264.1h0a.567.567,0,0,1,.083.069L7.733,3.55a.564.564,0,0,1-.8.8L4.513,1.926V9.591a.564.564,0,1,1-1.128,0Z" transform="translate(11737.298 150.117) rotate(45)"/>
160
+ </g>
161
+ </g>
162
+ </svg>
163
+ </a>
164
+ </div>
165
+ );
166
+ })
167
+ }
168
+ </div>
169
+
170
+ <div class="preview">
171
+ <div id="display-width" class="preview-header"></div>
172
+ <iframe width="640px"></iframe>
173
+
174
+ <script>
175
+ const dataString = document.querySelector("#data")?.innerHTML;
176
+ if (!dataString) throw new Error("No data found");
177
+
178
+ const data = JSON.parse(dataString);
179
+ console.debug("data", data);
180
+
181
+ // Download assets functionality
182
+ const downloadButton = document.querySelector("#download-assets");
183
+ const hasAssets = downloadButton && downloadButton.getAttribute("data-has-assets") === "true";
184
+
185
+ if (downloadButton) {
186
+ if (!hasAssets) {
187
+ downloadButton.disabled = true;
188
+ downloadButton.innerHTML = `
189
+ <svg width="12" height="12" viewBox="0 0 12 12" style="color: #6c757d;">
190
+ <circle cx="6" cy="6" r="4" stroke="currentColor" stroke-width="2" fill="none"/>
191
+ <path d="M4.5 4.5l3 3M7.5 4.5l-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
192
+ </svg>
193
+ No Assets Built
194
+ `;
195
+ downloadButton.title = "Run 'npm run build' to generate downloadable assets";
196
+ } else {
197
+ downloadButton.addEventListener("click", () => {
198
+ // Simple direct download of pre-built zip
199
+ const link = document.createElement("a");
200
+ link.href = "/assets.zip";
201
+ link.download = "assets.zip";
202
+ document.body.appendChild(link);
203
+ link.click();
204
+ document.body.removeChild(link);
205
+
206
+ // Show success feedback
207
+ const originalText = downloadButton.innerHTML;
208
+ downloadButton.innerHTML = `
209
+ <svg width="12" height="12" viewBox="0 0 12 12" style="color: #22c55e;">
210
+ <path d="M2 6l2.5 2.5L9 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
211
+ </svg>
212
+ Downloaded!
213
+ `;
214
+
215
+ setTimeout(() => {
216
+ downloadButton.innerHTML = originalText;
217
+ }, 1500);
218
+ });
219
+ }
220
+ }
221
+
222
+ const iframe = document.querySelector("iframe");
223
+ if (!iframe) throw new Error("No iframe found");
224
+
225
+ let fixedWidth = undefined;
226
+ let fixedHeight = undefined;
227
+
228
+ const updateFormat = (name?: string) => {
229
+ const config = data.pages.find((temp: { name: string }) => {
230
+ return temp.name === `${name}`;
231
+ })?.config;
232
+
233
+ fixedWidth = config.width;
234
+ fixedHeight = config.height;
235
+ const width = fixedWidth || iframe?.offsetWidth || 0;
236
+ const height = fixedHeight || iframe?.offsetHeight || 0;
237
+
238
+ const display = document.querySelector("#display-width");
239
+ if(display) {
240
+ display.innerHTML = `${width}x${height}px`;
241
+ }
242
+
243
+ if(iframe) {
244
+ iframe.width = `${width}`;
245
+ iframe.height = `${height}`;
246
+ }
247
+ }
248
+
249
+ let resizeListener: EventListener;
250
+
251
+ function showTemplate(name: string) {
252
+ if(!iframe) return;
253
+
254
+ const config = data.pages.find((temp: { name: string }) => {
255
+ return temp.name === `${name}`;
256
+ })?.config;
257
+
258
+ console.debug(name, config)
259
+
260
+ updateFormat(name);
261
+
262
+ iframe.src = `/${name}.html`;
263
+
264
+ // Update active state
265
+ document.querySelectorAll('.button').forEach(button => {
266
+ button.classList.remove('active');
267
+ });
268
+
269
+ const activeButton = document.querySelector(`[data-template="${name}"]`);
270
+ if (activeButton) {
271
+ activeButton.classList.add('active');
272
+ }
273
+
274
+ window.removeEventListener("resize", resizeListener);
275
+
276
+ resizeListener = () => updateFormat(name);
277
+
278
+ window.addEventListener("resize", resizeListener);
279
+ }
280
+
281
+ const template = location.hash.substring(1);
282
+ if (template) {
283
+ showTemplate(template)
284
+ } else {
285
+ const first = data.pages[0]?.name.replace(".astro", "");
286
+ showTemplate(first)
287
+ }
288
+
289
+ window.addEventListener("hashchange", () => {
290
+ showTemplate(location.hash.substring(1));
291
+ });
292
+ </script>
293
+ </div>
294
+ </div>
295
+
296
+ <style>
297
+ body {
298
+ font-family: sans-serif, Arial;
299
+ height: 100vh;
300
+ margin: 0;
301
+ overscroll-behavior: none;
302
+ overflow: hidden;
303
+ font-size: 1rem;
304
+ }
305
+
306
+ body {
307
+ background-color: #e5e5f7;
308
+ background-image: linear-gradient(#eee 1px, transparent 1px), linear-gradient(to right, #eee 1px, #fff 1px);
309
+ background-size: 20px 20px;
310
+ }
311
+
312
+ @media (prefers-color-scheme: dark) {
313
+ body {
314
+ background-color: hsl(258, 7%, 10%);
315
+ color: #fff;
316
+ }
317
+ }
318
+
319
+ iframe {
320
+ border: none;
321
+ max-height: 100%;
322
+ max-width: 100%;
323
+ flex: none;
324
+ }
325
+
326
+ h1 {
327
+ opacity: 0.65;
328
+ font-size: 0.65rem;
329
+ font-weight: normal;
330
+ text-transform: uppercase;
331
+ margin: 0;
332
+ }
333
+
334
+ .preview-header {
335
+ display: flex;
336
+ justify-content: center;
337
+ align-items: center;
338
+ padding: 5px 20px;
339
+ font-size: 0.75rem;
340
+ color: #666;
341
+ }
342
+
343
+ .preview {
344
+ display: grid;
345
+ grid-template-rows: auto 1fr;
346
+ justify-content: center;
347
+ justify-items: center;
348
+ height: 100%;
349
+ margin: 0 20px;
350
+ }
351
+
352
+ .layout {
353
+ height: 100%;
354
+ display: grid;
355
+ grid-template-columns: 280px 1fr;
356
+ grid-template-rows: 1fr;
357
+ }
358
+
359
+ .sidebar {
360
+ margin: 8px;
361
+ border-radius: 8px;
362
+ background: #fff;
363
+ padding: 1rem 0.75rem;
364
+ box-sizing: border-box;
365
+ border-right: 1px solid #e0e0e0;
366
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
367
+ }
368
+
369
+ .sidebar-header {
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: 1rem;
373
+ margin-bottom: 1rem;
374
+ }
375
+
376
+ .download-button {
377
+ appearance: none;
378
+ background: #007bff;
379
+ color: white;
380
+ border: none;
381
+ border-radius: 6px;
382
+ padding: 0.5rem 1rem;
383
+ font-size: 0.75rem;
384
+ font-weight: 500;
385
+ cursor: pointer;
386
+ transition: all 0.2s;
387
+ text-transform: uppercase;
388
+ letter-spacing: 0.5px;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ gap: 0.5rem;
393
+ min-height: 2.25rem;
394
+ }
395
+
396
+ .download-button:hover:not(:disabled) {
397
+ background: #0056b3;
398
+ transform: translateY(-1px);
399
+ box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
400
+ }
401
+
402
+ .download-button:disabled {
403
+ opacity: 0.8;
404
+ cursor: not-allowed;
405
+ transform: none;
406
+ box-shadow: none;
407
+ background: #6c757d;
408
+ }
409
+
410
+ @keyframes spin {
411
+ 0% { transform: rotate(0deg); }
412
+ 100% { transform: rotate(360deg); }
413
+ }
414
+
415
+ @media (prefers-color-scheme: dark) {
416
+ .sidebar {
417
+ background-color: hsl(258, 4%, 14%);
418
+ border-right: 1px solid #000000;
419
+ }
420
+ }
421
+
422
+ a[href] {
423
+ color: inherit;
424
+ text-decoration: none;
425
+ }
426
+
427
+ .button,
428
+ button {
429
+ display: inline-block;
430
+ box-sizing: border-box;
431
+ appearance: none;
432
+ width: 100%;
433
+ background: transparent;
434
+ color: inherit;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: space-between;
438
+ padding: 0px 4px 0px 6px;
439
+ border: 1px solid #ddd;
440
+ border-radius: 8px;
441
+ margin-bottom: 0.5rem;
442
+ transition: all 0.2s;
443
+ }
444
+
445
+ .button a[href^="#"] {
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 0.75rem;
449
+ flex: 1;
450
+ min-height: 2rem;
451
+ }
452
+
453
+ .template-number {
454
+ font-size: 0.75rem;
455
+ opacity: 0.6;
456
+ font-weight: 600;
457
+ min-width: 1.5rem;
458
+ text-align: center;
459
+ }
460
+
461
+ .template-name {
462
+ flex: 1;
463
+ font-size: 0.8rem;
464
+ }
465
+
466
+ .button.active {
467
+ background: #007bff;
468
+ color: #fff;
469
+ border-color: #007bff;
470
+ }
471
+
472
+ .button.active .template-number {
473
+ opacity: 0.9;
474
+ }
475
+
476
+ .template-link {
477
+ font-size: 0.65rem;
478
+ padding: 8px 10px;
479
+ border-radius: 8px;
480
+ transition: all 0.2s;
481
+ }
482
+
483
+ .template-link:hover {
484
+ background: #8282823e;
485
+ }
486
+
487
+ .button:hover,
488
+ button:hover {
489
+ box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.04);
490
+ }
491
+
492
+ .button:active,
493
+ button:active {
494
+ transition: none;
495
+ box-shadow: none;
496
+ }
497
+
498
+ @media (prefers-color-scheme: dark) {
499
+ button {
500
+ border: 1px solid #4b4b4b;
501
+ }
502
+
503
+ button:hover {
504
+ background: #3f3f3f;
505
+ }
506
+
507
+ .button.active {
508
+ background: #0066cc;
509
+ border-color: #0066cc;
510
+ }
511
+
512
+ .download-button:hover:not(:disabled) {
513
+ background: #0056b3;
514
+ }
515
+
516
+ .download-button:disabled {
517
+ background: #6c757d;
518
+ }
519
+ }
520
+ </style>
521
+ </body>
522
+ </html>
package/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import * as vite from "vite";
5
+ import child_process from "node:child_process";
6
+
7
+ /**
8
+ * @param {object} options
9
+ * @param {string | ((name: string) => string)} options.filename - The filename to use for the output files. Use `[name]` to insert the page name.
10
+ * @returns {import('astro').AstroIntegration}
11
+ */
12
+ export default function email(options) {
13
+ return {
14
+ name: "email",
15
+ configureServer(server) {
16
+ server.middlewares.use((req, res, next) => {
17
+ const url = req.url;
18
+ if (!url) return next();
19
+
20
+ // Extract the filename from any sub-path
21
+ const segments = url.split("/").filter(Boolean);
22
+ const filename = segments[segments.length - 1];
23
+
24
+ // Only handle requests for files with extensions
25
+ if (filename?.includes(".")) {
26
+ const publicPath = path.join(process.cwd(), "public", filename);
27
+
28
+ if (fs.existsSync(publicPath)) {
29
+ const stat = fs.statSync(publicPath);
30
+ if (stat.isFile()) {
31
+ res.setHeader("Content-Length", stat.size);
32
+ const stream = fs.createReadStream(publicPath);
33
+ stream.pipe(res);
34
+ return;
35
+ }
36
+ }
37
+ }
38
+
39
+ next();
40
+ });
41
+ },
42
+ hooks: {
43
+ "astro:config:setup": ({
44
+ command,
45
+ updateConfig,
46
+ injectRoute,
47
+ addRenderer,
48
+ addDevToolbarApp,
49
+ }) => {
50
+ addRenderer({
51
+ name: "astro-html/react",
52
+ serverEntrypoint: "@astrojs/react/server.js",
53
+ });
54
+
55
+ updateConfig({
56
+ vite: {
57
+ plugins: [
58
+ react(),
59
+ optionsPlugin(false), // required for @astrojs/react/server.js to work.
60
+ ],
61
+ build: {
62
+ assetsInlineLimit: 1024 * 20, // inline all assets
63
+ },
64
+ },
65
+ compressHTML: false,
66
+ build: {
67
+ format: "file",
68
+ },
69
+ });
70
+
71
+ injectRoute({
72
+ pattern: "/",
73
+ entrypoint: "astro-html/index.astro",
74
+ });
75
+
76
+ addDevToolbarApp("astro-html/toolbar.js");
77
+ },
78
+ "astro:build:done": async ({ dir, pages }) => {
79
+ if (options.filename) {
80
+ const manifest = [];
81
+
82
+ for (const page of pages) {
83
+ const pathname = page.pathname;
84
+ const basename = pathname.split(".")[0];
85
+
86
+ if (!pathname) continue; // index has none
87
+
88
+ const name =
89
+ typeof options.filename === "string"
90
+ ? options.filename.replace("[name]", basename)
91
+ : options.filename(basename);
92
+
93
+ fs.renameSync(
94
+ path.resolve(dir.pathname, `${pathname}.html`),
95
+ path.resolve(dir.pathname, name),
96
+ );
97
+
98
+ const files = [name];
99
+
100
+ // Add all public files
101
+ const publicDir = path.resolve("public");
102
+ if (fs.existsSync(publicDir)) {
103
+ const publicFiles = fs.readdirSync(publicDir);
104
+ for (const publicFile of publicFiles) {
105
+ const publicFilePath = path.resolve(publicDir, publicFile);
106
+ if (fs.statSync(publicFilePath).isFile()) {
107
+ files.push(publicFile);
108
+ }
109
+ }
110
+ }
111
+
112
+ // check if an .jpg file with the same name exists and copy it to dist too.
113
+ const jpgPath = path.resolve("src/pages", `${pathname}.jpg`);
114
+ if (fs.existsSync(jpgPath)) {
115
+ const newJpgName =
116
+ typeof options.filename === "string"
117
+ ? options.filename
118
+ .replace("[name]", basename)
119
+ .replace(/\.[^.]+$/, ".jpg")
120
+ : options.filename(basename).replace(/\.[^.]+$/, ".jpg");
121
+ fs.copyFileSync(jpgPath, path.resolve(dir.pathname, newJpgName));
122
+ files.push(newJpgName);
123
+ }
124
+
125
+ manifest.push({
126
+ name: pathname,
127
+ files: files,
128
+ });
129
+ }
130
+
131
+ fs.writeFileSync(
132
+ path.resolve(dir.pathname, "manifest.json"),
133
+ JSON.stringify(manifest, null, 2),
134
+ );
135
+ }
136
+ },
137
+ "astro:server:setup": ({ server }) => {
138
+ server.ws.on("astro-dev-toolbar:astro-html:toggled", (data) => {
139
+ if (data.state === true) {
140
+ child_process.exec("astro build");
141
+ }
142
+ });
143
+ },
144
+ },
145
+ };
146
+ }
147
+
148
+ /** @type {(xperimentalReactChildren?: boolean) => vite.Plugin} */
149
+ function optionsPlugin(experimentalReactChildren) {
150
+ const virtualModule = "astro:react:opts";
151
+ const virtualModuleId = `\0${virtualModule}`;
152
+ return {
153
+ name: "astro-html/react:opts",
154
+ resolveId(id) {
155
+ if (id === virtualModule) {
156
+ return virtualModuleId;
157
+ }
158
+ },
159
+ load(id) {
160
+ if (id === virtualModuleId) {
161
+ return {
162
+ code: `export default {
163
+ experimentalReactChildren: ${JSON.stringify(experimentalReactChildren)}
164
+ }`,
165
+ };
166
+ }
167
+ },
168
+ };
169
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "astro-html",
3
+ "description": "Integration for Astro to Generate HTML emails with React (react-email)",
4
+ "type": "module",
5
+ "author": {
6
+ "name": "Tim Havlicek",
7
+ "email": "tim.h4vlicek@gmail.com"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/luckydye/astro-html.git"
12
+ },
13
+ "version": "0.19.0",
14
+ "main": "index.js",
15
+ "scripts": {
16
+ "dev": "astro dev",
17
+ "build": "astro build"
18
+ },
19
+ "dependencies": {
20
+ "@astrojs/react": "^4.4.0",
21
+ "@vitejs/plugin-react": "^4.7.0",
22
+ "@react-email/components": "^0.5.6"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.17",
26
+ "@types/react": "^18.2.55",
27
+ "@types/react-dom": "^18.2.19"
28
+ },
29
+ "files": [
30
+ "Mail.astro",
31
+ "Banner.astro",
32
+ "index.astro",
33
+ "index.js",
34
+ "toolbar.js"
35
+ ]
36
+ }
package/toolbar.js ADDED
@@ -0,0 +1,32 @@
1
+ const icon = `
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15" height="15" viewBox="0 0 15 15">
3
+ <defs>
4
+ <clipPath id="clip-path">
5
+ <rect id="Rectangle_1688" data-name="Rectangle 1688" width="15" height="15" transform="translate(11726 152)" fill="none"/>
6
+ </clipPath>
7
+ </defs>
8
+ <g id="Mask_Group_4" data-name="Mask Group 4" transform="translate(-11726 -152)" clip-path="url(#clip-path)">
9
+ <g fill="#fff" id="Group_423" data-name="Group 423" transform="translate(0.5)">
10
+ <path id="Path_4183" data-name="Path 4183" d="M14.6,14H7.4A2.4,2.4,0,0,1,5,11.6V8.018H6.2V11.6a1.2,1.2,0,0,0,1.2,1.2h7.2a1.2,1.2,0,0,0,1.2-1.2V8.018H17V11.6A2.4,2.4,0,0,1,14.6,14Z" transform="translate(11722 152)"/>
11
+ <path id="Path_4184" data-name="Path 4184" d="M3.385,9.591V1.927L.963,4.349a.564.564,0,0,1-.8-.8L3.54.176a.564.564,0,0,1,.684-.1h0l.01.006,0,0,.007,0,.006,0,0,0L4.264.1h0a.567.567,0,0,1,.083.069L7.733,3.55a.564.564,0,0,1-.8.8L4.513,1.926V9.591a.564.564,0,1,1-1.128,0Z" transform="translate(11736.949 162.579) rotate(180)"/>
12
+ </g>
13
+ </g>
14
+ </svg>
15
+ `;
16
+
17
+ export default {
18
+ id: 'astro-html',
19
+ name: 'Export emails',
20
+ icon,
21
+ init(canvas, eventTarget) {
22
+ eventTarget.addEventListener('app-toggled', async (event) => {
23
+ if (event.detail.state) {
24
+ setTimeout(() => {eventTarget.dispatchEvent(
25
+ new CustomEvent('toggle-app', {
26
+ detail: { state: false },
27
+ })
28
+ )}, 150)
29
+ }
30
+ })
31
+ }
32
+ }