@maily-to/migration 2.0.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.
- package/dist/index.cjs +709 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +707 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
- package/readme.md +50 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["DEFAULT_CONTAINER_WIDTH"],"sources":["../src/utils/border.ts","../src/transforms/button.ts","../src/transforms/column.ts","../src/transforms/columns.ts","../src/transforms/footer.ts","../src/utils/id.ts","../src/transforms/global.ts","../src/transforms/html-code-block.ts","../src/transforms/image.ts","../src/transforms/inline-image.ts","../src/transforms/link.ts","../src/transforms/logo.ts","../src/transforms/repeat.ts","../src/transforms/section.ts","../src/transforms/spacer.ts","../src/utils/container-width.ts","../src/migrate.ts"],"sourcesContent":["/**\n * Expands a single border-radius value into the four individual\n * corner properties used by v2 nodes: borderTopLeftRadius,\n * borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius.\n * In v1, border radius was stored as a single number; v2 stores\n * each corner independently to support non-uniform rounding.\n */\nexport function splitBorderRadius(radius: number) {\n return {\n borderTopLeftRadius: radius,\n borderTopRightRadius: radius,\n borderBottomRightRadius: radius,\n borderBottomLeftRadius: radius,\n };\n}\n\n/**\n * Expands a single border-width value into the four individual\n * side properties used by v2 nodes: borderTopWidth, borderRightWidth,\n * borderBottomWidth, borderLeftWidth. In v1, border width was stored\n * as a single number; v2 stores each side independently to support\n * non-uniform borders.\n */\nexport function splitBorderWidth(width: number) {\n return {\n borderTopWidth: width,\n borderRightWidth: width,\n borderBottomWidth: width,\n borderLeftWidth: width,\n };\n}\n","import type { MigrationWarning } from '../types';\nimport { splitBorderRadius } from '../utils/border';\n\nconst BORDER_RADIUS_MAP: Record<string, number> = {\n sharp: 0,\n smooth: 6,\n round: 9999,\n};\n\n/**\n * Migrates a v1 button node to the v2 schema.\n * Performs the following conversions:\n * - Moves `attrs.text` into `content` as a text node (or a variable\n * node when `isTextVariable` is true).\n * - Renames `buttonColor` → `backgroundColor`, `textColor` → `color`.\n * - Converts `variant: 'outline'` to `backgroundColor: 'transparent'`.\n * - Converts the `borderRadius` string enum (sharp/smooth/round) into\n * four numeric corner properties via splitBorderRadius.\n * - Sets v2 defaults: kind \"tight\", paddingMode \"mixed\",\n * borderRadiusMode/borderWidthMode \"uniform\", borderStyle \"solid\".\n * - Drops deprecated flags: isTextVariable, isUrlVariable, variant.\n */\nexport function button(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Move text → content\n if (!node.content) {\n const text = attrs.text ?? '';\n\n if (attrs.isTextVariable && text) {\n node.content = [\n {\n type: 'variable',\n attrs: { id: text },\n },\n ];\n } else if (text) {\n node.content = [{ type: 'text', text }];\n }\n }\n if ('text' in attrs) {\n warnings.push({\n nodeType: 'button',\n field: 'text',\n message: 'Migrated \"text\" → inline content node',\n });\n delete attrs.text;\n }\n if ('isTextVariable' in attrs) {\n warnings.push({\n nodeType: 'button',\n field: 'isTextVariable',\n message: 'Migrated \"isTextVariable\" → variable content node',\n });\n delete attrs.isTextVariable;\n }\n\n // Rename buttonColor → backgroundColor\n if ('buttonColor' in attrs) {\n warnings.push({\n nodeType: 'button',\n field: 'buttonColor',\n message: 'Migrated \"buttonColor\" → \"backgroundColor\"',\n });\n attrs.backgroundColor = attrs.buttonColor;\n delete attrs.buttonColor;\n }\n\n // Rename textColor → color\n if ('textColor' in attrs) {\n warnings.push({\n nodeType: 'button',\n field: 'textColor',\n message: 'Migrated \"textColor\" → \"color\"',\n });\n attrs.color = attrs.textColor;\n delete attrs.textColor;\n }\n\n // Handle variant: outline → transparent bg, border takes buttonColor\n // In v1, outline buttons had: background=transparent, border=2px solid buttonColor\n if (attrs.variant === 'outline') {\n warnings.push({\n nodeType: 'button',\n field: 'variant',\n message:\n 'Migrated \"variant: outline\" → transparent backgroundColor + border properties',\n });\n attrs.borderColor = attrs.backgroundColor ?? '#000000';\n attrs.borderTopWidth = 2;\n attrs.borderRightWidth = 2;\n attrs.borderBottomWidth = 2;\n attrs.borderLeftWidth = 2;\n attrs.backgroundColor = 'transparent';\n }\n delete attrs.variant;\n\n // Convert borderRadius enum → number → split to 4 corners\n if (typeof attrs.borderRadius === 'string') {\n warnings.push({\n nodeType: 'button',\n field: 'borderRadius',\n message:\n 'Migrated \"borderRadius\" enum → individual corner radius properties',\n });\n const radius = BORDER_RADIUS_MAP[attrs.borderRadius] ?? 9999;\n Object.assign(attrs, splitBorderRadius(radius));\n delete attrs.borderRadius;\n }\n\n // Set defaults\n attrs.kind ??= 'tight';\n attrs.paddingMode ??= 'mixed';\n attrs.borderRadiusMode ??= 'uniform';\n attrs.borderWidthMode ??= 'uniform';\n attrs.borderStyle ??= 'solid';\n attrs.borderColor ??= '#000000';\n attrs.borderTopWidth ??= 0;\n attrs.borderRightWidth ??= 0;\n attrs.borderBottomWidth ??= 0;\n attrs.borderLeftWidth ??= 0;\n\n // Drop with warning\n if ('isUrlVariable' in attrs) {\n warnings.push({\n nodeType: 'button',\n field: 'isUrlVariable',\n message:\n 'Attribute \"isUrlVariable\" is not supported in v2 and was dropped',\n });\n delete attrs.isUrlVariable;\n }\n}\n","import type { MigrationWarning } from '../types';\n\nconst DROPPED_COLUMN_ATTRS = [\n 'columnId',\n 'backgroundColor',\n 'borderRadius',\n 'borderWidth',\n 'borderColor',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n // visibilityRule is the converted form of showIfKey (handled by global transform)\n 'visibilityRule',\n] as const;\n\n/**\n * Migrates a v1 column node to the v2 schema.\n * Performs the following conversions:\n * - Converts `width: 'auto'` → `null` (v2 uses null for equal-space columns).\n * - Converts percentage width strings (e.g. \"50%\") to numbers (e.g. 50).\n * - Drops v1 styling attributes that are not supported on columns in v2:\n * columnId, backgroundColor, borderRadius, borderWidth, borderColor,\n * paddingTop/Right/Bottom/Left, and visibilityRule (converted from showIfKey\n * by the global transform). Each dropped attribute emits a warning.\n */\nexport function column(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Convert width: 'auto' → null, percentage string → number\n if (attrs.width === 'auto') {\n attrs.width = null;\n } else if (typeof attrs.width === 'string') {\n const parsed = parseFloat(attrs.width);\n attrs.width = Number.isNaN(parsed) ? null : parsed;\n }\n\n // Drop unsupported attrs with warnings\n for (const field of DROPPED_COLUMN_ATTRS) {\n if (field in attrs && attrs[field] != null) {\n warnings.push({\n nodeType: 'column',\n field,\n message: `Column attribute \"${field}\" is not supported in v2 and was dropped`,\n });\n delete attrs[field];\n }\n }\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 columns node to the v2 schema.\n * In v2, columns tracks its child count and gap explicitly:\n * - Sets `columnCount` to the number of column children in `content`.\n * - Sets `gap` to the default value of 8 if not already present.\n */\nexport function columns(\n node: Record<string, any>,\n _warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Add columnCount = count of content children\n attrs.columnCount ??= node.content?.length ?? 0;\n\n // Add gap default\n attrs.gap ??= 8;\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 footer node to the v2 schema.\n * Removes the `maily-component` attribute which was used in v1\n * as a marker to identify the footer node. In v2, the node type\n * alone is sufficient for identification.\n */\nexport function footer(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n if ('maily-component' in attrs) {\n warnings.push({\n nodeType: 'footer',\n field: 'maily-component',\n message:\n 'v1 marker attribute \"maily-component\" is not needed in v2 and was dropped',\n });\n delete attrs['maily-component'];\n }\n}\n","/**\n * Generates a unique identifier for a node.\n * Uses the Web Crypto API to produce a v4 UUID string,\n * e.g. \"3b241101-e2bb-4d7a-8613-e4d4e2f1b9c1\".\n */\nexport function uid(): string {\n return crypto.randomUUID();\n}\n","import type { MigrationWarning } from '../types';\nimport { uid } from '../utils/id';\n\n/**\n * Applies transforms common to every node in the document tree.\n * Handles three conversions that apply regardless of node type:\n * 1. showIfKey → visibilityRule: converts the v1 conditional\n * visibility string into a structured v2 VisibilityRule object\n * with action \"show\", operator \"is_true\".\n * 2. textDirection → dir: renames the attribute to match the v2\n * schema (mirrors the HTML `dir` attribute).\n * 3. id generation: assigns a crypto UUID to any node that lacks\n * an id, except for `doc` and `text` nodes which don't use ids.\n */\nexport function global(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n // Text nodes don't have attrs\n if (node.type === 'text') {\n return;\n }\n\n if (!node.attrs) {\n node.attrs = {};\n }\n\n const attrs = node.attrs;\n\n // showIfKey → visibilityRule\n if (attrs.showIfKey) {\n warnings.push({\n nodeType: node.type,\n field: 'showIfKey',\n message: 'Migrated \"showIfKey\" → \"visibilityRule\"',\n });\n attrs.visibilityRule = {\n action: 'show',\n variable: attrs.showIfKey,\n operator: 'is_true',\n value: '',\n };\n }\n delete attrs.showIfKey;\n\n // textDirection → dir\n if ('textDirection' in attrs) {\n if (attrs.textDirection) {\n warnings.push({\n nodeType: node.type,\n field: 'textDirection',\n message: 'Migrated \"textDirection\" → \"dir\"',\n });\n attrs.dir = attrs.textDirection;\n }\n delete attrs.textDirection;\n }\n\n // Generate id if missing (skip doc node)\n if (node.type !== 'doc' && !attrs.id) {\n attrs.id = uid();\n }\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 htmlCodeBlock node to the v2 schema.\n * Strips the `activeTab` attribute which was editor-only UI state\n * persisted into the document JSON.\n */\nexport function htmlCodeBlock(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Drop editor-only UI state with warning\n if ('activeTab' in attrs) {\n warnings.push({\n nodeType: 'htmlCodeBlock',\n field: 'activeTab',\n message:\n 'Editor-only attribute \"activeTab\" is not supported in v2 and was dropped',\n });\n delete attrs.activeTab;\n }\n}\n","import {\n MAX_IMAGE_WIDTH_PERCENTAGE,\n MIN_IMAGE_WIDTH_PERCENTAGE,\n} from '@maily-to/shared';\n\nimport type { MigrationWarning } from '../types';\nimport { splitBorderRadius } from '../utils/border';\n\nconst DEFAULT_CONTAINER_WIDTH = 600;\n\n/**\n * Migrates a v1 image node to the v2 schema.\n * Performs the following conversions:\n * - Renames `alignment` → `align` to match the v2 attribute name.\n * - Splits the single `borderRadius` number into four corner properties.\n * - Converts `width: 'auto'` → `'100%'` (v2 uses percentage strings).\n * - Sets v2 border defaults: borderRadiusMode/borderWidthMode \"uniform\",\n * borderStyle \"solid\", all border widths to 0.\n * - Drops deprecated attrs: height, isSrcVariable, isExternalLinkVariable,\n * lockAspectRatio, aspectRatio.\n */\nexport function image(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // alignment → align\n if ('alignment' in attrs) {\n warnings.push({\n nodeType: 'image',\n field: 'alignment',\n message: 'Migrated \"alignment\" → \"align\"',\n });\n attrs.align = attrs.alignment;\n delete attrs.alignment;\n }\n\n // Split borderRadius (number) → 4 corners\n if (typeof attrs.borderRadius === 'number') {\n warnings.push({\n nodeType: 'image',\n field: 'borderRadius',\n message: 'Migrated \"borderRadius\" → individual corner radius properties',\n });\n Object.assign(attrs, splitBorderRadius(attrs.borderRadius));\n delete attrs.borderRadius;\n }\n\n // Convert width to percentage string\n const container = attrs._containerWidth ?? DEFAULT_CONTAINER_WIDTH;\n delete attrs._containerWidth;\n\n if (attrs.width === 'auto') {\n attrs.width = '100%';\n } else if (\n typeof attrs.width === 'number' ||\n (typeof attrs.width === 'string' && !attrs.width.endsWith('%'))\n ) {\n const px = Number(attrs.width);\n if (!Number.isNaN(px) && px > 0) {\n const percent = Math.min(\n MAX_IMAGE_WIDTH_PERCENTAGE,\n Math.max(MIN_IMAGE_WIDTH_PERCENTAGE, Math.round((px / container) * 100))\n );\n attrs.width = `${percent}%`;\n }\n }\n\n // Set defaults\n attrs.borderRadiusMode ??= 'uniform';\n attrs.borderWidthMode ??= 'uniform';\n attrs.borderStyle ??= 'solid';\n attrs.borderColor ??= '#000000';\n attrs.borderTopWidth ??= 0;\n attrs.borderRightWidth ??= 0;\n attrs.borderBottomWidth ??= 0;\n attrs.borderLeftWidth ??= 0;\n\n // Drop with warnings\n const DROPPED_IMAGE_ATTRS = [\n 'height',\n 'isSrcVariable',\n 'isExternalLinkVariable',\n 'lockAspectRatio',\n 'aspectRatio',\n ] as const;\n for (const field of DROPPED_IMAGE_ATTRS) {\n if (field in attrs) {\n warnings.push({\n nodeType: 'image',\n field,\n message: `Attribute \"${field}\" is not supported in v2 and was dropped`,\n });\n delete attrs[field];\n }\n }\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 inlineImage node to the v2 schema.\n * Performs the following conversions:\n * - Converts `src: null` or `src: undefined` to an empty string,\n * since v2 expects src to always be a string.\n * - Drops deprecated flags: isSrcVariable, isExternalLinkVariable.\n */\nexport function inlineImage(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Convert src: null → ''\n if (attrs.src == null) {\n attrs.src = '';\n }\n\n // Drop with warnings\n const DROPPED_INLINE_IMAGE_ATTRS = [\n 'isSrcVariable',\n 'isExternalLinkVariable',\n ] as const;\n for (const field of DROPPED_INLINE_IMAGE_ATTRS) {\n if (field in attrs) {\n warnings.push({\n nodeType: 'inlineImage',\n field,\n message: `Attribute \"${field}\" is not supported in v2 and was dropped`,\n });\n delete attrs[field];\n }\n }\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 link mark to the v2 schema.\n * Removes the `isUrlVariable` flag from the mark attrs.\n * In v1, this boolean indicated whether the href was a variable\n * reference; v2 handles variable detection differently and no\n * longer needs this flag.\n */\nexport function link(\n mark: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = mark.attrs ?? {};\n\n if ('isUrlVariable' in attrs) {\n warnings.push({\n nodeType: 'link',\n field: 'isUrlVariable',\n message:\n 'Attribute \"isUrlVariable\" is not supported in v2 and was dropped',\n });\n delete attrs.isUrlVariable;\n }\n}\n","import {\n MAX_IMAGE_WIDTH_PERCENTAGE,\n MIN_IMAGE_WIDTH_PERCENTAGE,\n} from '@maily-to/shared';\n\nimport type { MigrationWarning } from '../types';\n\nconst LOGO_SIZE_MAP: Record<string, number> = {\n sm: 40,\n md: 48,\n lg: 64,\n};\n\nconst DEFAULT_CONTAINER_WIDTH = 600;\n\n/**\n * Migrates a v1 logo node by converting it into a v2 image node.\n * The logo node type was removed in v2 — logos are now just images.\n * Performs the following conversions:\n * - Changes `type: 'logo'` → `type: 'image'`.\n * - Maps the `size` enum (sm/md/lg) to a numeric `width` string\n * (40/48/64 pixels respectively).\n * - Renames `alignment` → `align`.\n * - Sets v2 image border defaults (all radii and widths to 0).\n * - Drops deprecated `isSrcVariable` flag.\n */\nexport function logo(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n // Change type: 'logo' → 'image'\n node.type = 'image';\n\n const attrs = node.attrs ?? {};\n\n // Map size → width (percentage)\n if ('size' in attrs) {\n const px = LOGO_SIZE_MAP[attrs.size] ?? 48;\n const container = attrs._containerWidth ?? DEFAULT_CONTAINER_WIDTH;\n const percent = Math.min(\n MAX_IMAGE_WIDTH_PERCENTAGE,\n Math.max(MIN_IMAGE_WIDTH_PERCENTAGE, Math.round((px / container) * 100))\n );\n warnings.push({\n nodeType: 'logo',\n field: 'size',\n message: `Migrated \"size: ${attrs.size}\" → \"width: ${percent}%\"`,\n });\n attrs.width = `${percent}%`;\n delete attrs.size;\n }\n\n // Clean up temporary annotation\n delete attrs._containerWidth;\n\n // alignment → align\n if ('alignment' in attrs) {\n warnings.push({\n nodeType: 'logo',\n field: 'alignment',\n message: 'Migrated \"alignment\" → \"align\"',\n });\n attrs.align = attrs.alignment;\n delete attrs.alignment;\n }\n\n // Set image defaults\n attrs.borderRadiusMode ??= 'uniform';\n attrs.borderWidthMode ??= 'uniform';\n attrs.borderStyle ??= 'solid';\n attrs.borderColor ??= '#000000';\n attrs.borderTopWidth ??= 0;\n attrs.borderRightWidth ??= 0;\n attrs.borderBottomWidth ??= 0;\n attrs.borderLeftWidth ??= 0;\n attrs.borderTopLeftRadius ??= 0;\n attrs.borderTopRightRadius ??= 0;\n attrs.borderBottomRightRadius ??= 0;\n attrs.borderBottomLeftRadius ??= 0;\n\n // Drop with warnings\n const DROPPED_LOGO_ATTRS = ['isSrcVariable', 'maily-component'] as const;\n for (const field of DROPPED_LOGO_ATTRS) {\n if (field in attrs) {\n warnings.push({\n nodeType: 'logo',\n field,\n message: `Attribute \"${field}\" is not supported in v2 and was dropped`,\n });\n delete attrs[field];\n }\n }\n}\n","import type { MigrationWarning } from '../types';\n\n/**\n * Migrates a v1 repeat node to the v2 schema.\n * Wraps the `each` attribute value in Handlebars-style template syntax\n * if it isn't already wrapped. For example, `'items'` becomes `'{{items}}'`.\n * In v2, the each value must be a template expression; v1 allowed\n * bare variable names without delimiters.\n */\nexport function repeat(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Wrap `each` value in template syntax if not already wrapped\n if (\n typeof attrs.each === 'string' &&\n attrs.each &&\n !attrs.each.startsWith('{{')\n ) {\n warnings.push({\n nodeType: 'repeat',\n field: 'each',\n message: `Migrated \"each: ${attrs.each}\" → \"each: {{${attrs.each}}}\"`,\n });\n attrs.each = `{{${attrs.each}}}`;\n }\n\n // Drop editor-only transient state with warning\n if ('isUpdatingKey' in attrs) {\n warnings.push({\n nodeType: 'repeat',\n field: 'isUpdatingKey',\n message:\n 'Editor-only attribute \"isUpdatingKey\" is not supported in v2 and was dropped',\n });\n delete attrs.isUpdatingKey;\n }\n}\n","import type { MigrationWarning } from '../types';\nimport { splitBorderRadius, splitBorderWidth } from '../utils/border';\n\n/**\n * Migrates a v1 section node to the v2 schema.\n * Performs the following conversions:\n * - Splits the single `borderRadius` number into four corner properties.\n * - Splits the single `borderWidth` number into four side properties.\n * - Sets v2 mode defaults: borderRadiusMode/borderWidthMode \"uniform\",\n * paddingMode \"uniform\", marginMode \"mixed\", borderStyle \"solid\".\n */\nexport function section(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n // Split borderRadius (number) → 4 corners\n if (typeof attrs.borderRadius === 'number') {\n warnings.push({\n nodeType: 'section',\n field: 'borderRadius',\n message: 'Migrated \"borderRadius\" → individual corner radius properties',\n });\n Object.assign(attrs, splitBorderRadius(attrs.borderRadius));\n delete attrs.borderRadius;\n }\n\n // Split borderWidth (number) → 4 sides\n if (typeof attrs.borderWidth === 'number') {\n warnings.push({\n nodeType: 'section',\n field: 'borderWidth',\n message: 'Migrated \"borderWidth\" → individual side width properties',\n });\n Object.assign(attrs, splitBorderWidth(attrs.borderWidth));\n delete attrs.borderWidth;\n }\n\n // Set mode defaults\n attrs.borderRadiusMode ??= 'uniform';\n attrs.borderWidthMode ??= 'uniform';\n attrs.paddingMode ??= 'uniform';\n attrs.marginMode ??= 'mixed';\n attrs.borderStyle ??= 'solid';\n attrs.borderColor ??= '#000000';\n}\n","import type { MigrationWarning } from '../types';\n\nconst DEFAULT_SPACER_HEIGHT = 8;\nconst SPACING = [\n {\n name: 'Extra Small',\n short: 'xs',\n value: 4,\n },\n {\n name: 'Small',\n short: 'sm',\n value: 8,\n },\n {\n name: 'Medium',\n short: 'md',\n value: 16,\n },\n {\n name: 'Large',\n short: 'lg',\n value: 32,\n },\n {\n name: 'Extra Large',\n short: 'xl',\n value: 64,\n },\n];\n\nconst ALLOWED_SPACING_SHORT_NAMES = SPACING.map((s) => s.short);\n\n/**\n * Migrates a v1 spacer node to the v2 schema.\n * Sets `heightMode` to \"uniform\" if not already present.\n * In v2, spacers support per-side height control via heightMode;\n * v1 spacers only had a single uniform height value.\n */\nexport function spacer(\n node: Record<string, any>,\n warnings: MigrationWarning[]\n): void {\n const attrs = node.attrs ?? {};\n\n let height = node.attrs?.height;\n if (\n typeof height === 'string' &&\n ALLOWED_SPACING_SHORT_NAMES.includes(height)\n ) {\n const spacing = SPACING.find((s) => s.short === height);\n warnings.push({\n nodeType: 'spacer',\n field: 'height',\n message: `Migrated \"height\" from string enum to number: ${height} → ${spacing?.value}`,\n });\n\n height = spacing?.value ?? DEFAULT_SPACER_HEIGHT;\n }\n\n attrs.height = height;\n attrs.heightMode ??= 'uniform';\n}\n","const ROOT_WIDTH = 600;\nconst DEFAULT_COLUMNS_GAP = 8;\n\n/**\n * Parses a v1 column width attribute.\n * Returns the numeric percentage, or `null` for \"auto\" / unparseable values.\n */\nfunction parseColumnWidth(width: unknown): number | null {\n if (typeof width === 'string') {\n if (width === 'auto') {\n return null;\n }\n\n const parsed = parseFloat(width);\n return Number.isNaN(parsed) ? null : parsed;\n }\n\n if (typeof width === 'number') {\n return width;\n }\n\n return null;\n}\n\n/**\n * Recursively walks a v1 document tree and stamps `_containerWidth` on every\n * `image` and `logo` node. The value reflects the available content width at\n * that position in the tree, accounting for section padding/border and column\n * percentage widths + gap.\n *\n * Must run **before** the transform walker so the transforms can read\n * `_containerWidth` and convert pixel sizes to accurate percentages.\n */\nexport function annotateContainerWidths(\n node: Record<string, any>,\n availableWidth: number = ROOT_WIDTH\n): void {\n if (!node) {\n return;\n }\n\n const attrs = node.attrs ?? {};\n\n // Stamp image and logo nodes\n if (node.type === 'image' || node.type === 'logo') {\n if (!node.attrs) {\n node.attrs = {};\n }\n node.attrs._containerWidth = availableWidth;\n }\n\n // Narrow available width through sections\n let childWidth = availableWidth;\n\n if (node.type === 'section') {\n const paddingLeft =\n typeof attrs.paddingLeft === 'number' ? attrs.paddingLeft : 0;\n const paddingRight =\n typeof attrs.paddingRight === 'number' ? attrs.paddingRight : 0;\n const borderWidth =\n typeof attrs.borderWidth === 'number' ? attrs.borderWidth : 0;\n childWidth = availableWidth - paddingLeft - paddingRight - borderWidth * 2;\n }\n\n // Handle columns → column width narrowing\n if (node.type === 'columns' && Array.isArray(node.content)) {\n const gap = typeof attrs.gap === 'number' ? attrs.gap : DEFAULT_COLUMNS_GAP;\n const columns = node.content;\n const columnCount = columns.length;\n\n // First pass: figure out how much width is explicitly claimed\n let claimedPercent = 0;\n let autoCount = 0;\n\n for (const col of columns) {\n const colWidth = parseColumnWidth(col.attrs?.width);\n if (colWidth !== null) {\n claimedPercent += colWidth;\n } else {\n autoCount++;\n }\n }\n\n // Auto columns split the remaining percentage equally\n const remainingPercent = Math.max(0, 100 - claimedPercent);\n const autoPercent = autoCount > 0 ? remainingPercent / autoCount : 0;\n\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i];\n const colPercent = parseColumnWidth(col.attrs?.width) ?? autoPercent;\n\n // Gap: inner columns get gap/2 on each inner side\n // First column: no left gap, gap/2 right\n // Last column: gap/2 left, no right gap\n // Middle columns: gap/2 on both sides\n let gapDeduction = 0;\n if (columnCount > 1) {\n if (i === 0 || i === columnCount - 1) {\n gapDeduction = gap / 2;\n } else {\n gapDeduction = gap;\n }\n }\n\n const colWidth = childWidth * (colPercent / 100) - gapDeduction;\n annotateContainerWidths(col, Math.max(0, colWidth));\n }\n\n // Don't recurse into content again — we already handled columns' children\n return;\n }\n\n // Recurse into children\n if (Array.isArray(node.content)) {\n for (const child of node.content) {\n annotateContainerWidths(child, childWidth);\n }\n }\n}\n","import { button } from './transforms/button';\nimport { column } from './transforms/column';\nimport { columns } from './transforms/columns';\nimport { footer } from './transforms/footer';\nimport { global } from './transforms/global';\nimport { htmlCodeBlock } from './transforms/html-code-block';\nimport { image } from './transforms/image';\nimport { inlineImage } from './transforms/inline-image';\nimport { link } from './transforms/link';\nimport { logo } from './transforms/logo';\nimport { repeat } from './transforms/repeat';\nimport { section } from './transforms/section';\nimport { spacer } from './transforms/spacer';\nimport type { MigrationResult, MigrationWarning } from './types';\nimport { annotateContainerWidths } from './utils/container-width';\n\ntype NodeTransform = (\n node: Record<string, any>,\n warnings: MigrationWarning[]\n) => void;\n\ntype MarkTransform = (\n mark: Record<string, any>,\n warnings: MigrationWarning[]\n) => void;\n\nconst NODE_TRANSFORMS: Record<string, NodeTransform> = {\n button,\n htmlCodeBlock,\n image,\n logo,\n section,\n columns,\n column,\n spacer,\n repeat,\n footer,\n inlineImage,\n};\n\nconst MARK_TRANSFORMS: Record<string, MarkTransform> = {\n link,\n};\n\n/**\n * Checks whether a Maily JSON document needs migration.\n * Returns `true` when the document has no `version` attribute\n * or when `version` is less than 2, indicating it is a v1 document\n * that should be passed through `migrate` before use.\n */\nexport function requireContentMigration(json: Record<string, any>): boolean {\n return !(json.attrs?.version >= 2);\n}\n\n/**\n * Converts a Maily v1 JSON document to the v2 schema.\n * Deep-clones the input so the original is never mutated, then\n * walks the tree iteratively (stack-based) applying global transforms\n * (showIfKey → visibilityRule, textDirection → dir, id generation)\n * followed by node-specific transforms (button, image, section, etc.)\n * and mark transforms (link).\n *\n * If the document is already v2+ (version >= 2), it is returned as-is\n * with an empty warnings array. Warnings are emitted when v1 attributes\n * are dropped without a v2 equivalent (e.g. column styling attributes).\n */\nexport function migrate(json: Record<string, any>): MigrationResult {\n if (!requireContentMigration(json)) {\n return { json, warnings: [] };\n }\n\n // Deep clone\n const doc = structuredClone(json);\n const warnings: MigrationWarning[] = [];\n\n // Set version\n if (!doc.attrs) {\n doc.attrs = {};\n }\n doc.attrs.version = 2;\n\n // Pre-pass: stamp _containerWidth on image/logo nodes so transforms can\n // compute accurate pixel → percentage conversions.\n annotateContainerWidths(doc);\n\n // Walk tree iteratively (stack-based)\n const stack: Record<string, any>[] = [doc];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n\n // Apply global transforms\n global(node, warnings);\n\n // Apply node-specific transform\n const transform = NODE_TRANSFORMS[node.type];\n if (transform) {\n transform(node, warnings);\n }\n\n // Process marks\n if (Array.isArray(node.marks)) {\n for (const mark of node.marks) {\n const markTransform = MARK_TRANSFORMS[mark.type];\n if (markTransform) {\n markTransform(mark, warnings);\n }\n }\n }\n\n // Push children to stack (reverse order for correct traversal)\n if (Array.isArray(node.content)) {\n for (let i = node.content.length - 1; i >= 0; i--) {\n stack.push(node.content[i]);\n }\n }\n }\n\n return { json: doc, warnings };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maily-to/migration",
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Migration utilities for converting Maily v1 JSON to v2 format",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"email",
|
|
8
|
+
"maily.to",
|
|
9
|
+
"migration"
|
|
10
|
+
],
|
|
11
|
+
"author": "Arik Chakma <arikchangma@gmail.com>",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/arikchakma/maily.to.git",
|
|
15
|
+
"directory": "packages/migration"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.mjs",
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.mjs",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "vp pack",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"dev": "vp pack --watch",
|
|
39
|
+
"release": "bumpp",
|
|
40
|
+
"test": "vp test run"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@maily-to/shared": "workspace:*"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@maily-to/tsconfig": "workspace:*",
|
|
47
|
+
"bumpp": "catalog:",
|
|
48
|
+
"typescript": "catalog:",
|
|
49
|
+
"vite-plus": "catalog:",
|
|
50
|
+
"vitest": "catalog:"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div align="center"><img height="150" src="https://maily.to/brand/icon.svg" /></div>
|
|
2
|
+
<br>
|
|
3
|
+
|
|
4
|
+
<div align="center"><strong>@maily-to/migration</strong></div>
|
|
5
|
+
<div align="center">Migration utilities for converting <a href="https://maily.to">Maily</a> v1 JSON to v2 format.</div>
|
|
6
|
+
<br />
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://github.com/arikchakma/maily.to/blob/main/LICENSE">
|
|
10
|
+
<img src="https://img.shields.io/badge/License-MIT-222222.svg" />
|
|
11
|
+
</a>
|
|
12
|
+
<a href="https://buymeacoffee.com/arikchakma">
|
|
13
|
+
<img src="https://img.shields.io/badge/-buy_me_a%C2%A0coffee-222222?logo=buy-me-a-coffee" alt="Buy me a coffee" />
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<br>
|
|
18
|
+
|
|
19
|
+
### Install
|
|
20
|
+
|
|
21
|
+
Install `@maily-to/migration` from your command line.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpm add @maily-to/migration
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { migrate } from '@maily-to/migration';
|
|
31
|
+
|
|
32
|
+
const v1Doc = {
|
|
33
|
+
type: 'doc',
|
|
34
|
+
content: [
|
|
35
|
+
// ... your v1 Maily JSON
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const v2Doc = migrate(v1Doc);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `migrate` function transforms a Maily v1 document schema into the v2 format, handling all node and attribute changes automatically.
|
|
43
|
+
|
|
44
|
+
### Contributions
|
|
45
|
+
|
|
46
|
+
Feel free to submit pull requests, create issues, or spread the word.
|
|
47
|
+
|
|
48
|
+
### License
|
|
49
|
+
|
|
50
|
+
MIT © [Arik Chakma](https://x.com/imarikchakma)
|