diff --git a/packages/ui/src/components/MarkdownRenderer.svelte b/packages/ui/src/components/MarkdownRenderer.svelte index 79a30f0..0dca3d1 100644 --- a/packages/ui/src/components/MarkdownRenderer.svelte +++ b/packages/ui/src/components/MarkdownRenderer.svelte @@ -1,19 +1,6 @@ unsafe link', }); - await tick(); + await waitForMarkdown(container); expect(container.querySelector('script')).toBeNull(); expect(container.querySelector('[onerror]')).toBeNull(); @@ -30,7 +39,9 @@ describe('MarkdownRenderer security and enhancement', () => { const { container } = render(MarkdownRenderer, { content: '```js\nconst answer = 42;\n```', }); - await tick(); + await waitFor(() => { + expect(container.querySelector('.code-block-wrapper')).not.toBeNull(); + }); const wrapper = container.querySelector('.code-block-wrapper'); const code = wrapper?.querySelector('code'); @@ -55,7 +66,7 @@ describe('MarkdownRenderer security and enhancement', () => { await rerender({ content: '```">\nunsafe\n```', }); - await tick(); + await waitForMarkdown(container); const header = container.querySelector('.code-block-wrapper > div'); expect(header).not.toBeNull(); @@ -80,10 +91,11 @@ describe('MarkdownRenderer security and enhancement', () => { originalAddEventListener.call(this, type, listener, options); }); - const { rerender } = render(MarkdownRenderer, { content: '```text\none\n```' }); + const { container, rerender } = render(MarkdownRenderer, { content: '```text\none\n```' }); + await waitForMarkdown(container); await rerender({ content: '```text\ntwo\n```' }); await rerender({ content: '```text\nthree\n```' }); - await tick(); + await waitForMarkdown(container); expect(delegatedClickBindings).toBe(1); }); diff --git a/scripts/check-package-packs.ts b/scripts/check-package-packs.ts index 1cfb7db..d475150 100644 --- a/scripts/check-package-packs.ts +++ b/scripts/check-package-packs.ts @@ -31,6 +31,7 @@ interface PackageManifest { bin?: string | Record; exports?: unknown; dependencies?: Record; + devDependencies?: Record; peerDependencies?: Record; } @@ -43,6 +44,12 @@ const repositoryRoot = resolve(import.meta.dir, '..'); const packagesRoot = join(repositoryRoot, 'packages'); const tscPath = join(repositoryRoot, 'node_modules', 'typescript', 'bin', 'tsc'); const pnpmVersion = '11.11.0'; +const optionalMarkdownPeers = [ + 'highlight.js', + 'isomorphic-dompurify', + 'marked', + 'marked-highlight', +] as const; const expectations: PackageExpectation[] = [ { @@ -291,9 +298,16 @@ async function verifyUiPnpmPeerTree( await readFile(join(repositoryRoot, 'package.json'), 'utf8'), ) as { overrides?: Record }; const svelteVersion = rootManifest.overrides?.svelte; + const viteVersion = rootManifest.overrides?.vite; const queryVersion = uiManifest.peerDependencies?.['@tanstack/svelte-query']; + const sveltePluginVersion = uiManifest.devDependencies?.['@sveltejs/vite-plugin-svelte']; assert(svelteVersion, 'root package.json: overrides.svelte is required for pnpm verification'); + assert(viteVersion, 'root package.json: overrides.vite is required for pnpm verification'); assert(queryVersion, '@svadmin/ui: @tanstack/svelte-query peer range is required for pnpm verification'); + assert( + sveltePluginVersion, + '@svadmin/ui: @sveltejs/vite-plugin-svelte dev dependency is required for pnpm verification', + ); const consumerDirectory = join(packDirectory, 'pnpm-peer-consumer'); await mkdir(consumerDirectory, { recursive: true }); @@ -309,6 +323,10 @@ async function verifyUiPnpmPeerTree( '@tanstack/svelte-query': queryVersion, svelte: svelteVersion, }, + devDependencies: { + '@sveltejs/vite-plugin-svelte': sveltePluginVersion, + vite: viteVersion, + }, }, null, 2)}\n`, ); @@ -352,6 +370,12 @@ async function verifyUiPnpmPeerTree( ); const virtualStoreEntries = await readdir(join(consumerDirectory, 'node_modules', '.pnpm')); + for (const optionalPeer of optionalMarkdownPeers) { + assert( + !virtualStoreEntries.some((entry) => entry.startsWith(`${optionalPeer}@`)), + `@svadmin/ui: pnpm strict consumer unexpectedly installed optional peer ${optionalPeer}`, + ); + } const svelteVersions = new Set( virtualStoreEntries .map((entry) => /^svelte@([^_]+)(?:_|$)/.exec(entry)?.[1]) @@ -372,7 +396,25 @@ async function verifyUiPnpmPeerTree( ['--yes', `pnpm@${pnpmVersion}`, 'list', 'svelte', '--depth', 'Infinity'], consumerDirectory, ); - return `pnpm@${pnpmVersion} strict packed consumer passed\nforbidden dependencies absent: cmdk-sv, sonner-svelte, @melt-ui/svelte\n${dependencyTree.trim()}\nresolved Svelte versions: ${resolvedSvelteVersion}`; + + const consumerEntry = join(consumerDirectory, 'markdown-import.ts'); + await writeFile( + consumerEntry, + `import { MarkdownRenderer } from '@svadmin/ui';\nconsole.info(typeof MarkdownRenderer);\n`, + ); + const viteConfig = join(consumerDirectory, 'vite.config.mjs'); + await writeFile( + viteConfig, + `import { svelte } from '@sveltejs/vite-plugin-svelte';\nexport default { root: ${JSON.stringify(consumerDirectory)}, plugins: [svelte()], build: { lib: { entry: ${JSON.stringify(consumerEntry)}, formats: ['es'] } } };\n`, + ); + const vitePath = join(repositoryRoot, 'node_modules', 'vite', 'bin', 'vite.js'); + const optionalPeerBuild = run( + 'node', + [vitePath, 'build', '--config', viteConfig], + consumerDirectory, + ); + + return `pnpm@${pnpmVersion} strict packed consumer passed\nforbidden dependencies absent: cmdk-sv, sonner-svelte, @melt-ui/svelte\noptional markdown peers absent: ${optionalMarkdownPeers.join(', ')}\n${optionalPeerBuild.trim()}\n${dependencyTree.trim()}\nresolved Svelte versions: ${resolvedSvelteVersion}`; } async function createConsumer(packDirectory: string, results: Map): Promise {