@eldrin-project/eldrin-app-core 0.0.4 → 0.0.5

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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Core types for @eldrin-project/eldrin-app-core
3
+ */
4
+ /**
5
+ * Migration file representation
6
+ */
7
+ interface MigrationFile {
8
+ /** Full filename including timestamp and description (e.g., "20250115120000-create-invoices.sql") */
9
+ name: string;
10
+ /** Raw SQL content of the migration file */
11
+ content: string;
12
+ }
13
+
14
+ /**
15
+ * Marketplace utilities for migration packaging
16
+ *
17
+ * Provides tools to generate migration manifests for marketplace distribution.
18
+ * This is used at build time to prepare apps for submission.
19
+ */
20
+
21
+ /**
22
+ * Migration entry in the marketplace manifest
23
+ */
24
+ interface MigrationManifestEntry {
25
+ /** Migration ID (timestamp) */
26
+ id: string;
27
+ /** Full filename */
28
+ file: string;
29
+ /** SHA-256 checksum with prefix */
30
+ checksum: string;
31
+ }
32
+ /**
33
+ * Marketplace migration manifest format
34
+ */
35
+ interface MigrationManifest {
36
+ /** Logical database name (used for table prefixing) */
37
+ database: string;
38
+ /** List of migrations in order */
39
+ migrations: MigrationManifestEntry[];
40
+ }
41
+ /**
42
+ * Options for generating migration manifest
43
+ */
44
+ interface GenerateMigrationManifestOptions {
45
+ /** Directory containing migration SQL files */
46
+ migrationsDir: string;
47
+ /** Logical database name for the app */
48
+ database: string;
49
+ }
50
+ /**
51
+ * Result of migration manifest generation
52
+ */
53
+ interface GenerateMigrationManifestResult {
54
+ /** The generated manifest */
55
+ manifest: MigrationManifest;
56
+ /** Migration files with content (for copying to output) */
57
+ files: MigrationFile[];
58
+ }
59
+ /**
60
+ * Generate a marketplace migration manifest from migration files
61
+ *
62
+ * Reads migration files from the specified directory and generates
63
+ * a manifest with checksums suitable for marketplace distribution.
64
+ *
65
+ * @param options - Generation options
66
+ * @returns Manifest and file contents
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { generateMigrationManifest } from '@eldrin-project/eldrin-app-core';
71
+ *
72
+ * const result = await generateMigrationManifest({
73
+ * migrationsDir: './migrations',
74
+ * database: 'invoicing',
75
+ * });
76
+ *
77
+ * // Write manifest to output directory
78
+ * await writeFile(
79
+ * 'dist/migrations/index.json',
80
+ * JSON.stringify(result.manifest, null, 2)
81
+ * );
82
+ *
83
+ * // Copy migration files
84
+ * for (const file of result.files) {
85
+ * await writeFile(`dist/migrations/${file.name}`, file.content);
86
+ * }
87
+ * ```
88
+ */
89
+ declare function generateMigrationManifest(options: GenerateMigrationManifestOptions): Promise<GenerateMigrationManifestResult>;
90
+ /**
91
+ * Validate a migration manifest against actual files
92
+ *
93
+ * Useful for CI/CD validation in the marketplace-dist repository.
94
+ *
95
+ * @param manifest - The manifest to validate
96
+ * @param files - The actual migration files
97
+ * @returns Validation result with any errors
98
+ */
99
+ declare function validateMigrationManifest(manifest: MigrationManifest, files: MigrationFile[]): Promise<{
100
+ valid: boolean;
101
+ errors: string[];
102
+ }>;
103
+
104
+ export { type GenerateMigrationManifestOptions, type GenerateMigrationManifestResult, type MigrationManifest, type MigrationManifestEntry, generateMigrationManifest, validateMigrationManifest };
package/dist/node.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Core types for @eldrin-project/eldrin-app-core
3
+ */
4
+ /**
5
+ * Migration file representation
6
+ */
7
+ interface MigrationFile {
8
+ /** Full filename including timestamp and description (e.g., "20250115120000-create-invoices.sql") */
9
+ name: string;
10
+ /** Raw SQL content of the migration file */
11
+ content: string;
12
+ }
13
+
14
+ /**
15
+ * Marketplace utilities for migration packaging
16
+ *
17
+ * Provides tools to generate migration manifests for marketplace distribution.
18
+ * This is used at build time to prepare apps for submission.
19
+ */
20
+
21
+ /**
22
+ * Migration entry in the marketplace manifest
23
+ */
24
+ interface MigrationManifestEntry {
25
+ /** Migration ID (timestamp) */
26
+ id: string;
27
+ /** Full filename */
28
+ file: string;
29
+ /** SHA-256 checksum with prefix */
30
+ checksum: string;
31
+ }
32
+ /**
33
+ * Marketplace migration manifest format
34
+ */
35
+ interface MigrationManifest {
36
+ /** Logical database name (used for table prefixing) */
37
+ database: string;
38
+ /** List of migrations in order */
39
+ migrations: MigrationManifestEntry[];
40
+ }
41
+ /**
42
+ * Options for generating migration manifest
43
+ */
44
+ interface GenerateMigrationManifestOptions {
45
+ /** Directory containing migration SQL files */
46
+ migrationsDir: string;
47
+ /** Logical database name for the app */
48
+ database: string;
49
+ }
50
+ /**
51
+ * Result of migration manifest generation
52
+ */
53
+ interface GenerateMigrationManifestResult {
54
+ /** The generated manifest */
55
+ manifest: MigrationManifest;
56
+ /** Migration files with content (for copying to output) */
57
+ files: MigrationFile[];
58
+ }
59
+ /**
60
+ * Generate a marketplace migration manifest from migration files
61
+ *
62
+ * Reads migration files from the specified directory and generates
63
+ * a manifest with checksums suitable for marketplace distribution.
64
+ *
65
+ * @param options - Generation options
66
+ * @returns Manifest and file contents
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { generateMigrationManifest } from '@eldrin-project/eldrin-app-core';
71
+ *
72
+ * const result = await generateMigrationManifest({
73
+ * migrationsDir: './migrations',
74
+ * database: 'invoicing',
75
+ * });
76
+ *
77
+ * // Write manifest to output directory
78
+ * await writeFile(
79
+ * 'dist/migrations/index.json',
80
+ * JSON.stringify(result.manifest, null, 2)
81
+ * );
82
+ *
83
+ * // Copy migration files
84
+ * for (const file of result.files) {
85
+ * await writeFile(`dist/migrations/${file.name}`, file.content);
86
+ * }
87
+ * ```
88
+ */
89
+ declare function generateMigrationManifest(options: GenerateMigrationManifestOptions): Promise<GenerateMigrationManifestResult>;
90
+ /**
91
+ * Validate a migration manifest against actual files
92
+ *
93
+ * Useful for CI/CD validation in the marketplace-dist repository.
94
+ *
95
+ * @param manifest - The manifest to validate
96
+ * @param files - The actual migration files
97
+ * @returns Validation result with any errors
98
+ */
99
+ declare function validateMigrationManifest(manifest: MigrationManifest, files: MigrationFile[]): Promise<{
100
+ valid: boolean;
101
+ errors: string[];
102
+ }>;
103
+
104
+ export { type GenerateMigrationManifestOptions, type GenerateMigrationManifestResult, type MigrationManifest, type MigrationManifestEntry, generateMigrationManifest, validateMigrationManifest };
package/dist/node.js ADDED
@@ -0,0 +1,107 @@
1
+ import { readdir, readFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { resolve, basename } from 'path';
4
+
5
+ // src/migrations/marketplace.ts
6
+
7
+ // src/migrations/checksum.ts
8
+ var CHECKSUM_PREFIX = "sha256:";
9
+ async function calculateChecksum(content, options = {}) {
10
+ const encoder = new TextEncoder();
11
+ const data = encoder.encode(content);
12
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
13
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
14
+ const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
15
+ return options.prefixed ? `${CHECKSUM_PREFIX}${hex}` : hex;
16
+ }
17
+ async function calculatePrefixedChecksum(content) {
18
+ return calculateChecksum(content, { prefixed: true });
19
+ }
20
+
21
+ // src/migrations/sql-parser.ts
22
+ function isValidMigrationFilename(filename) {
23
+ if (!filename.endsWith(".sql") || filename.endsWith(".rollback.sql")) {
24
+ return false;
25
+ }
26
+ const pattern = /^\d{14}-[a-z0-9-]+\.sql$/;
27
+ return pattern.test(filename);
28
+ }
29
+ function extractTimestamp(filename) {
30
+ const match = filename.match(/^(\d{14})-/);
31
+ return match ? match[1] : null;
32
+ }
33
+
34
+ // src/migrations/marketplace.ts
35
+ async function readMigrationFiles(dir) {
36
+ if (!existsSync(dir)) {
37
+ return [];
38
+ }
39
+ const files = await readdir(dir);
40
+ const sqlFiles = files.filter((f) => isValidMigrationFilename(f)).sort();
41
+ const migrations = [];
42
+ for (const file of sqlFiles) {
43
+ const content = await readFile(resolve(dir, file), "utf-8");
44
+ migrations.push({
45
+ name: basename(file),
46
+ content
47
+ });
48
+ }
49
+ return migrations;
50
+ }
51
+ async function generateMigrationManifest(options) {
52
+ const { migrationsDir, database } = options;
53
+ const files = await readMigrationFiles(migrationsDir);
54
+ const migrations = await Promise.all(
55
+ files.map(async (file) => {
56
+ const timestamp = extractTimestamp(file.name);
57
+ const checksum = await calculatePrefixedChecksum(file.content);
58
+ return {
59
+ id: timestamp || file.name.replace(".sql", ""),
60
+ file: file.name,
61
+ checksum
62
+ };
63
+ })
64
+ );
65
+ return {
66
+ manifest: {
67
+ database,
68
+ migrations
69
+ },
70
+ files
71
+ };
72
+ }
73
+ async function validateMigrationManifest(manifest, files) {
74
+ const errors = [];
75
+ for (const entry of manifest.migrations) {
76
+ const file = files.find((f) => f.name === entry.file);
77
+ if (!file) {
78
+ errors.push(`Missing file: ${entry.file}`);
79
+ continue;
80
+ }
81
+ const actualChecksum = await calculatePrefixedChecksum(file.content);
82
+ if (actualChecksum !== entry.checksum) {
83
+ errors.push(
84
+ `Checksum mismatch for ${entry.file}: expected ${entry.checksum}, got ${actualChecksum}`
85
+ );
86
+ }
87
+ const timestamp = extractTimestamp(file.name);
88
+ if (timestamp !== entry.id) {
89
+ errors.push(
90
+ `ID mismatch for ${entry.file}: expected ${timestamp}, got ${entry.id}`
91
+ );
92
+ }
93
+ }
94
+ for (const file of files) {
95
+ if (!manifest.migrations.some((m) => m.file === file.name)) {
96
+ errors.push(`File not in manifest: ${file.name}`);
97
+ }
98
+ }
99
+ return {
100
+ valid: errors.length === 0,
101
+ errors
102
+ };
103
+ }
104
+
105
+ export { generateMigrationManifest, validateMigrationManifest };
106
+ //# sourceMappingURL=node.js.map
107
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrations/checksum.ts","../src/migrations/sql-parser.ts","../src/migrations/marketplace.ts"],"names":[],"mappings":"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","file":"node.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 * 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eldrin-project/eldrin-app-core",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Standard library for Eldrin apps providing database migrations, storage isolation, event communication, and common utilities",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,6 +24,16 @@
24
24
  "default": "./dist/index.cjs"
25
25
  }
26
26
  },
27
+ "./node": {
28
+ "import": {
29
+ "types": "./dist/node.d.ts",
30
+ "default": "./dist/node.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/node.d.cts",
34
+ "default": "./dist/node.cjs"
35
+ }
36
+ },
27
37
  "./vite": {
28
38
  "import": {
29
39
  "types": "./dist/vite.d.ts",