@hatk/hatk 0.0.1-alpha.23 → 0.0.1-alpha.25

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 (63) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +94 -0
  4. package/dist/backfill.d.ts.map +1 -1
  5. package/dist/backfill.js +12 -0
  6. package/dist/cli.js +186 -66
  7. package/dist/config.d.ts +1 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/database/db.d.ts.map +1 -1
  11. package/dist/database/db.js +5 -1
  12. package/dist/dev-entry.d.ts +8 -0
  13. package/dist/dev-entry.d.ts.map +1 -0
  14. package/dist/dev-entry.js +109 -0
  15. package/dist/feeds.d.ts +4 -0
  16. package/dist/feeds.d.ts.map +1 -1
  17. package/dist/feeds.js +41 -2
  18. package/dist/hooks.d.ts +7 -0
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +11 -1
  21. package/dist/labels.d.ts +14 -0
  22. package/dist/labels.d.ts.map +1 -1
  23. package/dist/labels.js +13 -1
  24. package/dist/main.js +49 -17
  25. package/dist/oauth/server.d.ts +2 -0
  26. package/dist/oauth/server.d.ts.map +1 -1
  27. package/dist/oauth/server.js +91 -1
  28. package/dist/oauth/session.d.ts +9 -0
  29. package/dist/oauth/session.d.ts.map +1 -0
  30. package/dist/oauth/session.js +65 -0
  31. package/dist/opengraph.d.ts +10 -0
  32. package/dist/opengraph.d.ts.map +1 -1
  33. package/dist/opengraph.js +102 -4
  34. package/dist/pds-proxy.d.ts +39 -0
  35. package/dist/pds-proxy.d.ts.map +1 -0
  36. package/dist/pds-proxy.js +173 -0
  37. package/dist/renderer.d.ts +27 -0
  38. package/dist/renderer.d.ts.map +1 -0
  39. package/dist/renderer.js +46 -0
  40. package/dist/response.d.ts +16 -0
  41. package/dist/response.d.ts.map +1 -0
  42. package/dist/response.js +69 -0
  43. package/dist/scanner.d.ts +21 -0
  44. package/dist/scanner.d.ts.map +1 -0
  45. package/dist/scanner.js +88 -0
  46. package/dist/server-init.d.ts +8 -0
  47. package/dist/server-init.d.ts.map +1 -0
  48. package/dist/server-init.js +59 -0
  49. package/dist/server.d.ts +26 -3
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +473 -616
  52. package/dist/setup.d.ts +7 -0
  53. package/dist/setup.d.ts.map +1 -1
  54. package/dist/setup.js +13 -1
  55. package/dist/test.d.ts.map +1 -1
  56. package/dist/test.js +12 -22
  57. package/dist/vite-plugin.d.ts +1 -1
  58. package/dist/vite-plugin.d.ts.map +1 -1
  59. package/dist/vite-plugin.js +245 -75
  60. package/dist/xrpc.d.ts +13 -0
  61. package/dist/xrpc.d.ts.map +1 -1
  62. package/dist/xrpc.js +87 -1
  63. package/package.json +8 -5
package/dist/setup.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface SetupContext {
14
14
  }) => Promise<BulkInserter>;
15
15
  };
16
16
  }
17
+ export type SetupHandler = (ctx: SetupContext) => Promise<void>;
18
+ export declare function defineSetup(handler: SetupHandler): {
19
+ __type: "setup";
20
+ handler: SetupHandler;
21
+ };
17
22
  /**
18
23
  * Run all setup scripts in the given directory on server boot.
19
24
  *
@@ -25,4 +30,6 @@ export interface SetupContext {
25
30
  * @param setupDir - Absolute path to the `setup/` directory
26
31
  */
27
32
  export declare function initSetup(setupDir: string): Promise<void>;
