@stravigor/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/auth/auth.ts +2 -1
  3. package/src/broadcast/broadcast_manager.ts +18 -5
  4. package/src/broadcast/client.ts +10 -4
  5. package/src/cache/cache_manager.ts +6 -2
  6. package/src/cache/http_cache.ts +1 -5
  7. package/src/core/container.ts +2 -6
  8. package/src/database/database.ts +11 -7
  9. package/src/database/migration/runner.ts +3 -1
  10. package/src/database/query_builder.ts +553 -60
  11. package/src/encryption/encryption_manager.ts +7 -1
  12. package/src/exceptions/errors.ts +1 -5
  13. package/src/exceptions/http_exception.ts +4 -1
  14. package/src/generators/api_generator.ts +8 -1
  15. package/src/generators/doc_generator.ts +33 -28
  16. package/src/generators/model_generator.ts +3 -1
  17. package/src/generators/test_generator.ts +81 -91
  18. package/src/i18n/helpers.ts +5 -1
  19. package/src/i18n/i18n_manager.ts +3 -1
  20. package/src/i18n/middleware.ts +2 -8
  21. package/src/mail/helpers.ts +1 -1
  22. package/src/mail/index.ts +4 -0
  23. package/src/mail/mail_manager.ts +20 -1
  24. package/src/mail/transports/alibaba_transport.ts +88 -0
  25. package/src/mail/transports/mailgun_transport.ts +74 -0
  26. package/src/mail/transports/resend_transport.ts +3 -4
  27. package/src/mail/transports/sendgrid_transport.ts +12 -9
  28. package/src/mail/transports/smtp_transport.ts +5 -5
  29. package/src/mail/types.ts +19 -1
  30. package/src/notification/channels/discord_channel.ts +6 -1
  31. package/src/notification/channels/webhook_channel.ts +8 -3
  32. package/src/notification/helpers.ts +7 -7
  33. package/src/notification/notification_manager.ts +7 -6
  34. package/src/orm/base_model.ts +4 -2
  35. package/src/queue/queue.ts +3 -1
  36. package/src/scheduler/cron.ts +12 -6
  37. package/src/scheduler/schedule.ts +17 -8
  38. package/src/session/session_manager.ts +3 -1
  39. package/src/storage/storage_manager.ts +3 -1
  40. package/src/view/compiler.ts +1 -3
  41. package/src/view/islands/island_builder.ts +4 -4
  42. package/src/view/islands/vue_plugin.ts +11 -15
  43. package/src/view/tokenizer.ts +11 -1
