@useprint/preview 0.1.1 → 0.1.3

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 (228) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +5 -5
  3. package/.next/diagnostics/framework.json +1 -1
  4. package/.next/images-manifest.json +1 -0
  5. package/.next/next-minimal-server.js.nft.json +1 -1
  6. package/.next/next-server.js.nft.json +1 -1
  7. package/.next/prerender-manifest.json +3 -5
  8. package/.next/required-server-files.js +324 -0
  9. package/.next/required-server-files.json +11 -8
  10. package/.next/routes-manifest.json +1 -1
  11. package/.next/server/app/_global-error/page.js +3 -3
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +2 -2
  15. package/.next/server/app/_global-error.meta +2 -1
  16. package/.next/server/app/_global-error.rsc +2 -3
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +2 -3
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  20. package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
  21. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  22. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -8
  23. package/.next/server/app/_not-found/page.js +2 -2
  24. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  25. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  26. package/.next/server/app/favicon.ico/route.js +1 -1
  27. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  28. package/.next/server/app/favicon.ico.body +0 -0
  29. package/.next/server/app/page.js +2 -2
  30. package/.next/server/app/page.js.nft.json +1 -1
  31. package/.next/server/app/page_client-reference-manifest.js +1 -1
  32. package/.next/server/app/preview/[...slug]/page.js +102 -219
  33. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  34. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  35. package/.next/server/chunks/337.js +3 -0
  36. package/.next/server/chunks/377.js +1 -0
  37. package/.next/server/chunks/394.js +1 -0
  38. package/.next/server/chunks/445.js +22 -0
  39. package/.next/server/chunks/471.js +13 -0
  40. package/.next/server/chunks/95.js +4 -4
  41. package/.next/server/chunks/static/media/pdf.worker.min.3327dba1.mjs +8 -0
  42. package/.next/server/middleware-build-manifest.js +1 -1
  43. package/.next/server/next-font-manifest.js +1 -1
  44. package/.next/server/next-font-manifest.json +1 -1
  45. package/.next/server/pages/500.html +2 -2
  46. package/.next/server/server-reference-manifest.js +1 -1
  47. package/.next/server/server-reference-manifest.json +1 -1
  48. package/.next/server/webpack-runtime.js +1 -1
  49. package/.next/static/chunks/134-e1901aa4c8d7c5e4.js +1 -0
  50. package/.next/static/chunks/399-82b1df7ae5633759.js +1 -0
  51. package/.next/static/chunks/4bd1b696-096d35a2bd1da3af.js +1 -0
  52. package/.next/static/chunks/576-c76dd52ea4e7fe70.js +1 -0
  53. package/.next/static/chunks/794-96cfa93198fb4e82.js +2 -0
  54. package/.next/static/chunks/818-09ed5fa3e6aa2cec.js +9 -0
  55. package/.next/static/chunks/aad4249a-49e9609864e9e734.js +2 -0
  56. package/.next/static/chunks/app/layout-ab1f12a855e9ab71.js +1 -0
  57. package/.next/static/chunks/app/preview/[...slug]/page-21d4bab9478629da.js +1 -0
  58. package/.next/static/chunks/{c828b8b5-6f56633e9da941cb.js → c828b8b5-fe0be434537dde60.js} +2 -2
  59. package/.next/static/chunks/framework-2d4b35820b5e3e1e.js +1 -0
  60. package/.next/static/chunks/main-d2bae8996dc5307c.js +5 -0
  61. package/.next/static/chunks/{webpack-b8e17eaab329ee72.js → webpack-96cba10d3fe872ec.js} +1 -1
  62. package/.next/static/css/30dcbab3549c2dfe.css +4 -0
  63. package/.next/static/media/inter-cyrillic-400-normal.372704ff.woff2 +0 -0
  64. package/.next/static/media/inter-cyrillic-400-normal.a6b6ef6f.woff +0 -0
  65. package/.next/static/media/inter-cyrillic-500-normal.7c15bba8.woff2 +0 -0
  66. package/.next/static/media/inter-cyrillic-500-normal.b9f8c929.woff +0 -0
  67. package/.next/static/media/inter-cyrillic-600-normal.2f42892a.woff2 +0 -0
  68. package/.next/static/media/inter-cyrillic-600-normal.c3987adc.woff +0 -0
  69. package/.next/static/media/inter-cyrillic-700-normal.93eba3c3.woff +0 -0
  70. package/.next/static/media/inter-cyrillic-700-normal.e9e5b2dc.woff2 +0 -0
  71. package/.next/static/media/inter-cyrillic-ext-400-normal.2a31c04b.woff +0 -0
  72. package/.next/static/media/inter-cyrillic-ext-400-normal.f572b170.woff2 +0 -0
  73. package/.next/static/media/inter-cyrillic-ext-500-normal.5a6bb1da.woff +0 -0
  74. package/.next/static/media/inter-cyrillic-ext-500-normal.fe0d9b14.woff2 +0 -0
  75. package/.next/static/media/inter-cyrillic-ext-600-normal.ecbdecad.woff +0 -0
  76. package/.next/static/media/inter-cyrillic-ext-600-normal.f7b3c15b.woff2 +0 -0
  77. package/.next/static/media/inter-cyrillic-ext-700-normal.4b4022a6.woff +0 -0
  78. package/.next/static/media/inter-cyrillic-ext-700-normal.74b516d2.woff2 +0 -0
  79. package/.next/static/media/inter-greek-400-normal.cc58c11b.woff +0 -0
  80. package/.next/static/media/inter-greek-400-normal.d7020e3c.woff2 +0 -0
  81. package/.next/static/media/inter-greek-500-normal.d9a33207.woff +0 -0
  82. package/.next/static/media/inter-greek-500-normal.f41f43db.woff2 +0 -0
  83. package/.next/static/media/inter-greek-600-normal.4ec0c1c1.woff +0 -0
  84. package/.next/static/media/inter-greek-600-normal.cc532937.woff2 +0 -0
  85. package/.next/static/media/inter-greek-700-normal.5ec6c758.woff +0 -0
  86. package/.next/static/media/inter-greek-700-normal.97f0eeeb.woff2 +0 -0
  87. package/.next/static/media/inter-greek-ext-400-normal.4ce1df5d.woff2 +0 -0
  88. package/.next/static/media/inter-greek-ext-400-normal.88ede1ea.woff +0 -0
  89. package/.next/static/media/inter-greek-ext-500-normal.7a4aa726.woff +0 -0
  90. package/.next/static/media/inter-greek-ext-500-normal.cbd51e2d.woff2 +0 -0
  91. package/.next/static/media/inter-greek-ext-600-normal.089a95ee.woff +0 -0
  92. package/.next/static/media/inter-greek-ext-600-normal.1f33d317.woff2 +0 -0
  93. package/.next/static/media/inter-greek-ext-700-normal.31f1075d.woff +0 -0
  94. package/.next/static/media/inter-greek-ext-700-normal.827cd618.woff2 +0 -0
  95. package/.next/static/media/inter-latin-400-normal.2c7a775c.woff +0 -0
  96. package/.next/static/media/inter-latin-400-normal.ef6d3f52.woff2 +0 -0
  97. package/.next/static/media/inter-latin-500-normal.b7b43ace.woff2 +0 -0
  98. package/.next/static/media/inter-latin-500-normal.cb4c8ceb.woff +0 -0
  99. package/.next/static/media/inter-latin-600-normal.8fb1a964.woff2 +0 -0
  100. package/.next/static/media/inter-latin-600-normal.ce0f5f43.woff +0 -0
  101. package/.next/static/media/inter-latin-700-normal.953b7aa5.woff2 +0 -0
  102. package/.next/static/media/inter-latin-700-normal.9c21d4dc.woff +0 -0
  103. package/.next/static/media/inter-latin-ext-400-normal.32a25442.woff2 +0 -0
  104. package/.next/static/media/inter-latin-ext-400-normal.4edcaace.woff +0 -0
  105. package/.next/static/media/inter-latin-ext-500-normal.a19a84a6.woff +0 -0
  106. package/.next/static/media/inter-latin-ext-500-normal.d9b491de.woff2 +0 -0
  107. package/.next/static/media/inter-latin-ext-600-normal.38b075d8.woff2 +0 -0
  108. package/.next/static/media/inter-latin-ext-600-normal.49faa47a.woff +0 -0
  109. package/.next/static/media/inter-latin-ext-700-normal.93534b50.woff +0 -0
  110. package/.next/static/media/inter-latin-ext-700-normal.b63daa1a.woff2 +0 -0
  111. package/.next/static/media/inter-vietnamese-400-normal.a9dd2faf.woff +0 -0
  112. package/.next/static/media/inter-vietnamese-400-normal.de4fc44f.woff2 +0 -0
  113. package/.next/static/media/inter-vietnamese-500-normal.7c0a695f.woff2 +0 -0
  114. package/.next/static/media/inter-vietnamese-500-normal.a3a73b95.woff +0 -0
  115. package/.next/static/media/inter-vietnamese-600-normal.9d518599.woff2 +0 -0
  116. package/.next/static/media/inter-vietnamese-600-normal.c5ce3fcb.woff +0 -0
  117. package/.next/static/media/inter-vietnamese-700-normal.bc68b199.woff +0 -0
  118. package/.next/static/media/inter-vietnamese-700-normal.faf12809.woff2 +0 -0
  119. package/.next/static/media/pdf.worker.min.67f75a11.mjs +8 -0
  120. package/.next/trace +32 -32
  121. package/.next/trace-build +1 -1
  122. package/CHANGELOG.md +19 -0
  123. package/next.config.js +5 -1
  124. package/package.json +4 -3
  125. package/public/apple-touch-icon.png +0 -0
  126. package/public/favicon-96x96.png +0 -0
  127. package/public/favicon.ico +0 -0
  128. package/public/favicon.svg +3 -0
  129. package/public/site.webmanifest +21 -0
  130. package/public/web-app-manifest-192x192.png +0 -0
  131. package/public/web-app-manifest-512x512.png +0 -0
  132. package/readme.md +9 -27
  133. package/scripts/utils/default-seed/contracts/project-proposal.tsx +68 -0
  134. package/scripts/utils/default-seed/finance/payment-summary.tsx +55 -0
  135. package/scripts/utils/default-seed/reports/incident-report.tsx +49 -0
  136. package/scripts/utils/default-seed/updates/release-notes.tsx +58 -0
  137. package/src/actions/generate-pdf-from-html.ts +0 -8
  138. package/src/actions/render-document-by-path.tsx +0 -3
  139. package/src/app/favicon.ico +0 -0
  140. package/src/app/fonts.ts +0 -7
  141. package/src/app/globals.css +9 -0
  142. package/src/app/layout.tsx +13 -8
  143. package/src/app/page.tsx +14 -12
  144. package/src/app/preview/[...slug]/page.tsx +9 -61
  145. package/src/app/preview/[...slug]/preview.tsx +5 -6
  146. package/src/components/logo.tsx +21 -13
  147. package/src/components/pdf-viewer.tsx +61 -20
  148. package/src/components/print.tsx +37 -5
  149. package/src/components/shell.tsx +1 -2
  150. package/src/utils/__snapshots__/get-document-component.spec.ts.snap +25 -181
  151. package/src/utils/canidocument/ast/get-object-variables.ts +1 -1
  152. package/src/utils/canidocument/ast/get-used-style-properties.ts +1 -1
  153. package/src/utils/canidocument/get-compatibility-stats-for-entry.ts +1 -1
  154. package/src/utils/canidocument/tailwind/get-tailwind-config.ts +1 -1
  155. package/src/utils/canidocument/tailwind/get-tailwind-metadata.spec.ts +2 -2
  156. package/src/utils/canidocument/tailwind/get-tailwind-metadata.ts +1 -1
  157. package/src/utils/canidocument/types.ts +12 -0
  158. package/src/utils/contains-document-template.spec.ts +1 -1
  159. package/src/utils/esbuild/renderring-utilities-exporter.ts +248 -0
  160. package/src/utils/get-document-component.spec.ts +6 -4
  161. package/src/utils/get-document-component.ts +61 -10
  162. package/src/utils/get-documents-directory-metadata.spec.ts +18 -55
  163. package/src/utils/iframe-processing.spec.ts +13 -25
  164. package/src/utils/js-document-detection.spec.ts +11 -5
  165. package/src/utils/linting.ts +29 -33
  166. package/src/utils/margin-calculation.spec.ts +5 -5
  167. package/src/utils/testing/js-document-export-default.js +3 -0
  168. package/src/utils/testing/js-document-test.js +3 -0
  169. package/src/utils/testing/mdx-document-test.js +3 -0
  170. package/src/utils/testing/request-response-document.tsx +15 -0
  171. package/.next/server/chunks/134.js +0 -22
  172. package/.next/server/chunks/286.js +0 -27
  173. package/.next/server/chunks/489.js +0 -13
  174. package/.next/server/chunks/731.js +0 -1
  175. package/.next/server/chunks/static/media/pdf.worker.min.3fa7f4f1.mjs +0 -8
  176. package/.next/static/chunks/134-488429db271f07ce.js +0 -1
  177. package/.next/static/chunks/399-38210d4b480adc3a.js +0 -1
  178. package/.next/static/chunks/4bd1b696-f2999b4e17b2fd5f.js +0 -1
  179. package/.next/static/chunks/576-d2a7761fb68a178e.js +0 -1
  180. package/.next/static/chunks/794-de04f3efd45c53dd.js +0 -20
  181. package/.next/static/chunks/818-708fabc8a2438ba2.js +0 -9
  182. package/.next/static/chunks/aad4249a-8397af6f169152d5.js +0 -2
  183. package/.next/static/chunks/app/layout-38d6d79a819ab362.js +0 -1
  184. package/.next/static/chunks/app/preview/[...slug]/page-1097b557142aa22c.js +0 -1
  185. package/.next/static/chunks/framework-9fb1a057e8adf51d.js +0 -1
  186. package/.next/static/chunks/main-66c819c51fec57bf.js +0 -23
  187. package/.next/static/css/2f4a632dc94d1fb7.css +0 -3
  188. package/.next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  189. package/.next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  190. package/.next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  191. package/.next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  192. package/.next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  193. package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  194. package/.next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  195. package/.next/static/media/pdf.worker.min.539a26ca.mjs +0 -8
  196. package/.next/types/app/layout.ts +0 -86
  197. package/.next/types/cache-life.d.ts +0 -145
  198. package/scripts/utils/default-seed/auth/account-confirmation.tsx +0 -68
  199. package/scripts/utils/default-seed/auth/forgot-password.tsx +0 -71
  200. package/scripts/utils/default-seed/communications/payment-overdue.tsx +0 -82
  201. package/scripts/utils/default-seed/communications/team-invite.tsx +0 -78
  202. package/scripts/utils/default-seed/communications/webhooks-failed.tsx +0 -89
  203. package/scripts/utils/default-seed/feedback-request.tsx +0 -78
  204. package/scripts/utils/default-seed/marketing/changelog.tsx +0 -98
  205. package/src/actions/document-validation/__snapshots__/check-images.spec.tsx.snap +0 -84
  206. package/src/actions/document-validation/canidocument-data.ts +0 -85993
  207. package/src/actions/document-validation/check-compatibility.ts +0 -333
  208. package/src/actions/document-validation/check-images.spec.tsx +0 -21
  209. package/src/actions/document-validation/check-images.ts +0 -160
  210. package/src/actions/document-validation/check-links.spec.tsx +0 -113
  211. package/src/actions/document-validation/check-links.ts +0 -113
  212. package/src/actions/document-validation/get-code-location-from-ast-element.ts +0 -18
  213. package/src/actions/document-validation/quick-fetch.ts +0 -14
  214. package/src/animated-icons-data/help.json +0 -1082
  215. package/src/animated-icons-data/link.json +0 -1309
  216. package/src/animated-icons-data/mail.json +0 -1320
  217. package/src/components/toolbar/checking-results.tsx +0 -150
  218. package/src/components/toolbar/code-preview-line-link.tsx +0 -39
  219. package/src/components/toolbar/compatibility.tsx +0 -113
  220. package/src/components/toolbar/linter.tsx +0 -278
  221. package/src/components/toolbar/results-table.tsx +0 -0
  222. package/src/components/toolbar/results.tsx +0 -52
  223. package/src/components/toolbar/spam-assassin.tsx +0 -153
  224. package/src/components/toolbar/toolbar-button.tsx +0 -52
  225. package/src/components/toolbar/use-cached-state.ts +0 -33
  226. package/src/components/toolbar.tsx +0 -349
  227. /package/.next/static/{ZCqBZ8ZswPjX7K5Zalrpy → w_xYcS_2x1gNv-LaTlIOy}/_buildManifest.js +0 -0
  228. /package/.next/static/{ZCqBZ8ZswPjX7K5Zalrpy → w_xYcS_2x1gNv-LaTlIOy}/_ssgManifest.js +0 -0
