ReviewOS

also looking at this

craft-native/craft

fix(svelte): expose public store declaration types

#310
Merged glennmichael123 wants to merge codex/svelte-public-store-types into main
3 files +58 -2
packages/svelte/src/stores/craft.tsmodified+2-1
Changes to packages/svelte/src/stores/craft.ts
@@ -1,4 +1,5 @@
11import { writable, derived } from 'svelte/store';
2import type { Readable } from 'svelte/store';
23
34interface CraftAPI {
45 getPlatform(): Promise<{ platform: string; version: string }>;
@@ -18,7 +19,7 @@ declare global {
1819 }
1920}
2021
21function createCraftStore() {
22function createCraftStore(): Readable<CraftAPI | null> {
2223 const { subscribe, set } = writable<CraftAPI | null>(null);
2324
2425 if (typeof window !== 'undefined') {
packages/svelte/src/stores/platform.tsmodified+2-1
Changes to packages/svelte/src/stores/platform.ts
@@ -1,7 +1,8 @@
11import { writable } from 'svelte/store';
2import type { Readable } from 'svelte/store';
23import { craft } from './craft';
34
4export function createPlatformStore() {
5export function createPlatformStore(): Readable<{ platform: string; version: string } | null> {
56 const { subscribe, set } = writable<{ platform: string; version: string } | null>(null);
67
78 craft.subscribe(async (_$craft) => {
scripts/svelte-package.test.tsmodified+54-0
Changes to scripts/svelte-package.test.ts
@@ -31,3 +31,57 @@ test('packed Svelte condition resolves outside the workspace', async () => {
3131 rmSync(temp, { recursive: true, force: true })
3232 }
3333})
34
35test('store declarations expose the public Readable contract', () => {
36 const root = resolve(import.meta.dir, '..')
37 const temp = mkdtempSync(join(tmpdir(), 'craft-svelte-types-'))
38 const run = (args: string[], cwd = temp): void => {
39 const result = Bun.spawnSync([
40 process.execPath, join(root, 'packages/typescript/scripts/tsc.ts'), ...args,
41 ], { cwd, stdout: 'pipe', stderr: 'pipe' })
42 expect(`${result.stdout}\n${result.stderr}`).not.toContain('error TS')
43 expect(result.exitCode).toBe(0)
44 }
45 try {
46 run([
47 '-p', join(root, 'packages/svelte/tsconfig.json'), '--emitDeclarationOnly',
48 '--outDir', join(temp, 'declarations'),
49 ])
50 // Consumers only need Svelte's public store interface. Inferred subscribe
51 // signatures must not leak helper exports from the build-time peer.
52 writeFileSync(join(temp, 'store-contract.d.ts'), `
53declare module 'svelte/store' {
54 export interface Readable<T> {
55 subscribe(run: (value: T) => void, invalidate?: (value?: T) => void): () => void
56 }
57}
58`)
59 writeFileSync(join(temp, 'consumer.mts'), `
60import type { Readable } from 'svelte/store'
61import { craft, isReady } from './declarations/stores/craft.js'
62import { platform, createPlatformStore } from './declarations/stores/platform.js'
63const ready: Readable<boolean> = isReady
64const info: Readable<{ platform: string; version: string } | null> = platform
65const another: typeof info = createPlatformStore()
66const unsubscribe: () => void = craft.subscribe(api => {
67 if (api) {
68 const result: Promise<{ platform: string; version: string }> = api.getPlatform()
69 void result
70 }
71})
72// @ts-expect-error the Craft store is read-only to consumers
73craft.set(null)
74// @ts-expect-error readiness remains boolean
75const wrong: Readable<string> = isReady
76void [ready, info, another, unsubscribe, wrong]
77`)
78 run([
79 'consumer.mts', 'store-contract.d.ts', '--noEmit', '--strict',
80 '--module', 'nodenext', '--moduleResolution', 'nodenext',
81 '--target', 'esnext', '--ignoreConfig',
82 ])
83 }
84 finally {
85 rmSync(temp, { recursive: true, force: true })
86 }
87}, 60_000)