33
+ /** Run a single setup handler with a SetupContext. */
34
+ export declare function runSetupHandler(name: string, handler: SetupHandler): Promise<void>;
28
35
  //# sourceMappingURL=setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnD,QAAQ,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,GAAG,EAAE,CAAA;SAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC9E,kBAAkB,EAAE,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,KAChE,OAAO,CAAC,YAAY,CAAC,CAAA;KAC3B,CAAA;CACF;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnD,QAAQ,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,GAAG,EAAE,CAAA;SAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC9E,kBAAkB,EAAE,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,KAChE,OAAO,CAAC,YAAY,CAAC,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE/D,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY;;;EAEhD;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D;AAED,sDAAsD;AACtD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxF"}
package/dist/setup.js CHANGED
@@ -34,6 +34,9 @@ import { resolve, relative } from 'node:path';
34
34
  import { readdirSync, statSync } from 'node:fs';
35
35
  import { log } from "./logger.js";
36
36
  import { querySQL, runSQL, runBatch, createBulkInserterSQL } from "./database/db.js";
37
+ export function defineSetup(handler) {
38
+ return { __type: 'setup', handler };
39
+ }
37
40
  /** Recursively collect .ts/.js files in a directory, skipping files prefixed with `_`. */
38
41
  function walkDir(dir) {
39
42
  const results = [];
@@ -67,7 +70,7 @@ export async function initSetup(setupDir) {
67
70
  return;
68
71
  for (const scriptPath of files) {
69
72
  const name = relative(setupDir, scriptPath).replace(/\.(ts|js)$/, '');
70
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
73
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
71
74
  const handler = mod.default?.handler || mod.default;
72
75
  if (typeof handler !== 'function') {
73
76
  console.warn(`[setup] ${name}: no handler function found, skipping`);
@@ -81,3 +84,12 @@ export async function initSetup(setupDir) {
81
84
  log(`[setup] done: ${name}`);
82
85
  }
83
86
  }
87
+ /** Run a single setup handler with a SetupContext. */
88
+ export async function runSetupHandler(name, handler) {
89
+ const ctx = {
90
+ db: { query: querySQL, run: runSQL, runBatch, createBulkInserter: createBulkInserterSQL },
91
+ };
92
+ log(`[setup] running: ${name}`);
93
+ await handler(ctx);
94
+ log(`[setup] done: ${name}`);
95
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAmBzD,OAAO,EAAE,IAAI,IAAI,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACpD,CAAA;IACD,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC5E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IACnE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,KAAK,WAAW,CAAA;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAA;IACpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7E,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAiL9D;AA8BD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAgD3D"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkBzD,OAAO,EAAE,IAAI,IAAI,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACpD,CAAA;IACD,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC5E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IACnE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,KAAK,WAAW,CAAA;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAA;IACpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7E,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAwK9D;AA8BD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAgD3D"}
package/dist/test.js CHANGED
@@ -5,13 +5,12 @@ import { loadConfig } from "./config.js";
5
5
  import { loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL, } from "./database/schema.js";
6
6
  import { initDatabase, querySQL, runSQL, insertRecord, closeDatabase } from "./database/db.js";
7
7
  import { createAdapter } from "./database/adapter-factory.js";
8
+ import { SQLITE_DIALECT } from "./database/dialect.js";
8
9
  import { setSearchPort } from "./database/fts.js";
9
- import { initFeeds, executeFeed, listFeeds, createPaginate } from "./feeds.js";
10
- import { initXrpc, executeXrpc, listXrpc, configureRelay } from "./xrpc.js";
11
- import { initOpengraph } from "./opengraph.js";
12
- import { initLabels } from "./labels.js";
10
+ import { executeFeed, listFeeds, createPaginate } from "./feeds.js";
11
+ import { executeXrpc, listXrpc, configureRelay } from "./xrpc.js";
12
+ import { initServer } from "./server-init.js";
13
13
  import { discoverViews } from "./views.js";
14
- import { loadOnLoginHook } from "./hooks.js";
15
14
  import { validateLexicons } from '@bigmoves/lexicon';
16
15
  import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./database/db.js";
17
16
  import { seed as createSeedHelpers } from "./seed.js";
@@ -58,25 +57,16 @@ export async function createTestContext() {
58
57
  continue;
59
58
  const schema = generateTableSchema(nsid, lexicon, lexicons);
60
59
  schemas.push(schema);
61
- ddlStatements.push(generateCreateTableSQL(schema));
60
+ ddlStatements.push(generateCreateTableSQL(schema, SQLITE_DIALECT));
62
61
  }
63
- // In-memory database
64
- const { adapter, searchPort } = await createAdapter('duckdb');
62
+ // In-memory SQLite — faster startup, no native module issues in Vite's module runner
63
+ const { adapter, searchPort } = await createAdapter('sqlite');
65
64
  setSearchPort(searchPort);
66
65
  await initDatabase(adapter, ':memory:', schemas, ddlStatements);
67
- // Discover views + hooks
66
+ // Discover views
68
67
  discoverViews();
69
- try {
70
- await loadOnLoginHook(resolve(configDir, 'hooks'));
71
- }
72
- catch { }
73
- // Skip setup hooks in test context — they're for server boot-time
74
- // initialization (e.g. importing large datasets) and not appropriate for tests
75
- // Discover feeds, xrpc, labels
76
- await initFeeds(resolve(configDir, 'feeds'));
77
- await initXrpc(resolve(configDir, 'xrpc'));
78
- await initOpengraph(resolve(configDir, 'og'));
79
- await initLabels(resolve(configDir, 'labels'));
68
+ // Discover feeds, xrpc, labels, hooks, og from server/ directory (skip setup scripts in tests)
69
+ await initServer(resolve(configDir, 'server'), { skipSetup: true });
80
70
  return {
81
71
  db: { query: querySQL, run: runSQL },
82
72
  _config: config,
@@ -234,8 +224,8 @@ export async function startTestServer() {
234
224
  // Import startServer — it creates the HTTP server and returns it
235
225
  const { startServer } = await import("./server.js");
236
226
  // Start server on port 0 (random available port)
237
- const resolveViewer = (req) => {
238
- const did = req.headers['x-test-viewer'];
227
+ const resolveViewer = (request) => {
228
+ const did = request.headers.get('x-test-viewer');
239
229
  return typeof did === 'string' ? { did } : null;
240
230
  };
241
231
  const httpServer = startServer(0, ctx._collections, ctx._config.publicDir, ctx._config.oauth, ctx._config.admins, resolveViewer);
@@ -1,4 +1,4 @@
1
- import type { Plugin } from 'vite';
1
+ import { type Plugin } from 'vite';
2
2
  export declare function hatk(opts?: {
3
3
  port?: number;
4
4
  }): Plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAKlC,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAiGrD"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,MAAM,EAA6C,MAAM,MAAM,CAAA;AA8D3G,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA2NrD"}
@@ -1,97 +1,267 @@
1
- import { spawn } from 'node:child_process';
1
+ import { createRunnableDevEnvironment } from 'vite';
2
2
  import { resolve } from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
+ import { execSync } from 'node:child_process';
5
+ import { isHatkRoute } from "./adapter.js";
6
+ /** Boot the local PDS if a docker-compose.yml exists. */
7
+ async function ensurePds() {
8
+ if (!existsSync(resolve('docker-compose.yml')))
9
+ return;
10
+ try {
11
+ const res = await fetch('http://localhost:2583/xrpc/_health');
12
+ if (res.ok)
13
+ return;
14
+ }
15
+ catch { }
16
+ console.log('[hatk] Starting PDS...');
17
+ execSync('docker compose up -d', { stdio: 'inherit', cwd: process.cwd() });
18
+ for (let i = 0; i < 30; i++) {
19
+ try {
20
+ const res = await fetch('http://localhost:2583/xrpc/_health');
21
+ if (res.ok) {
22
+ console.log('[hatk] PDS ready');
23
+ return;
24
+ }
25
+ }
26
+ catch { }
27
+ await new Promise((r) => setTimeout(r, 1000));
28
+ }
29
+ console.error('[hatk] PDS failed to start');
30
+ }
31
+ /** Run seed file if it exists. */
32
+ function runSeed() {
33
+ const seedFile = resolve('seeds/seed.ts');
34
+ if (!existsSync(seedFile))
35
+ return;
36
+ try {
37
+ execSync(`npx tsx ${seedFile}`, { stdio: 'inherit', cwd: process.cwd() });
38
+ }
39
+ catch { }
40
+ }
41
+ /** Walk all loaded modules in the module graphs to collect CSS URLs for SSR. */
42
+ function collectAllCss(server) {
43
+ const cssUrls = new Set();
44
+ for (const envName of ['hatk', 'client']) {
45
+ const env = server.environments[envName];
46
+ if (!env?.moduleGraph)
47
+ continue;
48
+ // TODO: uses internal Vite module graph API — may break across Vite minor versions
49
+ for (const mod of env.moduleGraph.idToModuleMap?.values?.() ?? []) {
50
+ const url = mod.url || '';
51
+ if (/\.(css|scss|less|styl|stylus|pcss|postcss)(\?|$)/.test(url)) {
52
+ cssUrls.add(url);
53
+ }
54
+ if (url.includes('type=style')) {
55
+ cssUrls.add(url);
56
+ }
57
+ }
58
+ }
59
+ if (cssUrls.size === 0)
60
+ return '';
61
+ return Array.from(cssUrls)
62
+ .map((url) => `<link rel="stylesheet" href="${url}">`)
63
+ .join('\n');
64
+ }
4
65
  export function hatk(opts) {
5
- const devPort = 3000;
6
- const backendPort = opts?.port ?? devPort + 1;
7
- const issuer = `http://127.0.0.1:${devPort}`;
8
- let serverProcess = null;
66
+ const devPort = opts?.port ?? 3000;
67
+ let handler = null;
68
+ let ssrRenderPage = null;
69
+ let ssrGetRenderer = null;
70
+ let reloadServer = null;
71
+ let reloadTimer = null;
9
72
  return {
10
73
  name: 'vite-plugin-hatk',
74
+ // Rewrite $hatk imports in source code so SSR module runners can resolve them.
75
+ // vite-plus's fetchModule bypasses resolve.alias for bare imports.
76
+ transform(code, id) {
77
+ if (!code.includes('$hatk'))
78
+ return;
79
+ const hatk = resolve('hatk.generated.ts');
80
+ const hatkClient = resolve('hatk.generated.client.ts');
81
+ return code
82
+ .replace(/from\s+['"](\$hatk\/client)['"]/g, `from '${hatkClient}'`)
83
+ .replace(/from\s+['"](\$hatk)['"]/g, `from '${hatk}'`);
84
+ },
11
85
  config() {
12
- const target = `http://127.0.0.1:${backendPort}`;
13
- // changeOrigin: false preserves the original Host header so DPoP htu matches
14
- const rule = { target, changeOrigin: false };
15
86
  return {
87
+ environments: {
88
+ hatk: {
89
+ resolve: {
90
+ conditions: ['svelte'],
91
+ noExternal: ['svelte', '@tanstack/svelte-query'],
92
+ external: true,
93
+ },
94
+ dev: {
95
+ createEnvironment(name, config) {
96
+ return createRunnableDevEnvironment(name, config);
97
+ },
98
+ optimizeDeps: {
99
+ exclude: ['better-sqlite3', '@duckdb/node-api'],
100
+ },
101
+ },
102
+ build: {
103
+ outDir: 'dist/server',
104
+ ssr: true,
105
+ rollupOptions: {
106
+ external: ['better-sqlite3', '@duckdb/node-api'],
107
+ },
108
+ },
109
+ },
110
+ },
16
111
  server: {
17
112
  host: '127.0.0.1',
18
113
  port: devPort,
114
+ fs: {
115
+ allow: ['.'],
116
+ },
19
117
  watch: {
20
118
  ignored: ['**/db/**', '**/data/**'],
21
119
  },
22
- proxy: {
23
- '/xrpc': rule,
24
- '/oauth/par': rule,
25
- '/oauth/token': rule,
26
- '/oauth/jwks': rule,
27
- '/oauth/authorize': rule,
28
- '/oauth/callback': {
29
- ...rule,
30
- // Only proxy the PDS callback (iss !== our issuer) to the backend.
31
- // The client-side callback (iss === our issuer) should reach the SPA.
32
- bypass(req) {
33
- const url = new URL(req.url, issuer);
34
- if (url.searchParams.get('iss') === issuer)
35
- return req.url;
36
- },
37
- },
38
- '/oauth/client-metadata.json': rule,
39
- '/oauth-client-metadata.json': rule,
40
- '/.well-known': rule,
41
- '/info': rule,
42
- '/repos': rule,
43
- '/og': rule,
44
- '/admin': rule,
45
- '/_health': rule,
46
- },
47
- },
48
- test: {
49
- projects: [
50
- {
51
- test: {
52
- name: 'unit',
53
- include: ['test/feeds/**/*.test.ts', 'test/xrpc/**/*.test.ts'],
54
- },
55
- },
56
- {
57
- test: {
58
- name: 'integration',
59
- include: ['test/integration/**/*.test.ts'],
60
- },
61
- },
62
- ],
63
120
  },
64
121
  };
65
122
  },
66
- configureServer(server) {
67
- const mainPath = resolve(import.meta.dirname, 'main.js');
68
- const watchDirs = ['xrpc', 'feeds', 'labels', 'jobs', 'setup', 'lexicons'].filter((d) => existsSync(d));
69
- const watchArgs = watchDirs.flatMap((d) => ['--watch-path', d]);
70
- serverProcess = spawn('npx', ['tsx', 'watch', ...watchArgs, mainPath, 'hatk.config.ts'], {
71
- stdio: 'inherit',
72
- cwd: process.cwd(),
73
- env: {
74
- ...process.env,
75
- PORT: String(backendPort),
76
- OAUTH_ISSUER: process.env.OAUTH_ISSUER || issuer,
77
- DEV_MODE: '1',
78
- },
79
- });
80
- // Suppress ECONNREFUSED proxy errors while backend is booting
81
- const origError = server.config.logger.error;
82
- server.config.logger.error = (msg, opts) => {
83
- if (typeof msg === 'string' && msg.includes('ECONNREFUSED'))
123
+ async configureServer(server) {
124
+ // Skip hatk server boot in test mode — tests manage their own context
125
+ if (process.env.VITEST)
126
+ return;
127
+ // Boot PDS and run seeds before starting
128
+ await ensurePds();
129
+ runSeed();
130
+ const env = server.environments.hatk;
131
+ if (!env || !('runner' in env)) {
132
+ console.error('[hatk] hatk environment not available — is Vite 8 with Environment API?');
133
+ return;
134
+ }
135
+ // Load the hatk boot module through the module runner
136
+ const mainPath = resolve(import.meta.dirname, 'dev-entry.js');
137
+ const mod = await env.runner.import(mainPath);
138
+ handler = mod.handler;
139
+ ssrRenderPage = mod.renderPage;
140
+ ssrGetRenderer = mod.getRenderer;
141
+ reloadServer = mod.reloadServer;
142
+ globalThis.__hatk_callXrpc = mod.callXrpc;
143
+ // Capture cookie parser and name for SSR viewer resolution
144
+ const ssrParseSessionCookie = mod.parseSessionCookie ?? null;
145
+ globalThis.__hatk_parseSessionCookie = ssrParseSessionCookie;
146
+ globalThis.__hatk_sessionCookieName = mod.getSessionCookieName?.() ?? '__hatk_session';
147
+ if (ssrGetRenderer?.()) {
148
+ console.log('[hatk] SSR ready');
149
+ }
150
+ // API routes — must run before Vite's static middleware
151
+ server.middlewares.use(async (req, res, next) => {
152
+ const url = new URL(req.url, `http://localhost:${devPort}`);
153
+ if (!isHatkRoute(url.pathname) || !handler) {
154
+ next();
84
155
  return;
85
- origError(msg, opts);
86
- };
87
- server.httpServer?.on('close', () => {
88
- serverProcess?.kill();
89
- serverProcess = null;
156
+ }
157
+ try {
158
+ const { toRequest, sendResponse } = await import('./adapter.js');
159
+ const request = toRequest(req, `http://localhost:${devPort}`);
160
+ const response = await handler(request);
161
+ if (response.status === 404) {
162
+ next();
163
+ return;
164
+ }
165
+ await sendResponse(res, response);
166
+ }
167
+ catch (err) {
168
+ console.error('[hatk]', err.message);
169
+ next(err);
170
+ }
90
171
  });
172
+ // SSR middleware — returned function runs after htmlFallback but before indexHtmlMiddleware
173
+ return () => {
174
+ server.middlewares.use(async (req, res, next) => {
175
+ if (!ssrGetRenderer?.()) {
176
+ next();
177
+ return;
178
+ }
179
+ const accept = req.headers.accept || '';
180
+ const url = req.originalUrl || req.url;
181
+ if (!accept.includes('text/html') || !url) {
182
+ next();
183
+ return;
184
+ }
185
+ try {
186
+ const { readFileSync } = await import('node:fs');
187
+ const rawHtml = readFileSync(resolve('index.html'), 'utf-8');
188
+ const template = await server.transformIndexHtml(url, rawHtml);
189
+ const fullUrl = new URL(url, `http://localhost:${devPort}`);
190
+ const headers = {};
191
+ if (req.headers.cookie)
192
+ headers.cookie = req.headers.cookie;
193
+ const request = new Request(fullUrl.href, { headers });
194
+ // Resolve viewer from session cookie for SSR
195
+ // TODO: globalThis.__hatk_viewer is not safe for concurrent SSR requests.
196
+ // Replace with AsyncLocalStorage when callXrpc supports per-request context.
197
+ let viewer = null;
198
+ if (ssrParseSessionCookie) {
199
+ try {
200
+ viewer = await ssrParseSessionCookie(request);
201
+ }
202
+ catch { }
203
+ }
204
+ ;
205
+ globalThis.__hatk_viewer = viewer;
206
+ let renderedHtml;
207
+ try {
208
+ renderedHtml = await ssrRenderPage(template, request);
209
+ }
210
+ finally {
211
+ ;
212
+ globalThis.__hatk_viewer = null;
213
+ }
214
+ // Inject viewer into HTML so client has it before OAuth initializes
215
+ if (renderedHtml && viewer) {
216
+ const script = `<script>globalThis.__hatk_viewer=${JSON.stringify(viewer)}</script>`;
217
+ renderedHtml = renderedHtml.replace('</head>', `${script}\n</head>`);
218
+ }
219
+ if (!renderedHtml) {
220
+ next();
221
+ return;
222
+ }
223
+ // Collect CSS from all loaded modules to prevent FOUC
224
+ const cssLinks = collectAllCss(server);
225
+ let html = renderedHtml;
226
+ if (cssLinks) {
227
+ html = html.replace('</head>', `${cssLinks}\n</head>`);
228
+ }
229
+ res.setHeader('Content-Type', 'text/html');
230
+ res.end(html);
231
+ }
232
+ catch (err) {
233
+ console.error('[hatk] SSR error:', err.message);
234
+ next(err);
235
+ }
236
+ });
237
+ };
238
+ },
239
+ // Handle HMR for server/ files in the hatk environment
240
+ hotUpdate(options) {
241
+ if (options.file.includes('/server/') && reloadServer) {
242
+ // Debounce: hotUpdate fires once per environment, only reload once
243
+ if (!reloadTimer) {
244
+ reloadTimer = setTimeout(() => {
245
+ reloadTimer = null;
246
+ reloadServer().then(() => {
247
+ console.log('[hatk] Server handlers reloaded');
248
+ }).catch((err) => {
249
+ console.error('[hatk] Failed to reload server handlers:', err.message);
250
+ });
251
+ }, 50);
252
+ }
253
+ }
91
254
  },
92
- buildEnd() {
93
- serverProcess?.kill();
94
- serverProcess = null;
255
+ // Two-stage production build
256
+ async buildApp(builder) {
257
+ // Stage 1: Build client
258
+ if (builder.environments.client) {
259
+ await builder.build(builder.environments.client);
260
+ }
261
+ // Stage 2: Build hatk server (if environment exists)
262
+ if (builder.environments.hatk) {
263
+ await builder.build(builder.environments.hatk);
264
+ }
95
265
  },
96
266
  };
97
267
  }
package/dist/xrpc.d.ts CHANGED
@@ -67,10 +67,23 @@ export declare function blobUrl(did: string, ref: unknown, preset?: 'avatar' | '
67
67
  * coerced against the matching lexicon definition.
68
68
  */
69
69
  export declare function initXrpc(xrpcDir: string): Promise<void>;
70
+ /** Register a single XRPC handler from a scanned server/ module. */
71
+ export declare function registerXrpcHandler(nsid: string, handlerModule: {
72
+ handler: (ctx: any) => Promise<any>;
73
+ }): void;
70
74
  /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
71
75
  export declare function executeXrpc(name: string, params: Record<string, string>, cursor: string | undefined, limit: number, viewer?: {
72
76
  did: string;
73
77
  } | null, input?: unknown): Promise<any | null>;
78
+ /** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
79
+ export declare function callXrpc(nsid: string, params?: Record<string, any>, input?: unknown): Promise<any>;
80
+ /**
81
+ * Register a core XRPC handler directly (no XrpcContext wrapping).
82
+ * Used for built-in dev.hatk.* handlers that manage their own dependencies.
83
+ */
84
+ export declare function registerCoreXrpcHandler(nsid: string, fn: (params: Record<string, string>, cursor: string | undefined, limit: number, viewer: {
85
+ did: string;
86
+ } | null, input?: unknown) => Promise<any>): void;
74
87
  /** Return all registered XRPC method names. */
75
88
  export declare function listXrpc(): string[];
76
89
  //# sourceMappingURL=xrpc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
package/dist/xrpc.js CHANGED
@@ -98,7 +98,7 @@ export async function initXrpc(xrpcDir) {
98
98
  for (const scriptPath of files) {
99
99
  const rel = relative(xrpcDir, scriptPath).replace(/\.(ts|js)$/, '');
100
100
  const name = rel.replace(/[\\/]/g, '.');
101
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
101
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
102
102
  const handler = mod.default;
103
103
  // Extract param schema from lexicon for validation and defaults
104
104
  const lexicon = getLexicon(name);
@@ -161,6 +161,65 @@ export async function initXrpc(xrpcDir) {
161
161
  log(`[xrpc] discovered: ${name}`);
162
162
  }
163
163
  }
164
+ /** Register a single XRPC handler from a scanned server/ module. */
165
+ export function registerXrpcHandler(nsid, handlerModule) {
166
+ const lexicon = getLexicon(nsid);
167
+ const paramsDef = lexicon?.defs?.main?.parameters;
168
+ const requiredParams = paramsDef?.required || [];
169
+ const paramProperties = paramsDef?.properties || {};
170
+ handlers.set(nsid, {
171
+ name: nsid,
172
+ execute: async (params, cursor, limit, viewer, input) => {
173
+ for (const [key, def] of Object.entries(paramProperties)) {
174
+ if (params[key] == null && def.default != null) {
175
+ params[key] = String(def.default);
176
+ }
177
+ if (params[key] != null && def.type === 'integer') {
178
+ params[key] = Number(params[key]);
179
+ }
180
+ }
181
+ for (const param of requiredParams) {
182
+ if (!params[param]) {
183
+ throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
184
+ }
185
+ }
186
+ const ctx = {
187
+ db: { query: querySQL, run: runSQL },
188
+ params,
189
+ input: input || {},
190
+ cursor,
191
+ limit,
192
+ viewer,
193
+ packCursor,
194
+ unpackCursor,
195
+ isTakendown: isTakendownDid,
196
+ filterTakendownDids,
197
+ search: searchRecords,
198
+ resolve: resolveRecords,
199
+ lookup: async (collection, field, values) => {
200
+ if (values.length === 0)
201
+ return new Map();
202
+ const unique = [...new Set(values.filter(Boolean))];
203
+ return lookupByFieldBatch(collection, field, unique);
204
+ },
205
+ count: async (collection, field, values) => {
206
+ if (values.length === 0)
207
+ return new Map();
208
+ const unique = [...new Set(values.filter(Boolean))];
209
+ return countByFieldBatch(collection, field, unique);
210
+ },
211
+ exists: async (collection, filters) => {
212
+ const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
213
+ const uri = await findUriByFields(collection, conditions);
214
+ return uri !== null;
215
+ },
216
+ labels: queryLabelsForUris,
217
+ blobUrl,
218
+ };
219
+ return handlerModule.handler(ctx);
220
+ },
221
+ });
222
+ }
164
223
  /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
165
224
  export async function executeXrpc(name, params, cursor, limit, viewer, input) {
166
225
  const handler = handlers.get(name);
@@ -168,6 +227,33 @@ export async function executeXrpc(name, params, cursor, limit, viewer, input) {
168
227
  return null;
169
228
  return handler.execute(params, cursor, limit, viewer || null, input);
170
229
  }
230
+ /** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
231
+ export async function callXrpc(nsid, params = {}, input) {
232
+ const viewer = globalThis.__hatk_viewer ?? null;
233
+ // In externalized module context (e.g. SSR), delegate to the runner's callXrpc via globalThis.
234
+ // The runner's module instance has all registered handlers; this (Node's) instance may not.
235
+ if (handlers.size === 0 && globalThis.__hatk_callXrpc) {
236
+ return globalThis.__hatk_callXrpc(nsid, params, input);
237
+ }
238
+ const stringParams = {};
239
+ for (const [k, v] of Object.entries(params)) {
240
+ if (v != null)
241
+ stringParams[k] = String(v);
242
+ }
243
+ const limit = params.limit ? Number(params.limit) : 20;
244
+ const cursor = params.cursor ?? undefined;
245
+ const result = await executeXrpc(nsid, stringParams, cursor, limit, viewer, input);
246
+ if (result === null)
247
+ throw new Error(`No XRPC handler registered for ${nsid}`);
248
+ return result;
249
+ }
250
+ /**
251
+ * Register a core XRPC handler directly (no XrpcContext wrapping).
252
+ * Used for built-in dev.hatk.* handlers that manage their own dependencies.
253
+ */
254
+ export function registerCoreXrpcHandler(nsid, fn) {
255
+ handlers.set(nsid, { name: nsid, execute: fn });
256
+ }
171
257
  /** Return all registered XRPC method names. */
172
258
  export function listXrpc() {
173
259
  return Array.from(handlers.keys());