@studiocms/migrator 0.0.0-beta.0 → 0.1.0-beta.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/package.json +54 -7
  4. package/public/favicon.svg +5 -0
  5. package/src/components/ErrorCard.astro +20 -0
  6. package/src/components/MigrationMeta.astro +74 -0
  7. package/src/components/MigrationStatus.astro +71 -0
  8. package/src/components/PageHeader.astro +24 -0
  9. package/src/components/StatusTables.astro +45 -0
  10. package/src/db/astro-db-drizzle-client.ts +14 -0
  11. package/src/db/astro-db-schema.ts +193 -0
  12. package/src/db/astrodb.ts +88 -0
  13. package/src/db/client.ts +155 -0
  14. package/src/db/drizzle-schema.ts +54 -0
  15. package/src/env.d.ts +3 -0
  16. package/src/fonts/css/onest-variable.css +16 -0
  17. package/src/fonts/woff2/onest-variable.woff2 +0 -0
  18. package/src/layouts/Layout.astro +30 -0
  19. package/src/lib/astro-db-drizzle-compat/core-types.ts +88 -0
  20. package/src/lib/astro-db-drizzle-compat/error-map.ts +105 -0
  21. package/src/lib/astro-db-drizzle-compat/index.ts +149 -0
  22. package/src/lib/astro-db-drizzle-compat/schemas.ts +249 -0
  23. package/src/lib/astro-db-drizzle-compat/types.ts +141 -0
  24. package/src/lib/astro-db-drizzle-compat/utils.ts +55 -0
  25. package/src/lib/astro-db-drizzle-compat/virtual.ts +91 -0
  26. package/src/lib/errors.ts +57 -0
  27. package/src/lib/logger.ts +4 -0
  28. package/src/lib/remapUtils.ts +170 -0
  29. package/src/lib/response-utils.ts +12 -0
  30. package/src/lib/tableMap.ts +236 -0
  31. package/src/pages/data-migrations.ts +268 -0
  32. package/src/pages/index.astro +259 -0
  33. package/src/pages/schema-migrations.ts +31 -0
  34. package/src/styles/global.css +165 -0
  35. package/start.mjs +163 -0
  36. package/utils/logger.mjs +93 -0
  37. package/utils/resolver.mjs +60 -0
