emdash 0.4.0 → 0.6.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.
Files changed (212) hide show
  1. package/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
  2. package/dist/adapters-Di31kZ28.d.mts.map +1 -0
  3. package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
  4. package/dist/apply-B4MsLM-w.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +208 -34
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +5 -5
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +34 -9
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.mjs +1 -1
  14. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  15. package/dist/astro/middleware/request-context.mjs +5 -3
  16. package/dist/astro/middleware/request-context.mjs.map +1 -1
  17. package/dist/astro/middleware/setup.mjs +1 -1
  18. package/dist/astro/middleware.d.mts.map +1 -1
  19. package/dist/astro/middleware.mjs +460 -180
  20. package/dist/astro/middleware.mjs.map +1 -1
  21. package/dist/astro/types.d.mts +8 -8
  22. package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
  23. package/dist/byline-C4OVd8b3.mjs.map +1 -0
  24. package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
  25. package/dist/bylines-hPTW79hw.mjs.map +1 -0
  26. package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
  27. package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
  28. package/dist/chunks-HGz06Soa.mjs +19 -0
  29. package/dist/chunks-HGz06Soa.mjs.map +1 -0
  30. package/dist/cli/index.mjs +9 -8
  31. package/dist/cli/index.mjs.map +1 -1
  32. package/dist/client/cf-access.d.mts +1 -1
  33. package/dist/client/index.d.mts +1 -1
  34. package/dist/client/index.mjs +1 -1
  35. package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
  36. package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
  37. package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
  38. package/dist/connection-2igzM-AT.mjs.map +1 -0
  39. package/dist/database/instrumentation.d.mts +45 -0
  40. package/dist/database/instrumentation.d.mts.map +1 -0
  41. package/dist/database/instrumentation.mjs +61 -0
  42. package/dist/database/instrumentation.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs.map +1 -1
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/postgres.d.mts +1 -1
  47. package/dist/db/sqlite.d.mts +1 -1
  48. package/dist/db-errors-D0UT85nC.mjs +41 -0
  49. package/dist/db-errors-D0UT85nC.mjs.map +1 -0
  50. package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
  51. package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
  52. package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
  53. package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
  54. package/dist/{index-CRg3PWfZ.d.mts → index-BYv0mB9g.d.mts} +135 -19
  55. package/dist/index-BYv0mB9g.d.mts.map +1 -0
  56. package/dist/index.d.mts +11 -11
  57. package/dist/index.mjs +20 -18
  58. package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
  59. package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
  60. package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
  61. package/dist/loader-DeiBJEMe.mjs.map +1 -0
  62. package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
  63. package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
  64. package/dist/media/index.d.mts +1 -1
  65. package/dist/media/index.mjs +1 -1
  66. package/dist/media/local-runtime.d.mts +7 -7
  67. package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
  68. package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
  69. package/dist/page/index.d.mts +11 -2
  70. package/dist/page/index.d.mts.map +1 -1
  71. package/dist/page/index.mjs +23 -1
  72. package/dist/page/index.mjs.map +1 -1
  73. package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
  74. package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
  75. package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
  76. package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
  77. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  78. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  79. package/dist/{query-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
  80. package/dist/query-Bk_3vKvU.mjs.map +1 -0
  81. package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
  82. package/dist/registry-Ci3WxVAr.mjs.map +1 -0
  83. package/dist/request-cache-DiR961CV.mjs +79 -0
  84. package/dist/request-cache-DiR961CV.mjs.map +1 -0
  85. package/dist/request-context.d.mts +19 -16
  86. package/dist/request-context.d.mts.map +1 -1
  87. package/dist/request-context.mjs.map +1 -1
  88. package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
  89. package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
  90. package/dist/runtime.d.mts +6 -6
  91. package/dist/runtime.mjs +1 -1
  92. package/dist/{search-B5p9D36n.mjs → search-DI4bM2w9.mjs} +110 -209
  93. package/dist/search-DI4bM2w9.mjs.map +1 -0
  94. package/dist/seed/index.d.mts +2 -2
  95. package/dist/seed/index.mjs +8 -7
  96. package/dist/seo/index.d.mts +1 -1
  97. package/dist/storage/local.d.mts +1 -1
  98. package/dist/storage/local.mjs +1 -1
  99. package/dist/storage/s3.d.mts +1 -1
  100. package/dist/storage/s3.mjs +1 -1
  101. package/dist/taxonomies-DbrKzDju.mjs +308 -0
  102. package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
  103. package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
  104. package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
  105. package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
  106. package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
  107. package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
  108. package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
  109. package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
  110. package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
  111. package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
  112. package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
  113. package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
  114. package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
  115. package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
  116. package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
  117. package/dist/{types-gLYVCXCQ.d.mts → types-CnZYHyLW.d.mts} +55 -5
  118. package/dist/types-CnZYHyLW.d.mts.map +1 -0
  119. package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
  120. package/dist/types-DDS4MxsT.mjs.map +1 -0
  121. package/dist/{types-BYWYxLcp.d.mts → types-DgrIP0tF.d.mts} +9 -2
  122. package/dist/types-DgrIP0tF.d.mts.map +1 -0
  123. package/dist/{validate-CcNRWH6I.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
  124. package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
  125. package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
  126. package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
  127. package/dist/version-Uaf2ynPX.mjs +7 -0
  128. package/dist/{version-DlTDRdpv.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
  129. package/package.json +10 -5
  130. package/src/after.ts +62 -0
  131. package/src/api/handlers/oauth-authorization.ts +2 -32
  132. package/src/api/handlers/oauth-clients.ts +40 -4
  133. package/src/api/handlers/taxonomies.ts +13 -0
  134. package/src/api/oauth/redirect-uri.ts +34 -0
  135. package/src/api/openapi/document.ts +126 -118
  136. package/src/api/schemas/auth.ts +7 -0
  137. package/src/api/schemas/media.ts +26 -15
  138. package/src/api/schemas/schema.ts +1 -0
  139. package/src/astro/integration/font-provider.ts +176 -0
  140. package/src/astro/integration/index.ts +42 -0
  141. package/src/astro/integration/routes.ts +17 -1
  142. package/src/astro/integration/runtime.ts +63 -0
  143. package/src/astro/integration/virtual-modules.ts +41 -39
  144. package/src/astro/integration/vite-config.ts +16 -5
  145. package/src/astro/middleware/auth.ts +39 -6
  146. package/src/astro/middleware/request-context.ts +15 -3
  147. package/src/astro/middleware.ts +340 -263
  148. package/src/astro/routes/admin.astro +10 -5
  149. package/src/astro/routes/api/auth/invite/register-options.ts +78 -0
  150. package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
  151. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
  152. package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
  153. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
  154. package/src/astro/routes/api/media/upload-url.ts +10 -2
  155. package/src/astro/routes/api/media.ts +10 -7
  156. package/src/astro/routes/api/oauth/register.ts +178 -0
  157. package/src/astro/routes/api/oauth/token.ts +15 -0
  158. package/src/astro/routes/api/openapi.json.ts +15 -5
  159. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
  160. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
  161. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
  162. package/src/astro/routes/api/search/index.ts +5 -0
  163. package/src/astro/routes/api/search/suggest.ts +3 -0
  164. package/src/astro/routes/api/taxonomies/index.ts +1 -0
  165. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +6 -4
  166. package/src/bylines/index.ts +22 -45
  167. package/src/components/EmDashHead.astro +23 -7
  168. package/src/components/Table.astro +73 -41
  169. package/src/components/index.ts +2 -12
  170. package/src/components/marks.ts +20 -0
  171. package/src/database/connection.ts +23 -1
  172. package/src/database/instrumentation.ts +98 -0
  173. package/src/db/adapters.ts +15 -0
  174. package/src/emdash-runtime.ts +309 -91
  175. package/src/index.ts +6 -0
  176. package/src/loader.ts +19 -24
  177. package/src/menus/index.ts +6 -3
  178. package/src/page/index.ts +1 -1
  179. package/src/page/seo-contributions.ts +36 -0
  180. package/src/plugins/context.ts +1 -0
  181. package/src/plugins/email-console.ts +9 -2
  182. package/src/plugins/types.ts +8 -0
  183. package/src/query.ts +104 -7
  184. package/src/request-cache.ts +106 -0
  185. package/src/request-context.ts +19 -0
  186. package/src/schema/query.ts +5 -2
  187. package/src/schema/registry.ts +243 -166
  188. package/src/schema/types.ts +13 -2
  189. package/src/schema/zod-generator.ts +4 -0
  190. package/src/search/fts-manager.ts +19 -5
  191. package/src/search/query.ts +4 -3
  192. package/src/seed/apply.ts +15 -1
  193. package/src/settings/index.ts +24 -5
  194. package/src/taxonomies/index.ts +324 -124
  195. package/src/utils/db-errors.ts +46 -0
  196. package/src/virtual-modules.d.ts +31 -10
  197. package/src/widgets/index.ts +54 -25
  198. package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
  199. package/dist/apply-Cma_PiF6.mjs.map +0 -1
  200. package/dist/byline-WuOq9MFJ.mjs.map +0 -1
  201. package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
  202. package/dist/connection-B4zVnQIa.mjs.map +0 -1
  203. package/dist/index-CRg3PWfZ.d.mts.map +0 -1
  204. package/dist/loader-BYzwzORf.mjs.map +0 -1
  205. package/dist/query-B6Vu0d2i.mjs.map +0 -1
  206. package/dist/registry-BgnP3ysR.mjs.map +0 -1
  207. package/dist/search-B5p9D36n.mjs.map +0 -1
  208. package/dist/types-BYWYxLcp.d.mts.map +0 -1
  209. package/dist/types-gLYVCXCQ.d.mts.map +0 -1
  210. package/dist/types-xxCWI3j0.mjs.map +0 -1
  211. package/dist/validate-CcNRWH6I.d.mts.map +0 -1
  212. package/dist/version-DlTDRdpv.mjs +0 -7
@@ -13,47 +13,19 @@ import type { BylineSummary, ContentBylineCredit } from "../database/repositorie
13
13
  import { validateIdentifier } from "../database/validate.js";
14
14
  import { getDb } from "../loader.js";
15
15
  import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
16
+ import { isMissingTableError } from "../utils/db-errors.js";
16
17
 
17
18
  /**
18
- * Cached result of "does any byline exist in the database?"
19
- * null = not yet checked, true/false = cached result.
20
- * Invalidated when bylines are created or deleted.
21
- */
22
- let hasBylines: boolean | null = null;
23
-
24
- /**
25
- * Invalidate the cached "has any bylines" check.
26
- * Call this when bylines are created, updated, or deleted.
19
+ * No-op kept for API compatibility.
20
+ *
21
+ * Used to invalidate a worker-lifetime "has any byline?" probe. That
22
+ * probe added a query on every cold isolate to save one query on sites
23
+ * with zero bylines (i.e. the wrong tradeoff), so we dropped it. The
24
+ * batch byline join below returns an empty map for empty sites at the
25
+ * same cost as the probe, without the pre-check.
27
26
  */
28
27
  export function invalidateBylineCache(): void {
29
- hasBylines = null;
30
- }
31
-
32
- /**
33
- * Check if any bylines exist in the database. Result is cached
34
- * for the lifetime of the worker/process and invalidated on writes.
35
- */
36
- async function hasAnyBylines(): Promise<boolean> {
37
- if (hasBylines !== null) return hasBylines;
38
-
39
- try {
40
- const db = await getDb();
41
- const result = await sql<{ id: string }>`
42
- SELECT id FROM _emdash_bylines LIMIT 1
43
- `.execute(db);
44
- hasBylines = result.rows.length > 0;
45
- } catch (error: unknown) {
46
- // Only treat "no such table" as a safe false -- anything else should
47
- // not be cached so the next request retries.
48
- const message = error instanceof Error ? error.message : "";
49
- if (message.includes("no such table")) {
50
- hasBylines = false;
51
- } else {
52
- return false;
53
- }
54
- }
55
-
56
- return hasBylines;
28
+ // Intentionally empty.
57
29
  }
58
30
 
59
31
  /**
@@ -176,17 +148,22 @@ export async function getBylinesForEntries(
176
148
  return result;
177
149
  }
178
150
 
179
- // Skip DB queries entirely when no bylines have been created.
180
- // The cache is invalidated when bylines are created/deleted.
181
- if (!(await hasAnyBylines())) {
182
- return result;
183
- }
184
-
185
151
  const db = await getDb();
186
152
  const repo = new BylineRepository(db);
187
153
 
188
- // 1. Batch fetch all explicit byline credits
189
- const bylinesMap = await repo.getContentBylinesMany(collection, entryIds);
154
+ // 1. Batch fetch all explicit byline credits. Sites with no bylines
155
+ // get an empty map back for one query — the previous "has any bylines"
156
+ // probe traded an extra round-trip on every request to save that one
157
+ // query on empty sites, which is exactly backwards for the common case.
158
+ // Pre-migration databases (bylines table missing) fall through to the
159
+ // `isMissingTableError` catch below and return empty results.
160
+ let bylinesMap;
161
+ try {
162
+ bylinesMap = await repo.getContentBylinesMany(collection, entryIds);
163
+ } catch (error) {
164
+ if (isMissingTableError(error)) return result;
165
+ throw error;
166
+ }
190
167
 
191
168
  // 2. Collect entry IDs that need fallback lookup
192
169
  const fallbackEntryIds: string[] = [];
@@ -16,8 +16,12 @@
16
16
  import type { PublicPageContext, PageMetadataContribution } from "../plugins/types.js";
17
17
  import { resolvePageMetadata, renderPageMetadata } from "../page/metadata.js";
18
18
  import { renderFragments } from "../page/fragments.js";
19
- import { generateBaseSeoContributions } from "../page/seo-contributions.js";
19
+ import {
20
+ generateBaseSeoContributions,
21
+ generateSiteSeoContributions,
22
+ } from "../page/seo-contributions.js";
20
23
  import { getPageRuntime } from "../page/index.js";
24
+ import { getSiteSetting } from "../settings/index.js";
21
25
 
22
26
  interface Props {
23
27
  page: PublicPageContext;
@@ -33,14 +37,26 @@ let metadataHtml = "";
33
37
  let fragmentsHtml = "";
34
38
 
35
39
  if (runtime) {
36
- // Plugin contributions come BEFORE base, so resolvePageMetadata's
37
- // first-wins dedup lets plugins override base SEO defaults
38
- const pluginContributions = await runtime.collectPageMetadata(page);
39
- const allContributions = [...pluginContributions, ...baseContributions];
40
+ // Run independent async loads in parallel: site SEO settings (for
41
+ // search engine verification meta tags) and plugin page-metadata
42
+ // contributions. Plugin contributions come BEFORE site/base in the
43
+ // array, so resolvePageMetadata's first-wins dedup lets plugins
44
+ // override defaults.
45
+ //
46
+ // `getSiteSetting("seo")` is request-cached and — crucially — reads
47
+ // from `getSiteSettings()`'s cached batch when a parent template has
48
+ // already called it. So this is either a single-key query or free,
49
+ // not a second round-trip.
50
+ const [seoSettings, pluginContributions, fragments] = await Promise.all([
51
+ getSiteSetting("seo"),
52
+ runtime.collectPageMetadata(page),
53
+ runtime.collectPageFragments(page),
54
+ ]);
55
+
56
+ const siteContributions = generateSiteSeoContributions(seoSettings);
57
+ const allContributions = [...pluginContributions, ...siteContributions, ...baseContributions];
40
58
  const resolved = resolvePageMetadata(allContributions);
41
59
  metadataHtml = renderPageMetadata(resolved);
42
-
43
- const fragments = await runtime.collectPageFragments(page);
44
60
  fragmentsHtml = renderFragments(fragments, "head");
45
61
  } else {
46
62
  // No runtime (EmDash not initialized) — still render base SEO
@@ -2,8 +2,13 @@
2
2
  /**
3
3
  * Portable Text Table block component
4
4
  *
5
- * Renders tables from WordPress imports.
5
+ * Renders tables from content imports. Cell content is rendered through the
6
+ * standard Portable Text pipeline so all marks (bold, italic, links, etc.) are
7
+ * handled by the registered mark components -- including `sanitizeHref` for links.
6
8
  */
9
+ import { PortableText } from "astro-portabletext";
10
+ import { emdashMarkComponents } from "./marks.js";
11
+
7
12
  export interface Props {
8
13
  node: {
9
14
  _type: "table";
@@ -32,6 +37,8 @@ export interface Props {
32
37
  };
33
38
  }
34
39
 
40
+ const markComponents = { mark: emdashMarkComponents };
41
+
35
42
  const { node } = Astro.props;
36
43
  const rows = node?.rows ?? [];
37
44
 
@@ -39,45 +46,65 @@ if (!rows.length) {
39
46
  return null;
40
47
  }
41
48
 
42
- // Helper to render cell content as text (simplified - doesn't handle marks)
43
- function renderCellText(
44
- content: Array<{ text: string; marks?: string[] }>,
45
- ): string {
46
- return content.map((span) => span.text).join("");
49
+ /**
50
+ * Wrap a table cell's inline content as a Portable Text block so it can be
51
+ * rendered through the standard pipeline (marks, markDefs, etc.).
52
+ */
53
+ function cellToBlock(cell: Props["node"]["rows"][number]["cells"][number]) {
54
+ return [
55
+ {
56
+ _type: "block" as const,
57
+ _key: cell._key,
58
+ children: cell.content,
59
+ markDefs: cell.markDefs ?? [],
60
+ },
61
+ ];
47
62
  }
48
- ---
49
63
 
50
- {() => {
51
- const hasHeader = node?.hasHeaderRow;
52
- const headerRow = hasHeader ? rows[0] : null;
53
- const bodyRows = hasHeader ? rows.slice(1) : rows;
64
+ const hasHeader = node?.hasHeaderRow;
65
+ const headerRow = hasHeader ? rows[0] : null;
66
+ const bodyRows = hasHeader ? rows.slice(1) : rows;
67
+ ---
54
68
 
55
- return (
56
- <div class="emdash-table-wrapper">
57
- <table class="emdash-table">
58
- {headerRow && (
59
- <thead>
60
- <tr>
61
- {headerRow.cells.map((cell) => (
62
- <th>{renderCellText(cell.content)}</th>
63
- ))}
64
- </tr>
65
- </thead>
66
- )}
67
- <tbody>
68
- {bodyRows.map((row) => (
69
- <tr>
70
- {row.cells.map((cell) => {
71
- const CellTag = cell.isHeader ? "th" : "td";
72
- return <CellTag>{renderCellText(cell.content)}</CellTag>;
73
- })}
74
- </tr>
75
- ))}
76
- </tbody>
77
- </table>
78
- </div>
79
- );
80
- }}
69
+ <div class="emdash-table-wrapper">
70
+ <table class="emdash-table">
71
+ {
72
+ headerRow && (
73
+ <thead>
74
+ <tr>
75
+ {headerRow.cells.map((cell) => (
76
+ <th>
77
+ <PortableText
78
+ value={cellToBlock(cell)}
79
+ components={markComponents}
80
+ />
81
+ </th>
82
+ ))}
83
+ </tr>
84
+ </thead>
85
+ )
86
+ }
87
+ <tbody>
88
+ {
89
+ bodyRows.map((row) => (
90
+ <tr>
91
+ {row.cells.map((cell) => {
92
+ const CellTag = cell.isHeader ? "th" : "td";
93
+ return (
94
+ <CellTag>
95
+ <PortableText
96
+ value={cellToBlock(cell)}
97
+ components={markComponents}
98
+ />
99
+ </CellTag>
100
+ );
101
+ })}
102
+ </tr>
103
+ ))
104
+ }
105
+ </tbody>
106
+ </table>
107
+ </div>
81
108
 
82
109
  <style>
83
110
  .emdash-table-wrapper {
@@ -91,18 +118,23 @@ function renderCellText(
91
118
  }
92
119
  .emdash-table th,
93
120
  .emdash-table td {
94
- border: 1px solid #ddd;
121
+ border: 1px solid var(--color-border, #ddd);
95
122
  padding: 0.75rem;
96
123
  text-align: left;
97
124
  }
125
+ /* Remove default block margins inside cells */
126
+ .emdash-table th :global(p),
127
+ .emdash-table td :global(p) {
128
+ margin: 0;
129
+ }
98
130
  .emdash-table th {
99
- background: #f5f5f5;
131
+ background: var(--color-surface, #f5f5f5);
100
132
  font-weight: 600;
101
133
  }
102
134
  .emdash-table tbody tr:nth-child(even) {
103
- background: #fafafa;
135
+ background: var(--color-bg-subtle, #fafafa);
104
136
  }
105
137
  .emdash-table tbody tr:hover {
106
- background: #f0f0f0;
138
+ background: var(--color-surface, #f0f0f0);
107
139
  }
108
140
  </style>
@@ -69,11 +69,7 @@ import GalleryComponent from "./Gallery.astro";
69
69
  import HtmlBlockComponent from "./HtmlBlock.astro";
70
70
  // Pre-configured components object for PortableText
71
71
  import ImageComponent from "./Image.astro";
72
- import LinkMark from "./marks/Link.astro";
73
- import StrikeThroughMark from "./marks/StrikeThrough.astro";
74
- import SubscriptMark from "./marks/Subscript.astro";
75
- import SuperscriptMark from "./marks/Superscript.astro";
76
- import UnderlineMark from "./marks/Underline.astro";
72
+ import { emdashMarkComponents } from "./marks.js";
77
73
  import PullquoteComponent from "./Pullquote.astro";
78
74
  import TableComponent from "./Table.astro";
79
75
 
@@ -101,13 +97,7 @@ export const emdashComponents = {
101
97
  file: FileComponent,
102
98
  pullquote: PullquoteComponent,
103
99
  },
104
- mark: {
105
- superscript: SuperscriptMark,
106
- subscript: SubscriptMark,
107
- underline: UnderlineMark,
108
- "strike-through": StrikeThroughMark,
109
- link: LinkMark,
110
- },
100
+ mark: emdashMarkComponents,
111
101
  };
112
102
 
113
103
  // Public page contribution components
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared mark component map for Portable Text rendering.
3
+ *
4
+ * Used by both the top-level `emdashComponents` config and individual block
5
+ * components (e.g. Table) that render nested inline content through the PT
6
+ * pipeline.
7
+ */
8
+ import LinkMark from "./marks/Link.astro";
9
+ import StrikeThroughMark from "./marks/StrikeThrough.astro";
10
+ import SubscriptMark from "./marks/Subscript.astro";
11
+ import SuperscriptMark from "./marks/Superscript.astro";
12
+ import UnderlineMark from "./marks/Underline.astro";
13
+
14
+ export const emdashMarkComponents = {
15
+ superscript: SuperscriptMark,
16
+ subscript: SubscriptMark,
17
+ underline: UnderlineMark,
18
+ "strike-through": StrikeThroughMark,
19
+ link: LinkMark,
20
+ };
@@ -1,6 +1,7 @@
1
1
  import BetterSqlite3 from "better-sqlite3";
2
2
  import { Kysely, SqliteDialect } from "kysely";
3
3
 
4
+ import { kyselyLogOption } from "./instrumentation.js";
4
5
  import type { Database } from "./types.js";
5
6
 
6
7
  export interface DatabaseConfig {
@@ -18,6 +19,23 @@ export class EmDashDatabaseError extends Error {
18
19
  }
19
20
  }
20
21
 
22
+ /**
23
+ * Returns a helpful, actionable message when better-sqlite3's native binary
24
+ * was compiled against a different Node.js version than the one running. This
25
+ * happens after upgrading Node without rebuilding native deps.
26
+ *
27
+ * Returns null if the error is not a NODE_MODULE_VERSION mismatch.
28
+ */
29
+ export function formatNativeModuleVersionError(error: unknown): string | null {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ if (!message.includes("NODE_MODULE_VERSION")) return null;
32
+ return (
33
+ "better-sqlite3's native binary was compiled against a different Node.js version. " +
34
+ "Rebuild it with `pnpm rebuild better-sqlite3` (or `npm rebuild better-sqlite3`), " +
35
+ "or reinstall dependencies with your current Node.js version."
36
+ );
37
+ }
38
+
21
39
  /**
22
40
  * Creates a Kysely database instance
23
41
  * Supports:
@@ -45,7 +63,7 @@ export function createDatabase(config: DatabaseConfig): Kysely<Database> {
45
63
  database: sqlite,
46
64
  });
47
65
 
48
- return new Kysely<Database>({ dialect });
66
+ return new Kysely<Database>({ dialect, log: kyselyLogOption() });
49
67
  }
50
68
 
51
69
  // Handle libSQL (Turso)
@@ -62,6 +80,10 @@ export function createDatabase(config: DatabaseConfig): Kysely<Database> {
62
80
  if (error instanceof EmDashDatabaseError) {
63
81
  throw error;
64
82
  }
83
+ const nativeVersionHint = formatNativeModuleVersionError(error);
84
+ if (nativeVersionHint) {
85
+ throw new EmDashDatabaseError(nativeVersionHint, error);
86
+ }
65
87
  throw new EmDashDatabaseError("Failed to create database", error);
66
88
  }
67
89
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Query instrumentation
3
+ *
4
+ * Dev/test-only: captures every Kysely query executed inside a request,
5
+ * tagged with the route, method, and a caller-supplied phase (e.g. "cold"
6
+ * or "warm"). Events are emitted as prefixed NDJSON on stdout so the
7
+ * harness can capture them from both Node and workerd — workerd has no
8
+ * filesystem access, but `console.log` is portable.
9
+ *
10
+ * The recorder lives on the request context (AsyncLocalStorage). The
11
+ * Kysely `log` hook reads the recorder at query time and appends an
12
+ * event. When no recorder is attached, the hook is a null check.
13
+ */
14
+
15
+ import type { LogEvent, Logger } from "kysely";
16
+
17
+ import { getRequestContext } from "../request-context.js";
18
+
19
+ export const QUERY_LOG_ENV = "EMDASH_QUERY_LOG";
20
+ export const QUERY_LOG_PREFIX = "[emdash-query-log]";
21
+
22
+ export interface QueryEvent {
23
+ sql: string;
24
+ params: readonly unknown[];
25
+ durationMs: number;
26
+ route: string;
27
+ method: string;
28
+ phase: string;
29
+ }
30
+
31
+ export interface QueryRecorder {
32
+ events: QueryEvent[];
33
+ route: string;
34
+ method: string;
35
+ phase: string;
36
+ }
37
+
38
+ export function createRecorder(route: string, method: string, phase: string): QueryRecorder {
39
+ return { events: [], route, method, phase };
40
+ }
41
+
42
+ export function recordEvent(
43
+ rec: QueryRecorder,
44
+ sql: string,
45
+ params: readonly unknown[],
46
+ durationMs: number,
47
+ ): void {
48
+ rec.events.push({
49
+ sql,
50
+ params,
51
+ durationMs,
52
+ route: rec.route,
53
+ method: rec.method,
54
+ phase: rec.phase,
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Emit all events from a recorder as prefixed NDJSON on stdout. The
60
+ * harness pipes the child's stdout, filters lines beginning with
61
+ * QUERY_LOG_PREFIX, and writes them to its own file. Using stdout means
62
+ * the sink works uniformly in Node and in workerd (which has no fs).
63
+ */
64
+ export function flushRecorder(rec: QueryRecorder): void {
65
+ if (rec.events.length === 0) return;
66
+ for (const e of rec.events) {
67
+ console.log(`${QUERY_LOG_PREFIX} ${JSON.stringify(e)}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Whether query instrumentation is enabled. Read at Kysely construction
73
+ * time and middleware entry — the env var is a process-lifetime flag, not
74
+ * per-request. Gated via `process.env` so adapters that ship env through
75
+ * to the worker (e.g. Miniflare via wrangler.jsonc `vars` or host env
76
+ * pass-through) can enable it at runtime.
77
+ */
78
+ export function isInstrumentationEnabled(): boolean {
79
+ return Boolean(
80
+ typeof process !== "undefined" && process.env && process.env[QUERY_LOG_ENV] === "1",
81
+ );
82
+ }
83
+
84
+ function kyselyLog(event: LogEvent): void {
85
+ if (event.level !== "query") return;
86
+ const rec = getRequestContext()?.queryRecorder;
87
+ if (!rec) return;
88
+ recordEvent(rec, event.query.sql, event.query.parameters, event.queryDurationMillis);
89
+ }
90
+
91
+ /**
92
+ * Returns a Kysely `log` option when instrumentation is enabled, or undefined.
93
+ * Pass as `new Kysely({ dialect, log: kyselyLogOption() })` so disabled mode
94
+ * has zero overhead — Kysely skips query timing entirely when `log` is absent.
95
+ */
96
+ export function kyselyLogOption(): Logger | undefined {
97
+ return isInstrumentationEnabled() ? kyselyLog : undefined;
98
+ }
@@ -33,6 +33,21 @@ export interface DatabaseDescriptor {
33
33
  entrypoint: string;
34
34
  config: unknown;
35
35
  type: DatabaseDialectType;
36
+ /**
37
+ * When true, the adapter's runtime entrypoint MUST export a named
38
+ * `createRequestScopedDb` function matching the signature declared in
39
+ * `virtual:emdash/dialect`. The virtual-module generator re-exports it
40
+ * by name, so a missing export becomes a build-time bundler error.
41
+ *
42
+ * The function is called once per request and decides — based on its own
43
+ * runtime config (e.g. whether the user opted into D1 sessions) — whether
44
+ * to return a per-request Kysely or null. Use this for features like D1
45
+ * read-replica sessions, bookmark cookies, or any per-request DB handle.
46
+ *
47
+ * When false or absent, the generator emits a stub that returns null and
48
+ * the middleware takes its default (singleton) path.
49
+ */
50
+ supportsRequestScope?: boolean;
36
51
  }
37
52
 
38
53
  export interface SqliteConfig {