react-email 3.0.7 → 4.0.0-alpha.1

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +10 -4
  3. package/dist/cli/index.mjs +9 -3
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +14 -12
  6. package/dist/preview/.next/build-manifest.json +3 -3
  7. package/dist/preview/.next/cache/.rscinfo +1 -1
  8. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  14. package/dist/preview/.next/next-server.js.nft.json +1 -1
  15. package/dist/preview/.next/prerender-manifest.json +1 -1
  16. package/dist/preview/.next/required-server-files.json +1 -1
  17. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  20. package/dist/preview/.next/server/app/page.js +1 -1
  21. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  22. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  23. package/dist/preview/.next/server/app/preview/[...slug]/page.js +5 -5
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  25. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  26. package/dist/preview/.next/server/chunks/196.js +1 -1
  27. package/dist/preview/.next/server/chunks/282.js +15 -0
  28. package/dist/preview/.next/server/chunks/667.js +1 -0
  29. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  30. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  31. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  32. package/dist/preview/.next/server/pages/500.html +1 -1
  33. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  34. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  35. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  36. package/dist/preview/.next/static/chunks/207-7ab46c2d84f60fed.js +1 -0
  37. package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +1 -0
  38. package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
  39. package/dist/preview/.next/static/chunks/app/layout-f1bad3fcfbc7eb6b.js +1 -0
  40. package/dist/preview/.next/static/chunks/app/page-800163ba6c6d943d.js +1 -0
  41. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +1 -0
  42. package/dist/preview/.next/static/chunks/{main-app-771a0fc4ad5aa154.js → main-app-d1b0aa870bcfb13e.js} +1 -1
  43. package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +3 -0
  44. package/dist/preview/.next/trace +22 -21
  45. package/dist/preview/.next/types/app/layout.ts +1 -1
  46. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  47. package/package.json +8 -2
  48. package/src/actions/email-validation/check-images.spec.tsx +90 -0
  49. package/src/actions/email-validation/check-images.ts +142 -0
  50. package/src/actions/email-validation/check-links.spec.tsx +92 -0
  51. package/src/actions/email-validation/check-links.ts +91 -0
  52. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +22 -0
  53. package/src/actions/email-validation/get-line-and-column-from-index.ts +43 -0
  54. package/src/actions/email-validation/quick-fetch.ts +12 -0
  55. package/src/animated-icons-data/help.json +1082 -0
  56. package/src/animated-icons-data/link.json +1309 -0
  57. package/src/animated-icons-data/load.json +443 -0
  58. package/src/animated-icons-data/mail.json +1320 -0
  59. package/src/app/globals.css +0 -24
  60. package/src/app/layout.tsx +6 -2
  61. package/src/app/page.tsx +8 -9
  62. package/src/app/preview/[...slug]/page.tsx +1 -0
  63. package/src/app/preview/[...slug]/preview.tsx +3 -3
  64. package/src/app/preview/[...slug]/rendering-error.tsx +6 -6
  65. package/src/components/button.tsx +53 -42
  66. package/src/components/code-container.tsx +6 -6
  67. package/src/components/code-snippet.tsx +11 -0
  68. package/src/components/code.tsx +4 -4
  69. package/src/components/icons/icon-button.tsx +1 -1
  70. package/src/components/icons/icon-circle-check.tsx +21 -0
  71. package/src/components/icons/icon-circle-close.tsx +17 -0
  72. package/src/components/icons/icon-circle-warning.tsx +17 -0
  73. package/src/components/icons/icon-email.tsx +18 -0
  74. package/src/components/icons/icon-image.tsx +19 -0
  75. package/src/components/icons/icon-link.tsx +14 -0
  76. package/src/components/icons/icon-stamp.tsx +14 -0
  77. package/src/components/send.tsx +9 -9
  78. package/src/components/shell.tsx +32 -34
  79. package/src/components/sidebar/checking-results.tsx +150 -0
  80. package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx} +19 -15
  81. package/src/components/sidebar/{sidebar-directory.tsx → file-tree-directory.tsx} +9 -10
  82. package/src/components/sidebar/file-tree.tsx +31 -0
  83. package/src/components/sidebar/image-checker.tsx +161 -0
  84. package/src/components/sidebar/link-checker.tsx +151 -0
  85. package/src/components/sidebar/sidebar.tsx +344 -22
  86. package/src/components/tooltip-content.tsx +2 -2
  87. package/src/components/topbar.tsx +13 -16
  88. package/src/hooks/use-icon-animation.ts +41 -0
  89. package/tsconfig.json +1 -0
  90. package/dist/preview/.next/server/chunks/693.js +0 -1
  91. package/dist/preview/.next/server/chunks/720.js +0 -10
  92. package/dist/preview/.next/static/chunks/12-b9450aa0845e7574.js +0 -1
  93. package/dist/preview/.next/static/chunks/154-f7f86c8589140c56.js +0 -1
  94. package/dist/preview/.next/static/chunks/app/layout-6d33e2ffcffd58d4.js +0 -1
  95. package/dist/preview/.next/static/chunks/app/page-43a07e4b8c5c0840.js +0 -1
  96. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-71202390d5f9a34b.js +0 -1
  97. package/dist/preview/.next/static/css/a34876a6c565fff8.css +0 -3
  98. /package/dist/preview/.next/static/{RZga3-2qKYa2RLg-hxunV → Mn2FuRztLqr32yO8CKHi9}/_buildManifest.js +0 -0
  99. /package/dist/preview/.next/static/{RZga3-2qKYa2RLg-hxunV → Mn2FuRztLqr32yO8CKHi9}/_ssgManifest.js +0 -0
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email.git/canary/packages/react-email/src/app/layout.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/chore-1/packages/react-email/src/app/layout.tsx
2
2
  import * as entry from '../../../src/app/layout.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/gabriel/Projects/Resend/react-email.git/canary/packages/react-email/src/app/preview/[...slug]/page.tsx
