@craft-native/svelte 0.0.490 → 0.0.491

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/svelte",
3
- "version": "0.0.490",
3
+ "version": "0.0.491",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/svelte#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,14 +41,14 @@
41
41
  "author": "Craft Team",
42
42
  "license": "MIT",
43
43
  "peerDependencies": {
44
- "svelte": "^3.0.0 || ^4.0.0"
44
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "svelte": "^4.0.0",
47
+ "svelte": "^5.0.0",
48
48
  "tsup": "^8.0.0",
49
49
  "typescript": "7.1.0-dev.20261011.1"
50
50
  },
51
51
  "dependencies": {
52
- "craft-native": "0.0.490"
52
+ "craft-native": "0.0.491"
53
53
  }
54
54
  }
@@ -0,0 +1,123 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { readFileSync, readdirSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ /**
6
+ * The peer range admits every Svelte that provides what this adapter uses.
7
+ *
8
+ * The range used to stop at `^4.0.0`. Six of the Svelte advisories on the
9
+ * dependency dashboard (#399) are fixed only in 5.51.5, 5.53.5 and 5.55.7, so
10
+ * a consumer honouring that range could not reach a fixed Svelte at all —
11
+ * the cap, not the dependency, was what held them on a vulnerable major.
12
+ *
13
+ * Nothing technical required the cap. This adapter's entire Svelte surface is
14
+ * `writable`, `derived` and `get` from `svelte/store`, and Svelte 5 still
15
+ * exports all three — checked against the published 5.57.3 tarball, whose
16
+ * `src/store/index-client.js` re-exports `derived`, `get`, `readable`,
17
+ * `readonly` and `writable` from `shared/index.js`.
18
+ *
19
+ * So this test ties the range to the surface rather than to a version
20
+ * someone picked. If the adapter starts importing runes, the compiler, or
21
+ * anything outside `svelte/store`, that assumption no longer holds and this
22
+ * fails — which is the moment to re-examine the range, not after a consumer's
23
+ * build breaks.
24
+ */
25
+ const root = join(import.meta.dir, '..')
26
+
27
+ /** Every `… from 'svelte…'` specifier in the adapter's sources. */
28
+ function svelteImports(): { specifier: string, named: string[] }[] {
29
+ const found: { specifier: string, named: string[] }[] = []
30
+ const walk = (directory: string): void => {
31
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
32
+ const path = join(directory, entry.name)
33
+ if (entry.isDirectory()) { walk(path); continue }
34
+ if (!entry.name.endsWith('.ts')) continue
35
+ const source = readFileSync(path, 'utf8')
36
+ for (const match of source.matchAll(/import\s+(type\s+)?\{([^}]*)\}\s+from\s+'(svelte[^']*)'/g)) {
37
+ found.push({ specifier: match[3]!, named: match[2]!.split(',').map(name => name.trim()).filter(Boolean) })
38
+ }
39
+ for (const match of source.matchAll(/import\s+[^{][^\n]*from\s+'(svelte[^']*)'/g)) {
40
+ found.push({ specifier: match[1]!, named: [] })
41
+ }
42
+ }
43
+ }
44
+ walk(join(root, 'src'))
45
+ return found
46
+ }
47
+
48
+ /**
49
+ * What `svelte/store` has exported continuously through Svelte 3, 4 and 5.
50
+ *
51
+ * Svelte 3 and 4 cannot be resolved from here — only one version is
52
+ * installed — so this half is a claim. The half that matters is not:
53
+ * `the adapter's imports exist in the installed Svelte` below asks the
54
+ * installed package what it actually exports, so widening the range cannot
55
+ * rest on my reading of a changelog.
56
+ */
57
+ const storeSurface = new Set(['writable', 'readable', 'derived', 'readonly', 'get', 'Readable', 'Writable', 'Unsubscriber', 'StartStopNotifier'])
58
+
59
+ test('the adapter only uses svelte/store', () => {
60
+ const imports = svelteImports()
61
+ expect(imports.length, 'the adapter must import something from Svelte').toBeGreaterThan(0)
62
+ const specifiers = [...new Set(imports.map(entry => entry.specifier))]
63
+ // `svelte` itself, `svelte/compiler`, `svelte/internal` and the rune APIs
64
+ // are all majors-sensitive in a way `svelte/store` is not.
65
+ expect(specifiers, `the peer range below assumes svelte/store only, found ${specifiers.join(', ')}`)
66
+ .toEqual(['svelte/store'])
67
+ })
68
+
69
+ test('every name it imports survived into Svelte 5', () => {
70
+ for (const entry of svelteImports()) {
71
+ for (const name of entry.named) {
72
+ expect(storeSurface.has(name), `${name} is not in the store surface this range was widened on`).toBe(true)
73
+ }
74
+ }
75
+ })
76
+
77
+ test('the peer range reaches the majors where the advisories are fixed', () => {
78
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
79
+ peerDependencies?: Record<string, string>
80
+ }
81
+ const range = manifest.peerDependencies?.svelte
82
+ expect(range, 'the adapter must declare a Svelte peer range').toBeString()
83
+ // The point of the change: a consumer can resolve a Svelte 5 that carries
84
+ // the fixes for GHSA-crpf-4hrx-3jrp, -f7gr-6p89-r883, -m56q-vw4c-c2cp,
85
+ // -phwv-c562-gvmh, -pr6f-5x2q-rwfp and -rcqx-6q8c-2c42.
86
+ expect(range, 'the range must not cap below the majors carrying the fixes').toContain('^5.0.0')
87
+ // Without dropping the consumers who are still on 3 or 4.
88
+ expect(range).toContain('^3.0.0')
89
+ expect(range).toContain('^4.0.0')
90
+ })
91
+
92
+ test('the adapter builds against the major its range points consumers at', async () => {
93
+ // The defect this closes: the peer range was widened to `^5.0.0` while the
94
+ // devDependency stayed `^4.0.0`, so nothing ever built or tested the adapter
95
+ // against the major being advertised — and the installed Svelte still
96
+ // carried the six advisories the widening was for (#399).
97
+ const installed = JSON.parse(readFileSync(join(root, '../../node_modules/svelte/package.json'), 'utf8')) as { version: string }
98
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
99
+ devDependencies?: Record<string, string>
100
+ peerDependencies?: Record<string, string>
101
+ }
102
+ const range = manifest.peerDependencies?.svelte ?? ''
103
+ const highest = [...range.matchAll(/\^(\d+)\./g)].map(found => Number(found[1])).sort((a, b) => b - a)[0]
104
+ expect(highest, 'the peer range must name a major').toBeGreaterThan(0)
105
+ expect(Number(installed.version.split('.')[0]),
106
+ `the range offers ^${highest}.x but the adapter is built against Svelte ${installed.version}`).toBe(highest)
107
+ // And the devDependency, not just whatever happens to be hoisted.
108
+ expect(manifest.devDependencies?.svelte).toBe(`^${highest}.0.0`)
109
+ })
110
+
111
+ test("the adapter's imports exist in the installed Svelte", async () => {
112
+ // Asked of the package rather than asserted from a list, so the range rests
113
+ // on what Svelte exports instead of on what I believed it exports.
114
+ const store = await import('svelte/store') as Record<string, unknown>
115
+ const exported = new Set(Object.keys(store))
116
+ expect(exported.size, 'svelte/store must export something').toBeGreaterThan(0)
117
+ const wanted = [...new Set(svelteImports().flatMap(entry => entry.named))]
118
+ // Type-only names are not runtime exports.
119
+ .filter(name => !/^[A-Z]/.test(name))
120
+ expect(wanted.length, 'the adapter must import runtime names').toBeGreaterThan(0)
121
+ const missing = wanted.filter(name => !exported.has(name))
122
+ expect(missing, `the installed Svelte does not export ${missing.join(', ')}`).toEqual([])
123
+ })