@wuchale/svelte 0.8.2 → 0.8.3

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": "@wuchale/svelte",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "i18n for svelte without turning your codebase upside down",
5
5
  "scripts": {
6
6
  "dev": "tsc --watch",
@@ -32,16 +32,18 @@
32
32
  ],
33
33
  "files": [
34
34
  "dist",
35
- "src/runtime.svelte"
35
+ "src"
36
36
  ],
37
37
  "exports": {
38
38
  ".": {
39
39
  "types": "./dist/src/index.d.ts",
40
- "import": "./dist/src/index.js"
40
+ "import": "./dist/src/index.js",
41
+ "source": "./src/index.ts"
41
42
  },
42
43
  "./runtime.svelte.js": {
43
44
  "types": "./dist/src/runtime.svelte.d.ts",
44
- "import": "./dist/src/runtime.svelte.js"
45
+ "import": "./dist/src/runtime.svelte.js",
46
+ "source": "./src/runtime.svelte.ts"
45
47
  },
46
48
  "./runtime.svelte": "./src/runtime.svelte"
47
49
  },
package/src/adapter.ts ADDED
@@ -0,0 +1,468 @@
1
+ import MagicString from "magic-string"
2
+ import type { Program, AnyNode } from "acorn"
3
+ import { parse, type AST } from "svelte/compiler"
4
+ import { defaultHeuristic, NestText } from 'wuchale/adapter'
5
+ import { deepMergeObjects } from 'wuchale/config'
6
+ import { Transformer, parseScript, proxyModuleHotUpdate, proxyModuleDefault, runtimeConst } from 'wuchale/adapter-vanilla'
7
+ import type {
8
+ IndexTracker,
9
+ HeuristicFunc,
10
+ TransformOutput,
11
+ AdapterFunc,
12
+ AdapterArgs,
13
+ CommentDirectives,
14
+ ProxyModuleFunc
15
+ } from 'wuchale/adapter'
16
+
17
+ const nodesWithChildren = ['RegularElement', 'Component']
18
+ const topLevelDeclarationsInside = ['$derived', '$derived.by']
19
+ const ignoreElements = ['path']
20
+
21
+ const svelteHeuristic: HeuristicFunc = (text, details) => {
22
+ if (!defaultHeuristic(text, details)) {
23
+ return false
24
+ }
25
+ if (ignoreElements.includes(details.element)) {
26
+ return false
27
+ }
28
+ if (details.scope !== 'script') {
29
+ return true
30
+ }
31
+ if (details.topLevel === 'variable' && !topLevelDeclarationsInside.includes(details.topLevelCall)) {
32
+ return false
33
+ }
34
+ if (details.call === '$inspect') {
35
+ return false
36
+ }
37
+ return true
38
+ }
39
+
40
+ const rtComponent = 'WuchaleTrans'
41
+ const snipPrefix = 'wuchaleSnippet'
42
+ const rtFuncCtx = `${runtimeConst}.cx`
43
+ const rtFuncCtxTrans = `${runtimeConst}.tx`
44
+
45
+ export class SvelteTransformer extends Transformer {
46
+
47
+ // state
48
+ currentElement?: string
49
+ inCompoundText: boolean = false
50
+ commentDirectivesStack: CommentDirectives[] = []
51
+ lastVisitIsComment: boolean = false
52
+ currentSnippet: number = 0
53
+
54
+ constructor(key: string, content: string, filename: string, index: IndexTracker, heuristic: HeuristicFunc, pluralsFunc: string) {
55
+ super(key, content, filename, index, heuristic, pluralsFunc)
56
+ }
57
+
58
+ visitExpressionTag = (node: AST.ExpressionTag): NestText[] => this.visit(node.expression)
59
+
60
+ nonWhitespaceText = (node: AST.Text): [number, string, number] => {
61
+ let trimmedS = node.data.trimStart()
62
+ const startWh = node.data.length - trimmedS.length
63
+ let trimmed = trimmedS.trimEnd()
64
+ const endWh = trimmedS.length - trimmed.length
65
+ return [startWh, trimmed, endWh]
66
+ }
67
+
68
+ separatelyVisitChildren = (node: AST.Fragment): [boolean, boolean, boolean, NestText[]] => {
69
+ let hasTextChild = false
70
+ let hasNonTextChild = false
71
+ let heurTxt = ''
72
+ let hasCommentDirectives = false
73
+ for (const child of node.nodes) {
74
+ if (child.type === 'Text') {
75
+ const txt = child.data.trim()
76
+ if (!txt) {
77
+ continue
78
+ }
79
+ hasTextChild = true
80
+ heurTxt += child.data + ' '
81
+ } else if (child.type === 'Comment') {
82
+ if (child.data.trim().startsWith('@wc-')) {
83
+ hasCommentDirectives = true
84
+ }
85
+ } else {
86
+ hasNonTextChild = true
87
+ heurTxt += `# `
88
+ }
89
+ }
90
+ heurTxt = heurTxt.trimEnd()
91
+ const [passHeuristic] = this.checkHeuristic(heurTxt, { scope: 'markup', element: this.currentElement })
92
+ let hasCompoundText = hasTextChild && hasNonTextChild
93
+ const visitAsOne = passHeuristic && !hasCommentDirectives
94
+ if (this.inCompoundText || hasCompoundText && visitAsOne) {
95
+ return [false, hasTextChild, hasCompoundText, []]
96
+ }
97
+ const txts = []
98
+ // can't be extracted as one; visitSv each separately
99
+ for (const child of node.nodes) {
100
+ txts.push(...this.visitSv(child))
101
+ }
102
+ return [true, false, false, txts]
103
+ }
104
+
105
+ visitFragment = (node: AST.Fragment): NestText[] => {
106
+ if (node.nodes.length === 0) {
107
+ return []
108
+ }
109
+ const [visitedSeparately, hasTextChild, hasCompoundText, separateTxts] = this.separatelyVisitChildren(node)
110
+ if (visitedSeparately) {
111
+ return separateTxts
112
+ }
113
+ let txt = ''
114
+ let iArg = 0
115
+ let iTag = 0
116
+ const lastChildEnd = node.nodes.slice(-1)[0].end
117
+ const childrenForSnippets: [number, number, boolean][] = []
118
+ let hasTextDescendants = false
119
+ const txts = []
120
+ for (const child of node.nodes) {
121
+ if (child.type === 'Comment') {
122
+ continue
123
+ }
124
+ if (child.type === 'Text') {
125
+ const [startWh, trimmed, endWh] = this.nonWhitespaceText(child)
126
+ const nTxt = new NestText(trimmed, 'markup', this.commentDirectives.context)
127
+ if (startWh && !txt.endsWith(' ')) {
128
+ txt += ' '
129
+ }
130
+ if (!trimmed) { // whitespace
131
+ continue
132
+ }
133
+ txt += nTxt.text
134
+ if (endWh) {
135
+ txt += ' '
136
+ }
137
+ this.mstr.remove(child.start, child.end)
138
+ continue
139
+ }
140
+ if (child.type === 'ExpressionTag') {
141
+ txts.push(...this.visitExpressionTag(child))
142
+ if (!hasCompoundText) {
143
+ continue
144
+ }
145
+ txt += `{${iArg}}`
146
+ let moveStart = child.start
147
+ if (iArg > 0) {
148
+ this.mstr.update(child.start, child.start + 1, ', ')
149
+ } else {
150
+ moveStart++
151
+ this.mstr.remove(child.start, child.start + 1)
152
+ }
153
+ this.mstr.move(moveStart, child.end - 1, lastChildEnd)
154
+ this.mstr.remove(child.end - 1, child.end)
155
+ iArg++
156
+ continue
157
+ }
158
+ // elements, components and other things as well
159
+ const nestedTextSupported = nodesWithChildren.includes(child.type)
160
+ const inCompoundTextPrev = this.inCompoundText
161
+ this.inCompoundText = nestedTextSupported
162
+ const childTxts = this.visitSv(child)
163
+ this.inCompoundText = inCompoundTextPrev // restore
164
+ let snippNeedsCtx = false
165
+ let chTxt = ''
166
+ for (const txt of childTxts) {
167
+ if (nodesWithChildren.includes(child.type) && txt.scope === 'markup') {
168
+ chTxt += txt.text[0]
169
+ hasTextDescendants = true
170
+ snippNeedsCtx = true
171
+ } else { // attributes, blocks
172
+ txts.push(txt)
173
+ }
174
+ }
175
+ childrenForSnippets.push([child.start, child.end, snippNeedsCtx])
176
+ if (nodesWithChildren.includes(child.type) && chTxt) {
177
+ chTxt = `<${iTag}>${chTxt}</${iTag}>`
178
+ } else {
179
+ // childless elements and everything else
180
+ chTxt = `<${iTag}/>`
181
+ }
182
+ iTag++
183
+ txt += chTxt
184
+ }
185
+ txt = txt.trim()
186
+ if (!txt) {
187
+ return txts
188
+ }
189
+ const nTxt = new NestText(txt, 'markup', this.commentDirectives.context)
190
+ if (hasTextChild || hasTextDescendants) {
191
+ txts.push(nTxt)
192
+ } else {
193
+ return txts
194
+ }
195
+ if (childrenForSnippets.length) {
196
+ const snippets = []
197
+ // create and reference snippets
198
+ for (const [childStart, childEnd, haveCtx] of childrenForSnippets) {
199
+ const snippetName = `${snipPrefix}${this.currentSnippet}`
200
+ snippets.push(snippetName)
201
+ this.currentSnippet++
202
+ const snippetBegin = `\n{#snippet ${snippetName}(${haveCtx ? 'ctx' : ''})}\n`
203
+ this.mstr.appendRight(childStart, snippetBegin)
204
+ this.mstr.prependLeft(childEnd, '\n{/snippet}')
205
+ }
206
+ let begin = `\n<${rtComponent} tags={[${snippets.join(', ')}]} ctx=`
207
+ if (this.inCompoundText) {
208
+ begin += `{ctx} nest`
209
+ } else {
210
+ const index = this.index.get(nTxt.toKey())
211
+ begin += `{${rtFuncCtx}(${index})}`
212
+ }
213
+ let end = ' />\n'
214
+ if (iArg > 0) {
215
+ begin += ' args={['
216
+ end = ']}' + end
217
+ }
218
+ this.mstr.appendLeft(lastChildEnd, begin)
219
+ this.mstr.appendRight(lastChildEnd, end)
220
+ } else if (hasTextChild) {
221
+ // no need for component use
222
+ let begin = '{'
223
+ let end = ')}'
224
+ if (this.inCompoundText) {
225
+ begin += `${rtFuncCtxTrans}(ctx`
226
+ } else {
227
+ begin += `${this.rtFunc}(${this.index.get(nTxt.toKey())}`
228
+ }
229
+ if (iArg) {
230
+ begin += ', ['
231
+ end = ']' + end
232
+ }
233
+ this.mstr.appendLeft(lastChildEnd, begin)
234
+ this.mstr.appendRight(lastChildEnd, end)
235
+ }
236
+ return txts
237
+ }
238
+
239
+ visitRegularElement = (node: AST.ElementLike): NestText[] => {
240
+ const currentElement = this.currentElement
241
+ this.currentElement = node.name
242
+ const txts: NestText[] = []
243
+ for (const attrib of node.attributes) {
244
+ txts.push(...this.visitSv(attrib))
245
+ }
246
+ txts.push(...this.visitFragment(node.fragment))
247
+ this.currentElement = currentElement
248
+ return txts
249
+ }
250
+
251
+ visitComponent = this.visitRegularElement
252
+
253
+ visitText = (node: AST.Text): NestText[] => {
254
+ const [startWh, trimmed, endWh] = this.nonWhitespaceText(node)
255
+ const [pass, txt] = this.checkHeuristic(trimmed, { scope: 'markup' })
256
+ if (!pass) {
257
+ return []
258
+ }
259
+ this.mstr.update(node.start + startWh, node.end - endWh, `{${this.rtFunc}(${this.index.get(txt.toKey())})}`)
260
+ return [txt]
261
+ }
262
+
263
+ visitSpreadAttribute = (node: AST.SpreadAttribute): NestText[] => this.visit(node.expression)
264
+
265
+ visitAttribute = (node: AST.Attribute): NestText[] => {
266
+ if (node.value === true) {
267
+ return []
268
+ }
269
+ const txts = []
270
+ let values: (AST.ExpressionTag | AST.Text)[]
271
+ if (Array.isArray(node.value)) {
272
+ values = node.value
273
+ } else {
274
+ values = [node.value]
275
+ }
276
+ for (const value of values) {
277
+ if (value.type !== 'Text') { // ExpressionTag
278
+ txts.push(...this.visitSv(value))
279
+ continue
280
+ }
281
+ // Text
282
+ const { start, end } = value
283
+ const [pass, txt] = this.checkHeuristic(value.data, {
284
+ scope: 'attribute',
285
+ element: this.currentElement,
286
+ attribute: node.name,
287
+ })
288
+ if (!pass) {
289
+ continue
290
+ }
291
+ txts.push(txt)
292
+ this.mstr.update(value.start, value.end, `{${this.rtFunc}(${this.index.get(txt.toKey())})}`)
293
+ if (!`'"`.includes(this.content[start - 1])) {
294
+ continue
295
+ }
296
+ this.mstr.remove(start - 1, start)
297
+ this.mstr.remove(end, end + 1)
298
+ }
299
+ return txts
300
+ }
301
+
302
+ visitSnippetBlock = (node: AST.SnippetBlock): NestText[] => this.visitFragment(node.body)
303
+
304
+ visitIfBlock = (node: AST.IfBlock): NestText[] => {
305
+ const txts = this.visit(node.test)
306
+ txts.push(...this.visitSv(node.consequent))
307
+ if (node.alternate) {
308
+ txts.push(...this.visitSv(node.alternate))
309
+ }
310
+ return txts
311
+ }
312
+
313
+ visitEachBlock = (node: AST.EachBlock): NestText[] => {
314
+ const txts = [
315
+ ...this.visit(node.expression),
316
+ ...this.visitSv(node.body),
317
+ ]
318
+ if (node.key) {
319
+ txts.push(...this.visit(node.key))
320
+ }
321
+ if (node.fallback) {
322
+ txts.push(...this.visitSv(node.fallback))
323
+ }
324
+ return txts
325
+ }
326
+
327
+ visitKeyBlock = (node: AST.KeyBlock): NestText[] => {
328
+ return [
329
+ ...this.visit(node.expression),
330
+ ...this.visitSv(node.fragment),
331
+ ]
332
+ }
333
+
334
+ visitAwaitBlock = (node: AST.AwaitBlock): NestText[] => {
335
+ const txts = [
336
+ ...this.visit(node.expression),
337
+ ...this.visitFragment(node.then),
338
+ ]
339
+ if (node.pending) {
340
+ txts.push(...this.visitFragment(node.pending),)
341
+ }
342
+ if (node.catch) {
343
+ txts.push(...this.visitFragment(node.catch),)
344
+ }
345
+ return txts
346
+ }
347
+
348
+ visitSvelteBody = (node: AST.SvelteBody): NestText[] => node.attributes.map(this.visitSv).flat()
349
+
350
+ visitSvelteDocument = (node: AST.SvelteDocument): NestText[] => node.attributes.map(this.visitSv).flat()
351
+
352
+ visitSvelteElement = (node: AST.SvelteElement): NestText[] => node.attributes.map(this.visitSv).flat()
353
+
354
+ visitSvelteBoundary = (node: AST.SvelteBoundary): NestText[] => [
355
+ ...node.attributes.map(this.visitSv).flat(),
356
+ ...this.visitSv(node.fragment),
357
+ ]
358
+
359
+ visitSvelteHead = (node: AST.SvelteHead): NestText[] => this.visitSv(node.fragment)
360
+
361
+ visitSvelteWindow = (node: AST.SvelteWindow): NestText[] => node.attributes.map(this.visitSv).flat()
362
+
363
+ visitRoot = (node: AST.Root): NestText[] => {
364
+ const txts = this.visitFragment(node.fragment)
365
+ if (node.instance) {
366
+ this.commentDirectives = {} // reset
367
+ txts.push(...this.visitProgram(node.instance.content))
368
+ }
369
+ // @ts-ignore: module is a reserved keyword, not sure how to specify the type
370
+ if (node.module) {
371
+ this.commentDirectives = {} // reset
372
+ // @ts-ignore
373
+ txts.push(...this.visitProgram(node.module.content))
374
+ }
375
+ return txts
376
+ }
377
+
378
+ visitSv = (node: AST.SvelteNode | AnyNode): NestText[] => {
379
+ if (node.type === 'Comment') {
380
+ const directives = this.processCommentDirectives(node.data.trim())
381
+ if (this.lastVisitIsComment) {
382
+ this.commentDirectivesStack[this.commentDirectivesStack.length - 1] = directives
383
+ } else {
384
+ this.commentDirectivesStack.push(directives)
385
+ }
386
+ this.lastVisitIsComment = true
387
+ return []
388
+ }
389
+ let txts = []
390
+ const commentDirectivesPrev = this.commentDirectives
391
+ if (this.lastVisitIsComment) {
392
+ this.commentDirectives = this.commentDirectivesStack.pop()
393
+ }
394
+ if (this.commentDirectives.forceInclude !== false) {
395
+ txts = this.visit(node)
396
+ }
397
+ this.commentDirectives = commentDirectivesPrev
398
+ this.lastVisitIsComment = false
399
+ return txts
400
+ }
401
+
402
+ transform = (): TransformOutput => {
403
+ const isComponent = this.filename.endsWith('.svelte')
404
+ let ast: AST.Root | Program
405
+ if (isComponent) {
406
+ ast = parse(this.content, { modern: true })
407
+ } else {
408
+ ast = parseScript(this.content)
409
+ }
410
+ this.mstr = new MagicString(this.content)
411
+ const txts = this.visitSv(ast)
412
+ if (!txts.length) {
413
+ return this.finalize(txts)
414
+ }
415
+ const getCtxFunc = '_wrs_'
416
+ const importComponent = `import ${rtComponent} from "@wuchale/svelte/runtime.svelte"`
417
+ const importStmt = `
418
+ import { ${getCtxFunc} } from "@wuchale/svelte/runtime.svelte.js"
419
+ ${ast.type === 'Root' ? importComponent : ''}
420
+ const ${runtimeConst} = $derived(${getCtxFunc}("${this.key}"))
421
+ `
422
+ if (ast.type === 'Program') {
423
+ this.mstr.appendRight(0, importStmt + '\n')
424
+ return this.finalize(txts)
425
+ }
426
+ if (ast.module) {
427
+ // @ts-ignore
428
+ this.mstr.appendRight(ast.module.content.start, importStmt)
429
+ } else if (ast.instance) {
430
+ // @ts-ignore
431
+ this.mstr.appendRight(ast.instance.content.start, importStmt)
432
+ } else {
433
+ this.mstr.prepend(`<script>${importStmt}</script>\n`)
434
+ }
435
+ return this.finalize(txts)
436
+ }
437
+ }
438
+
439
+ const proxyModuleDev: ProxyModuleFunc = (virtModEvent) => `
440
+ import defaultData, {key, pluralsRule} from '${virtModEvent}'
441
+ const data = $state(defaultData)
442
+ ${proxyModuleHotUpdate(virtModEvent)}
443
+ export {key, pluralsRule}
444
+ export default data
445
+ `
446
+
447
+ const defaultArgs: AdapterArgs = {
448
+ files: ['src/**/*.svelte', 'src/**/*.svelte.{js,ts}'],
449
+ catalog: './src/locales/{locale}',
450
+ pluralsFunc: 'plural',
451
+ heuristic: svelteHeuristic,
452
+ }
453
+
454
+ export const adapter: AdapterFunc = (args: AdapterArgs = defaultArgs) => {
455
+ const { heuristic, pluralsFunc, files, catalog } = deepMergeObjects(args, defaultArgs)
456
+ return {
457
+ transform: (content, filename, index, key) => {
458
+ return new SvelteTransformer(key, content, filename, index, heuristic, pluralsFunc).transform()
459
+ },
460
+ files,
461
+ catalog,
462
+ compiledExt: '.svelte.js',
463
+ proxyModule: {
464
+ dev: proxyModuleDev,
465
+ default: proxyModuleDefault,
466
+ }
467
+ }
468
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { adapter } from './adapter.js'
@@ -0,0 +1,18 @@
1
+ import { Runtime, _wre_, type CatalogModule } from "wuchale/runtime"
2
+
3
+ export let _wrs_: (key: string) => Runtime
4
+
5
+ const dataCollection: {[key: string]: Runtime} = $state({})
6
+
7
+ // no $app/environment.browser because it must work without sveltekit
8
+ if (globalThis.window) {
9
+ _wrs_ = key => dataCollection[key] ?? fallback
10
+ } else {
11
+ _wrs_ = _wre_
12
+ }
13
+
14
+ const fallback = new Runtime()
15
+
16
+ export function setCatalog(mod: CatalogModule) {
17
+ dataCollection[mod.key] = new Runtime(mod)
18
+ }