diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdeb754c..b055185da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ ### Fixed - Type `parameterConsumptionMap`, `propRefValue`, and `expressionValue` in the Kiwi `NodeChange` codec, which `fig.kiwi` declares but the TypeScript definitions omitted, so reading them no longer needs a cast. +- Reject malformed effects assigned to `node.effects` in the plugin API with an error naming the invalid field, as Figma does, instead of storing them. `node.effects` now reads back in Figma's shape: layer blurs are `LAYER_BLUR` with `blurType`, and blurs no longer carry shadow fields (#786). - Render fragments (`<>…`) nested inside other elements in JSX from the AI and MCP `render` tool, which previously failed with `Unknown element: <>`. - Judge text contrast in the AI and MCP `describe` tool by its WCAG 2 ratio (4.5:1, or 3:1 for large text), the same ratio the `color-contrast` lint rule computes. It no longer reports passing dark text on mid-tone backgrounds as "dark on dark", now reports low-contrast light text, measures translucent and faded text as it is drawn, skips text whose color is bound to a variable, and says the ratio and the threshold it missed (#735). - Warn about options the paint and effect helpers ignore when rendering JSX instead of dropping them silently, and point `blur` in effect helpers at `radius`, the name Figma uses (#736). diff --git a/bun.lock b/bun.lock index 431a40aab..ba4517c56 100644 --- a/bun.lock +++ b/bun.lock @@ -231,6 +231,7 @@ "@types/opentype.js": "^1.3.10", "@types/rbush": "^4.0.0", "typescript": "~5.8.3", + "unplugin-raw": "^0.7.0", }, }, "packages/design-jsx": { @@ -368,12 +369,8 @@ "packages/pen": { "name": "@open-pencil/pen", "version": "0.15.1", - "dependencies": { - "culori": "^4.0.2", - }, "devDependencies": { "@types/bun": "^1.3.14", - "@types/culori": "^4.0.1", "tsdown": "^0.22.14", "typescript": "~5.8.3", }, diff --git a/packages/core/package.json b/packages/core/package.json index a81ee1c3e..8bbc69cee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -208,6 +208,7 @@ "@types/diff": "^8.0.0", "@types/opentype.js": "^1.3.10", "@types/rbush": "^4.0.0", - "typescript": "~5.8.3" + "typescript": "~5.8.3", + "unplugin-raw": "^0.7.0" } } diff --git a/packages/core/src/figma-api/accessors/visual.ts b/packages/core/src/figma-api/accessors/visual.ts index 81f630ff0..191edf545 100644 --- a/packages/core/src/figma-api/accessors/visual.ts +++ b/packages/core/src/figma-api/accessors/visual.ts @@ -1,6 +1,6 @@ -import type { Effect, Fill, SceneNode, Stroke } from '@open-pencil/scene-graph' +import type { Fill, SceneNode, Stroke } from '@open-pencil/scene-graph' import { normalizeColor } from '@open-pencil/scene-graph/color' -import { copyEffects, copyFills, copyStrokes } from '@open-pencil/scene-graph/copy' +import { copyFills, copyStrokes } from '@open-pencil/scene-graph/copy' import { raw, @@ -8,6 +8,7 @@ import { type NodeProxyInternals, type ProxyThis } from '#core/figma-api/accessor-utils' +import { parseFigmaEffects, toFigmaEffect, type FigmaEffect } from '#core/figma-api/effects' export function installVisualNodeProxyAccessors( prototype: object, @@ -43,13 +44,11 @@ export function installVisualNodeProxyAccessors( } }, effects: { - get(this: ProxyThis): readonly Effect[] { - return Object.freeze(copyEffects(raw(this, internals).effects)) + get(this: ProxyThis): readonly FigmaEffect[] { + return Object.freeze(raw(this, internals).effects.map(toFigmaEffect)) }, - set(this: ProxyThis, value: readonly Effect[]) { - updateNode(this, internals, { - effects: value.map((effect) => ({ ...effect, color: normalizeColor(effect.color) })) - }) + set(this: ProxyThis, value: readonly FigmaEffect[]) { + updateNode(this, internals, { effects: parseFigmaEffects(value) }) } }, opacity: { diff --git a/packages/core/src/figma-api/compatibility.ts b/packages/core/src/figma-api/compatibility.ts index c76d3630b..87b41704d 100644 --- a/packages/core/src/figma-api/compatibility.ts +++ b/packages/core/src/figma-api/compatibility.ts @@ -89,3 +89,7 @@ type InstancePropertySurfaceMatch = Expect< > const _instancePropertySurfaceMatch: InstancePropertySurfaceMatch = true + +// `Effect` here is Figma's plugin-typings union; OpenPencil reads and writes a subset of it. +type EffectShapeMatch = Expect> +const _effectShapeMatch: EffectShapeMatch = true diff --git a/packages/core/src/figma-api/effects.ts b/packages/core/src/figma-api/effects.ts new file mode 100644 index 000000000..7d420268b --- /dev/null +++ b/packages/core/src/figma-api/effects.ts @@ -0,0 +1,169 @@ +import * as v from 'valibot' + +import type { BlendMode, Effect } from '@open-pencil/scene-graph' + +import { TRANSPARENT } from '#core/constants' + +const BLEND_MODE_KEYS = { + NORMAL: true, + DARKEN: true, + MULTIPLY: true, + COLOR_BURN: true, + LIGHTEN: true, + SCREEN: true, + COLOR_DODGE: true, + OVERLAY: true, + SOFT_LIGHT: true, + HARD_LIGHT: true, + DIFFERENCE: true, + EXCLUSION: true, + HUE: true, + SATURATION: true, + COLOR: true, + LUMINOSITY: true, + PASS_THROUGH: true +} satisfies Record + +const BLEND_MODES = Object.keys(BLEND_MODE_KEYS) as BlendMode[] + +/** Effect kinds Figma's plugin API accepts that OpenPencil does not model yet. */ +const UNSUPPORTED_EFFECT_TYPES = new Set(['NOISE', 'TEXTURE', 'GLASS', 'SHADER']) + +// Figma rejects Infinity as well as NaN ("Number must be finite"). +const finite = v.pipe(v.number(), v.finite()) +const unit = v.pipe(finite, v.minValue(0), v.maxValue(1)) +const nonNegative = v.pipe(finite, v.minValue(0)) + +const color = v.strictObject({ r: unit, g: unit, b: unit, a: unit }) +const vector = v.strictObject({ x: finite, y: finite }) +// Variable bindings on effects are not supported; Figma reports an empty object. +const boundVariables = v.optional(v.strictObject({})) + +const shadowEntries = { + color, + offset: vector, + radius: nonNegative, + spread: v.optional(finite), + visible: v.boolean(), + blendMode: v.picklist(BLEND_MODES), + boundVariables +} + +const effectSchema = v.variant('type', [ + v.strictObject({ + type: v.literal('DROP_SHADOW'), + ...shadowEntries, + showShadowBehindNode: v.optional(v.boolean()) + }), + v.strictObject({ type: v.literal('INNER_SHADOW'), ...shadowEntries }), + v.strictObject({ + type: v.picklist(['LAYER_BLUR', 'BACKGROUND_BLUR']), + radius: nonNegative, + visible: v.boolean(), + blurType: v.optional(v.literal('NORMAL'), 'NORMAL'), + boundVariables + }) +]) + +const effectsSchema = v.array(effectSchema) + +/** An effect as Figma's plugin API reads and writes it (`Effect` in `@figma/plugin-typings`). */ +export type FigmaEffect = v.InferOutput + +function issuePath(issue: v.BaseIssue): string { + return (issue.path ?? []) + .map((item) => (typeof item.key === 'number' ? `[${item.key}]` : `.${String(item.key)}`)) + .join('') +} + +function unsupportedEffect(value: unknown): string | null { + if (!Array.isArray(value)) return null + for (const [index, effect] of value.entries()) { + const type: unknown = v.is(v.object({ type: v.unknown() }), effect) ? effect.type : undefined + if (typeof type === 'string' && UNSUPPORTED_EFFECT_TYPES.has(type)) { + return `${type} effects are not supported at [${index}].type` + } + if (v.is(v.object({ blurType: v.literal('PROGRESSIVE') }), effect)) { + return `Progressive blur is not supported at [${index}].blurType` + } + } + return null +} + +/** + * Check `value` against Figma's effect shapes and convert it to scene effects. + * Throws with the first problem, as Figma's `effects` setter does. + */ +export function parseFigmaEffects(value: unknown): Effect[] { + const unsupported = unsupportedEffect(value) + if (unsupported) throw new Error(`Property "effects" failed validation: ${unsupported}`) + const result = v.safeParse(effectsSchema, value) + if (!result.success) { + const [issue] = result.issues + throw new Error( + `Property "effects" failed validation: ${issue.message} at ${issuePath(issue) || 'effects'}` + ) + } + return result.output.map(toSceneEffect) +} + +function toSceneEffect(effect: FigmaEffect): Effect { + if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') { + return { + type: effect.type, + color: { ...effect.color }, + offset: { ...effect.offset }, + radius: effect.radius, + spread: effect.spread ?? 0, + visible: effect.visible, + // Figma accepts PASS_THROUGH on shadows but stores NORMAL; it is a layer blend mode. + blendMode: effect.blendMode === 'PASS_THROUGH' ? 'NORMAL' : effect.blendMode, + ...(effect.type === 'DROP_SHADOW' && effect.showShadowBehindNode !== undefined + ? { showShadowBehindNode: effect.showShadowBehindNode } + : {}) + } + } + return { + type: effect.type, + color: { ...TRANSPARENT }, + offset: { x: 0, y: 0 }, + radius: effect.radius, + spread: 0, + visible: effect.visible + } +} + +/** A scene effect in the shape Figma's `effects` getter returns. */ +export function toFigmaEffect(effect: Effect): FigmaEffect { + if ( + effect.type === 'LAYER_BLUR' || + effect.type === 'FOREGROUND_BLUR' || + effect.type === 'BACKGROUND_BLUR' + ) { + return { + // `.fig` files call a layer blur FOREGROUND_BLUR; the plugin API calls it LAYER_BLUR. + type: effect.type === 'BACKGROUND_BLUR' ? 'BACKGROUND_BLUR' : 'LAYER_BLUR', + visible: effect.visible, + radius: effect.radius, + boundVariables: {}, + blurType: 'NORMAL' + } + } + const shadow = { + visible: effect.visible, + radius: effect.radius, + boundVariables: {}, + color: { ...effect.color }, + offset: { ...effect.offset }, + spread: effect.spread, + blendMode: effect.blendMode ?? 'NORMAL' + } + return effect.type === 'DROP_SHADOW' + ? { + type: 'DROP_SHADOW', + ...shadow, + // The renderer draws the shadow behind the node unless this is `false`. + showShadowBehindNode: effect.showShadowBehindNode ?? true + } + : { type: 'INNER_SHADOW', ...shadow } +} diff --git a/packages/core/src/figma-api/index.ts b/packages/core/src/figma-api/index.ts index 1fada27cc..8f17a5327 100644 --- a/packages/core/src/figma-api/index.ts +++ b/packages/core/src/figma-api/index.ts @@ -50,6 +50,7 @@ import { const noop = () => undefined export { FigmaNodeProxy } from './proxy' +export type { FigmaEffect } from './effects' export type { FigmaBooleanOperationNode, FigmaComponentNode, diff --git a/packages/core/src/figma-api/proxy.ts b/packages/core/src/figma-api/proxy.ts index 55208cd44..379cf755b 100644 --- a/packages/core/src/figma-api/proxy.ts +++ b/packages/core/src/figma-api/proxy.ts @@ -5,7 +5,6 @@ import type { NodeType, Fill, Stroke, - Effect, LayoutMode } from '@open-pencil/scene-graph' import { @@ -18,6 +17,7 @@ import type { OkHCLColor, OkHCLPayload } from '@open-pencil/scene-graph/color' import type { Rect } from '@open-pencil/scene-graph/primitives' import { assertNodeEditable } from '#core/editor/capabilities' +import type { FigmaEffect } from '#core/figma-api/effects' import { installBasicNodeProxyAccessors } from './accessors/basic' import { installLayoutNodeProxyAccessors } from './accessors/layout' @@ -78,7 +78,7 @@ export class FigmaNodeProxy { declare fills: readonly Fill[] declare strokes: readonly Stroke[] - declare effects: readonly Effect[] + declare effects: readonly FigmaEffect[] declare opacity: number declare visible: boolean declare locked: boolean diff --git a/packages/core/src/global.d.ts b/packages/core/src/global.d.ts index 023d46824..239b55450 100644 --- a/packages/core/src/global.d.ts +++ b/packages/core/src/global.d.ts @@ -10,11 +10,6 @@ interface Window { queryLocalFonts?(): Promise } -declare module '*.md' { - const content: string - export default content -} - declare module '*?raw' { const content: string export default content diff --git a/packages/core/src/tools/modify/effects.ts b/packages/core/src/tools/modify/effects.ts index 7d464aa90..18eed091c 100644 --- a/packages/core/src/tools/modify/effects.ts +++ b/packages/core/src/tools/modify/effects.ts @@ -1,9 +1,9 @@ import * as v from 'valibot' -import type { Effect } from '@open-pencil/scene-graph' import { parseColor } from '@open-pencil/scene-graph/color' -import { DEFAULT_SHADOW_COLOR, TRANSPARENT } from '#core/constants' +import { DEFAULT_SHADOW_COLOR } from '#core/constants' +import type { FigmaEffect } from '#core/figma-api/effects' import { toolNumber, nodeIdInput } from '#core/tools/input' import { defineTool, nodeNotFound } from '#core/tools/schema' @@ -32,18 +32,23 @@ export const setEffects = defineTool({ const node = figma.getNodeById(args.id) if (!node) return nodeNotFound(args.id) - const isBlur = args.type === 'FOREGROUND_BLUR' || args.type === 'BACKGROUND_BLUR' - let color = { ...DEFAULT_SHADOW_COLOR } - if (isBlur) color = { ...TRANSPARENT } - else if (args.color) color = parseColor(args.color) - const effect: Effect = { - type: args.type as Effect['type'], - visible: true, - radius: args.radius, - color, - offset: { x: isBlur ? 0 : args.offset_x, y: isBlur ? 0 : args.offset_y }, - spread: isBlur ? 0 : args.spread - } + const effect: FigmaEffect = + args.type === 'FOREGROUND_BLUR' || args.type === 'BACKGROUND_BLUR' + ? { + type: args.type === 'BACKGROUND_BLUR' ? 'BACKGROUND_BLUR' : 'LAYER_BLUR', + radius: args.radius, + visible: true, + blurType: 'NORMAL' + } + : { + type: args.type, + color: args.color ? parseColor(args.color) : { ...DEFAULT_SHADOW_COLOR }, + offset: { x: args.offset_x, y: args.offset_y }, + radius: args.radius, + spread: args.spread, + visible: true, + blendMode: 'NORMAL' + } node.effects = [...node.effects, effect] return { id: args.id, effects: node.effects.length } diff --git a/packages/core/tests/figma-api/effects.test.ts b/packages/core/tests/figma-api/effects.test.ts new file mode 100644 index 000000000..104ca79c4 --- /dev/null +++ b/packages/core/tests/figma-api/effects.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from 'bun:test' + +import { FigmaAPI } from '@open-pencil/core/figma-api' +import { SceneGraph } from '@open-pencil/scene-graph' + +// Accepted shapes, read-back values, and rejections were recorded by running the +// same assignments against a rectangle in live Figma through figma-use. + +const color = { r: 0, g: 0, b: 0, a: 0.25 } +const shadow = { + type: 'DROP_SHADOW', + color, + offset: { x: 0, y: 4 }, + radius: 8, + visible: true, + blendMode: 'NORMAL' +} as const + +function createRectangle(graph = new SceneGraph()) { + return new FigmaAPI(graph).createRectangle() +} + +function assign(value: unknown) { + const rect = createRectangle() + // Scripts are untyped; the setter must check what it receives at runtime. + Reflect.set(rect, 'effects', value) + return rect +} + +describe('node.effects', () => { + test('reads a drop shadow back with Figma defaults', () => { + expect(assign([shadow]).effects).toEqual([ + { + type: 'DROP_SHADOW', + visible: true, + radius: 8, + boundVariables: {}, + color, + offset: { x: 0, y: 4 }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: true + } + ]) + }) + + test('keeps spread and showShadowBehindNode', () => { + const [effect] = assign([{ ...shadow, spread: 2, showShadowBehindNode: false }]).effects + expect(effect).toMatchObject({ spread: 2, showShadowBehindNode: false }) + }) + + test('stores PASS_THROUGH on shadows as NORMAL', () => { + for (const type of ['DROP_SHADOW', 'INNER_SHADOW'] as const) { + const [effect] = assign([{ ...shadow, type, blendMode: 'PASS_THROUGH' }]).effects + expect(effect).toMatchObject({ type, blendMode: 'NORMAL' }) + } + }) + + test('reads blurs back with blurType and without shadow fields', () => { + expect(assign([{ type: 'LAYER_BLUR', radius: 4, visible: true }]).effects).toEqual([ + { type: 'LAYER_BLUR', visible: true, radius: 4, boundVariables: {}, blurType: 'NORMAL' } + ]) + expect( + assign([{ type: 'BACKGROUND_BLUR', blurType: 'NORMAL', radius: 4, visible: true }]).effects + ).toEqual([ + { type: 'BACKGROUND_BLUR', visible: true, radius: 4, boundVariables: {}, blurType: 'NORMAL' } + ]) + }) + + test('reads a .fig foreground blur as a layer blur', () => { + const graph = new SceneGraph() + const rect = createRectangle(graph) + graph.updateNode(rect.id, { + effects: [ + { + type: 'FOREGROUND_BLUR', + color: { r: 0, g: 0, b: 0, a: 0 }, + offset: { x: 0, y: 0 }, + radius: 6, + spread: 0, + visible: true + } + ] + }) + expect(rect.effects[0]).toMatchObject({ type: 'LAYER_BLUR', radius: 6 }) + }) + + test('accepts its own effects back', () => { + const rect = assign([shadow, { type: 'LAYER_BLUR', radius: 4, visible: true }]) + const before = rect.effects + rect.effects = rect.effects + expect(rect.effects).toEqual(before) + }) + + test.each([ + ['missing required fields', [{ type: 'DROP_SHADOW', blur: 12 }]], + ['a negative radius', [{ ...shadow, radius: -5 }]], + ['a NaN radius', [{ ...shadow, radius: Number.NaN }]], + ['an infinite radius', [{ ...shadow, radius: Number.POSITIVE_INFINITY }]], + ['an infinite offset', [{ ...shadow, offset: { x: Number.POSITIVE_INFINITY, y: 0 } }]], + ['an infinite spread', [{ ...shadow, spread: Number.NEGATIVE_INFINITY }]], + ['an infinite blur radius', [{ type: 'LAYER_BLUR', radius: Number.POSITIVE_INFINITY, visible: true }]], + ['an unknown type', [{ type: 'NOT_AN_EFFECT' }]], + ['a foreground blur', [{ type: 'FOREGROUND_BLUR', radius: 4, visible: true }]], + ['an unknown key', [{ ...shadow, foo: 1 }]], + ['a shadow without blendMode', [{ ...shadow, blendMode: undefined }]], + ['an unknown blendMode', [{ ...shadow, blendMode: 'NOPE' }]], + ['showShadowBehindNode on an inner shadow', [{ ...shadow, type: 'INNER_SHADOW', showShadowBehindNode: true }]], + ['shadow fields on a blur', [{ type: 'LAYER_BLUR', radius: 4, visible: true, color, offset: { x: 0, y: 0 }, spread: 0 }]], + ['a color without alpha', [{ ...shadow, color: { r: 0, g: 0, b: 0 } }]], + ['a color channel above 1', [{ ...shadow, color: { r: 2, g: 0, b: 0, a: 1 } }]], + ['an incomplete offset', [{ ...shadow, offset: { x: 0 } }]], + ['a single effect instead of an array', shadow] + ])('rejects %s without changing the node', (_name, value) => { + const rect = assign([shadow]) + const before = rect.effects + expect(() => Reflect.set(rect, 'effects', value)).toThrow('Property "effects" failed validation') + expect(rect.effects).toEqual(before) + }) + + test('names the invalid field', () => { + expect(() => assign([{ ...shadow, radius: -5 }])).toThrow('at [0].radius') + expect(() => assign([shadow, { ...shadow, foo: 1 }])).toThrow('[1]') + }) + + test('reports Figma effects OpenPencil does not model yet', () => { + expect(() => assign([{ type: 'NOISE' }])).toThrow('NOISE effects are not supported') + expect(() => + assign([{ type: 'LAYER_BLUR', blurType: 'PROGRESSIVE', radius: 4, visible: true }]) + ).toThrow('Progressive blur is not supported') + }) +}) diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 4e3b91627..a479762a8 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -1,32 +1,16 @@ import { readFileSync } from 'node:fs' import { defineConfig } from 'tsdown' -import type { Rolldown } from 'tsdown' +import raw from 'unplugin-raw/rolldown' const packageJSON = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { dependencies?: Record } -function rawText(): Rolldown.Plugin { - return { - name: 'raw-text', - load(id) { - if (id.endsWith('?raw')) { - const path = id.slice(0, -'?raw'.length) - return `export default ${JSON.stringify(readFileSync(path, 'utf8'))}` - } - }, - transform(code, id) { - if (id.endsWith('.md')) { - return { code: `export default ${JSON.stringify(code)}`, map: null } - } - } - } -} - export default defineConfig({ entry: ['src/**/*.ts', '!src/**/*.d.ts'], - plugins: [rawText()], + // Prompts and the authoring reference import Markdown as text. + plugins: [raw()], unbundle: true, platform: 'neutral', format: ['esm'], diff --git a/packages/pen/package.json b/packages/pen/package.json index 655eee580..9efdf759c 100644 --- a/packages/pen/package.json +++ b/packages/pen/package.json @@ -38,12 +38,8 @@ "peerDependencies": { "@open-pencil/scene-graph": "workspace:*" }, - "dependencies": { - "culori": "^4.0.2" - }, "devDependencies": { "@types/bun": "^1.3.14", - "@types/culori": "^4.0.1", "tsdown": "^0.22.14", "typescript": "~5.8.3" } diff --git a/packages/pen/src/color.ts b/packages/pen/src/color.ts deleted file mode 100644 index d1c6b1632..000000000 --- a/packages/pen/src/color.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { converter, parse } from 'culori' - -import { BLACK } from '@open-pencil/scene-graph/constants' -import type { Color } from '@open-pencil/scene-graph/primitives' - -const rgbConverter = converter('rgb') - -export function parseColor(input: string): Color { - const parsedColor = parse(input) - const rgbColor = parsedColor ? rgbConverter(parsedColor) : null - - return rgbColor - ? { - r: rgbColor.r, - g: rgbColor.g, - b: rgbColor.b, - a: parsedColor?.alpha ?? 1 - } - : structuredClone(BLACK) -} diff --git a/packages/pen/src/convert.ts b/packages/pen/src/convert.ts index bd5f0127f..b228c927b 100644 --- a/packages/pen/src/convert.ts +++ b/packages/pen/src/convert.ts @@ -20,11 +20,10 @@ import type { VariableType, VariableValue } from '@open-pencil/scene-graph' +import { parseColor } from '@open-pencil/scene-graph/color' import { BLACK } from '@open-pencil/scene-graph/constants' import type { Vector } from '@open-pencil/scene-graph/primitives' -import { parseColor } from './color' - export interface PenDocument { version: string children: PenNode[] diff --git a/tests/engine/tools/cli.test.ts b/tests/engine/tools/cli.test.ts index cddc5e4d1..2c3352a3b 100644 --- a/tests/engine/tools/cli.test.ts +++ b/tests/engine/tools/cli.test.ts @@ -186,7 +186,8 @@ heavy('CLI tool operations via eval', () => { offset: { x: 0, y: 4 }, radius: 8, spread: 0, - visible: true + visible: true, + blendMode: 'NORMAL' }] return { count: f.effects.length, type: f.effects[0].type } `) diff --git a/tools/tsconfig.json b/tools/tsconfig.json index fddc1ace8..57ed1fa35 100644 --- a/tools/tsconfig.json +++ b/tools/tsconfig.json @@ -9,6 +9,7 @@ "*/*/src/**/*.ts", "*/*/tests/**/*.ts", "../src/global.d.ts", + "../src/markdown.d.ts", "../packages/core/src/global.d.ts", "../packages/vue/src/global.d.ts" ]