@@ -0,0 +1,259 @@
1
+ ---
2
+ import { Button } from 'studiocms:ui/components/button';
3
+ import { Card } from 'studiocms:ui/components/card';
4
+ import { Divider } from 'studiocms:ui/components/divider';
5
+ import { TabItem, Tabs } from 'studiocms:ui/components/tabs';
6
+ import { runEffect } from '@withstudiocms/effect';
7
+ import type { ComponentProps } from 'astro/types';
8
+ import ErrorCard from '../components/ErrorCard.astro';
9
+ import MigrationMeta from '../components/MigrationMeta.astro';
10
+ import MigrationStatus from '../components/MigrationStatus.astro';
11
+ import StatusTables from '../components/StatusTables.astro';
12
+ import { runConnectionTest, searchTables } from '../db/astrodb.js';
13
+ import {
14
+ getDataMigrationStatus,
15
+ getDialectFromEnv,
16
+ runConnectionTest as runStudioCMSDbConnectionTest,
17
+ studioCMSDbMigrator,
18
+ } from '../db/client.js';
19
+ import Layout from '../layouts/Layout.astro';
20
+ import { getErrorCases } from '../lib/errors.js';
21
+ import { astroDbTableKeys, migrationMeta } from '../lib/tableMap.js';
22
+
23
+ // Test AstroDB Connection
24
+ const isAstroDBConnected = await runConnectionTest();
25
+
26
+ // Test StudioCMS DB Connection
27
+ const isStudioCMSDbConnected = await runStudioCMSDbConnectionTest();
28
+
29
+ // AstroDB Tables
30
+ const astroDBTables = await searchTables('StudioCMS%').catch(() => []);
31
+
32
+ // StudioCMS Schema Migrations
33
+ const migrator = await studioCMSDbMigrator();
34
+ const migrationStatus = await runEffect(migrator.status).catch(() => []);
35
+ const migrationTotal = migrationStatus.length;
36
+ const appliedMigrations = migrationStatus.filter((m) => m.executedAt).length;
37
+
38
+ // Data Migration Status
39
+ const dataMigrationStatus = await getDataMigrationStatus();
40
+
41
+ // Determine Error Cases
42
+ const errorCases = getErrorCases({
43
+ astroDBTables,
44
+ astroDbTableKeys,
45
+ appliedMigrations,
46
+ migrationTotal,
47
+ dataMigrationStatus,
48
+ });
49
+
50
+ const migrationMetaData = await migrationMeta();
51
+
52
+ // Prepare Database Status Tables
53
+ const dbStatusTables: ComponentProps<typeof StatusTables>['dbStatusTables'] = [
54
+ {
55
+ label: 'AstroDB Database',
56
+ conditions: [
57
+ {
58
+ label: 'Connection Status',
59
+ value: isAstroDBConnected ? 'Connected' : 'Not Connected',
60
+ color: isAstroDBConnected ? 'success' : 'danger',
61
+ },
62
+ {
63
+ label: 'Required Tables',
64
+ value:
65
+ astroDBTables.length >= astroDbTableKeys.length
66
+ ? 'Available'
67
+ : `Missing ${astroDbTableKeys.length - astroDBTables.length} Tables`,
68
+ color: astroDBTables.length >= astroDbTableKeys.length ? 'success' : 'danger',
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ label: 'StudioCMS Database',
74
+ conditions: [
75
+ {
76
+ label: 'Connection Status',
77
+ value: isStudioCMSDbConnected ? 'Connected' : 'Not Connected',
78
+ color: isStudioCMSDbConnected ? 'success' : 'danger',
79
+ },
80
+ {
81
+ label: 'Dialect',
82
+ value: getDialectFromEnv(),
83
+ color: 'info',
84
+ },
85
+ {
86
+ label: 'Schema Migration Status',
87
+ value: appliedMigrations === migrationTotal ? 'Latest' : 'Pending Migrations',
88
+ color: appliedMigrations === migrationTotal ? 'success' : 'danger',
89
+ },
90
+ {
91
+ label: 'Data Migration Status',
92
+ value: dataMigrationStatus.canMigrate ? 'Available' : 'Not Available',
93
+ color: dataMigrationStatus.canMigrate ? 'success' : 'danger',
94
+ },
95
+ ],
96
+ },
97
+ ];
98
+ ---
99
+ <Layout title="StudioCMS Migrator">
100
+
101
+ {
102
+ errorCases
103
+ .filter((error) => error.condition)
104
+ .map(({ title, description }) => (
105
+ <ErrorCard {title} {description} />
106
+ ))
107
+ }
108
+
109
+ <Card fullWidth class="db-status-card" variant='filled'>
110
+
111
+ <div slot="header">
112
+ <h2>Database Status Overview</h2>
113
+ </div>
114
+
115
+ <div class="split-screen">
116
+
117
+ <StatusTables {dbStatusTables} />
118
+
119
+ </div>
120
+
121
+ <span class="divider-holder"><Divider /></span>
122
+
123
+ <h2>Migration Actions</h2>
124
+
125
+ <Tabs variant='starlight' syncKey='migration-type' storage='persistent'>
126
+
127
+ <TabItem label='Schema Migrations'>
128
+
129
+ <div class="button-shelf">
130
+ <Button
131
+ id='run-schema-migrations-button'
132
+ variant='solid'
133
+ color='primary'
134
+ size="md"
135
+ disabled={appliedMigrations === migrationTotal}
136
+ >Run Schema Migrations
137
+ </Button>
138
+ </div>
139
+
140
+ <MigrationStatus {migrationStatus} />
141
+ </TabItem>
142
+
143
+ <TabItem label='Migrate Data'>
144
+
145
+ <div class="button-shelf">
146
+ <Button
147
+ id='run-data-migrations-button'
148
+ variant='solid'
149
+ color='primary'
150
+ size="md"
151
+ disabled={!dataMigrationStatus.canMigrate}
152
+ >Run Data Migrations
153
+ </Button>
154
+ </div>
155
+
156
+ <MigrationMeta {migrationMetaData} />
157
+ </TabItem>
158
+
159
+ </Tabs>
160
+
161
+ </Card>
162
+
163
+ <script>
164
+ import { toast } from 'studiocms:ui/components/toaster/client';
165
+
166
+ // Helpers
167
+
168
+ const $ = <E extends Element>(selector: string) => document.querySelector<E>(selector);
169
+
170
+ function resetMigrationButton(element: HTMLButtonElement, text: string) {
171
+ element.disabled = false;
172
+ element.textContent = text;
173
+ };
174
+
175
+ // Consts
176
+
177
+ const schemaMigrations = $<HTMLButtonElement>('#run-schema-migrations-button');
178
+ const dataMigrations = $<HTMLButtonElement>('#run-data-migrations-button');
179
+
180
+ // Event Listeners
181
+
182
+ schemaMigrations?.addEventListener('click', async () => {
183
+ schemaMigrations.disabled = true;
184
+ schemaMigrations.textContent = 'Running Migrations...';
185
+
186
+ try {
187
+ const response = await fetch('/schema-migrations', {
188
+ method: 'POST',
189
+ });
190
+
191
+ if (response.ok) {
192
+ toast({
193
+ title: 'Schema Migrations Applied',
194
+ description: 'All pending schema migrations have been successfully applied.',
195
+ type: 'success',
196
+ })
197
+
198
+ setTimeout(() => {
199
+ window.location.reload();
200
+ }, 3000);
201
+ } else {
202
+ const errorData = await response.json();
203
+ toast({
204
+ title: 'Error Applying Migrations',
205
+ description: errorData.error,
206
+ type: 'danger',
207
+ });
208
+ resetMigrationButton(schemaMigrations, 'Run Schema Migrations');
209
+ }
210
+ } catch (error) {
211
+ toast({
212
+ title: 'Network Error',
213
+ description: String(error),
214
+ type: 'danger',
215
+ });
216
+ resetMigrationButton(schemaMigrations, 'Run Schema Migrations');
217
+ }
218
+ });
219
+
220
+ dataMigrations?.addEventListener('click', async () => {
221
+ dataMigrations.disabled = true;
222
+ dataMigrations.textContent = 'Migrating Data...';
223
+
224
+ try {
225
+ const response = await fetch('/data-migrations', {
226
+ method: 'POST',
227
+ });
228
+
229
+ if (response.ok) {
230
+ toast({
231
+ title: 'Data Migration Completed',
232
+ description: 'Data migration has been successfully completed.',
233
+ type: 'success',
234
+ })
235
+
236
+ setTimeout(() => {
237
+ window.location.reload();
238
+ }, 3000);
239
+ } else {
240
+ const errorData = await response.json();
241
+ toast({
242
+ title: 'Error During Data Migration',
243
+ description: errorData.error,
244
+ type: 'danger',
245
+ });
246
+ resetMigrationButton(dataMigrations, 'Run Data Migrations');
247
+ }
248
+ } catch (error) {
249
+ toast({
250
+ title: 'Network Error',
251
+ description: String(error),
252
+ type: 'danger',
253
+ });
254
+ resetMigrationButton(dataMigrations, 'Run Data Migrations');
255
+ }
256
+ });
257
+ </script>
258
+
259
+ </Layout>
@@ -0,0 +1,31 @@
1
+ import { runEffect } from '@withstudiocms/effect';
2
+ import type { APIRoute } from 'astro';
3
+ import { studioCMSDbMigrator } from '../db/client.js';
4
+ import logger from '../lib/logger.js';
5
+ import { jsonResponse } from '../lib/response-utils.js';
6
+
7
+ export const POST: APIRoute = async () => {
8
+ const migrator = await studioCMSDbMigrator();
9
+
10
+ const { error, results } = await runEffect(migrator.toLatest).catch((err) => ({
11
+ error: err,
12
+ results: undefined,
13
+ }));
14
+
15
+ if (results) {
16
+ for (const it of results) {
17
+ if (it.status === 'Success') {
18
+ logger.info(`Migration ${it.migrationName} applied successfully.`);
19
+ } else if (it.status === 'Error') {
20
+ logger.error(`Error applying migration ${it.migrationName}.`);
21
+ }
22
+ }
23
+ }
24
+
25
+ if (error) {
26
+ logger.error(`Migration failed with error: ${String(error)}`);
27
+ return jsonResponse({ success: false, error: String(error) }, 500);
28
+ }
29
+
30
+ return jsonResponse({ success: true });
31
+ };
@@ -0,0 +1,165 @@
1
+ main {
2
+ margin: 0 auto;
3
+ padding: 1rem;
4
+ max-width: 1000px;
5
+ width: 100%;
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ gap: 2rem;
10
+ }
11
+
12
+ .prose {
13
+ max-width: 100%;
14
+ min-width: 100%;
15
+ }
16
+
17
+ .header-content {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 1rem;
21
+ width: 100%;
22
+
23
+ .header-logo {
24
+ height: 50px;
25
+ width: 50px;
26
+ }
27
+
28
+ .header-text {
29
+ display: flex;
30
+ width: 100%;
31
+ min-width: 100%;
32
+ flex-direction: column;
33
+ justify-content: center;
34
+
35
+ * {
36
+ width: 100%;
37
+ min-width: 100%;
38
+ }
39
+ }
40
+ }
41
+
42
+ :root {
43
+ --color-error: var(--danger-base);
44
+ --color-error-background: color-mix(in srgb, var(--color-error) 10%, var(--background-base) 90%);
45
+ --color-error-foreground: var(--danger-vibrant);
46
+ }
47
+
48
+ .db-error-card {
49
+ border: 1px solid var(--color-error);
50
+ background-color: var(--color-error-background);
51
+ color: var(--color-error-foreground);
52
+ padding: 1rem;
53
+ border-radius: 8px;
54
+ width: 100%;
55
+
56
+ .error-header {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 0.5rem;
60
+ }
61
+
62
+ h2 {
63
+ margin-top: 0;
64
+ color: var(--color-error-foreground);
65
+ }
66
+
67
+ p {
68
+ margin-bottom: 0;
69
+ }
70
+ }
71
+
72
+ .db-status-card {
73
+ h2 {
74
+ margin-top: 0;
75
+ font-size: x-large;
76
+ }
77
+
78
+ p {
79
+ margin-bottom: 0;
80
+ }
81
+
82
+ .status-label {
83
+ display: flex;
84
+ align-items: center;
85
+ width: 100%;
86
+ font-weight: bold;
87
+ font-size: large;
88
+ border-bottom: 2px solid var(--border);
89
+ margin-bottom: 0.5rem;
90
+ padding-bottom: 0.25rem;
91
+ }
92
+
93
+ table {
94
+ border-collapse: collapse;
95
+ width: 100%;
96
+
97
+ tr {
98
+ border-bottom: 1px solid var(--border);
99
+ }
100
+
101
+ tr:last-child {
102
+ border-bottom: none;
103
+ }
104
+
105
+ /* tr every other row */
106
+ tr:nth-child(even) {
107
+ background-color: var(--background-step-3);
108
+ }
109
+
110
+ tr.not-applicable {
111
+ opacity: 0.5;
112
+ }
113
+
114
+ th,
115
+ td {
116
+ padding: 0.5rem;
117
+ text-align: left;
118
+ }
119
+
120
+ /* td first column */
121
+ td:first-child {
122
+ font-weight: bold;
123
+ }
124
+
125
+ thead {
126
+ border-bottom: 2px solid var(--border);
127
+ }
128
+ }
129
+
130
+ table.migration {
131
+ background-color: var(--background-step-2);
132
+ border: 1px solid var(--border);
133
+ }
134
+
135
+ .status {
136
+ font-weight: bold;
137
+ }
138
+ }
139
+
140
+ .split-screen {
141
+ display: flex;
142
+ gap: 2rem;
143
+ width: 100%;
144
+
145
+ .split-screen-item {
146
+ flex: 1;
147
+ width: 100%;
148
+ background-color: var(--background-step-2);
149
+ padding: 0.5rem 0.5rem;
150
+ border: 2px solid var(--border);
151
+ border-radius: 4px;
152
+ }
153
+ }
154
+
155
+ .divider-holder {
156
+ width: 100%;
157
+ margin: 1rem 0;
158
+ display: flex;
159
+ }
160
+
161
+ .button-shelf {
162
+ width: 100%;
163
+ display: flex;
164
+ margin-bottom: 1rem;
165
+ }
package/start.mjs ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { loadConfigFile, parseAndMerge } from '@withstudiocms/config-utils';
5
+ import dotenv from 'dotenv';
6
+ import { configPaths } from 'studiocms/consts';
7
+ import { StudioCMSOptionsSchema } from 'studiocms/schemas';
8
+ import { Logger } from './utils/logger.mjs';
9
+ import createPathResolver from './utils/resolver.mjs';
10
+
11
+ const logger = new Logger({ level: 'info' }, '@studiocms/migrator');
12
+
13
+ /**
14
+ * Asynchronously loads the StudioCMS configuration for the current project, merges it with
15
+ * a minimal default configuration, and populates process.env with derived environment variables.
16
+ *
17
+ * Behavior:
18
+ * - Computes the project root URL from process.cwd() using pathToFileURL.
19
+ * - Attempts to load a user configuration via loadConfigFile(rootPath, configPaths, 'db-migrator-util').
20
+ * - Logs whether a config file was found or if the default configuration will be used.
21
+ * - Constructs a minimal default configuration via StudioCMSOptionsSchema.parse({}).
22
+ * - Merges the loaded user configuration (if any) with the default using parseAndMerge.
23
+ * - Sets the STUDIOCMS_DIALECT environment variable from the merged user configuration
24
+ * and writes it into process.env using dotenv.populate(..., { quiet: true }).
25
+ *
26
+ * Notes:
27
+ * - The function produces side effects: console output and mutation of process.env.
28
+ * - The implementation depends on surrounding module-scope values/exports such as
29
+ * pathToFileURL, configPaths, loadConfigFile, StudioCMSOptionsSchema, parseAndMerge, and dotenv.
30
+ *
31
+ * @async
32
+ * @function loadCMSConfigFile
33
+ * @returns {Promise<void>} Resolves when the configuration has been loaded, merged, and environment variables populated.
34
+ * @throws {Error} If loading, parsing, or merging the configuration fails (errors bubbled from loadConfigFile,
35
+ * StudioCMSOptionsSchema.parse, parseAndMerge, or dotenv.populate).
36
+ * @example
37
+ * // Usage (top-level await or from an async function):
38
+ * await loadCMSConfigFile();
39
+ */
40
+ async function loadCMSConfigFile() {
41
+ try {
42
+ // Determine the root path of the project
43
+ const rootPath = pathToFileURL(join(createPathResolver(process.cwd()).resolve('.'), './'));
44
+
45
+ // Load StudioCMS Config file
46
+ const configFile = await loadConfigFile(rootPath, configPaths, 'db-migrator-util');
47
+
48
+ // Log whether the config file was found
49
+ if (configFile) {
50
+ logger.info('Loaded StudioCMS config file successfully.');
51
+ } else {
52
+ logger.warn('No StudioCMS config file found, using default configuration.');
53
+ }
54
+
55
+ // Merge user config with default config
56
+ const userConfig = parseAndMerge(StudioCMSOptionsSchema, configFile);
57
+
58
+ // Set custom environment variables based on user config
59
+ const customENV = { STUDIOCMS_DIALECT: userConfig.db.dialect };
60
+
61
+ // Populate process.env with custom environment variables
62
+ dotenv.populate(process.env, customENV, { quiet: true });
63
+ } catch (error) {
64
+ logger.error(
65
+ `Error loading StudioCMS configuration: ${error instanceof Error ? error.message : String(error)}`
66
+ );
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Asynchronously serves the Astro-based UI for the migrator tool.
73
+ *
74
+ * This function dynamically imports the necessary Astro modules and starts
75
+ * the development server with the StudioCMS UI integration for styles and components.
76
+ *
77
+ * Behavior:
78
+ * - Dynamically imports 'astro', '@astrojs/node', and '@studiocms/ui'.
79
+ * - Calls astro.dev() with the appropriate configuration to start the server.
80
+ * - Catches and logs any errors that occur during the server startup process.
81
+ *
82
+ * Notes:
83
+ * - The function produces side effects: launching server processes/listeners
84
+ * and modifying process.env.
85
+ *
86
+ * @async
87
+ * @function serveUI
88
+ * @returns {Promise<void>} Resolves when the Astro server has been started.
89
+ * @throws {Error} If starting the Astro server fails (errors bubbled from astro.dev).
90
+ */
91
+ async function serveUI() {
92
+ const astro = await import('astro');
93
+ const { default: node } = await import('@astrojs/node');
94
+ const { default: ui } = await import('@studiocms/ui');
95
+
96
+ const pkgRootPath = createPathResolver(import.meta.url).resolve('.');
97
+
98
+ try {
99
+ await astro.dev({
100
+ root: pkgRootPath,
101
+ output: 'server',
102
+ configFile: false,
103
+ logLevel: 'error',
104
+ devToolbar: {
105
+ enabled: false,
106
+ },
107
+ adapter: node({
108
+ mode: 'standalone',
109
+ }),
110
+ integrations: [
111
+ ui(),
112
+ {
113
+ name: 'studiocms-migrator-ui',
114
+ hooks: {
115
+ 'astro:server:start': ({ address }) => {
116
+ for (const line of [
117
+ '🚀 StudioCMS Migrator UI is running!',
118
+ `You can access it at: http://localhost:${address.port}`,
119
+ ]) {
120
+ logger.info(line);
121
+ }
122
+ },
123
+ },
124
+ },
125
+ ],
126
+ });
127
+ } catch (error) {
128
+ logger.error(
129
+ `Error starting Astro server for StudioCMS Migrator UI: ${error instanceof Error ? error.message : String(error)}`
130
+ );
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Initialize the application runtime by loading configuration and starting the Astro server.
137
+ *
138
+ * This async function performs two sequential startup tasks:
139
+ * 1. Loads the configuration file and applies any resulting environment variable settings.
140
+ * 2. Loads and starts the Astro server.
141
+ *
142
+ * Both operations are awaited; the returned promise resolves once configuration has been applied
143
+ * and the Astro server has been successfully started. Side effects include modification of
144
+ * process.env and launching server processes/listeners.
145
+ *
146
+ * @async
147
+ * @function start
148
+ * @returns {Promise<void>} Resolves when configuration is loaded and the Astro server is started.
149
+ * @throws {Error} If loading the configuration or starting the Astro server fails.
150
+ */
151
+ export async function start() {
152
+ logger.info('Starting StudioCMS Migrator...');
153
+ dotenv.config({ quiet: true });
154
+ await loadCMSConfigFile();
155
+ await serveUI();
156
+ }
157
+
158
+ start().catch((error) => {
159
+ logger.error(
160
+ `Failed to start StudioCMS Migrator: ${error instanceof Error ? error.message : String(error)}`
161
+ );
162
+ process.exit(1);
163
+ });
@@ -0,0 +1,93 @@
1
+ import { styleText } from 'node:util';
2
+
3
+ /**
4
+ * Intl.DateTimeFormat instance for formatting timestamps in log messages.
5
+ */
6
+ const dateTimeFormat = new Intl.DateTimeFormat([], {
7
+ hour: '2-digit',
8
+ minute: '2-digit',
9
+ second: '2-digit',
10
+ hour12: false,
11
+ });
12
+
13
+ /**
14
+ * Returns a formatted level prefix string based on the log level.
15
+ */
16
+ function getLevelPrefix(level) {
17
+ const levelLabel = level.toUpperCase();
18
+ switch (level) {
19
+ case 'error':
20
+ return `[${levelLabel}]`;
21
+ case 'warn':
22
+ return `[${levelLabel}]`;
23
+ case 'debug':
24
+ return `[${levelLabel}]`;
25
+ default:
26
+ return '';
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Generates a formatted event prefix string based on the log level and optional label.
32
+ */
33
+ const getEventPrefix = (level, label) => {
34
+ const timestamp = `${dateTimeFormat.format(/* @__PURE__ */ new Date())}`;
35
+ const prefix = [];
36
+ if (level === 'error' || level === 'warn' || level === 'debug') {
37
+ prefix.push(styleText('bold', timestamp));
38
+ prefix.push(getLevelPrefix(level));
39
+ } else {
40
+ prefix.push(timestamp);
41
+ }
42
+ if (label) {
43
+ prefix.push(`[${label}]`);
44
+ }
45
+ if (level === 'error') {
46
+ return styleText('red', prefix.join(' '));
47
+ }
48
+ if (level === 'warn') {
49
+ return styleText('yellow', prefix.join(' '));
50
+ }
51
+ if (level === 'debug') {
52
+ return styleText('blue', prefix.join(' '));
53
+ }
54
+ if (prefix.length === 1) {
55
+ /* v8 ignore start */
56
+ return styleText('dim', prefix[0]);
57
+ /* v8 ignore stop */
58
+ }
59
+ return `${styleText('dim', prefix[0])} ${styleText('blue', prefix.splice(1).join(' '))}`;
60
+ };
61
+
62
+ /**
63
+ * A simple logger class for logging messages with different levels and labels.
64
+ */
65
+ export class Logger {
66
+ options;
67
+ label;
68
+
69
+ constructor(logging, label) {
70
+ this.options = logging;
71
+ this.label = label;
72
+ }
73
+
74
+ /**
75
+ * Creates a new logger instance with a new label, but the same log options.
76
+ */
77
+ fork(label) {
78
+ return new Logger(this.options, label);
79
+ }
80
+
81
+ info(message) {
82
+ console.log(`${getEventPrefix('info', this.label)} ${message}`);
83
+ }
84
+ warn(message) {
85
+ console.warn(`${getEventPrefix('warn', this.label)} ${message}`);
86
+ }
87
+ error(message) {
88
+ console.error(`${getEventPrefix('error', this.label)} ${message}`);
89
+ }
90
+ debug(message) {
91
+ console.debug(`${getEventPrefix('debug', this.label)} ${message}`);
92
+ }
93
+ }