@warlock.js/core 5.5.0 → 5.6.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.
- package/CHANGELOG.md +14 -0
- package/esm/application/app.d.mts.map +1 -1
- package/esm/application/app.mjs +7 -1
- package/esm/application/app.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/commands/routes/route-row.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +11 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/container/container-instance-registry.mjs +40 -0
- package/esm/container/container-instance-registry.mjs.map +1 -0
- package/esm/container/index.d.mts +11 -0
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +11 -0
- package/esm/container/index.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/types.d.mts.map +1 -1
- package/esm/dev-server/utils.mjs.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +28 -0
- package/esm/errors/container-key-missing-error.mjs.map +1 -0
- package/esm/errors/unknown-locale-error.mjs +28 -0
- package/esm/errors/unknown-locale-error.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.feature.mjs.map +1 -1
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
- package/esm/generations/features/tailwind.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/context/request-memo.mjs.map +1 -1
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/port-preflight.d.mts.map +1 -1
- package/esm/http/port-preflight.mjs.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +2 -2
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +38 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +47 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/bundle-framework-dependencies.mjs +152 -0
- package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
- package/esm/production/production-builder.mjs +30 -3
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/tsconfig-path-aliases.mjs +99 -0
- package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
- package/esm/react/index.d.mts.map +1 -1
- package/esm/react/index.mjs.map +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -2
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/contain-path.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-setup-timeout.mjs.map +1 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/app-log.mjs.map +1 -1
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/utils/sleep.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-mail.mjs","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Options as NodemailerOptions } from \"nodemailer/lib/mailer\";\nimport { isDevelopmentMode, isTestMode, resolveMailConfig } from \"./config\";\nimport { MAIL_EVENTS, generateMailId, mailEvents } from \"./events\";\nimport { getMailer } from \"./mailer-pool\";\nimport { renderReactMail } from \"./react-mail\";\nimport { captureMail } from \"./test-mailbox\";\nimport {\n MailError,\n type CapturedMail,\n type MailAddress,\n type MailAttachment,\n type MailOptions,\n type MailPriority,\n type MailResult,\n type NormalizedMail,\n} from \"./types\";\n\n/**\n * Normalize email address to string\n */\nfunction addressToString(address: MailAddress): string {\n if (typeof address === \"string\") {\n return address;\n }\n\n return `\"${address.name}\" <${address.address}>`;\n}\n\n/**\n * Normalize recipients to array\n */\nfunction normalizeRecipients(recipients: string | string[] | undefined): string[] {\n if (!recipients) {\n return [];\n }\n\n return Array.isArray(recipients) ? recipients : [recipients];\n}\n\n/**\n * Map priority to nodemailer format\n */\nfunction mapPriority(priority: MailPriority): \"high\" | \"normal\" | \"low\" {\n return priority;\n}\n\n/**\n * Normalize mail options to internal format\n */\nfunction normalizeMail(options: MailOptions): NormalizedMail {\n const config = resolveMailConfig(options);\n\n return {\n to: normalizeRecipients(options.to),\n cc: normalizeRecipients(options.cc),\n bcc: normalizeRecipients(options.bcc),\n replyTo: options.replyTo,\n from: options.from || config.from || { name: \"No Reply\", address: \"noreply@localhost\" },\n subject: options.subject,\n html: options.html,\n text: options.text,\n attachments: options.attachments || [],\n priority: options.priority || \"normal\",\n headers: options.headers || {},\n tags: options.tags || [],\n correlationId: options.correlationId,\n config,\n };\n}\n\n/**\n * Resolve attachment - read file if path is provided\n */\nasync function resolveAttachment(attachment: MailAttachment): Promise<{\n filename: string;\n content: Buffer | string;\n contentType?: string;\n encoding?: string;\n cid?: string;\n}> {\n // If path is provided, read the file\n if (attachment.path) {\n try {\n const content = await readFile(attachment.path);\n return {\n filename: attachment.filename,\n content,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n } catch (error) {\n throw new MailError(\n `Failed to read attachment file \"${attachment.path}\": ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"CONFIG_ERROR\",\n error instanceof Error ? error : undefined,\n );\n }\n }\n\n // Already has content\n return {\n filename: attachment.filename,\n content: attachment.content!,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n}\n\n/**\n * Build nodemailer options from normalized mail\n */\nasync function buildNodemailerOptions(normalized: NormalizedMail): Promise<NodemailerOptions> {\n const options: NodemailerOptions = {\n to: normalized.to,\n from: addressToString(normalized.from),\n subject: normalized.subject,\n priority: mapPriority(normalized.priority),\n };\n\n if (normalized.cc.length > 0) {\n options.cc = normalized.cc;\n }\n\n if (normalized.bcc.length > 0) {\n options.bcc = normalized.bcc;\n }\n\n if (normalized.replyTo) {\n options.replyTo = normalized.replyTo;\n }\n\n if (normalized.html) {\n options.html = normalized.html;\n }\n\n if (normalized.text) {\n options.text = normalized.text;\n }\n\n if (normalized.attachments.length > 0) {\n // Resolve all attachments (read files if needed)\n const resolvedAttachments = await Promise.all(\n normalized.attachments.map((att) => resolveAttachment(att as MailAttachment)),\n );\n\n options.attachments = resolvedAttachments.map((att) => ({\n filename: att.filename,\n content: att.content,\n contentType: att.contentType,\n encoding: att.encoding,\n cid: att.cid,\n }));\n }\n\n if (Object.keys(normalized.headers).length > 0) {\n options.headers = normalized.headers;\n }\n\n return options;\n}\n\n/**\n * Run per-mail event handler\n */\nasync function runMailEvent<T extends (...args: any[]) => any>(\n handler: T | undefined,\n ...args: Parameters<T>\n): Promise<ReturnType<T> | undefined> {\n if (!handler) {\n return undefined;\n }\n\n try {\n return await handler(...args);\n } catch (error) {\n log.error(\"mail\", \"event\", `Per-mail event handler error: ${error}`);\n return undefined;\n }\n}\n\n/**\n * Trigger both global and mail-specific events\n */\nasync function triggerEvents(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n): Promise<any[]> {\n try {\n // Trigger global event (e.g., mail.success)\n const globalEventName =\n event === \"beforeSending\"\n ? \"BEFORE_SENDING\"\n : event === \"sent\"\n ? \"SENT\"\n : event === \"success\"\n ? \"SUCCESS\"\n : \"ERROR\";\n\n const globalResults = await mailEvents.trigger(\n globalEventName as keyof typeof MAIL_EVENTS,\n ...args,\n );\n\n // Trigger mail-specific event (e.g., mail.$id.success)\n const specificResults = await mailEvents.triggerForMail(mailId, event, ...args);\n\n return [...(globalResults || []), ...(specificResults || [])];\n } catch (error) {\n log.error(\"mail\", \"event\", `Event handler error: ${error}`);\n return [];\n }\n}\n\n/**\n * Send an email\n *\n * @param options Mail options including recipients, content, and configuration\n * @returns Result containing success status, message ID, and accepted/rejected recipients\n *\n * @example\n * ```typescript\n * // Basic usage\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n *\n * // With React component\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Welcome!\",\n * component: <WelcomeEmail name=\"John\" />,\n * });\n *\n * // Track specific mail with events\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail was sent!\");\n * });\n * await sendMail({ id: mailId, to: \"...\", subject: \"...\", html: \"...\" });\n * ```\n */\nexport async function sendMail(options: MailOptions): Promise<MailResult> {\n // Generate or use provided mail ID\n const mailId = options.id || generateMailId();\n\n // Render React component if provided\n if (options.component) {\n try {\n options.html = await renderReactMail(options.component);\n } catch (error) {\n const mailError = new MailError(\n `Failed to render React component: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"RENDER_ERROR\",\n error instanceof Error ? error : undefined,\n );\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n\n throw mailError;\n }\n }\n\n // Normalize mail\n const normalized = normalizeMail(options);\n const driver = normalized.config.driver || \"smtp\";\n\n // Run beforeSending event (per-mail, global, and mail-specific)\n const beforeResult = await runMailEvent(options.beforeSending, options);\n const globalBeforeResults = await triggerEvents(mailId, \"beforeSending\", options);\n\n // If any returns false, cancel sending\n if (beforeResult === false || globalBeforeResults.some((r) => r === false)) {\n log.info(\n `mail.${driver}`,\n \"cancelled\",\n \"Mail sending cancelled by beforeSending event\",\n );\n\n const result: MailResult = {\n success: false,\n accepted: [],\n rejected: normalized.to,\n response: \"Cancelled by beforeSending event\",\n };\n\n return result;\n }\n\n // Test mode - capture without sending\n if (isTestMode()) {\n log.info(\n `mail.${driver}`,\n \"test\",\n `[TEST MODE] Captured mail to: ${normalized.to.join(\", \")}`,\n );\n\n const result: MailResult = {\n success: true,\n messageId: `test-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Test mode - mail captured\",\n };\n\n const captured: CapturedMail = {\n options,\n normalized,\n timestamp: new Date(),\n result,\n };\n\n captureMail(captured);\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Development mode - log without sending\n if (isDevelopmentMode()) {\n log.info(\n `mail.${driver}`,\n \"dev\",\n `[DEV MODE] Would send mail to: ${normalized.to.join(\", \")}`,\n );\n log.info(`mail.${driver}`, \"dev\", `Subject: ${normalized.subject}`);\n\n if (normalized.html) {\n log.info(\n `mail.${driver}`,\n \"dev\",\n `HTML length: ${normalized.html.length} chars`,\n );\n }\n\n const result: MailResult = {\n success: true,\n messageId: `dev-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Development mode - mail logged\",\n };\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Production mode - actually send\n try {\n const config = resolveMailConfig(options);\n const mailer = await getMailer(config);\n const nodemailerOptions = await buildNodemailerOptions(normalized);\n\n log.info(\n `mail.${driver}`,\n \"send\",\n `Sending mail to: ${normalized.to.join(\", \")}`,\n );\n\n const output = await mailer.sendMail(nodemailerOptions);\n\n const accepted = output.accepted || (output.messageId ? normalized.to : []);\n const rejected = output.rejected || [];\n\n const result: MailResult = {\n success: accepted.length > 0,\n messageId: output.messageId,\n accepted: accepted as string[],\n rejected: rejected as string[],\n response: output.response,\n envelope: output.envelope,\n };\n\n if (result.success) {\n log.success(\n `mail.${driver}`,\n \"sent\",\n `Mail sent successfully (ID: ${result.messageId})`,\n );\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n } else {\n log.warn(\n `mail.${driver}`,\n \"partial\",\n `Mail partially rejected: ${rejected.join(\", \")}`,\n );\n }\n\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n let code: MailError[\"code\"] = \"UNKNOWN\";\n\n // Detect error type\n if (errorMessage.includes(\"ECONNREFUSED\") || errorMessage.includes(\"ENOTFOUND\")) {\n code = \"CONNECTION_ERROR\";\n } else if (errorMessage.includes(\"authentication\") || errorMessage.includes(\"auth\")) {\n code = \"AUTH_ERROR\";\n } else if (errorMessage.includes(\"timeout\") || errorMessage.includes(\"ETIMEDOUT\")) {\n code = \"TIMEOUT\";\n } else if (errorMessage.includes(\"rate\") || errorMessage.includes(\"limit\")) {\n code = \"RATE_LIMIT\";\n }\n\n const mailError = new MailError(\n `Failed to send mail: ${errorMessage}`,\n code,\n error instanceof Error ? error : undefined,\n );\n\n log.error(`mail.${driver}`, \"error\", mailError.message);\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n await runMailEvent(options.onSent, options, null, mailError);\n await triggerEvents(mailId, \"sent\", options, null, mailError);\n\n throw mailError;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"send-mail.mjs","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Options as NodemailerOptions } from \"nodemailer/lib/mailer\";\nimport { isDevelopmentMode, isTestMode, resolveMailConfig } from \"./config\";\nimport { MAIL_EVENTS, generateMailId, mailEvents } from \"./events\";\nimport { getMailer } from \"./mailer-pool\";\nimport { renderReactMail } from \"./react-mail\";\nimport { captureMail } from \"./test-mailbox\";\nimport {\n MailError,\n type CapturedMail,\n type MailAddress,\n type MailAttachment,\n type MailOptions,\n type MailPriority,\n type MailResult,\n type NormalizedMail,\n} from \"./types\";\n\n/**\n * Normalize email address to string\n */\nfunction addressToString(address: MailAddress): string {\n if (typeof address === \"string\") {\n return address;\n }\n\n return `\"${address.name}\" <${address.address}>`;\n}\n\n/**\n * Normalize recipients to array\n */\nfunction normalizeRecipients(recipients: string | string[] | undefined): string[] {\n if (!recipients) {\n return [];\n }\n\n return Array.isArray(recipients) ? recipients : [recipients];\n}\n\n/**\n * Map priority to nodemailer format\n */\nfunction mapPriority(priority: MailPriority): \"high\" | \"normal\" | \"low\" {\n return priority;\n}\n\n/**\n * Normalize mail options to internal format\n */\nfunction normalizeMail(options: MailOptions): NormalizedMail {\n const config = resolveMailConfig(options);\n\n return {\n to: normalizeRecipients(options.to),\n cc: normalizeRecipients(options.cc),\n bcc: normalizeRecipients(options.bcc),\n replyTo: options.replyTo,\n from: options.from || config.from || { name: \"No Reply\", address: \"noreply@localhost\" },\n subject: options.subject,\n html: options.html,\n text: options.text,\n attachments: options.attachments || [],\n priority: options.priority || \"normal\",\n headers: options.headers || {},\n tags: options.tags || [],\n correlationId: options.correlationId,\n config,\n };\n}\n\n/**\n * Resolve attachment - read file if path is provided\n */\nasync function resolveAttachment(attachment: MailAttachment): Promise<{\n filename: string;\n content: Buffer | string;\n contentType?: string;\n encoding?: string;\n cid?: string;\n}> {\n // If path is provided, read the file\n if (attachment.path) {\n try {\n const content = await readFile(attachment.path);\n return {\n filename: attachment.filename,\n content,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n } catch (error) {\n throw new MailError(\n `Failed to read attachment file \"${attachment.path}\": ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"CONFIG_ERROR\",\n error instanceof Error ? error : undefined,\n );\n }\n }\n\n // Already has content\n return {\n filename: attachment.filename,\n content: attachment.content!,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n}\n\n/**\n * Build nodemailer options from normalized mail\n */\nasync function buildNodemailerOptions(normalized: NormalizedMail): Promise<NodemailerOptions> {\n const options: NodemailerOptions = {\n to: normalized.to,\n from: addressToString(normalized.from),\n subject: normalized.subject,\n priority: mapPriority(normalized.priority),\n };\n\n if (normalized.cc.length > 0) {\n options.cc = normalized.cc;\n }\n\n if (normalized.bcc.length > 0) {\n options.bcc = normalized.bcc;\n }\n\n if (normalized.replyTo) {\n options.replyTo = normalized.replyTo;\n }\n\n if (normalized.html) {\n options.html = normalized.html;\n }\n\n if (normalized.text) {\n options.text = normalized.text;\n }\n\n if (normalized.attachments.length > 0) {\n // Resolve all attachments (read files if needed)\n const resolvedAttachments = await Promise.all(\n normalized.attachments.map((att) => resolveAttachment(att as MailAttachment)),\n );\n\n options.attachments = resolvedAttachments.map((att) => ({\n filename: att.filename,\n content: att.content,\n contentType: att.contentType,\n encoding: att.encoding,\n cid: att.cid,\n }));\n }\n\n if (Object.keys(normalized.headers).length > 0) {\n options.headers = normalized.headers;\n }\n\n return options;\n}\n\n/**\n * Run per-mail event handler\n */\nasync function runMailEvent<T extends (...args: any[]) => any>(\n handler: T | undefined,\n ...args: Parameters<T>\n): Promise<ReturnType<T> | undefined> {\n if (!handler) {\n return undefined;\n }\n\n try {\n return await handler(...args);\n } catch (error) {\n log.error(\"mail\", \"event\", `Per-mail event handler error: ${error}`);\n return undefined;\n }\n}\n\n/**\n * Trigger both global and mail-specific events\n */\nasync function triggerEvents(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n): Promise<any[]> {\n try {\n // Trigger global event (e.g., mail.success)\n const globalEventName =\n event === \"beforeSending\"\n ? \"BEFORE_SENDING\"\n : event === \"sent\"\n ? \"SENT\"\n : event === \"success\"\n ? \"SUCCESS\"\n : \"ERROR\";\n\n const globalResults = await mailEvents.trigger(\n globalEventName as keyof typeof MAIL_EVENTS,\n ...args,\n );\n\n // Trigger mail-specific event (e.g., mail.$id.success)\n const specificResults = await mailEvents.triggerForMail(mailId, event, ...args);\n\n return [...(globalResults || []), ...(specificResults || [])];\n } catch (error) {\n log.error(\"mail\", \"event\", `Event handler error: ${error}`);\n return [];\n }\n}\n\n/**\n * Send an email\n *\n * @param options Mail options including recipients, content, and configuration\n * @returns Result containing success status, message ID, and accepted/rejected recipients\n *\n * @example\n * ```typescript\n * // Basic usage\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n *\n * // With React component\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Welcome!\",\n * component: <WelcomeEmail name=\"John\" />,\n * });\n *\n * // Track specific mail with events\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail was sent!\");\n * });\n * await sendMail({ id: mailId, to: \"...\", subject: \"...\", html: \"...\" });\n * ```\n */\nexport async function sendMail(options: MailOptions): Promise<MailResult> {\n // Generate or use provided mail ID\n const mailId = options.id || generateMailId();\n\n // Render React component if provided\n if (options.component) {\n try {\n options.html = await renderReactMail(options.component);\n } catch (error) {\n const mailError = new MailError(\n `Failed to render React component: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"RENDER_ERROR\",\n error instanceof Error ? error : undefined,\n );\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n\n throw mailError;\n }\n }\n\n // Normalize mail\n const normalized = normalizeMail(options);\n const driver = normalized.config.driver || \"smtp\";\n\n // Run beforeSending event (per-mail, global, and mail-specific)\n const beforeResult = await runMailEvent(options.beforeSending, options);\n const globalBeforeResults = await triggerEvents(mailId, \"beforeSending\", options);\n\n // If any returns false, cancel sending\n if (beforeResult === false || globalBeforeResults.some((r) => r === false)) {\n log.info(`mail.${driver}`, \"cancelled\", \"Mail sending cancelled by beforeSending event\");\n\n const result: MailResult = {\n success: false,\n accepted: [],\n rejected: normalized.to,\n response: \"Cancelled by beforeSending event\",\n };\n\n return result;\n }\n\n // Test mode - capture without sending\n if (isTestMode()) {\n log.info(`mail.${driver}`, \"test\", `[TEST MODE] Captured mail to: ${normalized.to.join(\", \")}`);\n\n const result: MailResult = {\n success: true,\n messageId: `test-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Test mode - mail captured\",\n };\n\n const captured: CapturedMail = {\n options,\n normalized,\n timestamp: new Date(),\n result,\n };\n\n captureMail(captured);\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Development mode - log without sending\n if (isDevelopmentMode()) {\n log.info(`mail.${driver}`, \"dev\", `[DEV MODE] Would send mail to: ${normalized.to.join(\", \")}`);\n log.info(`mail.${driver}`, \"dev\", `Subject: ${normalized.subject}`);\n\n if (normalized.html) {\n log.info(`mail.${driver}`, \"dev\", `HTML length: ${normalized.html.length} chars`);\n }\n\n const result: MailResult = {\n success: true,\n messageId: `dev-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Development mode - mail logged\",\n };\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Production mode - actually send\n try {\n const config = resolveMailConfig(options);\n const mailer = await getMailer(config);\n const nodemailerOptions = await buildNodemailerOptions(normalized);\n\n log.info(`mail.${driver}`, \"send\", `Sending mail to: ${normalized.to.join(\", \")}`);\n\n const output = await mailer.sendMail(nodemailerOptions);\n\n const accepted = output.accepted || (output.messageId ? normalized.to : []);\n const rejected = output.rejected || [];\n\n const result: MailResult = {\n success: accepted.length > 0,\n messageId: output.messageId,\n accepted: accepted as string[],\n rejected: rejected as string[],\n response: output.response,\n envelope: output.envelope,\n };\n\n if (result.success) {\n log.success(`mail.${driver}`, \"sent\", `Mail sent successfully (ID: ${result.messageId})`);\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n } else {\n log.warn(`mail.${driver}`, \"partial\", `Mail partially rejected: ${rejected.join(\", \")}`);\n }\n\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n let code: MailError[\"code\"] = \"UNKNOWN\";\n\n // Detect error type\n if (errorMessage.includes(\"ECONNREFUSED\") || errorMessage.includes(\"ENOTFOUND\")) {\n code = \"CONNECTION_ERROR\";\n } else if (errorMessage.includes(\"authentication\") || errorMessage.includes(\"auth\")) {\n code = \"AUTH_ERROR\";\n } else if (errorMessage.includes(\"timeout\") || errorMessage.includes(\"ETIMEDOUT\")) {\n code = \"TIMEOUT\";\n } else if (errorMessage.includes(\"rate\") || errorMessage.includes(\"limit\")) {\n code = \"RATE_LIMIT\";\n }\n\n const mailError = new MailError(\n `Failed to send mail: ${errorMessage}`,\n code,\n error instanceof Error ? error : undefined,\n );\n\n log.error(`mail.${driver}`, \"error\", mailError.message);\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n await runMailEvent(options.onSent, options, null, mailError);\n await triggerEvents(mailId, \"sent\", options, null, mailError);\n\n throw mailError;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-contributions.mjs","names":[],"sources":["../../../../../../../core/src/production/build-contributions.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"build-contributions.mjs","names":[],"sources":["../../../../../../../core/src/production/build-contributions.ts"],"sourcesContent":["import type { Connector, ConnectorBuildContext, ConnectorEsbuildPatch } from \"../connectors/types\";\n\n/**\n * `Error` with the `cause` option typed. Same shim as\n * `application.ts` — lib.es2022 declares `cause` on `ErrorOptions`, but the\n * package still compiles against an older lib in some consumers.\n */\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\n\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\n\n/**\n * The only keys a `build` contribution may carry.\n *\n * The runtime half of the closed-contribution constraint: the\n * type already makes `build: { plugins: [...] }` unrepresentable, but a\n * plain `warlock.config.js`, an `as any`, or a connector compiled against an\n * older core all slip past the type. Rejected rather than ignored — the same\n * posture as the `tsconfig` guard in `resolve-build-config`: dropping the key\n * quietly would leave the author believing their plugin ran.\n */\nconst ALLOWED_CONTRIBUTION_KEYS = [\"generate\", \"emit\"] as const;\n\n/**\n * What the generate pass produced, merged across every contributor.\n */\nexport type GenerateContributionsResult = {\n /** Entry lines in contributor order, appended after `./routes` */\n entryImports: string[];\n /** The contributors' esbuild patches, merged into one */\n esbuild: ConnectorEsbuildPatch;\n};\n\n/**\n * Reject a contribution object carrying anything but the two hooks.\n *\n * @throws Error naming the connector and the offending key\n */\nexport function assertClosedContribution(connector: Connector): void {\n const contribution = connector.build;\n\n if (!contribution) return;\n\n for (const key of Object.keys(contribution)) {\n if ((ALLOWED_CONTRIBUTION_KEYS as readonly string[]).includes(key)) continue;\n\n throw new Error(\n `Connector \"${connector.name}\" declares an unsupported build contribution key \"${key}\". ` +\n `A build contribution carries exactly two optional hooks — ` +\n `${ALLOWED_CONTRIBUTION_KEYS.join(\" and \")} — and no data. ` +\n `Anything a hook needs (plugins, pipelines, aliases) must be constructed inside ` +\n `that hook, so it never lands in the connector's static import graph.`,\n );\n }\n}\n\n/**\n * Drain every connector's `generate` hook, in array order, sequentially.\n *\n * Semantics are copied from `Application.runStartupValidators`\n * (`application.ts:304-329`) on purpose: ordered, awaited one at a time, and\n * a throw is rethrown wrapped in an error NAMING the contributor. A\n * contributor failure IS the build failing — that is what makes\n * \"green build, no client bundle\" unrepresentable rather than merely\n * unlikely.\n */\nexport async function runGenerateContributions(\n connectors: readonly Connector[],\n context: ConnectorBuildContext,\n): Promise<GenerateContributionsResult> {\n const entryImports: string[] = [];\n let esbuild: ConnectorEsbuildPatch = {};\n\n for (const connector of connectors) {\n assertClosedContribution(connector);\n\n const generate = connector.build?.generate;\n\n if (!generate) continue;\n\n const result = await runHook(connector, \"generate\", () => generate(context));\n\n if (!result) continue;\n\n if (result.entryImports) {\n entryImports.push(...result.entryImports);\n }\n\n if (result.esbuild) {\n esbuild = mergeEsbuildPatches(esbuild, result.esbuild);\n }\n }\n\n return { entryImports, esbuild };\n}\n\n/**\n * Drain every connector's `emit` hook, in array order, sequentially.\n *\n * Runs after esbuild and before `.warlock/production` is removed, so a hook\n * may still read what `generate` wrote there.\n */\nexport async function runEmitContributions(\n connectors: readonly Connector[],\n context: ConnectorBuildContext,\n): Promise<void> {\n for (const connector of connectors) {\n assertClosedContribution(connector);\n\n const emit = connector.build?.emit;\n\n if (!emit) continue;\n\n await runHook(connector, \"emit\", () => emit(context));\n }\n}\n\n/**\n * Merge two contributor patches. Later wins, except:\n *\n * - `define` merges per key — two contributors defining different\n * `import.meta.env.*` entries must both survive; a wholesale replace would\n * drop one silently.\n * - `external` concatenates and dedupes — it is a set of package names, and\n * under `singleBundle` each contributor adds its own optional peers.\n * - `loader` merges per extension, for the same reason as `define`.\n */\nexport function mergeEsbuildPatches(\n base: ConnectorEsbuildPatch,\n patch: ConnectorEsbuildPatch,\n): ConnectorEsbuildPatch {\n const merged: ConnectorEsbuildPatch = { ...base, ...patch };\n\n if (base.define || patch.define) {\n merged.define = { ...base.define, ...patch.define };\n }\n\n if (base.external || patch.external) {\n merged.external = dedupe([...(base.external ?? []), ...(patch.external ?? [])]);\n }\n\n if (base.loader || patch.loader) {\n merged.loader = { ...base.loader, ...patch.loader };\n }\n\n return merged;\n}\n\n/**\n * Concatenation order preserved — esbuild does not care, but a stable order\n * keeps build output diffable.\n */\nexport function dedupe(values: string[]): string[] {\n return [...new Set(values)];\n}\n\n/**\n * Run one hook and rename its failure after the connector that owns it.\n */\nasync function runHook<Result>(\n connector: Connector,\n hook: \"generate\" | \"emit\",\n run: () => Result | Promise<Result>,\n): Promise<Result> {\n try {\n return await run();\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n\n throw new ErrorWithCause(\n `Build contribution \"${connector.name}\" failed during ${hook}: ${cause}. ` +\n `The build cannot continue — an artifact this connector was responsible for ` +\n `is missing, and shipping the bundle without it would fail at runtime instead.`,\n { cause: error },\n );\n }\n}\n"],"mappings":";AASA,MAAM,iBAAiB;;;;;;;;;;;AAYvB,MAAM,4BAA4B,CAAC,YAAY,MAAM;;;;;;AAiBrD,SAAgB,yBAAyB,WAA4B;CACnE,MAAM,eAAe,UAAU;CAE/B,IAAI,CAAC,cAAc;CAEnB,KAAK,MAAM,OAAO,OAAO,KAAK,YAAY,GAAG;EAC3C,IAAK,0BAAgD,SAAS,GAAG,GAAG;EAEpE,MAAM,IAAI,MACR,cAAc,UAAU,KAAK,oDAAoD,IAAI,+DAEhF,0BAA0B,KAAK,OAAO,EAAE,oKAG/C;CACF;AACF;;;;;;;;;;;AAYA,eAAsB,yBACpB,YACA,SACsC;CACtC,MAAM,eAAyB,CAAC;CAChC,IAAI,UAAiC,CAAC;CAEtC,KAAK,MAAM,aAAa,YAAY;EAClC,yBAAyB,SAAS;EAElC,MAAM,WAAW,UAAU,OAAO;EAElC,IAAI,CAAC,UAAU;EAEf,MAAM,SAAS,MAAM,QAAQ,WAAW,kBAAkB,SAAS,OAAO,CAAC;EAE3E,IAAI,CAAC,QAAQ;EAEb,IAAI,OAAO,cACT,aAAa,KAAK,GAAG,OAAO,YAAY;EAG1C,IAAI,OAAO,SACT,UAAU,oBAAoB,SAAS,OAAO,OAAO;CAEzD;CAEA,OAAO;EAAE;EAAc;CAAQ;AACjC;;;;;;;AAQA,eAAsB,qBACpB,YACA,SACe;CACf,KAAK,MAAM,aAAa,YAAY;EAClC,yBAAyB,SAAS;EAElC,MAAM,OAAO,UAAU,OAAO;EAE9B,IAAI,CAAC,MAAM;EAEX,MAAM,QAAQ,WAAW,cAAc,KAAK,OAAO,CAAC;CACtD;AACF;;;;;;;;;;;AAYA,SAAgB,oBACd,MACA,OACuB;CACvB,MAAM,SAAgC;EAAE,GAAG;EAAM,GAAG;CAAM;CAE1D,IAAI,KAAK,UAAU,MAAM,QACvB,OAAO,SAAS;EAAE,GAAG,KAAK;EAAQ,GAAG,MAAM;CAAO;CAGpD,IAAI,KAAK,YAAY,MAAM,UACzB,OAAO,WAAW,OAAO,CAAC,GAAI,KAAK,YAAY,CAAC,GAAI,GAAI,MAAM,YAAY,CAAC,CAAE,CAAC;CAGhF,IAAI,KAAK,UAAU,MAAM,QACvB,OAAO,SAAS;EAAE,GAAG,KAAK;EAAQ,GAAG,MAAM;CAAO;CAGpD,OAAO;AACT;;;;;AAMA,SAAgB,OAAO,QAA4B;CACjD,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B;;;;AAKA,eAAe,QACb,WACA,MACA,KACiB;CACjB,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,SAAS,OAAO;EACd,MAAM,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAEnE,MAAM,IAAI,eACR,uBAAuB,UAAU,KAAK,kBAAkB,KAAK,IAAI,MAAM,6JAGvE,EAAE,OAAO,MAAM,CACjB;CACF;AACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { isBuiltin } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/production/bundle-framework-dependencies.ts
|
|
6
|
+
/**
|
|
7
|
+
* The package name a bare specifier addresses, discarding any subpath:
|
|
8
|
+
* `ajv/dist/jtd` → `ajv`, `@fastify/static/lib/x` → `@fastify/static`.
|
|
9
|
+
*/
|
|
10
|
+
function packageNameOfSpecifier(specifier) {
|
|
11
|
+
const segments = specifier.split("/");
|
|
12
|
+
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? specifier;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A specifier Node would resolve through `node_modules` — not a path. Both
|
|
16
|
+
* separators are rejected because esbuild hands plugins Windows paths as
|
|
17
|
+
* written, and `path.isAbsolute` is what catches a drive-letter root.
|
|
18
|
+
*/
|
|
19
|
+
function isBareSpecifier(specifier) {
|
|
20
|
+
return specifier.length > 0 && !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("\\") && !path.isAbsolute(specifier);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The `dependencies` of the package that owns `file`, or `undefined` when the
|
|
24
|
+
* walk reaches the filesystem root without finding a package.
|
|
25
|
+
*
|
|
26
|
+
* **The nearest `package.json` is not necessarily the owner.** Packages
|
|
27
|
+
* publish TYPE-MARKER manifests — `glob/dist/commonjs/package.json` is the
|
|
28
|
+
* two-line `{"type":"commonjs"}` that tells Node how to read the files beside
|
|
29
|
+
* it — and a walk that stops at the first `package.json` it sees stops there,
|
|
30
|
+
* reads no `dependencies`, and concludes the package declares nothing. Every
|
|
31
|
+
* import from such a directory is then judged undeclared: `glob`'s own
|
|
32
|
+
* `require("path-scurry")` was left external and the built app died on it at
|
|
33
|
+
* startup. A manifest with no `name` is not a package, so the walk continues
|
|
34
|
+
* past it.
|
|
35
|
+
*/
|
|
36
|
+
function findOwningDependencies(file) {
|
|
37
|
+
let dir = path.dirname(file);
|
|
38
|
+
for (;;) {
|
|
39
|
+
const manifestPath = path.join(dir, "package.json");
|
|
40
|
+
if (existsSync(manifestPath)) try {
|
|
41
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
42
|
+
if (typeof manifest.name === "string" && manifest.name.length > 0) return new Set(Object.keys(manifest.dependencies ?? {}));
|
|
43
|
+
} catch {}
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir) return void 0;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether `file` is code the APP owns — inside the app root and not inside
|
|
51
|
+
* any `node_modules` below it. A file under `node_modules` is a package's
|
|
52
|
+
* code that happens to sit beneath the app directory, which is the opposite
|
|
53
|
+
* of what this asks.
|
|
54
|
+
*/
|
|
55
|
+
function isAppOwnedFile(file, appRoot) {
|
|
56
|
+
const relative = path.relative(appRoot, file);
|
|
57
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
58
|
+
return !relative.split(/[\\/]/).includes("node_modules");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decide, per import edge, whether a bare specifier stays a bare specifier in
|
|
62
|
+
* the emitted bundle or is bundled into it.
|
|
63
|
+
*
|
|
64
|
+
* **The defect.** The bundler ran `packages: "external"`, so EVERY bare
|
|
65
|
+
* specifier survived verbatim into an artifact that Node then resolves from
|
|
66
|
+
* the artifact's own location — inside the app's `dist/`. That is right for
|
|
67
|
+
* the app's own dependencies and wrong for everything else, because the
|
|
68
|
+
* bundle does not contain only app code: `buildAliasMapFromTsconfig` maps
|
|
69
|
+
* `@warlock.js/*` onto its on-disk SOURCE (mandatory — those packages have
|
|
70
|
+
* no built output in a source checkout, canon `5f684f28`), so the framework's
|
|
71
|
+
* own compiled source is inlined into the app's bundle, and with it the bare
|
|
72
|
+
* `@fastify/http-proxy`, `find-my-way`, `fast-jwt` and `@mongez/*` imports it
|
|
73
|
+
* makes. The app never declared any of those. Under npm/yarn hoisting they
|
|
74
|
+
* resolve by accident; under a strict pnpm tree the built app cannot boot.
|
|
75
|
+
*
|
|
76
|
+
* `assertGeneratedImports` already refuses this for the code the builder
|
|
77
|
+
* GENERATES. This is the same rule for the framework source that generated
|
|
78
|
+
* code pulls in — the half no assertion could reach, because rewriting the
|
|
79
|
+
* import is not an option there: the framework is entitled to its own
|
|
80
|
+
* dependencies.
|
|
81
|
+
*
|
|
82
|
+
* **The rule.** A bare specifier is external — today's behaviour, unchanged —
|
|
83
|
+
* unless BOTH of:
|
|
84
|
+
*
|
|
85
|
+
* 1. the importer is not app-owned code, and
|
|
86
|
+
* 2. the specifier names a package the importer's OWN package declares in
|
|
87
|
+
* `dependencies`.
|
|
88
|
+
*
|
|
89
|
+
* Then it is bundled, and esbuild resolves it from the importer, which is the
|
|
90
|
+
* only resolution that can be correct: `@fastify/ajv-compiler` needs ajv 8
|
|
91
|
+
* while another dependent needs ajv 6, and under pnpm each has its own nested
|
|
92
|
+
* copy. A flat `alias` map cannot express that — it collapses a name to one
|
|
93
|
+
* target for the whole build and produces exactly the "Could not resolve
|
|
94
|
+
* ajv/dist/jtd" class of failure. Resolving per edge is not an optimisation
|
|
95
|
+
* here; it is the difference between working and not.
|
|
96
|
+
*
|
|
97
|
+
* Condition 2 is what keeps canon `5f684f28` intact. An optional peer reached
|
|
98
|
+
* through `await import(...)` — `nodemailer`, `socket.io`, `vite`, the AI
|
|
99
|
+
* SDKs — is declared as a `peerDependency`, never a `dependency`, precisely
|
|
100
|
+
* because it may not be installed at all. Reading only `dependencies` leaves
|
|
101
|
+
* every one of them external without needing a list of their names to
|
|
102
|
+
* maintain, which is the failure mode a hand-written exclusion list has.
|
|
103
|
+
*
|
|
104
|
+
* Transitivity falls out rather than being coded: once `@fastify/http-proxy`
|
|
105
|
+
* is bundled, IT is a non-app-owned importer, so its own `@fastify/reply-from`
|
|
106
|
+
* dependency is judged by the same two conditions and resolved from
|
|
107
|
+
* http-proxy's directory — the nested copy Node itself would have loaded.
|
|
108
|
+
*/
|
|
109
|
+
function bundleFrameworkDependencies(options) {
|
|
110
|
+
const appRoot = path.resolve(options.appRoot);
|
|
111
|
+
const aliasKeys = [...options.aliasKeys];
|
|
112
|
+
function isAliased(specifier) {
|
|
113
|
+
return aliasKeys.some((key) => specifier === key || specifier.startsWith(`${key}/`));
|
|
114
|
+
}
|
|
115
|
+
const dependenciesByDir = /* @__PURE__ */ new Map();
|
|
116
|
+
function declaredDependenciesOf(importer) {
|
|
117
|
+
const dir = path.dirname(importer);
|
|
118
|
+
if (dependenciesByDir.has(dir)) return dependenciesByDir.get(dir);
|
|
119
|
+
const result = findOwningDependencies(importer);
|
|
120
|
+
dependenciesByDir.set(dir, result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
name: "warlock:bundle-framework-dependencies",
|
|
125
|
+
setup(build) {
|
|
126
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
127
|
+
if (args.namespace !== "file" && args.namespace !== "") return void 0;
|
|
128
|
+
const specifier = args.path;
|
|
129
|
+
if (!isBareSpecifier(specifier)) return void 0;
|
|
130
|
+
if (isAliased(specifier)) return void 0;
|
|
131
|
+
if (specifier.startsWith("node:") || isBuiltin(specifier)) return void 0;
|
|
132
|
+
const importer = args.importer;
|
|
133
|
+
if (!importer || !path.isAbsolute(importer)) return {
|
|
134
|
+
path: specifier,
|
|
135
|
+
external: true
|
|
136
|
+
};
|
|
137
|
+
if (isAppOwnedFile(importer, appRoot)) return {
|
|
138
|
+
path: specifier,
|
|
139
|
+
external: true
|
|
140
|
+
};
|
|
141
|
+
if (!declaredDependenciesOf(importer)?.has(packageNameOfSpecifier(specifier))) return {
|
|
142
|
+
path: specifier,
|
|
143
|
+
external: true
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { bundleFrameworkDependencies };
|
|
152
|
+
//# sourceMappingURL=bundle-framework-dependencies.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-framework-dependencies.mjs","names":[],"sources":["../../../../../../../core/src/production/bundle-framework-dependencies.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { isBuiltin } from \"node:module\";\nimport path from \"node:path\";\nimport type { Plugin } from \"esbuild\";\n\n/**\n * The package name a bare specifier addresses, discarding any subpath:\n * `ajv/dist/jtd` → `ajv`, `@fastify/static/lib/x` → `@fastify/static`.\n */\nexport function packageNameOfSpecifier(specifier: string): string {\n const segments = specifier.split(\"/\");\n\n return specifier.startsWith(\"@\") ? segments.slice(0, 2).join(\"/\") : (segments[0] ?? specifier);\n}\n\n/**\n * A specifier Node would resolve through `node_modules` — not a path. Both\n * separators are rejected because esbuild hands plugins Windows paths as\n * written, and `path.isAbsolute` is what catches a drive-letter root.\n */\nexport function isBareSpecifier(specifier: string): boolean {\n return (\n specifier.length > 0 &&\n !specifier.startsWith(\".\") &&\n !specifier.startsWith(\"/\") &&\n !specifier.startsWith(\"\\\\\") &&\n !path.isAbsolute(specifier)\n );\n}\n\n/**\n * The `dependencies` of the package that owns `file`, or `undefined` when the\n * walk reaches the filesystem root without finding a package.\n *\n * **The nearest `package.json` is not necessarily the owner.** Packages\n * publish TYPE-MARKER manifests — `glob/dist/commonjs/package.json` is the\n * two-line `{\"type\":\"commonjs\"}` that tells Node how to read the files beside\n * it — and a walk that stops at the first `package.json` it sees stops there,\n * reads no `dependencies`, and concludes the package declares nothing. Every\n * import from such a directory is then judged undeclared: `glob`'s own\n * `require(\"path-scurry\")` was left external and the built app died on it at\n * startup. A manifest with no `name` is not a package, so the walk continues\n * past it.\n */\nfunction findOwningDependencies(file: string): ReadonlySet<string> | undefined {\n let dir = path.dirname(file);\n\n for (;;) {\n const manifestPath = path.join(dir, \"package.json\");\n\n if (existsSync(manifestPath)) {\n try {\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as {\n name?: string;\n dependencies?: Record<string, string>;\n };\n\n if (typeof manifest.name === \"string\" && manifest.name.length > 0) {\n return new Set(Object.keys(manifest.dependencies ?? {}));\n }\n } catch {\n // An unreadable or malformed manifest is not evidence about\n // anything. Keep walking; the enclosing package may still answer.\n }\n }\n\n const parent = path.dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Whether `file` is code the APP owns — inside the app root and not inside\n * any `node_modules` below it. A file under `node_modules` is a package's\n * code that happens to sit beneath the app directory, which is the opposite\n * of what this asks.\n */\nfunction isAppOwnedFile(file: string, appRoot: string): boolean {\n const relative = path.relative(appRoot, file);\n\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) return false;\n\n return !relative.split(/[\\\\/]/).includes(\"node_modules\");\n}\n\n/**\n * Decide, per import edge, whether a bare specifier stays a bare specifier in\n * the emitted bundle or is bundled into it.\n *\n * **The defect.** The bundler ran `packages: \"external\"`, so EVERY bare\n * specifier survived verbatim into an artifact that Node then resolves from\n * the artifact's own location — inside the app's `dist/`. That is right for\n * the app's own dependencies and wrong for everything else, because the\n * bundle does not contain only app code: `buildAliasMapFromTsconfig` maps\n * `@warlock.js/*` onto its on-disk SOURCE (mandatory — those packages have\n * no built output in a source checkout, canon `5f684f28`), so the framework's\n * own compiled source is inlined into the app's bundle, and with it the bare\n * `@fastify/http-proxy`, `find-my-way`, `fast-jwt` and `@mongez/*` imports it\n * makes. The app never declared any of those. Under npm/yarn hoisting they\n * resolve by accident; under a strict pnpm tree the built app cannot boot.\n *\n * `assertGeneratedImports` already refuses this for the code the builder\n * GENERATES. This is the same rule for the framework source that generated\n * code pulls in — the half no assertion could reach, because rewriting the\n * import is not an option there: the framework is entitled to its own\n * dependencies.\n *\n * **The rule.** A bare specifier is external — today's behaviour, unchanged —\n * unless BOTH of:\n *\n * 1. the importer is not app-owned code, and\n * 2. the specifier names a package the importer's OWN package declares in\n * `dependencies`.\n *\n * Then it is bundled, and esbuild resolves it from the importer, which is the\n * only resolution that can be correct: `@fastify/ajv-compiler` needs ajv 8\n * while another dependent needs ajv 6, and under pnpm each has its own nested\n * copy. A flat `alias` map cannot express that — it collapses a name to one\n * target for the whole build and produces exactly the \"Could not resolve\n * ajv/dist/jtd\" class of failure. Resolving per edge is not an optimisation\n * here; it is the difference between working and not.\n *\n * Condition 2 is what keeps canon `5f684f28` intact. An optional peer reached\n * through `await import(...)` — `nodemailer`, `socket.io`, `vite`, the AI\n * SDKs — is declared as a `peerDependency`, never a `dependency`, precisely\n * because it may not be installed at all. Reading only `dependencies` leaves\n * every one of them external without needing a list of their names to\n * maintain, which is the failure mode a hand-written exclusion list has.\n *\n * Transitivity falls out rather than being coded: once `@fastify/http-proxy`\n * is bundled, IT is a non-app-owned importer, so its own `@fastify/reply-from`\n * dependency is judged by the same two conditions and resolved from\n * http-proxy's directory — the nested copy Node itself would have loaded.\n */\nexport function bundleFrameworkDependencies(options: {\n appRoot: string;\n /**\n * The keys of the esbuild `alias` map this same build is configured with.\n *\n * They MUST be excluded here, and the reason is a trap worth naming: an\n * `onResolve` callback pre-empts esbuild's built-in resolver, and `alias`\n * lives in that resolver. A plugin that answers `external: true` for\n * `@warlock.js/core` therefore beats the alias pointing it at core's\n * source — the framework silently stops being bundled at all, the build\n * still succeeds, and the artifact is a 2.8 KB entry file importing a\n * package the app cannot resolve. Returning `undefined` for these hands\n * them back to the resolver, alias intact.\n *\n * Matched as esbuild matches them: the exact key, or the key followed by a\n * subpath.\n */\n aliasKeys: readonly string[];\n}): Plugin {\n const appRoot = path.resolve(options.appRoot);\n const aliasKeys = [...options.aliasKeys];\n\n function isAliased(specifier: string): boolean {\n return aliasKeys.some((key) => specifier === key || specifier.startsWith(`${key}/`));\n }\n\n // Keyed by the importer's DIRECTORY: the walk and the manifest read are\n // identical for every file in it, and a bundle of this size resolves\n // thousands of edges out of a few hundred directories.\n const dependenciesByDir = new Map<string, ReadonlySet<string> | undefined>();\n\n function declaredDependenciesOf(importer: string): ReadonlySet<string> | undefined {\n const dir = path.dirname(importer);\n if (dependenciesByDir.has(dir)) return dependenciesByDir.get(dir);\n\n const result = findOwningDependencies(importer);\n dependenciesByDir.set(dir, result);\n\n return result;\n }\n\n return {\n name: \"warlock:bundle-framework-dependencies\",\n setup(build) {\n // Everything is inspected and the bare-specifier test is made in code:\n // a `filter` cheap enough to be worth writing cannot tell `C:\\…` (an\n // absolute Windows path, and what the tsconfig aliases resolve to) from\n // a package called `c`. Externalising one of those would be silent and\n // fatal.\n build.onResolve({ filter: /.*/ }, (args) => {\n if (args.namespace !== \"file\" && args.namespace !== \"\") return undefined;\n\n const specifier = args.path;\n\n if (!isBareSpecifier(specifier)) return undefined;\n\n if (isAliased(specifier)) return undefined;\n\n // A builtin is never external in the sense that matters — esbuild\n // already leaves `node:fs` alone and Node always has it.\n if (specifier.startsWith(\"node:\") || isBuiltin(specifier)) return undefined;\n\n const importer = args.importer;\n\n // Nothing on disk to ask. An entry point has no importer at all, and\n // a virtual or generated importer has no package around it — either\n // way there is no manifest that could declare this, so it keeps the\n // behaviour it had before this plugin existed.\n if (!importer || !path.isAbsolute(importer)) {\n return { path: specifier, external: true };\n }\n\n if (isAppOwnedFile(importer, appRoot)) {\n return { path: specifier, external: true };\n }\n\n const declared = declaredDependenciesOf(importer);\n\n if (!declared?.has(packageNameOfSpecifier(specifier))) {\n return { path: specifier, external: true };\n }\n\n // Declared by the package that imports it: let esbuild's own\n // resolver take it, from this importer, and bundle the result.\n return undefined;\n });\n },\n };\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,uBAAuB,WAA2B;CAChE,MAAM,WAAW,UAAU,MAAM,GAAG;CAEpC,OAAO,UAAU,WAAW,GAAG,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAK,SAAS,MAAM;AACtF;;;;;;AAOA,SAAgB,gBAAgB,WAA4B;CAC1D,OACE,UAAU,SAAS,KACnB,CAAC,UAAU,WAAW,GAAG,KACzB,CAAC,UAAU,WAAW,GAAG,KACzB,CAAC,UAAU,WAAW,IAAI,KAC1B,CAAC,KAAK,WAAW,SAAS;AAE9B;;;;;;;;;;;;;;;AAgBA,SAAS,uBAAuB,MAA+C;CAC7E,IAAI,MAAM,KAAK,QAAQ,IAAI;CAE3B,SAAS;EACP,MAAM,eAAe,KAAK,KAAK,KAAK,cAAc;EAElD,IAAI,WAAW,YAAY,GACzB,IAAI;GACF,MAAM,WAAW,KAAK,MAAM,aAAa,cAAc,MAAM,CAAC;GAK9D,IAAI,OAAO,SAAS,SAAS,YAAY,SAAS,KAAK,SAAS,GAC9D,OAAO,IAAI,IAAI,OAAO,KAAK,SAAS,gBAAgB,CAAC,CAAC,CAAC;EAE3D,QAAQ,CAGR;EAGF,MAAM,SAAS,KAAK,QAAQ,GAAG;EAC/B,IAAI,WAAW,KAAK,OAAO;EAC3B,MAAM;CACR;AACF;;;;;;;AAQA,SAAS,eAAe,MAAc,SAA0B;CAC9D,MAAM,WAAW,KAAK,SAAS,SAAS,IAAI;CAE5C,IAAI,SAAS,WAAW,IAAI,KAAK,KAAK,WAAW,QAAQ,GAAG,OAAO;CAEnE,OAAO,CAAC,SAAS,MAAM,OAAO,CAAC,CAAC,SAAS,cAAc;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,4BAA4B,SAkBjC;CACT,MAAM,UAAU,KAAK,QAAQ,QAAQ,OAAO;CAC5C,MAAM,YAAY,CAAC,GAAG,QAAQ,SAAS;CAEvC,SAAS,UAAU,WAA4B;EAC7C,OAAO,UAAU,MAAM,QAAQ,cAAc,OAAO,UAAU,WAAW,GAAG,IAAI,EAAE,CAAC;CACrF;CAKA,MAAM,oCAAoB,IAAI,IAA6C;CAE3E,SAAS,uBAAuB,UAAmD;EACjF,MAAM,MAAM,KAAK,QAAQ,QAAQ;EACjC,IAAI,kBAAkB,IAAI,GAAG,GAAG,OAAO,kBAAkB,IAAI,GAAG;EAEhE,MAAM,SAAS,uBAAuB,QAAQ;EAC9C,kBAAkB,IAAI,KAAK,MAAM;EAEjC,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,MAAM,OAAO;GAMX,MAAM,UAAU,EAAE,QAAQ,KAAK,IAAI,SAAS;IAC1C,IAAI,KAAK,cAAc,UAAU,KAAK,cAAc,IAAI,OAAO;IAE/D,MAAM,YAAY,KAAK;IAEvB,IAAI,CAAC,gBAAgB,SAAS,GAAG,OAAO;IAExC,IAAI,UAAU,SAAS,GAAG,OAAO;IAIjC,IAAI,UAAU,WAAW,OAAO,KAAK,UAAU,SAAS,GAAG,OAAO;IAElE,MAAM,WAAW,KAAK;IAMtB,IAAI,CAAC,YAAY,CAAC,KAAK,WAAW,QAAQ,GACxC,OAAO;KAAE,MAAM;KAAW,UAAU;IAAK;IAG3C,IAAI,eAAe,UAAU,OAAO,GAClC,OAAO;KAAE,MAAM;KAAW,UAAU;IAAK;IAK3C,IAAI,CAFa,uBAAuB,QAE5B,CAAC,EAAE,IAAI,uBAAuB,SAAS,CAAC,GAClD,OAAO;KAAE,MAAM;KAAW,UAAU;IAAK;GAM7C,CAAC;EACH;CACF;AACF"}
|
|
@@ -5,11 +5,13 @@ import { assertUniqueConnectorNames } from "../connectors/assert-unique-connecto
|
|
|
5
5
|
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
6
6
|
import { tsconfigManager } from "../dev-server/tsconfig-manager.mjs";
|
|
7
7
|
import { assertGeneratedImportsAreDeclared } from "./assert-generated-imports.mjs";
|
|
8
|
+
import { bundleFrameworkDependencies } from "./bundle-framework-dependencies.mjs";
|
|
8
9
|
import { dedupe, runEmitContributions, runGenerateContributions } from "./build-contributions.mjs";
|
|
9
10
|
import { writeDistBuildManifestAsync } from "./dist-build-manifest.mjs";
|
|
10
11
|
import { nativeNodeModulesPlugin } from "./esbuild-plugins.mjs";
|
|
11
12
|
import { commitDistAsync, createTempOutputDir, rollbackDistAsync, stageDistAsync } from "./promote-dist.mjs";
|
|
12
13
|
import { resolveBuildConfig } from "./resolve-build-config.mjs";
|
|
14
|
+
import { tsconfigPathAliases } from "./tsconfig-path-aliases.mjs";
|
|
13
15
|
import path from "path";
|
|
14
16
|
import { toCamelCase } from "@mongez/reinforcements";
|
|
15
17
|
import { colors } from "@mongez/copper";
|
|
@@ -425,6 +427,7 @@ bootstrap();
|
|
|
425
427
|
delete this.options.banner;
|
|
426
428
|
await ensureDirectoryAsync(writeOutDir);
|
|
427
429
|
const alias = this.buildAliasMapFromTsconfig();
|
|
430
|
+
const perEdgeExternals = !singleBundle && this.options.packages === void 0;
|
|
428
431
|
const isEsm = (this.options.format ?? "esm") === "esm";
|
|
429
432
|
const banner = mergeBanner(userBanner, esmShim && isEsm ? ESM_INTEROP_SHIM : void 0);
|
|
430
433
|
const { define: contributedDefine, external: contributedExternal, loader: contributedLoader, ...contributedOptions } = this.contributedEsbuild;
|
|
@@ -447,14 +450,21 @@ bootstrap();
|
|
|
447
450
|
entryPoints: [entryPoint],
|
|
448
451
|
bundle: true,
|
|
449
452
|
splitting: !singleBundle,
|
|
450
|
-
packages: singleBundle ? "bundle" : "external",
|
|
453
|
+
packages: singleBundle || perEdgeExternals ? "bundle" : "external",
|
|
451
454
|
minify: this.options.minify,
|
|
452
455
|
sourcemap: this.options.sourcemap === true ? "linked" : this.options.sourcemap,
|
|
453
456
|
format: "esm",
|
|
454
457
|
target: ["node22"],
|
|
455
458
|
entryNames: entryName,
|
|
456
459
|
alias,
|
|
457
|
-
plugins: [
|
|
460
|
+
plugins: [
|
|
461
|
+
nativeNodeModulesPlugin,
|
|
462
|
+
tsconfigPathAliases(this.tsconfigPathAliasConfig()),
|
|
463
|
+
...perEdgeExternals ? [bundleFrameworkDependencies({
|
|
464
|
+
appRoot: rootPath(),
|
|
465
|
+
aliasKeys: Object.keys(alias)
|
|
466
|
+
})] : []
|
|
467
|
+
],
|
|
458
468
|
...contributedOptions,
|
|
459
469
|
...this.options,
|
|
460
470
|
outdir: writeOutDir,
|
|
@@ -469,6 +479,21 @@ bootstrap();
|
|
|
469
479
|
}
|
|
470
480
|
}
|
|
471
481
|
/**
|
|
482
|
+
* The WILDCARD half of the same tsconfig `paths` map that
|
|
483
|
+
* {@link ProductionBuilder.buildAliasMapFromTsconfig} reads the exact half
|
|
484
|
+
* of. `tsconfig-path-aliases.ts` explains why the two halves need different
|
|
485
|
+
* mechanisms — in short, esbuild's `alias` option cannot express a wildcard,
|
|
486
|
+
* so the entries it skips have to be resolved by a plugin instead of being
|
|
487
|
+
* left to no one.
|
|
488
|
+
*/
|
|
489
|
+
tsconfigPathAliasConfig() {
|
|
490
|
+
tsconfigManager.init();
|
|
491
|
+
return {
|
|
492
|
+
baseUrl: path.resolve(process.cwd(), tsconfigManager.baseUrl),
|
|
493
|
+
paths: tsconfigManager.aliases
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
472
497
|
* Build an alias map from tsconfig `paths` so esbuild resolves local
|
|
473
498
|
* source aliases (e.g. `@warlock.js/cascade`) to their on-disk source
|
|
474
499
|
* folders during bundling. Without this, `packages: "external"` would
|
|
@@ -476,7 +501,9 @@ bootstrap();
|
|
|
476
501
|
* resolve them at runtime (they aren't installed in node_modules).
|
|
477
502
|
*
|
|
478
503
|
* Only exact (non-wildcard) aliases are included — esbuild's `alias`
|
|
479
|
-
* option doesn't support glob-style mappings.
|
|
504
|
+
* option doesn't support glob-style mappings. The wildcard half is not
|
|
505
|
+
* dropped, it is handled elsewhere: see
|
|
506
|
+
* {@link ProductionBuilder.tsconfigPathAliasConfig}.
|
|
480
507
|
*/
|
|
481
508
|
buildAliasMapFromTsconfig() {
|
|
482
509
|
tsconfigManager.init();
|