@strav/http 0.2.4 → 0.2.8

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.
@@ -1,136 +0,0 @@
1
- import { parse, compileScript, compileTemplate, compileStyle } from '@vue/compiler-sfc'
2
- import type { BunPlugin } from 'bun'
3
-
4
- function hashId(path: string): string {
5
- const hasher = new Bun.CryptoHasher('md5')
6
- hasher.update(path)
7
- return hasher.digest('hex').slice(0, 8)
8
- }
9
-
10
- export function vueSfcPlugin(): BunPlugin {
11
- return {
12
- name: 'vue-sfc',
13
- setup(build) {
14
- build.onLoad({ filter: /\.vue$/ }, async args => {
15
- const source = await Bun.file(args.path).text()
16
- const id = hashId(args.path)
17
- const scopeId = `data-v-${id}`
18
- const hasScoped = false // computed below
19
- const { descriptor, errors } = parse(source, { filename: args.path })
20
-
21
- if (errors.length > 0) {
22
- throw new Error(
23
- `Vue SFC parse error in ${args.path}:\n${errors.map(e => e.message).join('\n')}`
24
- )
25
- }
26
-
27
- const scoped = descriptor.styles.some(s => s.scoped)
28
-
29
- // ── Script ────────────────────────────────────────────────────────
30
- let scriptCode = ''
31
- let bindings: Record<string, any> | undefined
32
-
33
- if (descriptor.script || descriptor.scriptSetup) {
34
- const result = compileScript(descriptor, {
35
- id,
36
- inlineTemplate: !!descriptor.scriptSetup,
37
- sourceMap: false,
38
- templateOptions: scoped
39
- ? {
40
- scoped: true,
41
- id,
42
- compilerOptions: { scopeId },
43
- }
44
- : undefined,
45
- })
46
- scriptCode = result.content
47
- bindings = result.bindings
48
- }
49
-
50
- // ── Template (Options API only — script setup uses inlineTemplate) ─
51
- let templateCode = ''
52
-
53
- if (descriptor.template && !descriptor.scriptSetup) {
54
- const result = compileTemplate({
55
- source: descriptor.template.content,
56
- filename: args.path,
57
- id,
58
- scoped,
59
- compilerOptions: {
60
- bindingMetadata: bindings,
61
- scopeId: scoped ? scopeId : undefined,
62
- },
63
- })
64
-
65
- if (result.errors.length > 0) {
66
- throw new Error(
67
- `Vue template error in ${args.path}:\n${result.errors.map(e => (typeof e === 'string' ? e : e.message)).join('\n')}`
68
- )
69
- }
70
-
71
- templateCode = result.code
72
- }
73
-
74
- // ── Styles ────────────────────────────────────────────────────────
75
- const styles: string[] = []
76
-
77
- for (const styleBlock of descriptor.styles) {
78
- const result = compileStyle({
79
- source: styleBlock.content,
80
- filename: args.path,
81
- id: scopeId,
82
- scoped: !!styleBlock.scoped,
83
- })
84
-
85
- if (result.errors.length > 0) {
86
- console.warn(`[vue-sfc] Style warning in ${args.path}:`, result.errors)
87
- }
88
-
89
- styles.push(result.code)
90
- }
91
-
92
- // ── Assemble ──────────────────────────────────────────────────────
93
- let output = ''
94
-
95
- // Inject styles at module load time
96
- if (styles.length > 0) {
97
- const css = JSON.stringify(styles.join('\n'))
98
- output += `(function(){var s=document.createElement('style');s.textContent=${css};document.head.appendChild(s)})();\n`
99
- }
100
-
101
- if (descriptor.scriptSetup) {
102
- // <script setup> with inlineTemplate — scriptCode is a complete module
103
- // Rewrite the default export to capture the component and set __scopeId
104
- if (scoped) {
105
- output += scriptCode.replace(/export\s+default\s+/, 'const __sfc__ = ') + '\n'
106
- output += `__sfc__.__scopeId = ${JSON.stringify(scopeId)};\n`
107
- output += 'export default __sfc__;\n'
108
- } else {
109
- output += scriptCode + '\n'
110
- }
111
- } else {
112
- // Options API — stitch script + template render function
113
- if (scriptCode) {
114
- output += scriptCode.replace(/export\s+default\s*\{/, 'const __component__ = {') + '\n'
115
- } else {
116
- output += 'const __component__ = {};\n'
117
- }
118
-
119
- if (templateCode) {
120
- output += templateCode + '\n'
121
- output += '__component__.render = render;\n'
122
- }
123
-
124
- if (scoped) {
125
- output += `__component__.__scopeId = ${JSON.stringify(scopeId)};\n`
126
- }
127
-
128
- output += 'export default __component__;\n'
129
- }
130
-
131
- const isTs = descriptor.script?.lang === 'ts' || descriptor.scriptSetup?.lang === 'ts'
132
- return { contents: output, loader: isTs ? 'ts' : 'js' }
133
- })
134
- },
135
- }
136
- }
@@ -1,69 +0,0 @@
1
- import { resolve, normalize } from 'node:path'
2
- import type { Middleware } from '../../http/middleware.ts'
3
-
4
- export function staticFiles(root = 'public'): Middleware {
5
- const resolvedRoot = resolve(root)
6
-
7
- return async (ctx, next) => {
8
- // Only serve GET/HEAD requests
9
- if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
10
- return next()
11
- }
12
-
13
- // Skip hidden files (segments starting with .)
14
- const segments = ctx.path.split('/')
15
- if (segments.some(s => s.startsWith('.') && s.length > 1)) {
16
- return next()
17
- }
18
-
19
- // Skip pre-compressed files (served via content negotiation only)
20
- if (ctx.path.endsWith('.gz') || ctx.path.endsWith('.br')) {
21
- return next()
22
- }
23
-
24
- const filePath = normalize(resolve(resolvedRoot + ctx.path))
25
-
26
- // Directory traversal protection
27
- if (!filePath.startsWith(resolvedRoot)) {
28
- return next()
29
- }
30
-
31
- const file = Bun.file(filePath)
32
- const exists = await file.exists()
33
-
34
- if (!exists) {
35
- return next()
36
- }
37
-
38
- // Content negotiation for pre-compressed files
39
- const acceptEncoding = ctx.request.headers.get('accept-encoding') ?? ''
40
-
41
- if (acceptEncoding.includes('br')) {
42
- const brFile = Bun.file(filePath + '.br')
43
- if (await brFile.exists()) {
44
- return new Response(brFile, {
45
- headers: {
46
- 'Content-Encoding': 'br',
47
- 'Content-Type': file.type,
48
- Vary: 'Accept-Encoding',
49
- },
50
- })
51
- }
52
- }
53
-
54
- if (acceptEncoding.includes('gzip')) {
55
- const gzFile = Bun.file(filePath + '.gz')
56
- if (await gzFile.exists()) {
57
- return new Response(gzFile, {
58
- headers: {
59
- 'Content-Encoding': 'gzip',
60
- 'Content-Type': file.type,
61
- Vary: 'Accept-Encoding',
62
- },
63
- })
64
- }
65
- }
66
-
67
- return new Response(file)
68
- }
69
- }
@@ -1,182 +0,0 @@
1
- import { TemplateError } from '@strav/kernel/exceptions/errors'
2
- export type TokenType = 'text' | 'escaped' | 'raw' | 'comment' | 'directive' | 'vue_island'
3
-
4
- export interface VueAttr {
5
- value: string
6
- bound: boolean
7
- }
8
-
9
- export interface Token {
10
- type: TokenType
11
- value: string
12
- directive?: string
13
- args?: string
14
- tag?: string
15
- attrs?: Record<string, VueAttr>
16
- line: number
17
- }
18
-
19
- const DIRECTIVES = new Set([
20
- 'if',
21
- 'elseif',
22
- 'else',
23
- 'end',
24
- 'each',
25
- 'layout',
26
- 'block',
27
- 'include',
28
- 'islands',
29
- ])
30
-
31
- export function tokenize(source: string): Token[] {
32
- const tokens: Token[] = []
33
- let pos = 0
34
- let line = 1
35
- let textStart = 0
36
-
37
- function countLines(str: string): number {
38
- let count = 0
39
- for (let i = 0; i < str.length; i++) {
40
- if (str[i] === '\n') count++
41
- }
42
- return count
43
- }
44
-
45
- function flushText(): void {
46
- if (pos > textStart) {
47
- const value = source.slice(textStart, pos)
48
- if (value.length > 0) {
49
- tokens.push({ type: 'text', value, line: line - countLines(value) })
50
- }
51
- }
52
- }
53
-
54
- function remaining(): string {
55
- return source.slice(pos)
56
- }
57
-
58
- while (pos < source.length) {
59
- const rest = remaining()
60
-
61
- // 1. Comments: {{-- ... --}}
62
- if (rest.startsWith('{{--')) {
63
- flushText()
64
- const endIdx = source.indexOf('--}}', pos + 4)
65
- if (endIdx === -1) {
66
- throw new TemplateError(`Unclosed comment at line ${line}`)
67
- }
68
- const content = source.slice(pos + 4, endIdx)
69
- tokens.push({ type: 'comment', value: content.trim(), line })
70
- line += countLines(source.slice(pos, endIdx + 4))
71
- pos = endIdx + 4
72
- textStart = pos
73
- continue
74
- }
75
-
76
- // 2. Raw output: {!! ... !!}
77
- if (rest.startsWith('{!!')) {
78
- flushText()
79
- const endIdx = source.indexOf('!!}', pos + 3)
80
- if (endIdx === -1) {
81
- throw new TemplateError(`Unclosed raw expression at line ${line}`)
82
- }
83
- const expr = source.slice(pos + 3, endIdx).trim()
84
- tokens.push({ type: 'raw', value: expr, line })
85
- line += countLines(source.slice(pos, endIdx + 3))
86
- pos = endIdx + 3
87
- textStart = pos
88
- continue
89
- }
90
-
91
- // 3. Escaped output: {{ ... }}
92
- if (rest.startsWith('{{')) {
93
- flushText()
94
- const endIdx = source.indexOf('}}', pos + 2)
95
- if (endIdx === -1) {
96
- throw new TemplateError(`Unclosed expression at line ${line}`)
97
- }
98
- const expr = source.slice(pos + 2, endIdx).trim()
99
- tokens.push({ type: 'escaped', value: expr, line })
100
- line += countLines(source.slice(pos, endIdx + 2))
101
- pos = endIdx + 2
102
- textStart = pos
103
- continue
104
- }
105
-
106
- // 4. Vue islands: <vue:name ... /> (supports subpaths like <vue:forms/contact-form />)
107
- const vueMatch = rest.match(/^<vue:([\w/-]+)((?:\s+[\s\S]*?)?)\/>/)
108
- if (vueMatch) {
109
- flushText()
110
- const tag = vueMatch[1]!
111
- const attrsRaw = vueMatch[2]!.trim()
112
- const attrs = parseVueAttrs(attrsRaw)
113
- const full = vueMatch[0]
114
- tokens.push({ type: 'vue_island', value: full, tag, attrs, line })
115
- line += countLines(full)
116
- pos += full.length
117
- textStart = pos
118
- continue
119
- }
120
-
121
- // 5. Directives: @keyword or @keyword(...)
122
- const dirMatch = rest.match(/^@(\w+)/)
123
- if (dirMatch && DIRECTIVES.has(dirMatch[1]!)) {
124
- flushText()
125
- const directive = dirMatch[1]!
126
- pos += dirMatch[0].length
127
- let args: string | undefined
128
-
129
- // Parse arguments in parentheses (if present)
130
- if (pos < source.length && source[pos] === '(') {
131
- const argsStart = pos
132
- let depth = 1
133
- pos++ // skip opening (
134
- while (pos < source.length && depth > 0) {
135
- if (source[pos] === '(') depth++
136
- else if (source[pos] === ')') depth--
137
- if (depth > 0) pos++
138
- }
139
- if (depth !== 0) {
140
- throw new TemplateError(`Unclosed directive arguments at line ${line}`)
141
- }
142
- args = source.slice(argsStart + 1, pos)
143
- pos++ // skip closing )
144
- }
145
-
146
- tokens.push({ type: 'directive', value: directive, directive, args, line })
147
- textStart = pos
148
- continue
149
- }
150
-
151
- // 6. Regular text
152
- if (source[pos] === '\n') line++
153
- pos++
154
- }
155
-
156
- flushText()
157
- return tokens
158
- }
159
-
160
- function parseVueAttrs(raw: string): Record<string, VueAttr> {
161
- const attrs: Record<string, VueAttr> = {}
162
- const attrPattern = /([:@]?[\w.-]+)\s*=\s*"([^"]*)"/g
163
- let match: RegExpExecArray | null
164
-
165
- while ((match = attrPattern.exec(raw)) !== null) {
166
- const name = match[1]!
167
- const value = match[2]!
168
-
169
- if (name.startsWith(':')) {
170
- // Bound attribute — extract expression from {{ }} if present
171
- const exprMatch = value.match(/^\{\{\s*(.*?)\s*\}\}$/)
172
- attrs[name.slice(1)] = {
173
- value: exprMatch ? exprMatch[1]! : value,
174
- bound: true,
175
- }
176
- } else {
177
- attrs[name] = { value, bound: false }
178
- }
179
- }
180
-
181
- return attrs
182
- }