@@ -0,0 +1,88 @@
1
+ import crypto from 'node:crypto'
2
+ import { ExternalServiceError } from '../../exceptions/errors.ts'
3
+ import type { MailTransport, MailMessage, MailResult, AlibabaConfig } from '../types.ts'
4
+
5
+ /**
6
+ * Alibaba Cloud DirectMail (SingleSendMail) transport.
7
+ * Uses fetch with HMAC-SHA1 signature — no SDK dependency required.
8
+ *
9
+ * Note: SingleSendMail does not support CC, BCC, or attachments.
10
+ * Use the SMTP interface for those features.
11
+ *
12
+ * @see https://www.alibabacloud.com/help/en/directmail/latest/SingleSendMail
13
+ */
14
+ export class AlibabaTransport implements MailTransport {
15
+ private accessKeyId: string
16
+ private accessKeySecret: string
17
+ private accountName: string
18
+ private region: string
19
+
20
+ constructor(config: AlibabaConfig) {
21
+ this.accessKeyId = config.accessKeyId
22
+ this.accessKeySecret = config.accessKeySecret
23
+ this.accountName = config.accountName
24
+ this.region = config.region ?? 'cn-hangzhou'
25
+ }
26
+
27
+ async send(message: MailMessage): Promise<MailResult> {
28
+ const toAddress = Array.isArray(message.to) ? message.to.join(',') : message.to
29
+
30
+ const params: Record<string, string> = {
31
+ Action: 'SingleSendMail',
32
+ AccountName: this.accountName,
33
+ AddressType: '1',
34
+ ReplyToAddress: message.replyTo ? 'true' : 'false',
35
+ ToAddress: toAddress,
36
+ Subject: message.subject,
37
+ Format: 'JSON',
38
+ Version: '2015-11-23',
39
+ AccessKeyId: this.accessKeyId,
40
+ SignatureMethod: 'HMAC-SHA1',
41
+ SignatureVersion: '1.0',
42
+ SignatureNonce: crypto.randomUUID(),
43
+ Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
44
+ }
45
+
46
+ if (message.html) params.HtmlBody = message.html
47
+ if (message.text) params.TextBody = message.text
48
+ if (message.from) params.FromAlias = message.from
49
+
50
+ const signature = this.sign(params)
51
+ params.Signature = signature
52
+
53
+ const body = new URLSearchParams(params).toString()
54
+ const url = `https://dm.${this.region}.aliyuncs.com`
55
+
56
+ const response = await fetch(url, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
59
+ body,
60
+ })
61
+
62
+ if (!response.ok) {
63
+ const error = await response.text()
64
+ throw new ExternalServiceError('Alibaba DirectMail', response.status, error)
65
+ }
66
+
67
+ const data = (await response.json()) as { EnvId?: string; RequestId?: string }
68
+ const toArray = Array.isArray(message.to) ? message.to : [message.to]
69
+ return { messageId: data.EnvId ?? data.RequestId, accepted: toArray }
70
+ }
71
+
72
+ private percentEncode(str: string): string {
73
+ return encodeURIComponent(str).replace(/\+/g, '%20').replace(/\*/g, '%2A').replace(/~/g, '%7E')
74
+ }
75
+
76
+ private sign(params: Record<string, string>): string {
77
+ const sortedKeys = Object.keys(params).sort()
78
+ const canonicalized = sortedKeys
79
+ .map(k => `${this.percentEncode(k)}=${this.percentEncode(params[k]!)}`)
80
+ .join('&')
81
+
82
+ const stringToSign = `POST&${this.percentEncode('/')}&${this.percentEncode(canonicalized)}`
83
+
84
+ const hmac = crypto.createHmac('sha1', `${this.accessKeySecret}&`)
85
+ hmac.update(stringToSign)
86
+ return hmac.digest('base64')
87
+ }
88
+ }
@@ -0,0 +1,74 @@
1
+ import { ExternalServiceError } from '../../exceptions/errors.ts'
2
+ import type { MailTransport, MailMessage, MailResult, MailgunConfig } from '../types.ts'
3
+
4
+ /**
5
+ * Mailgun HTTP API transport.
6
+ * Uses fetch with FormData — no SDK dependency required.
7
+ *
8
+ * @see https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/
9
+ */
10
+ export class MailgunTransport implements MailTransport {
11
+ private apiKey: string
12
+ private domain: string
13
+ private baseUrl: string
14
+
15
+ constructor(config: MailgunConfig) {
16
+ this.apiKey = config.apiKey
17
+ this.domain = config.domain
18
+ this.baseUrl = config.baseUrl ?? 'https://api.mailgun.net'
19
+ }
20
+
21
+ async send(message: MailMessage): Promise<MailResult> {
22
+ const form = new FormData()
23
+
24
+ form.append('from', message.from)
25
+ form.append('subject', message.subject)
26
+
27
+ const toArray = Array.isArray(message.to) ? message.to : [message.to]
28
+ form.append('to', toArray.join(', '))
29
+
30
+ if (message.cc) {
31
+ const ccArray = Array.isArray(message.cc) ? message.cc : [message.cc]
32
+ form.append('cc', ccArray.join(', '))
33
+ }
34
+ if (message.bcc) {
35
+ const bccArray = Array.isArray(message.bcc) ? message.bcc : [message.bcc]
36
+ form.append('bcc', bccArray.join(', '))
37
+ }
38
+ if (message.replyTo) form.append('h:Reply-To', message.replyTo)
39
+ if (message.html) form.append('html', message.html)
40
+ if (message.text) form.append('text', message.text)
41
+
42
+ if (message.attachments?.length) {
43
+ for (const a of message.attachments) {
44
+ const content =
45
+ typeof a.content === 'string'
46
+ ? new Blob([a.content], { type: a.contentType ?? 'application/octet-stream' })
47
+ : new Blob([a.content], { type: a.contentType ?? 'application/octet-stream' })
48
+
49
+ if (a.cid) {
50
+ form.append('inline', content, a.filename)
51
+ } else {
52
+ form.append('attachment', content, a.filename)
53
+ }
54
+ }
55
+ }
56
+
57
+ const credentials = btoa(`api:${this.apiKey}`)
58
+ const response = await fetch(`${this.baseUrl}/v3/${this.domain}/messages`, {
59
+ method: 'POST',
60
+ headers: {
61
+ Authorization: `Basic ${credentials}`,
62
+ },
63
+ body: form,
64
+ })
65
+
66
+ if (!response.ok) {
67
+ const error = await response.text()
68
+ throw new ExternalServiceError('Mailgun', response.status, error)
69
+ }
70
+
71
+ const data = (await response.json()) as { id: string }
72
+ return { messageId: data.id, accepted: toArray }
73
+ }
74
+ }
@@ -30,11 +30,10 @@ export class ResendTransport implements MailTransport {
30
30
  if (message.text) body.text = message.text
31
31
 
32
32
  if (message.attachments?.length) {
33
- body.attachments = message.attachments.map((a) => ({
33
+ body.attachments = message.attachments.map(a => ({
34
34
  filename: a.filename,
35
- content: typeof a.content === 'string'
36
- ? a.content
37
- : Buffer.from(a.content).toString('base64'),
35
+ content:
36
+ typeof a.content === 'string' ? a.content : Buffer.from(a.content).toString('base64'),
38
37
  content_type: a.contentType,
39
38
  }))
40
39
  }
@@ -19,17 +19,19 @@ export class SendGridTransport implements MailTransport {
19
19
  async send(message: MailMessage): Promise<MailResult> {
20
20
  const toArray = Array.isArray(message.to) ? message.to : [message.to]
21
21
 
22
- const personalizations: Record<string, unknown>[] = [{
23
- to: toArray.map((email) => ({ email })),
24
- }]
22
+ const personalizations: Record<string, unknown>[] = [
23
+ {
24
+ to: toArray.map(email => ({ email })),
25
+ },
26
+ ]
25
27
 
26
28
  if (message.cc) {
27
29
  const ccArray = Array.isArray(message.cc) ? message.cc : [message.cc]
28
- personalizations[0]!.cc = ccArray.map((email) => ({ email }))
30
+ personalizations[0]!.cc = ccArray.map(email => ({ email }))
29
31
  }
30
32
  if (message.bcc) {
31
33
  const bccArray = Array.isArray(message.bcc) ? message.bcc : [message.bcc]
32
- personalizations[0]!.bcc = bccArray.map((email) => ({ email }))
34
+ personalizations[0]!.bcc = bccArray.map(email => ({ email }))
33
35
  }
34
36
 
35
37
  const content: { type: string; value: string }[] = []
@@ -46,11 +48,12 @@ export class SendGridTransport implements MailTransport {
46
48
  if (message.replyTo) body.reply_to = { email: message.replyTo }
47
49
 
48
50
  if (message.attachments?.length) {
49
- body.attachments = message.attachments.map((a) => ({
51
+ body.attachments = message.attachments.map(a => ({
50
52
  filename: a.filename,
51
- content: typeof a.content === 'string'
52
- ? Buffer.from(a.content).toString('base64')
53
- : Buffer.from(a.content).toString('base64'),
53
+ content:
54
+ typeof a.content === 'string'
55
+ ? Buffer.from(a.content).toString('base64')
56
+ : Buffer.from(a.content).toString('base64'),
54
57
  type: a.contentType,
55
58
  disposition: a.cid ? 'inline' : 'attachment',
56
59
  content_id: a.cid,
@@ -21,17 +21,17 @@ export class SmtpTransport implements MailTransport {
21
21
  const info = await this.transporter.sendMail({
22
22
  from: message.from,
23
23
  to: Array.isArray(message.to) ? message.to.join(', ') : message.to,
24
- cc: message.cc
25
- ? Array.isArray(message.cc) ? message.cc.join(', ') : message.cc
26
- : undefined,
24
+ cc: message.cc ? (Array.isArray(message.cc) ? message.cc.join(', ') : message.cc) : undefined,
27
25
  bcc: message.bcc
28
- ? Array.isArray(message.bcc) ? message.bcc.join(', ') : message.bcc
26
+ ? Array.isArray(message.bcc)
27
+ ? message.bcc.join(', ')
28
+ : message.bcc
29
29
  : undefined,
30
30
  replyTo: message.replyTo,
31
31
  subject: message.subject,
32
32
  html: message.html,
33
33
  text: message.text,
34
- attachments: message.attachments?.map((a) => ({
34
+ attachments: message.attachments?.map(a => ({
35
35
  filename: a.filename,
36
36
  content: a.content,
37
37
  contentType: a.contentType,
package/src/mail/types.ts CHANGED
@@ -55,6 +55,22 @@ export interface SendGridConfig {
55
55
  baseUrl?: string
56
56
  }
57
57
 
58
+ export interface MailgunConfig {
59
+ apiKey: string
60
+ domain: string
61
+ /** Default: 'https://api.mailgun.net'. EU: 'https://api.eu.mailgun.net' */
62
+ baseUrl?: string
63
+ }
64
+
65
+ export interface AlibabaConfig {
66
+ accessKeyId: string
67
+ accessKeySecret: string
68
+ /** Sender address configured in Alibaba DirectMail. */
69
+ accountName: string
70
+ /** Default: 'cn-hangzhou' */
71
+ region?: string
72
+ }
73
+
58
74
  export interface LogConfig {
59
75
  /** Write to 'console' or a file path. */
60
76
  output: 'console' | string
@@ -63,7 +79,7 @@ export interface LogConfig {
63
79
  // -- Top-level mail config ----------------------------------------------------
64
80
 
65
81
  export interface MailConfig {
66
- /** Default transport name: 'smtp' | 'resend' | 'sendgrid' | 'log' */
82
+ /** Default transport name: 'smtp' | 'resend' | 'sendgrid' | 'mailgun' | 'alibaba' | 'log' */
67
83
  default: string
68
84
  /** Default "from" address. */
69
85
  from: string
@@ -76,5 +92,7 @@ export interface MailConfig {
76
92
  smtp: SmtpConfig
77
93
  resend: ResendConfig
78
94
  sendgrid: SendGridConfig
95
+ mailgun: MailgunConfig
96
+ alibaba: AlibabaConfig
79
97
  log: LogConfig
80
98
  }
@@ -1,5 +1,10 @@
1
1
  import { ExternalServiceError } from '../../exceptions/errors.ts'
2
- import type { NotificationChannel, Notifiable, NotificationPayload, NotificationConfig } from '../types.ts'
2
+ import type {
3
+ NotificationChannel,
4
+ Notifiable,
5
+ NotificationPayload,
6
+ NotificationConfig,
7
+ } from '../types.ts'
3
8
 
4
9
  /**
5
10
  * Delivers notifications via Discord webhook.
@@ -1,5 +1,10 @@
1
1
  import { ExternalServiceError } from '../../exceptions/errors.ts'
2
- import type { NotificationChannel, Notifiable, NotificationPayload, NotificationConfig } from '../types.ts'
2
+ import type {
3
+ NotificationChannel,
4
+ Notifiable,
5
+ NotificationPayload,
6
+ NotificationConfig,
7
+ } from '../types.ts'
3
8
 
4
9
  /**
5
10
  * Delivers notifications via HTTP POST to a webhook URL.
@@ -28,8 +33,8 @@ export class WebhookChannel implements NotificationChannel {
28
33
 
29
34
  const headers: Record<string, string> = {
30
35
  'Content-Type': 'application/json',
31
- ...(this.config.webhooks?.default?.headers),
32
- ...(envelope.headers),
36
+ ...this.config.webhooks?.default?.headers,
37
+ ...envelope.headers,
33
38
  }
34
39
 
35
40
  const response = await fetch(url, {
@@ -16,7 +16,7 @@ import Database from '../database/database.ts'
16
16
  */
17
17
  export async function notify(
18
18
  notifiable: Notifiable | Notifiable[],
19
- notification: BaseNotification,
19
+ notification: BaseNotification
20
20
  ): Promise<void> {
21
21
  const recipients = Array.isArray(notifiable) ? notifiable : [notifiable]
22
22
 
@@ -44,7 +44,7 @@ export async function notify(
44
44
  queue: notification.queueOptions().queue ?? NotificationManager.config.queue,
45
45
  delay: notification.queueOptions().delay,
46
46
  attempts: notification.queueOptions().attempts,
47
- },
47
+ }
48
48
  )
49
49
  } else {
50
50
  await sendNow(recipient, notification)
@@ -187,10 +187,7 @@ export const notifications = {
187
187
  for (const binding of bindings) {
188
188
  const notification = binding.create(eventPayload)
189
189
  const recipients = await binding.recipients(eventPayload)
190
- await notify(
191
- Array.isArray(recipients) ? recipients : [recipients],
192
- notification,
193
- )
190
+ await notify(Array.isArray(recipients) ? recipients : [recipients], notification)
194
191
  }
195
192
  })
196
193
  }
@@ -207,7 +204,10 @@ function hydrateNotification(row: Record<string, unknown>): NotificationRecord {
207
204
  notifiableType: row.notifiable_type as string,
208
205
  notifiableId: row.notifiable_id as string,
209
206
  type: row.type as string,
210
- data: (typeof row.data === 'string' ? JSON.parse(row.data) : row.data) as Record<string, unknown>,
207
+ data: (typeof row.data === 'string' ? JSON.parse(row.data) : row.data) as Record<
208
+ string,
209
+ unknown
210
+ >,
211
211
  readAt: (row.read_at as Date) ?? null,
212
212
  createdAt: row.created_at as Date,
213
213
  }
@@ -32,10 +32,13 @@ export default class NotificationManager {
32
32
  constructor(db: Database, config: Configuration) {
33
33
  NotificationManager._db = db
34
34
  NotificationManager._config = {
35
- channels: (config.get('notification.channels', ['database']) as string[]),
35
+ channels: config.get('notification.channels', ['database']) as string[],
36
36
  queue: config.get('notification.queue', 'default') as string,
37
- webhooks: (config.get('notification.webhooks', {}) as Record<string, { url: string; headers?: Record<string, string> }>),
38
- discord: (config.get('notification.discord', {}) as Record<string, string>),
37
+ webhooks: config.get('notification.webhooks', {}) as Record<
38
+ string,
39
+ { url: string; headers?: Record<string, string> }
40
+ >,
41
+ discord: config.get('notification.discord', {}) as Record<string, string>,
39
42
  }
40
43
 
41
44
  // Register built-in channels
@@ -47,9 +50,7 @@ export default class NotificationManager {
47
50
 
48
51
  static get db(): Database {
49
52
  if (!NotificationManager._db) {
50
- throw new Error(
51
- 'NotificationManager not configured. Resolve it through the container first.'
52
- )
53
+ throw new Error('NotificationManager not configured. Resolve it through the container first.')
53
54
  }
54
55
  return NotificationManager._db
55
56
  }
@@ -22,7 +22,9 @@ export default class BaseModel {
22
22
  /** The underlying database connection. */
23
23
  static get db(): Database {
24
24
  if (!BaseModel._db) {
25
- throw new ConfigurationError('Database not configured. Resolve BaseModel through the container first.')
25
+ throw new ConfigurationError(
26
+ 'Database not configured. Resolve BaseModel through the container first.'
27
+ )
26
28
  }
27
29
  return BaseModel._db
28
30
  }
@@ -341,7 +343,7 @@ export default class BaseModel {
341
343
  }
342
344
 
343
345
  /** Convert a raw DB row to a plain object with camelCase keys and DateTime hydration. */
344
- function hydrateRow(row: Record<string, unknown>): Record<string, unknown> {
346
+ export function hydrateRow(row: Record<string, unknown>): Record<string, unknown> {
345
347
  const obj: Record<string, unknown> = {}
346
348
  for (const [column, value] of Object.entries(row)) {
347
349
  const prop = toCamelCase(column)
@@ -86,7 +86,9 @@ export default class Queue {
86
86
 
87
87
  static get db(): Database {
88
88
  if (!Queue._db)
89
- throw new ConfigurationError('Queue not configured. Resolve Queue through the container first.')
89
+ throw new ConfigurationError(
90
+ 'Queue not configured. Resolve Queue through the container first.'
91
+ )
90
92
  return Queue._db
91
93
  }
92
94
 
@@ -17,11 +17,11 @@ export interface CronExpression {
17
17
  }
18
18
 
19
19
  const FIELD_RANGES: [number, number][] = [
20
- [0, 59], // minute
21
- [0, 23], // hour
22
- [1, 31], // day of month
23
- [1, 12], // month
24
- [0, 6], // day of week
20
+ [0, 59], // minute
21
+ [0, 23], // hour
22
+ [1, 31], // day of month
23
+ [1, 12], // month
24
+ [0, 6], // day of week
25
25
  ]
26
26
 
27
27
  // Parse a 5-field cron string into expanded numeric arrays.
@@ -37,7 +37,13 @@ export function parseCron(expression: string): CronExpression {
37
37
  parseField(part, FIELD_RANGES[i]![0], FIELD_RANGES[i]![1])
38
38
  )
39
39
 
40
- return { minute: minute!, hour: hour!, dayOfMonth: dayOfMonth!, month: month!, dayOfWeek: dayOfWeek! }
40
+ return {
41
+ minute: minute!,
42
+ hour: hour!,
43
+ dayOfMonth: dayOfMonth!,
44
+ month: month!,
45
+ dayOfWeek: dayOfWeek!,
46
+ }
41
47
  }
42
48
 
43
49
  /**
@@ -4,13 +4,20 @@ import type { CronExpression } from './cron.ts'
4
4
  export type TaskHandler = () => void | Promise<void>
5
5
 
6
6
  const DAY_NAMES: Record<string, number> = {
7
- sunday: 0, sun: 0,
8
- monday: 1, mon: 1,
9
- tuesday: 2, tue: 2,
10
- wednesday: 3, wed: 3,
11
- thursday: 4, thu: 4,
12
- friday: 5, fri: 5,
13
- saturday: 6, sat: 6,
7
+ sunday: 0,
8
+ sun: 0,
9
+ monday: 1,
10
+ mon: 1,
11
+ tuesday: 2,
12
+ tue: 2,
13
+ wednesday: 3,
14
+ wed: 3,
15
+ thursday: 4,
16
+ thu: 4,
17
+ friday: 5,
18
+ fri: 5,
19
+ saturday: 6,
20
+ sat: 6,
14
21
  }
15
22
 
16
23
  /**
@@ -176,7 +183,9 @@ function dayToNumber(day: string): number {
176
183
  const n = DAY_NAMES[day.toLowerCase()]
177
184
  if (n === undefined) {
178
185
  throw new Error(
179
- `Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES).filter((_, i) => i % 2 === 0).join(', ')}`
186
+ `Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES)
187
+ .filter((_, i) => i % 2 === 0)
188
+ .join(', ')}`
180
189
  )
181
190
  }
182
191
  return n
@@ -41,7 +41,9 @@ export default class SessionManager {
41
41
 
42
42
  static get db(): Database {
43
43
  if (!SessionManager._db) {
44
- throw new ConfigurationError('SessionManager not configured. Resolve it through the container first.')
44
+ throw new ConfigurationError(
45
+ 'SessionManager not configured. Resolve it through the container first.'
46
+ )
45
47
  }
46
48
  return SessionManager._db
47
49
  }
@@ -48,7 +48,9 @@ export default class StorageManager {
48
48
 
49
49
  static get driver(): StorageDriver {
50
50
  if (!StorageManager._driver) {
51
- throw new ConfigurationError('StorageManager not configured. Resolve it through the container first.')
51
+ throw new ConfigurationError(
52
+ 'StorageManager not configured. Resolve it through the container first.'
53
+ )
52
54
  }
53
55
  return StorageManager._driver
54
56
  }
@@ -159,9 +159,7 @@ function compileDirective(
159
159
  }
160
160
 
161
161
  case 'islands': {
162
- const src = token.args
163
- ? token.args.replace(/^['"]|['"]$/g, '').trim()
164
- : '/islands.js'
162
+ const src = token.args ? token.args.replace(/^['"]|['"]$/g, '').trim() : '/islands.js'
165
163
  lines.push(`__out += '<script src="${escapeJs(src)}"><\\/script>';`)
166
164
  break
167
165
  }
@@ -25,7 +25,7 @@ export class IslandBuilder {
25
25
  this.islandsDir = resolve(options.islandsDir ?? './islands')
26
26
  this.outDir = resolve(options.outDir ?? './public')
27
27
  this.outFile = options.outFile ?? 'islands.js'
28
- this.minify = options.minify ?? (Bun.env.NODE_ENV === 'production')
28
+ this.minify = options.minify ?? Bun.env.NODE_ENV === 'production'
29
29
  }
30
30
 
31
31
  /** Discover all .vue files in the islands directory. */
@@ -123,9 +123,9 @@ export class IslandBuilder {
123
123
  target: 'browser',
124
124
  plugins: [virtualEntryPlugin, vueSfcPlugin()],
125
125
  define: {
126
- '__VUE_OPTIONS_API__': 'true',
127
- '__VUE_PROD_DEVTOOLS__': 'false',
128
- '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': 'false',
126
+ __VUE_OPTIONS_API__: 'true',
127
+ __VUE_PROD_DEVTOOLS__: 'false',
128
+ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
129
129
  },
130
130
  })
131
131
 
@@ -11,7 +11,7 @@ export function vueSfcPlugin(): BunPlugin {
11
11
  return {
12
12
  name: 'vue-sfc',
13
13
  setup(build) {
14
- build.onLoad({ filter: /\.vue$/ }, async (args) => {
14
+ build.onLoad({ filter: /\.vue$/ }, async args => {
15
15
  const source = await Bun.file(args.path).text()
16
16
  const id = hashId(args.path)
17
17
  const scopeId = `data-v-${id}`
@@ -35,11 +35,13 @@ export function vueSfcPlugin(): BunPlugin {
35
35
  id,
36
36
  inlineTemplate: !!descriptor.scriptSetup,
37
37
  sourceMap: false,
38
- templateOptions: scoped ? {
39
- scoped: true,
40
- id,
41
- compilerOptions: { scopeId },
42
- } : undefined,
38
+ templateOptions: scoped
39
+ ? {
40
+ scoped: true,
41
+ id,
42
+ compilerOptions: { scopeId },
43
+ }
44
+ : undefined,
43
45
  })
44
46
  scriptCode = result.content
45
47
  bindings = result.bindings
@@ -62,7 +64,7 @@ export function vueSfcPlugin(): BunPlugin {
62
64
 
63
65
  if (result.errors.length > 0) {
64
66
  throw new Error(
65
- `Vue template error in ${args.path}:\n${result.errors.map(e => typeof e === 'string' ? e : e.message).join('\n')}`
67
+ `Vue template error in ${args.path}:\n${result.errors.map(e => (typeof e === 'string' ? e : e.message)).join('\n')}`
66
68
  )
67
69
  }
68
70
 
@@ -100,10 +102,7 @@ export function vueSfcPlugin(): BunPlugin {
100
102
  // <script setup> with inlineTemplate — scriptCode is a complete module
101
103
  // Rewrite the default export to capture the component and set __scopeId
102
104
  if (scoped) {
103
- output += scriptCode.replace(
104
- /export\s+default\s+/,
105
- 'const __sfc__ = '
106
- ) + '\n'
105
+ output += scriptCode.replace(/export\s+default\s+/, 'const __sfc__ = ') + '\n'
107
106
  output += `__sfc__.__scopeId = ${JSON.stringify(scopeId)};\n`
108
107
  output += 'export default __sfc__;\n'
109
108
  } else {
@@ -112,10 +111,7 @@ export function vueSfcPlugin(): BunPlugin {
112
111
  } else {
113
112
  // Options API — stitch script + template render function
114
113
  if (scriptCode) {
115
- output += scriptCode.replace(
116
- /export\s+default\s*\{/,
117
- 'const __component__ = {'
118
- ) + '\n'
114
+ output += scriptCode.replace(/export\s+default\s*\{/, 'const __component__ = {') + '\n'
119
115
  } else {
120
116
  output += 'const __component__ = {};\n'
121
117
  }
@@ -16,7 +16,17 @@ export interface Token {
16
16
  line: number
17
17
  }
18
18
 
19
- const DIRECTIVES = new Set(['if', 'elseif', 'else', 'end', 'each', 'layout', 'block', 'include', 'islands'])
19
+ const DIRECTIVES = new Set([
20
+ 'if',
21
+ 'elseif',
22
+ 'else',
23
+ 'end',
24
+ 'each',
25
+ 'layout',
26
+ 'block',
27
+ 'include',
28
+ 'islands',
29
+ ])
20
30
 
21
31
  export function tokenize(source: string): Token[] {
22
32
  const tokens: Token[] = []