@@ -3,7 +3,7 @@ import type {
3
3
  DocumentClient,
4
4
  Platform,
5
5
  SupportEntry,
6
- } from '../../actions/document-validation/check-compatibility';
6
+ } from './types';
7
7
 
8
8
  export type SupportStatus = DetailedSupportStatus['status'];
9
9
 
@@ -3,7 +3,7 @@ import type { Node } from '@babel/traverse';
3
3
  import traverse from '@babel/traverse';
4
4
  import * as esbuild from 'esbuild';
5
5
  import type { Config as TailwindOriginalConfig } from 'tailwindcss';
6
- import type { AST } from '../../../actions/document-validation/check-compatibility';
6
+ import type { AST } from '../types';
7
7
  import { isErr } from '../../result';
8
8
  import { runBundledCode } from '../../run-bundled-code';
9
9
 
@@ -4,10 +4,10 @@ import { parse } from '@babel/parser';
4
4
  import { getTailwindMetadata } from './get-tailwind-metadata';
5
5
 
6
6
  describe('getTailwindMetadata()', () => {
7
- test('with the netlify-welcome demo document', async () => {
7
+ test('with the project-proposal seed document', async () => {
8
8
  const documentPath = path.resolve(
9
9
  __dirname,
10
- '../../../../../../apps/demo/documents/welcome/netlify-welcome.tsx',
10
+ '../../../../../../packages/preview-server/scripts/utils/default-seed/contracts/project-proposal.tsx',
11
11
  );
12
12
  const reactCode = await fs.readFile(documentPath, 'utf8');
13
13
  const ast = parse(reactCode, {
@@ -1,6 +1,6 @@
1
1
  import traverse from '@babel/traverse';
2
2
  import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils';
3
- import type { AST } from '../../../actions/document-validation/check-compatibility';
3
+ import type { AST } from '../types';
4
4
  import { getTailwindConfig, type TailwindConfig } from './get-tailwind-config';
5
5
  import { setupTailwindContext } from './setup-tailwind-context';
6
6
 
@@ -0,0 +1,12 @@
1
+ import type { Node } from '@babel/traverse';
2
+
3
+ export type AST = Node;
4
+
5
+ export type DocumentClient = string;
6
+
7
+ export type Platform = string;
8
+
9
+ export type SupportEntry = {
10
+ notes_by_num?: Record<number, string>;
11
+ stats: Record<string, Record<string, Record<string, string>[]>>;
12
+ };
@@ -76,7 +76,7 @@ describe('containsDocumentTemplate()', () => {
76
76
  documentFilenames: [
77
77
  'github-access-token',
78
78
  'papermark-year-in-review',
79
- 'vercel-invite-user',
79
+ 'project-brief',
80
80
  'yelp-recent-login',
81
81
  ],
82
82
  subDirectories: [],
@@ -3,6 +3,227 @@ import path from 'node:path';
3
3
  import type { Loader, PluginBuild, ResolveOptions } from 'esbuild';
4
4
  import { escapeStringForRegex } from './escape-string-for-regex';
5
5
 
6
+ const WORKSPACE_DIR_NAMES = ['apps', 'packages', 'examples'];
7
+
8
+ const fileExists = async (filePath: string) => {
9
+ try {
10
+ await fs.access(filePath);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ };
16
+
17
+ const getAncestorDirectories = (startingPath: string) => {
18
+ const directories: string[] = [];
19
+ let currentDirectory = path.dirname(startingPath);
20
+
21
+ while (true) {
22
+ directories.push(currentDirectory);
23
+ const parentDirectory = path.dirname(currentDirectory);
24
+ if (parentDirectory === currentDirectory) {
25
+ break;
26
+ }
27
+ currentDirectory = parentDirectory;
28
+ }
29
+
30
+ return directories;
31
+ };
32
+
33
+ const toCssImportPath = (fromFilePath: string, targetFilePath: string) => {
34
+ const relativePath = path
35
+ .relative(path.dirname(fromFilePath), targetFilePath)
36
+ .replaceAll('\\', '/');
37
+
38
+ if (relativePath.startsWith('.')) {
39
+ return relativePath;
40
+ }
41
+
42
+ return `./${relativePath}`;
43
+ };
44
+
45
+ const readJsonIfExists = async (filePath: string) => {
46
+ try {
47
+ return JSON.parse(await fs.readFile(filePath, 'utf8')) as { version?: string };
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ };
52
+
53
+ const getTailwindV4CssEntry = async (startingPath: string) => {
54
+ for (const baseDirectory of getAncestorDirectories(startingPath)) {
55
+ const rootCandidate = path.join(baseDirectory, 'node_modules/tailwindcss/package.json');
56
+ const rootCss = path.join(baseDirectory, 'node_modules/tailwindcss/index.css');
57
+ const rootPackageJson = await readJsonIfExists(rootCandidate);
58
+ if (rootPackageJson?.version?.startsWith('4.') && (await fileExists(rootCss))) {
59
+ return rootCss;
60
+ }
61
+
62
+ for (const directoryName of WORKSPACE_DIR_NAMES) {
63
+ const workspaceDir = path.join(baseDirectory, directoryName);
64
+ let entries: string[] = [];
65
+ try {
66
+ entries = await fs.readdir(workspaceDir);
67
+ } catch {
68
+ continue;
69
+ }
70
+
71
+ for (const entry of entries) {
72
+ const packageJsonPath = path.join(
73
+ workspaceDir,
74
+ entry,
75
+ 'node_modules/tailwindcss/package.json',
76
+ );
77
+ const cssEntryPath = path.join(
78
+ workspaceDir,
79
+ entry,
80
+ 'node_modules/tailwindcss/index.css',
81
+ );
82
+ const packageJson = await readJsonIfExists(packageJsonPath);
83
+ if (packageJson?.version?.startsWith('4.') && (await fileExists(cssEntryPath))) {
84
+ return cssEntryPath;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ throw new Error(
91
+ 'Could not find a Tailwind CSS v4 installation to compile imported globals.css files.',
92
+ );
93
+ };
94
+
95
+ const getTwAnimateCssEntry = async (startingPath: string) => {
96
+ for (const baseDirectory of getAncestorDirectories(startingPath)) {
97
+ const candidate = path.join(baseDirectory, 'node_modules/tw-animate-css/dist/tw-animate.css');
98
+ if (await fileExists(candidate)) {
99
+ return candidate;
100
+ }
101
+ }
102
+
103
+ throw new Error(
104
+ 'Could not find tw-animate-css to compile imported globals.css files.',
105
+ );
106
+ };
107
+
108
+ type PostcssProcessor = {
109
+ process: (css: string, options: { from: string }) => Promise<{ css: string }>;
110
+ };
111
+
112
+ type CssCompiler = {
113
+ postcss: (plugins: unknown[]) => PostcssProcessor;
114
+ tailwindPostcss: (options: { base: string }) => unknown;
115
+ };
116
+
117
+ let cssCompilerPromise: Promise<CssCompiler> | undefined;
118
+
119
+ const getCssCompiler = async () => {
120
+ cssCompilerPromise ??= (async () => {
121
+ const dynamicImport = new Function(
122
+ 'specifier',
123
+ 'return import(specifier)',
124
+ ) as <T>(specifier: string) => Promise<T>;
125
+
126
+ const [postcssModule, tailwindPostcssModule] = await Promise.all([
127
+ dynamicImport<typeof import('postcss')>('postcss'),
128
+ dynamicImport('@tailwindcss/postcss'),
129
+ ]);
130
+
131
+ const postcss =
132
+ (postcssModule as unknown as {
133
+ default?: (plugins: unknown[]) => PostcssProcessor;
134
+ }).default ??
135
+ (postcssModule as unknown as (plugins: unknown[]) => PostcssProcessor);
136
+ const tailwindPostcss =
137
+ (tailwindPostcssModule as {
138
+ default?: (options: { base: string }) => unknown;
139
+ }).default ??
140
+ (tailwindPostcssModule as (options: { base: string }) => unknown);
141
+
142
+ return { postcss, tailwindPostcss };
143
+ })();
144
+
145
+ return cssCompilerPromise;
146
+ };
147
+
148
+ const decodeHtmlEntities = (value: string) =>
149
+ value
150
+ .replaceAll('&quot;', '"')
151
+ .replaceAll('&#34;', '"')
152
+ .replaceAll('&#39;', "'")
153
+ .replaceAll('&apos;', "'")
154
+ .replaceAll('&lt;', '<')
155
+ .replaceAll('&gt;', '>')
156
+ .replaceAll('&amp;', '&');
157
+
158
+ const extractHtmlClassNames = (html: string) => {
159
+ const classNames = new Set<string>();
160
+ const classAttributePattern = /\sclass=(["'])(.*?)\1/gs;
161
+
162
+ for (const match of html.matchAll(classAttributePattern)) {
163
+ const rawValue = match[2];
164
+ if (!rawValue) {
165
+ continue;
166
+ }
167
+
168
+ const decodedValue = decodeHtmlEntities(rawValue);
169
+
170
+ for (const className of decodedValue.split(/\s+/)) {
171
+ if (className) {
172
+ classNames.add(className);
173
+ }
174
+ }
175
+ }
176
+
177
+ return Array.from(classNames);
178
+ };
179
+
180
+ const compileCssImport = async (filePath: string, classNames: string[]) => {
181
+ const { postcss, tailwindPostcss } = await getCssCompiler();
182
+ let css = await fs.readFile(filePath, 'utf8');
183
+
184
+ if (/@import\s+["']tailwindcss["'];/g.test(css)) {
185
+ const tailwindCssEntry = await getTailwindV4CssEntry(filePath);
186
+ css = css.replace(
187
+ /@import\s+["']tailwindcss["'];/g,
188
+ `@import "${toCssImportPath(filePath, tailwindCssEntry)}";`,
189
+ );
190
+ }
191
+
192
+ if (/@import\s+["']tw-animate-css["'];/g.test(css)) {
193
+ const twAnimateCssEntry = await getTwAnimateCssEntry(filePath);
194
+ css = css.replace(
195
+ /@import\s+["']tw-animate-css["'];/g,
196
+ `@import "${toCssImportPath(filePath, twAnimateCssEntry)}";`,
197
+ );
198
+ }
199
+
200
+ if (classNames.length > 0) {
201
+ css = `${css}\n${classNames
202
+ .map((className) => `@source inline(${JSON.stringify(className)});`)
203
+ .join('\n')}\n`;
204
+ }
205
+
206
+ const result = await postcss([
207
+ tailwindPostcss({
208
+ base: path.dirname(filePath),
209
+ }),
210
+ ]).process(css, { from: filePath });
211
+
212
+ return result.css;
213
+ };
214
+
215
+ export const compileDocumentCssImports = async (
216
+ filePaths: string[],
217
+ html: string,
218
+ ) => {
219
+ const uniquePaths = Array.from(new Set(filePaths));
220
+ const classNames = extractHtmlClassNames(html);
221
+
222
+ return Promise.all(
223
+ uniquePaths.map((filePath) => compileCssImport(filePath, classNames)),
224
+ );
225
+ };
226
+
6
227
  /**
7
228
  * Made to export the `render` function out of the user's document template
8
229
  * so that issues like https://github.com/maxscn/useprint/issues/649 don't
@@ -17,6 +238,13 @@ import { escapeStringForRegex } from './escape-string-for-regex';
17
238
  export const renderingUtilitiesExporter = (documentTemplates: string[]) => ({
18
239
  name: 'rendering-utilities-exporter',
19
240
  setup: (b: PluginBuild) => {
241
+ b.onResolve({ filter: /\.css$/ }, (args) => ({
242
+ path: path.isAbsolute(args.path)
243
+ ? args.path
244
+ : path.resolve(args.resolveDir, args.path),
245
+ namespace: 'useprint-css',
246
+ }));
247
+
20
248
  b.onLoad(
21
249
  {
22
250
  filter: new RegExp(
@@ -30,12 +258,32 @@ export const renderingUtilitiesExporter = (documentTemplates: string[]) => ({
30
258
  contents: `${await fs.readFile(pathToFile, 'utf8')};
31
259
  export { render } from 'useprint-module-that-will-export-render'
32
260
  export { createElement as reactDocumentCreateReactElement } from 'react';
261
+ export const useprintDocumentStylePaths = Array.from(
262
+ new Set(globalThis.__useprint_document_css_files ?? [])
263
+ );
33
264
  `,
34
265
  loader: path.extname(pathToFile).slice(1) as Loader,
35
266
  };
36
267
  },
37
268
  );
38
269
 
270
+ b.onLoad(
271
+ { filter: /\.css$/, namespace: 'useprint-css' },
272
+ async ({ path: pathToCssFile }) => {
273
+ return {
274
+ contents: `
275
+ const cssFilePath = ${JSON.stringify(pathToCssFile)};
276
+ globalThis.__useprint_document_css_files ??= [];
277
+ if (!globalThis.__useprint_document_css_files.includes(cssFilePath)) {
278
+ globalThis.__useprint_document_css_files.push(cssFilePath);
279
+ }
280
+ export default cssFilePath;
281
+ `,
282
+ loader: 'js',
283
+ };
284
+ },
285
+ );
286
+
39
287
  b.onResolve(
40
288
  { filter: /^useprint-module-that-will-export-render$/ },
41
289
  async (args) => {
@@ -7,9 +7,11 @@ describe('getDocumentComponent()', () => {
7
7
  const result = await getDocumentComponent(
8
8
  path.resolve(__dirname, './testing/request-response-document.tsx'),
9
9
  );
10
- if ('error' in result) {
11
- console.log(result.error);
12
- expect('error' in result, 'there should be no errors').toBe(false);
10
+ expect('error' in result, 'there should be no errors').toBe(false);
11
+
12
+ if (!('error' in result)) {
13
+ expect(result.documentComponent).toBeTruthy();
14
+ expect(result.sourceMapToOriginalFile).toBeTruthy();
13
15
  }
14
16
  });
15
17
  });
@@ -18,7 +20,7 @@ describe('getDocumentComponent()', () => {
18
20
  const result = await getDocumentComponent(
19
21
  path.resolve(
20
22
  __dirname,
21
- '../../../../apps/demo/documents/notifications/vercel-invite-user.tsx',
23
+ '../../scripts/utils/default-seed/contracts/project-proposal.tsx',
22
24
  ),
23
25
  );
24
26
 
@@ -4,7 +4,10 @@ import { type BuildFailure, build, type OutputFile } from 'esbuild';
4
4
  import type React from 'react';
5
5
  import type { RawSourceMap } from 'source-map-js';
6
6
  import { z } from 'zod';
7
- import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
7
+ import {
8
+ compileDocumentCssImports,
9
+ renderingUtilitiesExporter,
10
+ } from './esbuild/renderring-utilities-exporter';
8
11
  import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
9
12
  import { isErr } from './result';
10
13
  import { runBundledCode } from './run-bundled-code';
@@ -15,8 +18,29 @@ const DocumentComponentModule = z.object({
15
18
  default: z.any(),
16
19
  render: z.function(),
17
20
  reactDocumentCreateReactElement: z.function(),
21
+ useprintDocumentStylePaths: z.array(z.string()).optional(),
18
22
  });
19
23
 
24
+ const injectStylesIntoHtml = (html: string, styles: string[]) => {
25
+ if (styles.length === 0) {
26
+ return html;
27
+ }
28
+
29
+ const styleMarkup = styles
30
+ .map((css) => `<style data-useprint-document>${css.replaceAll('</style', '<\\/style')}</style>`)
31
+ .join('');
32
+
33
+ if (html.includes('</head>')) {
34
+ return html.replace('</head>', `${styleMarkup}</head>`);
35
+ }
36
+
37
+ if (html.includes('<html')) {
38
+ return html.replace(/<html([^>]*)>/, `<html$1><head>${styleMarkup}</head>`);
39
+ }
40
+
41
+ return `${styleMarkup}${html}`;
42
+ };
43
+
20
44
  export const getDocumentComponent = async (
21
45
  documentPath: string,
22
46
  ): Promise<
@@ -32,9 +56,7 @@ export const getDocumentComponent = async (
32
56
  | { error: ErrorObject }
33
57
  > => {
34
58
  let outputFiles: OutputFile[];
35
- console.log("constructing data")
36
59
  try {
37
- console.log("starting esbuild")
38
60
  const buildData = await build({
39
61
  bundle: true,
40
62
  entryPoints: [documentPath],
@@ -52,7 +74,6 @@ export const getDocumentComponent = async (
52
74
  outdir: 'stdout', // just a stub for esbuild, it won't actually write to this folder
53
75
  sourcemap: 'external',
54
76
  });
55
- console.log("built file")
56
77
  outputFiles = buildData.outputFiles;
57
78
  } catch (exception) {
58
79
  const buildFailure = exception as BuildFailure;
@@ -65,18 +86,36 @@ export const getDocumentComponent = async (
65
86
  },
66
87
  };
67
88
  }
68
- console.log("got data")
69
- const sourceMapFile = outputFiles[0]!;
70
- const bundledDocumentFile = outputFiles[1]!;
89
+ const sourceMapFile = outputFiles.find((file) => file.path.endsWith('.map'));
90
+ const bundledDocumentFile = outputFiles.find(
91
+ (file) => !file.path.endsWith('.map'),
92
+ );
93
+
94
+ if (!sourceMapFile || !bundledDocumentFile) {
95
+ return {
96
+ error: {
97
+ message: `Could not build the document component at ${documentPath}: esbuild did not return both bundled code and a sourcemap.`,
98
+ name: 'BuildFailure',
99
+ stack: undefined,
100
+ },
101
+ };
102
+ }
103
+
71
104
  const builtDocumentCode = bundledDocumentFile.text;
72
105
 
73
106
  const sourceMapToDocument = JSON.parse(sourceMapFile.text) as RawSourceMap;
74
107
  // because it will have a path like <tsconfigLocation>/stdout/document.js.map
75
108
  sourceMapToDocument.sourceRoot = path.resolve(sourceMapFile.path, '../..');
76
- sourceMapToDocument.sources = sourceMapToDocument.sources.map((source) =>
77
- path.resolve(sourceMapFile.path, '..', source),
109
+ sourceMapToDocument.sources = (sourceMapToDocument.sources ?? []).map(
110
+ (source) => path.resolve(sourceMapFile.path, '..', source),
78
111
  );
79
112
 
113
+ (
114
+ globalThis as typeof globalThis & {
115
+ __useprint_document_css_files?: string[];
116
+ }
117
+ ).__useprint_document_css_files = [];
118
+
80
119
  const runningResult = runBundledCode(builtDocumentCode, documentPath);
81
120
 
82
121
  if (isErr(runningResult)) {
@@ -128,7 +167,19 @@ export const getDocumentComponent = async (
128
167
 
129
168
  return {
130
169
  documentComponent: componentModule.default as DocumentComponent,
131
- render: componentModule.render as typeof render,
170
+ render: (async (...args: Parameters<typeof render>) => {
171
+ const html = await (
172
+ componentModule.render as typeof render
173
+ )(...args);
174
+ const documentStyles = await compileDocumentCssImports(
175
+ componentModule.useprintDocumentStylePaths ?? [],
176
+ html,
177
+ );
178
+ return injectStylesIntoHtml(
179
+ html,
180
+ documentStyles,
181
+ );
182
+ }) as typeof render,
132
183
  createElement:
133
184
  componentModule.reactDocumentCreateReactElement as typeof React.createElement,
134
185
 
@@ -4,77 +4,40 @@ import { getDocumentsDirectoryMetadata } from './get-documents-directory-metadat
4
4
  test('getDocumentsDirectoryMetadata on demo documents', async () => {
5
5
  const documentsDirectoryPath = path.resolve(
6
6
  __dirname,
7
- '../../../../apps/demo/documents',
7
+ '../../scripts/utils/default-seed',
8
8
  );
9
9
  expect(await getDocumentsDirectoryMetadata(documentsDirectoryPath)).toEqual({
10
10
  absolutePath: documentsDirectoryPath,
11
- directoryName: 'documents',
11
+ directoryName: 'default-seed',
12
12
  relativePath: '',
13
13
  documentFilenames: [],
14
14
  subDirectories: [
15
15
  {
16
- absolutePath: `${documentsDirectoryPath}/magic-links`,
17
- directoryName: 'magic-links',
18
- relativePath: 'magic-links',
19
- documentFilenames: [
20
- 'aws-verify-document',
21
- 'linear-login-code',
22
- 'notion-magic-link',
23
- 'plaid-verify-identity',
24
- 'raycast-magic-link',
25
- 'slack-confirm',
26
- ],
16
+ absolutePath: `${documentsDirectoryPath}/contracts`,
17
+ directoryName: 'contracts',
18
+ relativePath: 'contracts',
19
+ documentFilenames: ['project-proposal'],
27
20
  subDirectories: [],
28
21
  },
29
22
  {
30
- absolutePath: `${documentsDirectoryPath}/newsletters`,
31
- directoryName: 'newsletters',
32
- relativePath: 'newsletters',
33
- documentFilenames: [
34
- 'codepen-challengers',
35
- 'google-play-policy-update',
36
- 'stack-overflow-tips',
37
- ],
23
+ absolutePath: `${documentsDirectoryPath}/finance`,
24
+ directoryName: 'finance',
25
+ relativePath: 'finance',
26
+ documentFilenames: ['payment-summary'],
38
27
  subDirectories: [],
39
28
  },
40
29
  {
41
- absolutePath: `${documentsDirectoryPath}/notifications`,
42
- directoryName: 'notifications',
43
- relativePath: 'notifications',
44
- documentFilenames: [
45
- 'github-access-token',
46
- 'papermark-year-in-review',
47
- 'vercel-invite-user',
48
- 'yelp-recent-login',
49
- ],
30
+ absolutePath: `${documentsDirectoryPath}/reports`,
31
+ directoryName: 'reports',
32
+ relativePath: 'reports',
33
+ documentFilenames: ['incident-report'],
50
34
  subDirectories: [],
51
35
  },
52
36
  {
53
- absolutePath: `${documentsDirectoryPath}/receipts`,
54
- directoryName: 'receipts',
55
- relativePath: 'receipts',
56
- documentFilenames: ['apple-receipt', 'nike-receipt'],
57
- subDirectories: [],
58
- },
59
- {
60
- absolutePath: `${documentsDirectoryPath}/reset-password`,
61
- directoryName: 'reset-password',
62
- relativePath: 'reset-password',
63
- documentFilenames: ['dropbox-reset-password', 'twitch-reset-password'],
64
- subDirectories: [],
65
- },
66
- {
67
- absolutePath: `${documentsDirectoryPath}/reviews`,
68
- directoryName: 'reviews',
69
- relativePath: 'reviews',
70
- documentFilenames: ['airbnb-review', 'amazon-review'],
71
- subDirectories: [],
72
- },
73
- {
74
- absolutePath: `${documentsDirectoryPath}/welcome`,
75
- directoryName: 'welcome',
76
- relativePath: 'welcome',
77
- documentFilenames: ['koala-welcome', 'netlify-welcome', 'stripe-welcome'],
37
+ absolutePath: `${documentsDirectoryPath}/updates`,
38
+ directoryName: 'updates',
39
+ relativePath: 'updates',
40
+ documentFilenames: ['release-notes'],
78
41
  subDirectories: [],
79
42
  },
80
43
  ],
@@ -15,7 +15,7 @@ describe('iframe-processing', () => {
15
15
  });
16
16
 
17
17
  describe('applyMarginToIframeSrcDoc', () => {
18
- it('applies margin to first unbreakable element', () => {
18
+ it('injects a style rule for the first unbreakable element', () => {
19
19
  const srcDoc = `
20
20
  <html>
21
21
  <body>
@@ -26,24 +26,24 @@ describe('iframe-processing', () => {
26
26
 
27
27
  const result = applyMarginToIframeSrcDoc(srcDoc, 50);
28
28
 
29
+ expect(result).toContain('.useprint-unbreakable:first-of-type');
29
30
  expect(result).toContain('margin-top: 50px !important');
30
31
  expect(result).toContain('useprint-unbreakable');
31
32
  });
32
33
 
33
- it('replaces existing margin-top', () => {
34
+ it('inserts the style rule into an existing head', () => {
34
35
  const srcDoc = `
35
36
  <html>
36
- <body>
37
- <div class="useprint-unbreakable" style="margin-top: 10px; color: red;">Content</div>
38
- </body>
37
+ <head><meta charset="utf-8" /></head>
38
+ <body><div class="useprint-unbreakable">Content</div></body>
39
39
  </html>
40
40
  `;
41
41
 
42
42
  const result = applyMarginToIframeSrcDoc(srcDoc, 50);
43
43
 
44
+ expect(result).toContain('<meta charset="utf-8" />');
45
+ expect(result).toContain('</style></head>');
44
46
  expect(result).toContain('margin-top: 50px !important');
45
- expect(result).toContain('color: red');
46
- expect(result).not.toContain('margin-top: 10px');
47
47
  });
48
48
 
49
49
  it('preserves existing styles', () => {
@@ -62,7 +62,7 @@ describe('iframe-processing', () => {
62
62
  expect(result).toContain('margin-top: 30px !important');
63
63
  });
64
64
 
65
- it('handles srcDoc without unbreakable elements', () => {
65
+ it('creates a head element when the document only has a body', () => {
66
66
  const srcDoc = `
67
67
  <html>
68
68
  <body>
@@ -73,28 +73,16 @@ describe('iframe-processing', () => {
73
73
 
74
74
  const result = applyMarginToIframeSrcDoc(srcDoc, 50);
75
75
 
76
- expect(result).toBe(srcDoc);
76
+ expect(result).toContain('<head><style>');
77
+ expect(result).toContain('Regular content');
77
78
  });
78
79
 
79
- it('handles empty srcDoc', () => {
80
+ it('handles empty srcDoc by prepending a head', () => {
80
81
  const srcDoc = '';
81
82
  const result = applyMarginToIframeSrcDoc(srcDoc, 50);
82
83
 
83
- expect(result).toBe(srcDoc);
84
- });
85
-
86
- it('handles element without existing style attribute', () => {
87
- const srcDoc = `
88
- <html>
89
- <body>
90
- <div class="useprint-unbreakable">Content</div>
91
- </body>
92
- </html>
93
- `;
94
-
95
- const result = applyMarginToIframeSrcDoc(srcDoc, 25);
96
-
97
- expect(result).toContain('margin-top: 25px !important');
84
+ expect(result).toContain('<head><style>');
85
+ expect(result).toContain('margin-top: 50px !important');
98
86
  });
99
87
  });
100
88