1
+ // File: /home/gabriel/Projects/Resend/react-email.git/chore-1/packages/react-email/src/app/preview/[...slug]/page.tsx
2
2
  import * as entry from '../../../../../src/app/preview/[...slug]/page.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "3.0.7",
3
+ "version": "4.0.0-alpha.1",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -19,8 +19,8 @@
19
19
  "node": ">=18.0.0"
20
20
  },
21
21
  "dependencies": {
22
- "@babel/core": "7.24.5",
23
22
  "@babel/parser": "7.24.5",
23
+ "@babel/traverse": "7.25.6",
24
24
  "chalk": "4.1.2",
25
25
  "chokidar": "4.0.3",
26
26
  "commander": "11.1.0",
@@ -39,10 +39,12 @@
39
39
  "@radix-ui/react-collapsible": "1.1.0",
40
40
  "@radix-ui/react-popover": "1.1.1",
41
41
  "@radix-ui/react-slot": "1.1.0",
42
+ "@radix-ui/react-tabs": "1.1.1",
42
43
  "@radix-ui/react-toggle-group": "1.1.0",
43
44
  "@radix-ui/react-tooltip": "1.1.2",
44
45
  "@swc/core": "1.4.15",
45
46
  "@types/babel__core": "7.20.5",
47
+ "@types/babel__traverse": "*",
46
48
  "@types/fs-extra": "11.0.1",
47
49
  "@types/mime-types": "2.1.4",
48
50
  "@types/node": "22.10.2",
@@ -54,7 +56,11 @@
54
56
  "autoprefixer": "10.4.20",
55
57
  "clsx": "2.1.0",
56
58
  "framer-motion": "12.0.0-alpha.2",
59
+ "@lottiefiles/dotlottie-react": "0.12.3",
60
+ "node-html-parser": "6.1.13",
57
61
  "postcss": "8.4.40",
62
+ "prettier-plugin-tailwindcss": "0.6.6",
63
+ "pretty-bytes": "6.1.1",
58
64
  "prism-react-renderer": "2.1.0",
59
65
  "module-punycode": "npm:punycode@2.3.1",
60
66
  "react": "^19",
@@ -0,0 +1,90 @@
1
+ import { render } from '@react-email/render';
2
+ import * as React from 'react';
3
+ import { type ImageCheckingResult, checkImages } from './check-images';
4
+
5
+ test('checkImages()', async () => {
6
+ expect(
7
+ await checkImages(
8
+ await render(
9
+ <div>
10
+ {/* biome-ignore lint/a11y/useAltText: This is intentional to test the checking for accessibility */}
11
+ <img src="https://resend.com/static/brand/resend-icon-white.png" />,
12
+ <img src="/static/codepen-challengers.png" alt="codepen challenges" />
13
+ ,
14
+ </div>,
15
+ ),
16
+ 'https://demo.react.email',
17
+ ),
18
+ ).toEqual([
19
+ {
20
+ source: 'https://resend.com/static/brand/resend-icon-white.png',
21
+ checks: [
22
+ {
23
+ passed: false,
24
+ type: 'accessibility',
25
+ metadata: {
26
+ alt: undefined,
27
+ },
28
+ },
29
+ {
30
+ passed: true,
31
+ type: 'syntax',
32
+ },
33
+ {
34
+ passed: true,
35
+ type: 'security',
36
+ },
37
+ {
38
+ passed: true,
39
+ type: 'fetch_attempt',
40
+ metadata: {
41
+ fetchStatusCode: 200,
42
+ },
43
+ },
44
+ {
45
+ passed: true,
46
+ type: 'image_size',
47
+ metadata: {
48
+ byteCount: 23_138,
49
+ },
50
+ },
51
+ ],
52
+ status: 'warning',
53
+ },
54
+ {
55
+ checks: [
56
+ {
57
+ metadata: {
58
+ alt: 'codepen challenges',
59
+ },
60
+ passed: true,
61
+ type: 'accessibility',
62
+ },
63
+ {
64
+ passed: true,
65
+ type: 'syntax',
66
+ },
67
+ {
68
+ passed: true,
69
+ type: 'security',
70
+ },
71
+ {
72
+ metadata: {
73
+ fetchStatusCode: 200,
74
+ },
75
+ passed: true,
76
+ type: 'fetch_attempt',
77
+ },
78
+ {
79
+ metadata: {
80
+ byteCount: 111_922,
81
+ },
82
+ passed: true,
83
+ type: 'image_size',
84
+ },
85
+ ],
86
+ source: '/static/codepen-challengers.png',
87
+ status: 'success',
88
+ },
89
+ ] satisfies ImageCheckingResult[]);
90
+ });
@@ -0,0 +1,142 @@
1
+ 'use server';
2
+
3
+ import type { IncomingMessage } from 'node:http';
4
+ import { headers } from 'next/headers';
5
+ import { parse } from 'node-html-parser';
6
+ import { quickFetch } from './quick-fetch';
7
+
8
+ type Check = { passed: boolean } & (
9
+ | {
10
+ type: 'accessibility';
11
+ metadata: {
12
+ alt: string | undefined;
13
+ };
14
+ }
15
+ | {
16
+ type: 'fetch_attempt';
17
+ metadata: {
18
+ fetchStatusCode: number | undefined;
19
+ };
20
+ }
21
+ | {
22
+ type: 'image_size';
23
+ metadata: {
24
+ byteCount: number | undefined;
25
+ };
26
+ }
27
+ | {
28
+ type: 'syntax';
29
+ }
30
+ | {
31
+ type: 'security';
32
+ }
33
+ );
34
+
35
+ export interface ImageCheckingResult {
36
+ status: 'success' | 'warning' | 'error';
37
+ source: string;
38
+ checks: Check[];
39
+ }
40
+
41
+ const getResponseSizeInBytes = async (res: IncomingMessage) => {
42
+ let totalBytes = 0;
43
+ for await (const chunk of res) {
44
+ totalBytes += chunk.byteLength;
45
+ }
46
+ return totalBytes;
47
+ };
48
+
49
+ export const checkImages = async (code: string, base: string) => {
50
+ const ast = parse(code);
51
+
52
+ const imageCheckingResults: ImageCheckingResult[] = [];
53
+
54
+ const images = ast.querySelectorAll('img');
55
+ for await (const image of images) {
56
+ const rawSource = image.attributes.src;
57
+ if (!rawSource) continue;
58
+ if (imageCheckingResults.some((result) => result.source === rawSource))
59
+ continue;
60
+
61
+ const source = rawSource?.startsWith('/')
62
+ ? `${base}${rawSource}`
63
+ : rawSource;
64
+
65
+ const result: ImageCheckingResult = {
66
+ source: rawSource,
67
+ status: 'success',
68
+ checks: [],
69
+ };
70
+
71
+ const alt = image.attributes.alt;
72
+ result.checks.push({
73
+ passed: alt !== undefined,
74
+ type: 'accessibility',
75
+ metadata: {
76
+ alt,
77
+ },
78
+ });
79
+ if (alt === undefined) {
80
+ result.status = 'warning';
81
+ }
82
+
83
+ try {
84
+ const url = new URL(source);
85
+ result.checks.push({
86
+ passed: true,
87
+ type: 'syntax',
88
+ });
89
+
90
+ if (source.startsWith('https://')) {
91
+ result.checks.push({
92
+ passed: true,
93
+ type: 'security',
94
+ });
95
+ } else {
96
+ result.checks.push({
97
+ passed: false,
98
+ type: 'security',
99
+ });
100
+ result.status = 'warning';
101
+ }
102
+
103
+ const res = await quickFetch(url);
104
+ const hasSucceeded = res.statusCode?.toString().startsWith('2') ?? false;
105
+
106
+ result.checks.push({
107
+ type: 'fetch_attempt',
108
+ passed: hasSucceeded,
109
+ metadata: {
110
+ fetchStatusCode: res.statusCode,
111
+ },
112
+ });
113
+ if (!hasSucceeded) {
114
+ result.status = res.statusCode?.toString().startsWith('3')
115
+ ? 'warning'
116
+ : 'error';
117
+ }
118
+
119
+ const responseSizeBytes = await getResponseSizeInBytes(res);
120
+ result.checks.push({
121
+ type: 'image_size',
122
+ passed: responseSizeBytes < 1_048_576, // 1024 x 1024 bytes
123
+ metadata: {
124
+ byteCount: responseSizeBytes,
125
+ },
126
+ });
127
+ if (responseSizeBytes > 1_048_576) {
128
+ result.status = 'warning';
129
+ }
130
+ } catch (exception) {
131
+ result.checks.push({
132
+ passed: false,
133
+ type: 'syntax',
134
+ });
135
+ result.status = 'error';
136
+ }
137
+
138
+ imageCheckingResults.push(result);
139
+ }
140
+
141
+ return imageCheckingResults;
142
+ };
@@ -0,0 +1,92 @@
1
+ import { render } from '@react-email/render';
2
+ import * as React from 'react';
3
+ import { type LinkCheckingResult, checkLinks } from './check-links';
4
+
5
+ test('checkLinks()', async () => {
6
+ expect(
7
+ await checkLinks(
8
+ await render(
9
+ <div>
10
+ <a href="/">Root</a>
11
+ <a href="https://resend.com">Resend</a>
12
+ <a href="https://notion.so">Notion</a>
13
+ <a href="http://example.com">Example unsafe</a>
14
+ </div>,
15
+ ),
16
+ ),
17
+ ).toEqual([
18
+ {
19
+ status: 'error',
20
+ checks: [
21
+ {
22
+ type: 'syntax',
23
+ passed: false,
24
+ },
25
+ ],
26
+ link: '/',
27
+ },
28
+ {
29
+ status: 'success',
30
+ checks: [
31
+ {
32
+ type: 'syntax',
33
+ passed: true,
34
+ },
35
+ {
36
+ type: 'security',
37
+ passed: true,
38
+ },
39
+ {
40
+ type: 'fetch_attempt',
41
+ passed: true,
42
+ metadata: {
43
+ fetchStatusCode: 200,
44
+ },
45
+ },
46
+ ],
47
+ link: 'https://resend.com',
48
+ },
49
+ {
50
+ status: 'warning',
51
+ checks: [
52
+ {
53
+ type: 'syntax',
54
+ passed: true,
55
+ },
56
+ {
57
+ type: 'security',
58
+ passed: true,
59
+ },
60
+ {
61
+ type: 'fetch_attempt',
62
+ metadata: {
63
+ fetchStatusCode: 301,
64
+ },
65
+ passed: false,
66
+ },
67
+ ],
68
+ link: 'https://notion.so',
69
+ },
70
+ {
71
+ status: 'warning',
72
+ checks: [
73
+ {
74
+ type: 'syntax',
75
+ passed: true,
76
+ },
77
+ {
78
+ type: 'security',
79
+ passed: false,
80
+ },
81
+ {
82
+ type: 'fetch_attempt',
83
+ metadata: {
84
+ fetchStatusCode: 200,
85
+ },
86
+ passed: true,
87
+ },
88
+ ],
89
+ link: 'http://example.com',
90
+ },
91
+ ] satisfies LinkCheckingResult[]);
92
+ });
@@ -0,0 +1,91 @@
1
+ 'use server';
2
+
3
+ import { parse } from 'node-html-parser';
4
+ import { quickFetch } from './quick-fetch';
5
+
6
+ type Check = { passed: boolean } & (
7
+ | {
8
+ type: 'fetch_attempt';
9
+ metadata: {
10
+ fetchStatusCode: number | undefined;
11
+ };
12
+ }
13
+ | {
14
+ type: 'syntax';
15
+ }
16
+ | {
17
+ type: 'security';
18
+ }
19
+ );
20
+
21
+ export interface LinkCheckingResult {
22
+ status: 'success' | 'warning' | 'error';
23
+ link: string;
24
+ checks: Check[];
25
+ }
26
+
27
+ export const checkLinks = async (code: string) => {
28
+ const ast = parse(code);
29
+
30
+ const linkCheckingResults: LinkCheckingResult[] = [];
31
+
32
+ const anchors = ast.querySelectorAll('a');
33
+ for await (const anchor of anchors) {
34
+ const link = anchor.attributes.href;
35
+ if (!link) continue;
36
+ if (linkCheckingResults.some((result) => result.link === link)) continue;
37
+ if (link.startsWith('mailto:')) continue;
38
+
39
+ const result: LinkCheckingResult = {
40
+ link,
41
+ status: 'success',
42
+ checks: [],
43
+ };
44
+
45
+ try {
46
+ const url = new URL(link);
47
+ result.checks.push({
48
+ passed: true,
49
+ type: 'syntax',
50
+ });
51
+
52
+ if (link.startsWith('https://')) {
53
+ result.checks.push({
54
+ passed: true,
55
+ type: 'security',
56
+ });
57
+ } else {
58
+ result.checks.push({
59
+ passed: false,
60
+ type: 'security',
61
+ });
62
+ result.status = 'warning';
63
+ }
64
+
65
+ const res = await quickFetch(url);
66
+ const hasSucceeded = res.statusCode?.toString().startsWith('2') ?? false;
67
+ result.checks.push({
68
+ type: 'fetch_attempt',
69
+ passed: hasSucceeded,
70
+ metadata: {
71
+ fetchStatusCode: res.statusCode,
72
+ },
73
+ });
74
+ if (!hasSucceeded) {
75
+ result.status = res.statusCode?.toString().startsWith('3')
76
+ ? 'warning'
77
+ : 'error';
78
+ }
79
+ } catch (exception) {
80
+ result.checks.push({
81
+ passed: false,
82
+ type: 'syntax',
83
+ });
84
+ result.status = 'error';
85
+ }
86
+
87
+ linkCheckingResults.push(result);
88
+ }
89
+
90
+ return linkCheckingResults;
91
+ };
@@ -0,0 +1,22 @@
1
+ import { getLineAndColumnFromIndex } from './get-line-and-column-from-index';
2
+
3
+ test('getLineAndColumnFromIndex()', () => {
4
+ const code = `import { SomethingElse } from 'somewhere';
5
+
6
+ const myConstant = 'what';
7
+
8
+ const MyComponent = () => {
9
+ return <SomethingElse>
10
+ <div>
11
+ <a>Hello World!</a>{' '}
12
+ {myConstant}
13
+ </div>
14
+ </SomethingElse>;
15
+ }`;
16
+ const [line, column] = getLineAndColumnFromIndex(
17
+ code,
18
+ code.indexOf('Hello World!'),
19
+ );
20
+ expect(line).toBe(8);
21
+ expect(column).toBe(10);
22
+ });
@@ -0,0 +1,43 @@
1
+ const splitByLines = (text: string) => {
2
+ const properSplit: string[] = [];
3
+ const unevenSplit = text.split(/(?<eol>\n|\r|\r\n)/);
4
+
5
+ for (const [i, segment] of unevenSplit.entries()) {
6
+ if (i % 2 === 0) {
7
+ let segmentToInsert = segment;
8
+ if (i + 1 < unevenSplit.length) {
9
+ segmentToInsert += unevenSplit[i + 1];
10
+ }
11
+ properSplit.push(segmentToInsert);
12
+ }
13
+ }
14
+
15
+ return properSplit;
16
+ };
17
+
18
+ export const getLineAndColumnFromIndex = (
19
+ code: string,
20
+ index: number,
21
+ ): [line: number, column: number] => {
22
+ const lines = splitByLines(code);
23
+
24
+ let lineNumber = 1;
25
+ const line = () => {
26
+ const l = lines[lineNumber - 1];
27
+ if (l === undefined)
28
+ throw new Error(
29
+ 'Could not find the line for a specific index in the code',
30
+ { cause: { lines, lineNumber, index } },
31
+ );
32
+ return l;
33
+ };
34
+ let charactersUpToLineStart = 0;
35
+ while (charactersUpToLineStart + line().length < index) {
36
+ charactersUpToLineStart += line().length;
37
+ lineNumber++;
38
+ }
39
+
40
+ const columnNumber = index - charactersUpToLineStart + 1;
41
+
42
+ return [lineNumber, columnNumber];
43
+ };
@@ -0,0 +1,12 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import http from 'node:http';
3
+ import https from 'node:https';
4
+
5
+ export const quickFetch = (url: URL) => {
6
+ return new Promise<IncomingMessage>((resolve) => {
7
+ const caller = url.protocol === 'https:' ? https : http;
8
+ caller.get(url, (res) => {
9
+ resolve(res);
10
+ });
11
+ });
12
+ };