@eldrin-project/eldrin-app-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,562 @@
1
+ import { createContext, useContext, createElement } from 'react';
2
+ import { readdir, readFile } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { resolve, basename } from 'path';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ // src/app/createApp.tsx
8
+
9
+ // src/migrations/checksum.ts
10
+ var CHECKSUM_PREFIX = "sha256:";
11
+ async function calculateChecksum(content, options = {}) {
12
+ const encoder = new TextEncoder();
13
+ const data = encoder.encode(content);
14
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
15
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
16
+ const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
17
+ return options.prefixed ? `${CHECKSUM_PREFIX}${hex}` : hex;
18
+ }
19
+ async function calculatePrefixedChecksum(content) {
20
+ return calculateChecksum(content, { prefixed: true });
21
+ }
22
+ async function verifyChecksum(content, expectedChecksum) {
23
+ const normalizedExpected = expectedChecksum.startsWith(CHECKSUM_PREFIX) ? expectedChecksum.slice(CHECKSUM_PREFIX.length) : expectedChecksum;
24
+ const actualChecksum = await calculateChecksum(content);
25
+ return actualChecksum === normalizedExpected;
26
+ }
27
+
28
+ // src/migrations/sql-parser.ts
29
+ function parseSQLStatements(sql) {
30
+ const statements = [];
31
+ let current = "";
32
+ let inString = false;
33
+ let stringChar = "";
34
+ let i = 0;
35
+ const lines = sql.split("\n");
36
+ const cleanedLines = [];
37
+ for (const line of lines) {
38
+ let cleanLine = "";
39
+ let lineInString = false;
40
+ let lineStringChar = "";
41
+ for (let j = 0; j < line.length; j++) {
42
+ const char = line[j];
43
+ const nextChar = line[j + 1];
44
+ if (!lineInString) {
45
+ if (char === "-" && nextChar === "-") {
46
+ break;
47
+ }
48
+ if (char === "'" || char === '"') {
49
+ lineInString = true;
50
+ lineStringChar = char;
51
+ }
52
+ } else {
53
+ if (char === lineStringChar) {
54
+ if (nextChar === lineStringChar) {
55
+ cleanLine += char;
56
+ j++;
57
+ cleanLine += line[j];
58
+ continue;
59
+ }
60
+ lineInString = false;
61
+ }
62
+ }
63
+ cleanLine += char;
64
+ }
65
+ cleanedLines.push(cleanLine);
66
+ }
67
+ const cleanedSql = cleanedLines.join("\n");
68
+ for (i = 0; i < cleanedSql.length; i++) {
69
+ const char = cleanedSql[i];
70
+ if (!inString) {
71
+ if (char === "'" || char === '"') {
72
+ inString = true;
73
+ stringChar = char;
74
+ current += char;
75
+ } else if (char === ";") {
76
+ const trimmed2 = current.trim();
77
+ if (trimmed2.length > 0) {
78
+ statements.push(trimmed2);
79
+ }
80
+ current = "";
81
+ } else {
82
+ current += char;
83
+ }
84
+ } else {
85
+ current += char;
86
+ if (char === stringChar) {
87
+ const nextChar = cleanedSql[i + 1];
88
+ if (nextChar === stringChar) {
89
+ i++;
90
+ current += nextChar;
91
+ } else {
92
+ inString = false;
93
+ }
94
+ }
95
+ }
96
+ }
97
+ const trimmed = current.trim();
98
+ if (trimmed.length > 0) {
99
+ statements.push(trimmed);
100
+ }
101
+ return statements;
102
+ }
103
+ function isValidMigrationFilename(filename) {
104
+ if (!filename.endsWith(".sql") || filename.endsWith(".rollback.sql")) {
105
+ return false;
106
+ }
107
+ const pattern = /^\d{14}-[a-z0-9-]+\.sql$/;
108
+ return pattern.test(filename);
109
+ }
110
+ function isValidRollbackFilename(filename) {
111
+ if (!filename.endsWith(".rollback.sql")) {
112
+ return false;
113
+ }
114
+ const pattern = /^\d{14}-[a-z0-9-]+\.rollback\.sql$/;
115
+ return pattern.test(filename);
116
+ }
117
+ function extractTimestamp(filename) {
118
+ const match = filename.match(/^(\d{14})-/);
119
+ return match ? match[1] : null;
120
+ }
121
+ function getRollbackFilename(migrationFilename) {
122
+ return migrationFilename.replace(/\.sql$/, ".rollback.sql");
123
+ }
124
+
125
+ // src/migrations/runner.ts
126
+ var CREATE_MIGRATIONS_TABLE = `
127
+ CREATE TABLE IF NOT EXISTS _eldrin_migrations (
128
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
129
+ filename TEXT NOT NULL UNIQUE,
130
+ checksum TEXT NOT NULL,
131
+ executed_at INTEGER NOT NULL,
132
+ execution_time_ms INTEGER
133
+ )
134
+ `;
135
+ var defaultLogger = () => {
136
+ };
137
+ async function runMigrations(db, options = {}) {
138
+ const { migrations = [], skipChecksumVerification = false, onLog = defaultLogger } = options;
139
+ const log = (message, level = "info") => {
140
+ onLog(message, level);
141
+ };
142
+ const result = {
143
+ success: true,
144
+ executed: 0,
145
+ migrations: []
146
+ };
147
+ try {
148
+ log("Creating migrations tracking table if not exists...");
149
+ await db.batch([db.prepare(CREATE_MIGRATIONS_TABLE)]);
150
+ const validMigrations = migrations.filter((m) => isValidMigrationFilename(m.name)).sort((a, b) => a.name.localeCompare(b.name));
151
+ if (validMigrations.length === 0) {
152
+ log("No migration files found");
153
+ return result;
154
+ }
155
+ log(`Found ${validMigrations.length} migration file(s)`);
156
+ const { results: executed } = await db.prepare("SELECT filename, checksum FROM _eldrin_migrations ORDER BY filename").all();
157
+ const executedMap = new Map(executed?.map((e) => [e.filename, e.checksum]) ?? []);
158
+ if (!skipChecksumVerification) {
159
+ for (const migration of validMigrations) {
160
+ const storedChecksum = executedMap.get(migration.name);
161
+ if (storedChecksum) {
162
+ const currentChecksum = await calculateChecksum(migration.content);
163
+ if (currentChecksum !== storedChecksum) {
164
+ const error = new Error(
165
+ `Migration ${migration.name} has been modified after execution! Expected checksum: ${storedChecksum}, got: ${currentChecksum}`
166
+ );
167
+ log(error.message, "error");
168
+ return {
169
+ success: false,
170
+ executed: 0,
171
+ migrations: [],
172
+ error: {
173
+ migration: migration.name,
174
+ message: error.message
175
+ }
176
+ };
177
+ }
178
+ }
179
+ }
180
+ }
181
+ for (const [filename] of executedMap) {
182
+ const hasFile = validMigrations.some((m) => m.name === filename);
183
+ if (!hasFile) {
184
+ log(`Warning: Orphaned migration in database: ${filename}`, "warn");
185
+ }
186
+ }
187
+ const pending = validMigrations.filter((m) => !executedMap.has(m.name));
188
+ if (pending.length === 0) {
189
+ log("No pending migrations");
190
+ return result;
191
+ }
192
+ log(`Found ${pending.length} pending migration(s)`);
193
+ for (const migration of pending) {
194
+ log(`Executing migration: ${migration.name}`);
195
+ const startTime = Date.now();
196
+ try {
197
+ const statements = parseSQLStatements(migration.content);
198
+ if (statements.length === 0) {
199
+ log(`Warning: Migration ${migration.name} contains no SQL statements`, "warn");
200
+ continue;
201
+ }
202
+ const checksum = await calculateChecksum(migration.content);
203
+ const batch = [
204
+ // Execute all statements from the migration
205
+ ...statements.map((sql) => db.prepare(sql)),
206
+ // Record execution in tracking table
207
+ db.prepare(
208
+ `INSERT INTO _eldrin_migrations
209
+ (filename, checksum, executed_at, execution_time_ms)
210
+ VALUES (?, ?, ?, ?)`
211
+ ).bind(migration.name, checksum, Date.now(), Date.now() - startTime)
212
+ ];
213
+ await db.batch(batch);
214
+ const executionTime = Date.now() - startTime;
215
+ log(`Migration ${migration.name} completed (${executionTime}ms)`);
216
+ result.executed++;
217
+ result.migrations.push({
218
+ name: migration.name,
219
+ executionTimeMs: executionTime
220
+ });
221
+ } catch (error) {
222
+ const errorMessage = error instanceof Error ? error.message : String(error);
223
+ log(`Migration ${migration.name} failed: ${errorMessage}`, "error");
224
+ return {
225
+ success: false,
226
+ executed: result.executed,
227
+ migrations: result.migrations,
228
+ error: {
229
+ migration: migration.name,
230
+ message: errorMessage
231
+ }
232
+ };
233
+ }
234
+ }
235
+ log(`Successfully executed ${result.executed} migration(s)`);
236
+ return result;
237
+ } catch (error) {
238
+ const errorMessage = error instanceof Error ? error.message : String(error);
239
+ log(`Migration system error: ${errorMessage}`, "error");
240
+ return {
241
+ success: false,
242
+ executed: result.executed,
243
+ migrations: result.migrations,
244
+ error: {
245
+ migration: "system",
246
+ message: errorMessage
247
+ }
248
+ };
249
+ }
250
+ }
251
+ async function getMigrationStatus(db, migrations) {
252
+ await db.batch([db.prepare(CREATE_MIGRATIONS_TABLE)]);
253
+ const { results: executed } = await db.prepare("SELECT * FROM _eldrin_migrations ORDER BY filename").all();
254
+ const executedMap = new Map(executed?.map((e) => [e.filename, e]) ?? []);
255
+ const validMigrations = migrations.filter((m) => isValidMigrationFilename(m.name));
256
+ const pending = validMigrations.filter((m) => !executedMap.has(m.name)).map((m) => m.name).sort();
257
+ const orphaned = [];
258
+ for (const [filename] of executedMap) {
259
+ if (!validMigrations.some((m) => m.name === filename)) {
260
+ orphaned.push(filename);
261
+ }
262
+ }
263
+ const checksumMismatches = [];
264
+ for (const migration of validMigrations) {
265
+ const record = executedMap.get(migration.name);
266
+ if (record) {
267
+ const currentChecksum = await calculateChecksum(migration.content);
268
+ if (currentChecksum !== record.checksum) {
269
+ checksumMismatches.push(migration.name);
270
+ }
271
+ }
272
+ }
273
+ return {
274
+ pending,
275
+ executed: executed ?? [],
276
+ orphaned,
277
+ checksumMismatches
278
+ };
279
+ }
280
+
281
+ // src/migrations/rollback.ts
282
+ async function rollbackMigrations(db, options) {
283
+ const { rollbackFiles, targetMigration, onLog = () => {
284
+ } } = options;
285
+ const log = (message, level = "info") => {
286
+ onLog(message, level);
287
+ };
288
+ const result = {
289
+ success: true,
290
+ rolledBack: []
291
+ };
292
+ try {
293
+ const { results: executed } = await db.prepare("SELECT filename FROM _eldrin_migrations ORDER BY filename DESC").all();
294
+ if (!executed || executed.length === 0) {
295
+ log("No migrations to rollback");
296
+ return result;
297
+ }
298
+ let toRollback;
299
+ if (targetMigration) {
300
+ const targetIndex = executed.findIndex((m) => m.filename === targetMigration);
301
+ if (targetIndex === -1) {
302
+ return {
303
+ success: false,
304
+ rolledBack: [],
305
+ error: {
306
+ migration: targetMigration,
307
+ message: `Target migration not found: ${targetMigration}`
308
+ }
309
+ };
310
+ }
311
+ toRollback = executed.slice(0, targetIndex).map((m) => m.filename);
312
+ } else {
313
+ toRollback = [executed[0].filename];
314
+ }
315
+ if (toRollback.length === 0) {
316
+ log("No migrations to rollback");
317
+ return result;
318
+ }
319
+ const rollbackMap = new Map(
320
+ rollbackFiles.filter((f) => isValidRollbackFilename(f.name)).map((f) => [f.name, f])
321
+ );
322
+ for (const migrationFilename of toRollback) {
323
+ const rollbackFilename = getRollbackFilename(migrationFilename);
324
+ const rollbackFile = rollbackMap.get(rollbackFilename);
325
+ if (!rollbackFile) {
326
+ return {
327
+ success: false,
328
+ rolledBack: result.rolledBack,
329
+ error: {
330
+ migration: migrationFilename,
331
+ message: `Rollback file not found: ${rollbackFilename}`
332
+ }
333
+ };
334
+ }
335
+ log(`Rolling back: ${migrationFilename}`);
336
+ try {
337
+ const statements = parseSQLStatements(rollbackFile.content);
338
+ if (statements.length === 0) {
339
+ log(`Warning: Rollback file ${rollbackFilename} contains no SQL statements`, "warn");
340
+ }
341
+ const batch = [
342
+ ...statements.map((sql) => db.prepare(sql)),
343
+ db.prepare("DELETE FROM _eldrin_migrations WHERE filename = ?").bind(migrationFilename)
344
+ ];
345
+ await db.batch(batch);
346
+ log(`Rolled back: ${migrationFilename}`);
347
+ result.rolledBack.push(migrationFilename);
348
+ } catch (error) {
349
+ const errorMessage = error instanceof Error ? error.message : String(error);
350
+ log(`Rollback failed for ${migrationFilename}: ${errorMessage}`, "error");
351
+ return {
352
+ success: false,
353
+ rolledBack: result.rolledBack,
354
+ error: {
355
+ migration: migrationFilename,
356
+ message: errorMessage
357
+ }
358
+ };
359
+ }
360
+ }
361
+ log(`Successfully rolled back ${result.rolledBack.length} migration(s)`);
362
+ return result;
363
+ } catch (error) {
364
+ const errorMessage = error instanceof Error ? error.message : String(error);
365
+ log(`Rollback system error: ${errorMessage}`, "error");
366
+ return {
367
+ success: false,
368
+ rolledBack: result.rolledBack,
369
+ error: {
370
+ migration: "system",
371
+ message: errorMessage
372
+ }
373
+ };
374
+ }
375
+ }
376
+ async function readMigrationFiles(dir) {
377
+ if (!existsSync(dir)) {
378
+ return [];
379
+ }
380
+ const files = await readdir(dir);
381
+ const sqlFiles = files.filter((f) => isValidMigrationFilename(f)).sort();
382
+ const migrations = [];
383
+ for (const file of sqlFiles) {
384
+ const content = await readFile(resolve(dir, file), "utf-8");
385
+ migrations.push({
386
+ name: basename(file),
387
+ content
388
+ });
389
+ }
390
+ return migrations;
391
+ }
392
+ async function generateMigrationManifest(options) {
393
+ const { migrationsDir, database } = options;
394
+ const files = await readMigrationFiles(migrationsDir);
395
+ const migrations = await Promise.all(
396
+ files.map(async (file) => {
397
+ const timestamp = extractTimestamp(file.name);
398
+ const checksum = await calculatePrefixedChecksum(file.content);
399
+ return {
400
+ id: timestamp || file.name.replace(".sql", ""),
401
+ file: file.name,
402
+ checksum
403
+ };
404
+ })
405
+ );
406
+ return {
407
+ manifest: {
408
+ database,
409
+ migrations
410
+ },
411
+ files
412
+ };
413
+ }
414
+ async function validateMigrationManifest(manifest, files) {
415
+ const errors = [];
416
+ for (const entry of manifest.migrations) {
417
+ const file = files.find((f) => f.name === entry.file);
418
+ if (!file) {
419
+ errors.push(`Missing file: ${entry.file}`);
420
+ continue;
421
+ }
422
+ const actualChecksum = await calculatePrefixedChecksum(file.content);
423
+ if (actualChecksum !== entry.checksum) {
424
+ errors.push(
425
+ `Checksum mismatch for ${entry.file}: expected ${entry.checksum}, got ${actualChecksum}`
426
+ );
427
+ }
428
+ const timestamp = extractTimestamp(file.name);
429
+ if (timestamp !== entry.id) {
430
+ errors.push(
431
+ `ID mismatch for ${entry.file}: expected ${timestamp}, got ${entry.id}`
432
+ );
433
+ }
434
+ }
435
+ for (const file of files) {
436
+ if (!manifest.migrations.some((m) => m.file === file.name)) {
437
+ errors.push(`File not in manifest: ${file.name}`);
438
+ }
439
+ }
440
+ return {
441
+ valid: errors.length === 0,
442
+ errors
443
+ };
444
+ }
445
+ var EldrinDatabaseContext = createContext(null);
446
+ function DatabaseProvider({
447
+ db,
448
+ migrationsComplete,
449
+ migrationResult,
450
+ children
451
+ }) {
452
+ const value = {
453
+ db,
454
+ migrationsComplete,
455
+ migrationResult
456
+ };
457
+ return /* @__PURE__ */ jsx(EldrinDatabaseContext.Provider, { value, children });
458
+ }
459
+ function useDatabase() {
460
+ const context = useContext(EldrinDatabaseContext);
461
+ if (context === null) {
462
+ throw new Error("useDatabase must be used within a DatabaseProvider (via createApp)");
463
+ }
464
+ return context.db;
465
+ }
466
+ function useDatabaseContext() {
467
+ const context = useContext(EldrinDatabaseContext);
468
+ if (context === null) {
469
+ throw new Error("useDatabaseContext must be used within a DatabaseProvider (via createApp)");
470
+ }
471
+ return context;
472
+ }
473
+ function useMigrationsComplete() {
474
+ const context = useContext(EldrinDatabaseContext);
475
+ return context?.migrationsComplete ?? false;
476
+ }
477
+
478
+ // src/app/createApp.tsx
479
+ function createApp(options) {
480
+ const { name, root: RootComponent, migrations = [], onMigrationsComplete, onMigrationError } = options;
481
+ const state = {
482
+ db: null,
483
+ migrationsComplete: false,
484
+ migrationResult: void 0,
485
+ mountedElement: void 0,
486
+ reactRoot: void 0
487
+ };
488
+ async function bootstrap(props) {
489
+ const db = props.db ?? null;
490
+ state.db = db;
491
+ if (db && migrations.length > 0) {
492
+ try {
493
+ const result = await runMigrations(db, {
494
+ migrations,
495
+ onLog: (message, level) => {
496
+ const prefix = `[${name}]`;
497
+ if (level === "error") {
498
+ console.error(prefix, message);
499
+ } else if (level === "warn") {
500
+ console.warn(prefix, message);
501
+ } else {
502
+ console.log(prefix, message);
503
+ }
504
+ }
505
+ });
506
+ state.migrationResult = result;
507
+ if (result.success) {
508
+ state.migrationsComplete = true;
509
+ onMigrationsComplete?.(result);
510
+ } else {
511
+ const error = new Error(result.error?.message ?? "Migration failed");
512
+ onMigrationError?.(error);
513
+ throw error;
514
+ }
515
+ } catch (error) {
516
+ state.migrationsComplete = false;
517
+ const err = error instanceof Error ? error : new Error(String(error));
518
+ onMigrationError?.(err);
519
+ throw err;
520
+ }
521
+ } else {
522
+ state.migrationsComplete = true;
523
+ }
524
+ }
525
+ async function mount(props) {
526
+ const domElement = props.domElement ?? document.getElementById(`app-${name}`);
527
+ if (!domElement) {
528
+ throw new Error(`No DOM element found for app "${name}". Expected element with id="app-${name}" or domElement prop.`);
529
+ }
530
+ state.mountedElement = domElement;
531
+ const rootElement = createElement(RootComponent, props.customProps ?? {});
532
+ const appElement = createElement(DatabaseProvider, {
533
+ db: state.db,
534
+ migrationsComplete: state.migrationsComplete,
535
+ migrationResult: state.migrationResult,
536
+ children: rootElement
537
+ });
538
+ const ReactDOM = await import('react-dom/client');
539
+ const root = ReactDOM.createRoot(domElement);
540
+ root.render(appElement);
541
+ state.reactRoot = root;
542
+ }
543
+ async function unmount(_props) {
544
+ if (state.reactRoot) {
545
+ state.reactRoot.unmount();
546
+ state.reactRoot = void 0;
547
+ }
548
+ if (state.mountedElement) {
549
+ state.mountedElement.innerHTML = "";
550
+ state.mountedElement = void 0;
551
+ }
552
+ }
553
+ return {
554
+ bootstrap,
555
+ mount,
556
+ unmount
557
+ };
558
+ }
559
+
560
+ export { CHECKSUM_PREFIX, DatabaseProvider, calculateChecksum, calculatePrefixedChecksum, createApp, extractTimestamp, generateMigrationManifest, getMigrationStatus, getRollbackFilename, isValidMigrationFilename, isValidRollbackFilename, parseSQLStatements, rollbackMigrations, runMigrations, useDatabase, useDatabaseContext, useMigrationsComplete, validateMigrationManifest, verifyChecksum };
561
+ //# sourceMappingURL=index.js.map
562
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrations/checksum.ts","../src/migrations/sql-parser.ts","../src/migrations/runner.ts","../src/migrations/rollback.ts","../src/migrations/marketplace.ts","../src/database/context.tsx","../src/app/createApp.tsx"],"names":["trimmed"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Checksum utilities for migration integrity verification\n *\n * Uses SHA-256 for content hashing via Web Crypto API\n * (compatible with Cloudflare Workers runtime)\n */\n\n/** Prefix for SHA-256 checksums in marketplace format */\nexport const CHECKSUM_PREFIX = 'sha256:';\n\n/**\n * Calculate SHA-256 checksum of content\n *\n * @param content - String content to hash\n * @param options - Options for checksum calculation\n * @param options.prefixed - If true, returns \"sha256:...\" format (default: false for backwards compatibility)\n * @returns Hex-encoded SHA-256 hash, optionally with prefix\n */\nexport async function calculateChecksum(\n content: string,\n options: { prefixed?: boolean } = {}\n): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(content);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return options.prefixed ? `${CHECKSUM_PREFIX}${hex}` : hex;\n}\n\n/**\n * Calculate SHA-256 checksum with sha256: prefix\n *\n * Convenience function for marketplace format\n *\n * @param content - String content to hash\n * @returns Prefixed checksum (e.g., \"sha256:abc123...\")\n */\nexport async function calculatePrefixedChecksum(content: string): Promise<string> {\n return calculateChecksum(content, { prefixed: true });\n}\n\n/**\n * Verify that content matches an expected checksum\n *\n * Handles both prefixed (\"sha256:...\") and unprefixed checksums\n *\n * @param content - Content to verify\n * @param expectedChecksum - Expected SHA-256 hex string (with or without prefix)\n * @returns true if checksums match\n */\nexport async function verifyChecksum(\n content: string,\n expectedChecksum: string\n): Promise<boolean> {\n // Strip prefix if present for comparison\n const normalizedExpected = expectedChecksum.startsWith(CHECKSUM_PREFIX)\n ? expectedChecksum.slice(CHECKSUM_PREFIX.length)\n : expectedChecksum;\n\n const actualChecksum = await calculateChecksum(content);\n return actualChecksum === normalizedExpected;\n}\n","/**\n * SQL statement parser for migration files\n *\n * Parses SQL content into individual statements for execution via db.batch()\n */\n\n/**\n * Parse SQL content into individual statements\n *\n * - Removes SQL comments (-- style)\n * - Splits by semicolon\n * - Handles multi-line statements\n * - Preserves string literals containing semicolons\n *\n * @param sql - Raw SQL content\n * @returns Array of individual SQL statements\n */\nexport function parseSQLStatements(sql: string): string[] {\n // First, handle the content preserving strings\n const statements: string[] = [];\n let current = '';\n let inString = false;\n let stringChar = '';\n let i = 0;\n\n // Pre-process: remove single-line comments\n const lines = sql.split('\\n');\n const cleanedLines: string[] = [];\n\n for (const line of lines) {\n let cleanLine = '';\n let lineInString = false;\n let lineStringChar = '';\n\n for (let j = 0; j < line.length; j++) {\n const char = line[j];\n const nextChar = line[j + 1];\n\n if (!lineInString) {\n // Check for comment start\n if (char === '-' && nextChar === '-') {\n // Rest of line is a comment, skip it\n break;\n }\n // Check for string start\n if (char === \"'\" || char === '\"') {\n lineInString = true;\n lineStringChar = char;\n }\n } else {\n // Check for string end (handle escaped quotes)\n if (char === lineStringChar) {\n // Check if it's an escaped quote\n if (nextChar === lineStringChar) {\n cleanLine += char;\n j++; // Skip the next char\n cleanLine += line[j];\n continue;\n }\n lineInString = false;\n }\n }\n\n cleanLine += char;\n }\n\n cleanedLines.push(cleanLine);\n }\n\n const cleanedSql = cleanedLines.join('\\n');\n\n // Now split by semicolons, respecting string literals\n for (i = 0; i < cleanedSql.length; i++) {\n const char = cleanedSql[i];\n\n if (!inString) {\n if (char === \"'\" || char === '\"') {\n inString = true;\n stringChar = char;\n current += char;\n } else if (char === ';') {\n // End of statement\n const trimmed = current.trim();\n if (trimmed.length > 0) {\n statements.push(trimmed);\n }\n current = '';\n } else {\n current += char;\n }\n } else {\n current += char;\n\n // Check for end of string\n if (char === stringChar) {\n // Check for escaped quote\n const nextChar = cleanedSql[i + 1];\n if (nextChar === stringChar) {\n // Escaped quote, skip next char\n i++;\n current += nextChar;\n } else {\n inString = false;\n }\n }\n }\n }\n\n // Don't forget any remaining content\n const trimmed = current.trim();\n if (trimmed.length > 0) {\n statements.push(trimmed);\n }\n\n return statements;\n}\n\n/**\n * Validate that a filename follows the migration naming convention\n *\n * Expected format: TIMESTAMP-description.sql\n * - TIMESTAMP: 14 digits (YYYYMMDDHHmmss)\n * - description: kebab-case\n *\n * @param filename - Filename to validate\n * @returns true if valid, false otherwise\n */\nexport function isValidMigrationFilename(filename: string): boolean {\n // Must end with .sql (but not .rollback.sql)\n if (!filename.endsWith('.sql') || filename.endsWith('.rollback.sql')) {\n return false;\n }\n\n // Pattern: 14-digit timestamp, hyphen, description, .sql\n const pattern = /^\\d{14}-[a-z0-9-]+\\.sql$/;\n return pattern.test(filename);\n}\n\n/**\n * Validate that a filename follows the rollback naming convention\n *\n * Expected format: TIMESTAMP-description.rollback.sql\n *\n * @param filename - Filename to validate\n * @returns true if valid rollback file, false otherwise\n */\nexport function isValidRollbackFilename(filename: string): boolean {\n // Must end with .rollback.sql\n if (!filename.endsWith('.rollback.sql')) {\n return false;\n }\n\n // Pattern: 14-digit timestamp, hyphen, description, .rollback.sql\n const pattern = /^\\d{14}-[a-z0-9-]+\\.rollback\\.sql$/;\n return pattern.test(filename);\n}\n\n/**\n * Extract timestamp from migration filename\n *\n * @param filename - Migration filename\n * @returns Timestamp string or null if invalid\n */\nexport function extractTimestamp(filename: string): string | null {\n const match = filename.match(/^(\\d{14})-/);\n return match ? match[1] : null;\n}\n\n/**\n * Get the rollback filename for a migration\n *\n * @param migrationFilename - Migration filename (e.g., \"20250115120000-create-table.sql\")\n * @returns Rollback filename (e.g., \"20250115120000-create-table.rollback.sql\")\n */\nexport function getRollbackFilename(migrationFilename: string): string {\n return migrationFilename.replace(/\\.sql$/, '.rollback.sql');\n}\n","/**\n * Migration runner for Eldrin apps\n *\n * Executes SQL migrations against a D1 database with:\n * - Automatic tracking table creation\n * - Checksum verification for integrity\n * - Sequential execution (one migration at a time)\n * - Transaction-based execution per migration\n */\n\nimport type { MigrationFile, MigrationRecord, MigrationResult, MigrationOptions } from '../types';\nimport { calculateChecksum } from './checksum';\nimport { parseSQLStatements, isValidMigrationFilename } from './sql-parser';\n\n/**\n * SQL to create the migration tracking table\n */\nconst CREATE_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _eldrin_migrations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n filename TEXT NOT NULL UNIQUE,\n checksum TEXT NOT NULL,\n executed_at INTEGER NOT NULL,\n execution_time_ms INTEGER\n)\n`;\n\n/**\n * Default logger that does nothing\n */\nconst defaultLogger = () => {};\n\n/**\n * Run pending migrations against the database\n *\n * @param db - D1 Database instance\n * @param options - Migration options\n * @returns Result of migration execution\n */\nexport async function runMigrations(\n db: D1Database,\n options: MigrationOptions = {}\n): Promise<MigrationResult> {\n const { migrations = [], skipChecksumVerification = false, onLog = defaultLogger } = options;\n\n const log = (message: string, level: 'info' | 'warn' | 'error' = 'info') => {\n onLog(message, level);\n };\n\n const result: MigrationResult = {\n success: true,\n executed: 0,\n migrations: [],\n };\n\n try {\n // 1. Ensure tracking table exists\n log('Creating migrations tracking table if not exists...');\n await db.batch([db.prepare(CREATE_MIGRATIONS_TABLE)]);\n\n // 2. Filter and sort migration files\n const validMigrations = migrations\n .filter((m) => isValidMigrationFilename(m.name))\n .sort((a, b) => a.name.localeCompare(b.name));\n\n if (validMigrations.length === 0) {\n log('No migration files found');\n return result;\n }\n\n log(`Found ${validMigrations.length} migration file(s)`);\n\n // 3. Query executed migrations\n const { results: executed } = await db\n .prepare('SELECT filename, checksum FROM _eldrin_migrations ORDER BY filename')\n .all<Pick<MigrationRecord, 'filename' | 'checksum'>>();\n\n const executedMap = new Map(executed?.map((e) => [e.filename, e.checksum]) ?? []);\n\n // 4. Verify checksums of executed migrations (unless skipped)\n if (!skipChecksumVerification) {\n for (const migration of validMigrations) {\n const storedChecksum = executedMap.get(migration.name);\n if (storedChecksum) {\n const currentChecksum = await calculateChecksum(migration.content);\n if (currentChecksum !== storedChecksum) {\n const error = new Error(\n `Migration ${migration.name} has been modified after execution! ` +\n `Expected checksum: ${storedChecksum}, got: ${currentChecksum}`\n );\n log(error.message, 'error');\n return {\n success: false,\n executed: 0,\n migrations: [],\n error: {\n migration: migration.name,\n message: error.message,\n },\n };\n }\n }\n }\n }\n\n // 5. Check for orphaned migrations (in DB but file deleted)\n for (const [filename] of executedMap) {\n const hasFile = validMigrations.some((m) => m.name === filename);\n if (!hasFile) {\n log(`Warning: Orphaned migration in database: ${filename}`, 'warn');\n }\n }\n\n // 6. Calculate pending migrations\n const pending = validMigrations.filter((m) => !executedMap.has(m.name));\n\n if (pending.length === 0) {\n log('No pending migrations');\n return result;\n }\n\n log(`Found ${pending.length} pending migration(s)`);\n\n // 7. Execute pending migrations ONE AT A TIME\n for (const migration of pending) {\n log(`Executing migration: ${migration.name}`);\n const startTime = Date.now();\n\n try {\n // Parse SQL into statements\n const statements = parseSQLStatements(migration.content);\n\n if (statements.length === 0) {\n log(`Warning: Migration ${migration.name} contains no SQL statements`, 'warn');\n continue;\n }\n\n // Calculate checksum before execution\n const checksum = await calculateChecksum(migration.content);\n\n // Build batch with all statements + tracking record\n const batch: D1PreparedStatement[] = [\n // Execute all statements from the migration\n ...statements.map((sql) => db.prepare(sql)),\n\n // Record execution in tracking table\n db\n .prepare(\n `INSERT INTO _eldrin_migrations\n (filename, checksum, executed_at, execution_time_ms)\n VALUES (?, ?, ?, ?)`\n )\n .bind(migration.name, checksum, Date.now(), Date.now() - startTime),\n ];\n\n // Execute in a single transaction via batch\n await db.batch(batch);\n\n const executionTime = Date.now() - startTime;\n log(`Migration ${migration.name} completed (${executionTime}ms)`);\n\n result.executed++;\n result.migrations.push({\n name: migration.name,\n executionTimeMs: executionTime,\n });\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log(`Migration ${migration.name} failed: ${errorMessage}`, 'error');\n\n // Transaction auto-rolls back on error\n // Previous migrations remain intact\n return {\n success: false,\n executed: result.executed,\n migrations: result.migrations,\n error: {\n migration: migration.name,\n message: errorMessage,\n },\n };\n }\n }\n\n log(`Successfully executed ${result.executed} migration(s)`);\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log(`Migration system error: ${errorMessage}`, 'error');\n return {\n success: false,\n executed: result.executed,\n migrations: result.migrations,\n error: {\n migration: 'system',\n message: errorMessage,\n },\n };\n }\n}\n\n/**\n * Get migration status (pending and executed migrations)\n *\n * @param db - D1 Database instance\n * @param migrations - Available migration files\n * @returns Status information\n */\nexport async function getMigrationStatus(\n db: D1Database,\n migrations: MigrationFile[]\n): Promise<{\n pending: string[];\n executed: MigrationRecord[];\n orphaned: string[];\n checksumMismatches: string[];\n}> {\n // Ensure table exists\n await db.batch([db.prepare(CREATE_MIGRATIONS_TABLE)]);\n\n // Get executed migrations\n const { results: executed } = await db\n .prepare('SELECT * FROM _eldrin_migrations ORDER BY filename')\n .all<MigrationRecord>();\n\n const executedMap = new Map(executed?.map((e) => [e.filename, e]) ?? []);\n\n // Filter valid migrations\n const validMigrations = migrations.filter((m) => isValidMigrationFilename(m.name));\n\n // Calculate pending\n const pending = validMigrations\n .filter((m) => !executedMap.has(m.name))\n .map((m) => m.name)\n .sort();\n\n // Check for orphaned migrations\n const orphaned: string[] = [];\n for (const [filename] of executedMap) {\n if (!validMigrations.some((m) => m.name === filename)) {\n orphaned.push(filename);\n }\n }\n\n // Check for checksum mismatches\n const checksumMismatches: string[] = [];\n for (const migration of validMigrations) {\n const record = executedMap.get(migration.name);\n if (record) {\n const currentChecksum = await calculateChecksum(migration.content);\n if (currentChecksum !== record.checksum) {\n checksumMismatches.push(migration.name);\n }\n }\n }\n\n return {\n pending,\n executed: executed ?? [],\n orphaned,\n checksumMismatches,\n };\n}\n","/**\n * Migration rollback functionality\n *\n * Rolls back migrations using .rollback.sql files in reverse order\n */\n\nimport type { MigrationFile, MigrationRecord } from '../types';\nimport { parseSQLStatements, isValidRollbackFilename, getRollbackFilename } from './sql-parser';\n\n/**\n * Result of a rollback operation\n */\nexport interface RollbackResult {\n success: boolean;\n rolledBack: string[];\n error?: {\n migration: string;\n message: string;\n };\n}\n\n/**\n * Rollback migrations to a specific target (or just the last one)\n *\n * @param db - D1 Database instance\n * @param options - Rollback options\n * @returns Result of rollback operation\n */\nexport async function rollbackMigrations(\n db: D1Database,\n options: {\n /** Rollback files (.rollback.sql) */\n rollbackFiles: MigrationFile[];\n /** Target migration to rollback TO (exclusive - this migration stays) */\n targetMigration?: string;\n /** Callback for logging */\n onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;\n }\n): Promise<RollbackResult> {\n const { rollbackFiles, targetMigration, onLog = () => {} } = options;\n\n const log = (message: string, level: 'info' | 'warn' | 'error' = 'info') => {\n onLog(message, level);\n };\n\n const result: RollbackResult = {\n success: true,\n rolledBack: [],\n };\n\n try {\n // 1. Get executed migrations in reverse order\n const { results: executed } = await db\n .prepare('SELECT filename FROM _eldrin_migrations ORDER BY filename DESC')\n .all<Pick<MigrationRecord, 'filename'>>();\n\n if (!executed || executed.length === 0) {\n log('No migrations to rollback');\n return result;\n }\n\n // 2. Determine which migrations to rollback\n let toRollback: string[];\n\n if (targetMigration) {\n // Find the target migration\n const targetIndex = executed.findIndex((m) => m.filename === targetMigration);\n if (targetIndex === -1) {\n return {\n success: false,\n rolledBack: [],\n error: {\n migration: targetMigration,\n message: `Target migration not found: ${targetMigration}`,\n },\n };\n }\n // Rollback everything before the target (in the DESC sorted list, that means indices 0 to targetIndex-1)\n toRollback = executed.slice(0, targetIndex).map((m) => m.filename);\n } else {\n // Rollback only the last migration\n toRollback = [executed[0].filename];\n }\n\n if (toRollback.length === 0) {\n log('No migrations to rollback');\n return result;\n }\n\n // 3. Build rollback file map\n const rollbackMap = new Map(\n rollbackFiles.filter((f) => isValidRollbackFilename(f.name)).map((f) => [f.name, f])\n );\n\n // 4. Execute rollbacks in order (they're already in DESC order from query)\n for (const migrationFilename of toRollback) {\n const rollbackFilename = getRollbackFilename(migrationFilename);\n const rollbackFile = rollbackMap.get(rollbackFilename);\n\n if (!rollbackFile) {\n return {\n success: false,\n rolledBack: result.rolledBack,\n error: {\n migration: migrationFilename,\n message: `Rollback file not found: ${rollbackFilename}`,\n },\n };\n }\n\n log(`Rolling back: ${migrationFilename}`);\n\n try {\n const statements = parseSQLStatements(rollbackFile.content);\n\n if (statements.length === 0) {\n log(`Warning: Rollback file ${rollbackFilename} contains no SQL statements`, 'warn');\n }\n\n // Build batch: rollback statements + remove tracking record\n const batch: D1PreparedStatement[] = [\n ...statements.map((sql) => db.prepare(sql)),\n db.prepare('DELETE FROM _eldrin_migrations WHERE filename = ?').bind(migrationFilename),\n ];\n\n await db.batch(batch);\n\n log(`Rolled back: ${migrationFilename}`);\n result.rolledBack.push(migrationFilename);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log(`Rollback failed for ${migrationFilename}: ${errorMessage}`, 'error');\n\n return {\n success: false,\n rolledBack: result.rolledBack,\n error: {\n migration: migrationFilename,\n message: errorMessage,\n },\n };\n }\n }\n\n log(`Successfully rolled back ${result.rolledBack.length} migration(s)`);\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log(`Rollback system error: ${errorMessage}`, 'error');\n\n return {\n success: false,\n rolledBack: result.rolledBack,\n error: {\n migration: 'system',\n message: errorMessage,\n },\n };\n }\n}\n","/**\n * Marketplace utilities for migration packaging\n *\n * Provides tools to generate migration manifests for marketplace distribution.\n * This is used at build time to prepare apps for submission.\n */\n\nimport { readdir, readFile } from 'fs/promises';\nimport { existsSync } from 'fs';\nimport { resolve, basename } from 'path';\nimport { calculatePrefixedChecksum } from './checksum';\nimport { isValidMigrationFilename, extractTimestamp } from './sql-parser';\nimport type { MigrationFile } from '../types';\n\n/**\n * Migration entry in the marketplace manifest\n */\nexport interface MigrationManifestEntry {\n /** Migration ID (timestamp) */\n id: string;\n /** Full filename */\n file: string;\n /** SHA-256 checksum with prefix */\n checksum: string;\n}\n\n/**\n * Marketplace migration manifest format\n */\nexport interface MigrationManifest {\n /** Logical database name (used for table prefixing) */\n database: string;\n /** List of migrations in order */\n migrations: MigrationManifestEntry[];\n}\n\n/**\n * Options for generating migration manifest\n */\nexport interface GenerateMigrationManifestOptions {\n /** Directory containing migration SQL files */\n migrationsDir: string;\n /** Logical database name for the app */\n database: string;\n}\n\n/**\n * Result of migration manifest generation\n */\nexport interface GenerateMigrationManifestResult {\n /** The generated manifest */\n manifest: MigrationManifest;\n /** Migration files with content (for copying to output) */\n files: MigrationFile[];\n}\n\n/**\n * Read migration files from a directory\n *\n * @param dir - Directory containing migration files\n * @returns Array of migration files sorted by name\n */\nasync function readMigrationFiles(dir: string): Promise<MigrationFile[]> {\n if (!existsSync(dir)) {\n return [];\n }\n\n const files = await readdir(dir);\n const sqlFiles = files\n .filter((f) => isValidMigrationFilename(f))\n .sort();\n\n const migrations: MigrationFile[] = [];\n\n for (const file of sqlFiles) {\n const content = await readFile(resolve(dir, file), 'utf-8');\n migrations.push({\n name: basename(file),\n content,\n });\n }\n\n return migrations;\n}\n\n/**\n * Generate a marketplace migration manifest from migration files\n *\n * Reads migration files from the specified directory and generates\n * a manifest with checksums suitable for marketplace distribution.\n *\n * @param options - Generation options\n * @returns Manifest and file contents\n *\n * @example\n * ```typescript\n * import { generateMigrationManifest } from '@eldrin-project/eldrin-app-core';\n *\n * const result = await generateMigrationManifest({\n * migrationsDir: './migrations',\n * database: 'invoicing',\n * });\n *\n * // Write manifest to output directory\n * await writeFile(\n * 'dist/migrations/index.json',\n * JSON.stringify(result.manifest, null, 2)\n * );\n *\n * // Copy migration files\n * for (const file of result.files) {\n * await writeFile(`dist/migrations/${file.name}`, file.content);\n * }\n * ```\n */\nexport async function generateMigrationManifest(\n options: GenerateMigrationManifestOptions\n): Promise<GenerateMigrationManifestResult> {\n const { migrationsDir, database } = options;\n\n const files = await readMigrationFiles(migrationsDir);\n\n const migrations: MigrationManifestEntry[] = await Promise.all(\n files.map(async (file) => {\n const timestamp = extractTimestamp(file.name);\n const checksum = await calculatePrefixedChecksum(file.content);\n\n return {\n id: timestamp || file.name.replace('.sql', ''),\n file: file.name,\n checksum,\n };\n })\n );\n\n return {\n manifest: {\n database,\n migrations,\n },\n files,\n };\n}\n\n/**\n * Validate a migration manifest against actual files\n *\n * Useful for CI/CD validation in the marketplace-dist repository.\n *\n * @param manifest - The manifest to validate\n * @param files - The actual migration files\n * @returns Validation result with any errors\n */\nexport async function validateMigrationManifest(\n manifest: MigrationManifest,\n files: MigrationFile[]\n): Promise<{\n valid: boolean;\n errors: string[];\n}> {\n const errors: string[] = [];\n\n // Check all manifest entries have corresponding files\n for (const entry of manifest.migrations) {\n const file = files.find((f) => f.name === entry.file);\n\n if (!file) {\n errors.push(`Missing file: ${entry.file}`);\n continue;\n }\n\n // Verify checksum\n const actualChecksum = await calculatePrefixedChecksum(file.content);\n if (actualChecksum !== entry.checksum) {\n errors.push(\n `Checksum mismatch for ${entry.file}: ` +\n `expected ${entry.checksum}, got ${actualChecksum}`\n );\n }\n\n // Verify ID matches timestamp\n const timestamp = extractTimestamp(file.name);\n if (timestamp !== entry.id) {\n errors.push(\n `ID mismatch for ${entry.file}: ` +\n `expected ${timestamp}, got ${entry.id}`\n );\n }\n }\n\n // Check all files are in manifest\n for (const file of files) {\n if (!manifest.migrations.some((m) => m.file === file.name)) {\n errors.push(`File not in manifest: ${file.name}`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n","/**\n * Database context and hooks for Eldrin apps\n *\n * Provides access to the app's isolated D1 database instance\n */\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { DatabaseContext, MigrationResult } from '../types';\n\n/**\n * React context for database access\n */\nconst EldrinDatabaseContext = createContext<DatabaseContext | null>(null);\n\n/**\n * Props for DatabaseProvider\n */\nexport interface DatabaseProviderProps {\n /** D1 database instance (null if app has no database) */\n db: D1Database | null;\n /** Whether migrations have completed */\n migrationsComplete: boolean;\n /** Migration result */\n migrationResult?: MigrationResult;\n /** Child components */\n children: ReactNode;\n}\n\n/**\n * Provider component for database context\n */\nexport function DatabaseProvider({\n db,\n migrationsComplete,\n migrationResult,\n children,\n}: DatabaseProviderProps) {\n const value: DatabaseContext = {\n db,\n migrationsComplete,\n migrationResult,\n };\n\n return (\n <EldrinDatabaseContext.Provider value={value}>{children}</EldrinDatabaseContext.Provider>\n );\n}\n\n/**\n * Hook to access the app's D1 database\n *\n * @returns D1Database instance or null if app has no database\n * @throws Error if used outside of DatabaseProvider\n *\n * @example\n * ```tsx\n * function InvoiceList() {\n * const db = useDatabase();\n *\n * if (!db) {\n * return <div>No database configured</div>;\n * }\n *\n * const { results } = await db.prepare(\n * 'SELECT * FROM invoices ORDER BY created_at DESC'\n * ).all();\n *\n * return <div>{results.map(invoice => ...)}</div>;\n * }\n * ```\n */\nexport function useDatabase(): D1Database | null {\n const context = useContext(EldrinDatabaseContext);\n\n if (context === null) {\n throw new Error('useDatabase must be used within a DatabaseProvider (via createApp)');\n }\n\n return context.db;\n}\n\n/**\n * Hook to access full database context including migration status\n *\n * @returns DatabaseContext object\n * @throws Error if used outside of DatabaseProvider\n */\nexport function useDatabaseContext(): DatabaseContext {\n const context = useContext(EldrinDatabaseContext);\n\n if (context === null) {\n throw new Error('useDatabaseContext must be used within a DatabaseProvider (via createApp)');\n }\n\n return context;\n}\n\n/**\n * Hook to check if migrations have completed\n *\n * Useful for showing loading states while migrations run\n *\n * @returns true if migrations are complete\n */\nexport function useMigrationsComplete(): boolean {\n const context = useContext(EldrinDatabaseContext);\n return context?.migrationsComplete ?? false;\n}\n","/**\n * App factory for creating Eldrin apps\n *\n * Returns single-spa compatible lifecycle functions with:\n * - Automatic migration execution on bootstrap\n * - Database provider wrapping\n * - Error boundary integration\n */\n\nimport { createElement, type ComponentType, type ReactNode } from 'react';\nimport type { CreateAppOptions, MigrationResult, MigrationFile } from '../types';\nimport { runMigrations } from '../migrations';\nimport { DatabaseProvider } from '../database';\n\n/**\n * Single-spa lifecycle props passed to lifecycle functions\n */\nexport interface LifecycleProps {\n /** DOM element to mount the app into */\n domElement?: HTMLElement;\n /** App name */\n name?: string;\n /** D1 Database instance (provided by shell) */\n db?: D1Database;\n /** Custom props from shell */\n customProps?: Record<string, unknown>;\n}\n\n/**\n * Single-spa compatible lifecycle object\n */\nexport interface AppLifecycle {\n bootstrap: (props: LifecycleProps) => Promise<void>;\n mount: (props: LifecycleProps) => Promise<void>;\n unmount: (props: LifecycleProps) => Promise<void>;\n}\n\n/**\n * Internal state for the app\n */\ninterface AppState {\n db: D1Database | null;\n migrationsComplete: boolean;\n migrationResult?: MigrationResult;\n mountedElement?: HTMLElement;\n reactRoot?: {\n render: (element: ReactNode) => void;\n unmount: () => void;\n };\n}\n\n/**\n * Create an Eldrin app with single-spa compatible lifecycle\n *\n * @param options - App configuration\n * @returns Object with bootstrap, mount, and unmount lifecycle functions\n *\n * @example\n * ```tsx\n * // src/index.tsx\n * import { createApp } from '@eldrin-project/eldrin-app-core';\n * import App from './App';\n *\n * export const { bootstrap, mount, unmount } = createApp({\n * name: 'invoicing',\n * root: App,\n * // Migrations loaded via Vite plugin\n * });\n * ```\n */\nexport function createApp(options: CreateAppOptions): AppLifecycle {\n const { name, root: RootComponent, migrations = [], onMigrationsComplete, onMigrationError } = options;\n\n // App state persists across mount/unmount cycles\n const state: AppState = {\n db: null,\n migrationsComplete: false,\n migrationResult: undefined,\n mountedElement: undefined,\n reactRoot: undefined,\n };\n\n /**\n * Bootstrap phase - runs once when app is first loaded\n * Executes migrations here before any mounting\n */\n async function bootstrap(props: LifecycleProps): Promise<void> {\n const db = props.db ?? null;\n state.db = db;\n\n // Run migrations if we have a database and migrations\n if (db && migrations.length > 0) {\n try {\n const result = await runMigrations(db, {\n migrations,\n onLog: (message, level) => {\n const prefix = `[${name}]`;\n if (level === 'error') {\n console.error(prefix, message);\n } else if (level === 'warn') {\n console.warn(prefix, message);\n } else {\n console.log(prefix, message);\n }\n },\n });\n\n state.migrationResult = result;\n\n if (result.success) {\n state.migrationsComplete = true;\n onMigrationsComplete?.(result);\n } else {\n const error = new Error(result.error?.message ?? 'Migration failed');\n onMigrationError?.(error);\n throw error;\n }\n } catch (error) {\n state.migrationsComplete = false;\n const err = error instanceof Error ? error : new Error(String(error));\n onMigrationError?.(err);\n throw err;\n }\n } else {\n // No database or no migrations - mark as complete\n state.migrationsComplete = true;\n }\n }\n\n /**\n * Mount phase - renders the app into the DOM\n */\n async function mount(props: LifecycleProps): Promise<void> {\n const domElement = props.domElement ?? document.getElementById(`app-${name}`);\n\n if (!domElement) {\n throw new Error(`No DOM element found for app \"${name}\". Expected element with id=\"app-${name}\" or domElement prop.`);\n }\n\n state.mountedElement = domElement;\n\n // Create the app element with providers\n const rootElement = createElement(RootComponent as ComponentType, props.customProps ?? {});\n const appElement = createElement(DatabaseProvider, {\n db: state.db,\n migrationsComplete: state.migrationsComplete,\n migrationResult: state.migrationResult,\n children: rootElement,\n });\n\n // Dynamically import React DOM to avoid bundling issues\n // Apps using this library will have React DOM in their dependencies\n const ReactDOM = await import('react-dom/client');\n const root = ReactDOM.createRoot(domElement);\n root.render(appElement);\n\n state.reactRoot = root;\n }\n\n /**\n * Unmount phase - removes the app from the DOM\n */\n async function unmount(_props: LifecycleProps): Promise<void> {\n if (state.reactRoot) {\n state.reactRoot.unmount();\n state.reactRoot = undefined;\n }\n\n if (state.mountedElement) {\n state.mountedElement.innerHTML = '';\n state.mountedElement = undefined;\n }\n }\n\n return {\n bootstrap,\n mount,\n unmount,\n };\n}\n\n/**\n * Type helper for migration files loaded via Vite plugin\n *\n * @example\n * ```tsx\n * // With Vite plugin\n * import { createApp, type MigrationFiles } from '@eldrin-project/eldrin-app-core';\n * import migrations from 'virtual:eldrin/migrations';\n *\n * export const { bootstrap, mount, unmount } = createApp({\n * name: 'invoicing',\n * root: App,\n * migrations: migrations as MigrationFiles,\n * });\n * ```\n */\nexport type MigrationFiles = MigrationFile[];\n"]}