duto 0.0.0 → 0.260906.0
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/LICENSE +21 -0
- package/README.md +21 -12
- package/package.json +91 -2
- package/src/cli.ts +42 -0
- package/src/enum.ts +2 -0
- package/src/index.d.ts +61 -0
- package/src/index.ts +19 -0
- package/src/middleware.ts +2 -0
- package/src/pages/client/app.tsx +123 -0
- package/src/pages/client/hmr.ts +13 -0
- package/src/pages/client/island.ts +136 -0
- package/src/pages/client/link.ts +70 -0
- package/src/pages/client/router.ts +329 -0
- package/src/pages/client/storage.ts +95 -0
- package/src/pages/commands/build.ts +74 -0
- package/src/pages/commands/dev-old.ts +213 -0
- package/src/pages/commands/dev.ts +87 -0
- package/src/pages/commands/preview.ts +33 -0
- package/src/pages/compiler/aliases.ts +36 -0
- package/src/pages/compiler/assets.ts +43 -0
- package/src/pages/compiler/ast.ts +764 -0
- package/src/pages/compiler/constants.ts +24 -0
- package/src/pages/compiler/file.ts +60 -0
- package/src/pages/compiler/index.ts +307 -0
- package/src/pages/compiler/island.ts +108 -0
- package/src/pages/compiler/meta.ts +601 -0
- package/src/pages/compiler/routes.ts +29 -0
- package/src/pages/compiler/types.ts +209 -0
- package/src/pages/compiler/utils.ts +5 -0
- package/src/pages/compiler/vite-plugin.ts +324 -0
- package/src/pages/components/link.tsx +38 -0
- package/src/pages/i18n/client.ts +403 -0
- package/src/pages/i18n/element.ts +44 -0
- package/src/pages/i18n/server.ts +56 -0
- package/src/pages/index.ts +28 -0
- package/src/pages/preact/client-dev.ts +5 -0
- package/src/pages/preact/client.ts +87 -0
- package/src/pages/preact/context.ts +41 -0
- package/src/pages/preact/server.ts +186 -0
- package/src/pages/preact/signals.ts +89 -0
- package/src/pages/preact/static-html.ts +45 -0
- package/src/pages/preact/types.ts +26 -0
- package/src/server/adapter/aws.ts +4 -0
- package/src/server/adapter/workerd.ts +4 -0
- package/src/server/app.ts +96 -0
- package/src/server/auth/ability.ts +72 -0
- package/src/server/auth/authnz.ts +86 -0
- package/src/server/auth/index.ts +5 -0
- package/src/server/auth/token.ts +147 -0
- package/src/server/auth/types.ts +2 -0
- package/src/server/cli/commands/build.ts +41 -0
- package/src/server/cli/commands/deploy.ts +82 -0
- package/src/server/cli/commands/dev.ts +277 -0
- package/src/server/cli/commands/make.ts +98 -0
- package/src/server/cli/commands/migrate.ts +93 -0
- package/src/server/cli/commands/routes.ts +79 -0
- package/src/server/cli/index.ts +27 -0
- package/src/server/cli/stubs.ts +39 -0
- package/src/server/cli/types.ts +3 -0
- package/src/server/cli/utils.ts +443 -0
- package/src/server/config.ts +4 -0
- package/src/server/db/d1.d.ts +1 -0
- package/src/server/db/d1.ts +1 -0
- package/src/server/db/dynamodb.d.ts +1 -0
- package/src/server/db/dynamodb.ts +1 -0
- package/src/server/db/index.ts +1 -0
- package/src/server/entry.ts +19 -0
- package/src/server/handle.ts +30 -0
- package/src/server/http.ts +23 -0
- package/src/server/middlewares/auth.ts +40 -0
- package/src/server/middlewares/logger.ts +30 -0
- package/src/server/open-api/register.ts +29 -0
- package/src/server/open-api/spec.ts +107 -0
- package/src/server/request.ts +233 -0
- package/src/server/response.ts +142 -0
- package/src/server/routes.ts +474 -0
- package/src/server/types.ts +142 -0
- package/src/server/utils/environment.ts +17 -0
- package/src/server/utils.ts +46 -0
- package/src/server/validator.ts +70 -0
- package/src/utils.ts +5 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const PAGE_RE = /\/pages\/.*\.(tsx?|jsx?)$/i
|
|
2
|
+
export const EXT_RE = /\.(tsx?|jsx?)$/i
|
|
3
|
+
export const DYNAMIC_RE = /\[/
|
|
4
|
+
export const INTERNAL_RE = /(?:.*\/)?_?(layout|error|template|loading)\.(tsx?|jsx?)$/i
|
|
5
|
+
export const LAYOUT_RE = /(?:.*\/)?_?(layout)\.(tsx?|jsx?)$/i
|
|
6
|
+
export const DOCTYPE_RE = /<!doctype html/i
|
|
7
|
+
|
|
8
|
+
// export const USE_CLIENT_RE = /^[\s\r\n]*["']use client["'](?=[\s\r\n]|$)/m
|
|
9
|
+
export const USE_CLIENT_RE = /^(?:\s|\/\/.*\n|\/\*[\s\S]*?\*\/)*["']use client["'](?=\s|;|$)/
|
|
10
|
+
// export const IMPORT_RE = /import\s+(?:type\s+)?(?:[\w*\s{},]+from\s+)?['"]([^'"]+)['"]/g
|
|
11
|
+
export const IMPORT_RE = /(?:\/\/.*|\/\*[\s\S]*?\*\/)|import\s+(?:type\s+)?(?:[\w*\s{},]+from\s+)?['"]([^'"]+)['"]/g
|
|
12
|
+
export const BROWSER_ONLY = [
|
|
13
|
+
'preact', '@preact/signals',
|
|
14
|
+
'react', 'react-dom',
|
|
15
|
+
'wouter', 'wouter-preact',
|
|
16
|
+
'framer-motion', 'gsap', 'three',
|
|
17
|
+
]
|
|
18
|
+
export const BROWSER_ONLY_SET = new Set(BROWSER_ONLY)
|
|
19
|
+
|
|
20
|
+
export const LAYOUT_NAMES = ['_layout', 'layout']
|
|
21
|
+
export const NF_NAMES = ['404', 'not-found']
|
|
22
|
+
export const FILE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'] as const
|
|
23
|
+
export const CONFIG_EXTENSIONS = ['json', 'toml', 'yaml'] as const
|
|
24
|
+
export const EXTENSIONS = [...FILE_EXTENSIONS, ...CONFIG_EXTENSIONS] as const
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { extname, join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { EXTENSIONS, FILE_EXTENSIONS } from './constants'
|
|
4
|
+
import type { Metadata } from '@/types'
|
|
5
|
+
|
|
6
|
+
const extensionCache = new Map<string, string | null>()
|
|
7
|
+
export function resolveExtension(filePath: string): string | null {
|
|
8
|
+
const cacheKey = extname(filePath)
|
|
9
|
+
? filePath.slice(0, -extname(filePath).length)
|
|
10
|
+
: filePath
|
|
11
|
+
|
|
12
|
+
const cached = extensionCache.get(cacheKey)
|
|
13
|
+
if (cached !== undefined) return cached
|
|
14
|
+
|
|
15
|
+
const result = resolveExtensionUncached(filePath)
|
|
16
|
+
extensionCache.set(cacheKey, result)
|
|
17
|
+
return result
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveExtensionUncached(filePath: string): string | null {
|
|
21
|
+
if (extname(filePath))
|
|
22
|
+
return existsSync(filePath) ? filePath : null
|
|
23
|
+
|
|
24
|
+
for (const ext of EXTENSIONS) {
|
|
25
|
+
const direct = `${filePath}.${ext}`
|
|
26
|
+
if (existsSync(direct)) return direct
|
|
27
|
+
|
|
28
|
+
const index = join(filePath, `index.${ext}`)
|
|
29
|
+
if (existsSync(index)) return index
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findFileByName(meta: Metadata, dir: string, baseName: string): string | null {
|
|
36
|
+
for (const extension of FILE_EXTENSIONS) {
|
|
37
|
+
const candidate = join(meta.root, 'pages', dir, `_${baseName}.${extension}`)
|
|
38
|
+
if (existsSync(candidate))
|
|
39
|
+
return candidate
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// export function fileWhitoutExt(filePath: string): string {
|
|
46
|
+
// const parsed = parse(filePath)
|
|
47
|
+
// let name = parsed.name
|
|
48
|
+
// while (name.includes('.'))
|
|
49
|
+
// name = name.replace(/\.[^/.]+`$/, '')
|
|
50
|
+
|
|
51
|
+
// return join(parsed.dir, name)
|
|
52
|
+
// }
|
|
53
|
+
|
|
54
|
+
// export async function ensureDir(dir: string): Promise<void> {
|
|
55
|
+
// try {
|
|
56
|
+
// await access(dir)
|
|
57
|
+
// } catch (error) {
|
|
58
|
+
// await mkdir(dir, { recursive: true })
|
|
59
|
+
// }
|
|
60
|
+
// }
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { getLayouts, parsePage, resolveStaticOutputPath } from './meta'
|
|
4
|
+
|
|
5
|
+
import { h } from 'preact'
|
|
6
|
+
import { render } from 'preact-render-to-string'
|
|
7
|
+
import minify from '@minify-html/node'
|
|
8
|
+
|
|
9
|
+
import type { ResolvedConfig } from 'vite'
|
|
10
|
+
import { DOCTYPE_RE } from './constants'
|
|
11
|
+
import { getAsset, getChunk, normalizeId } from './assets'
|
|
12
|
+
|
|
13
|
+
import type { IRoute, Metadata, RouteParams, StaticProps, SummaryMetadata } from './types'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// import i18n from '../client/i18n'
|
|
17
|
+
import { setLocale } from '../i18n/client'
|
|
18
|
+
import { log, substep } from 't0n/log'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import { promisify } from 'node:util'
|
|
23
|
+
import { brotliCompress } from 'node:zlib'
|
|
24
|
+
|
|
25
|
+
const brCompress = promisify(brotliCompress);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export const selfCloseEmptyTags = (html: string, tagName: string) => html.replace(new RegExp(`<${tagName}\\b([^>]*?)>\\s*</${tagName}>`, 'g'), (_, attrs) => `<${tagName}${attrs} />`)
|
|
29
|
+
|
|
30
|
+
export async function renderPage(
|
|
31
|
+
meta: Metadata,
|
|
32
|
+
config: ResolvedConfig,
|
|
33
|
+
page: IRoute
|
|
34
|
+
) {
|
|
35
|
+
const {
|
|
36
|
+
mod, islands,
|
|
37
|
+
hasGetProps, hasGetStaticProps,
|
|
38
|
+
isClientOnly,
|
|
39
|
+
} = await parsePage(page, meta)
|
|
40
|
+
// const {
|
|
41
|
+
// mod, islands,
|
|
42
|
+
// hasGetProps, hasGetStaticProps,
|
|
43
|
+
// isClientOnly,
|
|
44
|
+
// } = page
|
|
45
|
+
|
|
46
|
+
if (!mod?.default) {
|
|
47
|
+
console.warn(`No default export in ${page.filePath} — skipping`)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// console.log(page.filePath)
|
|
52
|
+
// console.error('hasGetProps:', hasGetProps)
|
|
53
|
+
// console.error('hasGetStaticProps:', hasGetStaticProps)
|
|
54
|
+
// console.error('isClientOnly:', isClientOnly)
|
|
55
|
+
|
|
56
|
+
if (page.isInternal || page.is404 || (page.isDynamic && !hasGetStaticProps)) {
|
|
57
|
+
meta.summary.set('/'+ page.fileName, {
|
|
58
|
+
type: 2,
|
|
59
|
+
path: '/'+ page.fileName,
|
|
60
|
+
size: 0,
|
|
61
|
+
sizeBr: 0,
|
|
62
|
+
sizeJS: 0,
|
|
63
|
+
})
|
|
64
|
+
// meta.routes.set(page.fullFilePath, page)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// console.log(meta.routes.size)
|
|
69
|
+
// meta.routes.set(page.filePath, page)
|
|
70
|
+
|
|
71
|
+
const { filePath, pattern, params } = page
|
|
72
|
+
|
|
73
|
+
let props = []
|
|
74
|
+
if (hasGetStaticProps) {
|
|
75
|
+
let sProps = []
|
|
76
|
+
try {
|
|
77
|
+
const _sProps = await Promise.resolve(mod.getStaticProps!())
|
|
78
|
+
sProps = Array.isArray(_sProps) ? _sProps : [_sProps]
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return console.error(`getStaticProps failed in ${filePath}: ${e}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const entry of sProps) {
|
|
84
|
+
const _props = {
|
|
85
|
+
routeParams: {},
|
|
86
|
+
pageProps: {},
|
|
87
|
+
} as {
|
|
88
|
+
routeParams: RouteParams,
|
|
89
|
+
pageProps: StaticProps,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [k, v] of Object.entries(entry as Record<string, unknown>)) {
|
|
93
|
+
if (k in params) {
|
|
94
|
+
_props.routeParams[k] = v as string | string[]
|
|
95
|
+
} else {
|
|
96
|
+
_props.pageProps[k] = v
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
props.push(_props)
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
props.push({ routeParams: {}, pageProps: {} })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const layouts = await getLayouts(meta, page.filePath)
|
|
107
|
+
|
|
108
|
+
for (const prop of props) {
|
|
109
|
+
const allProps = { ...prop.pageProps, ...prop.routeParams }
|
|
110
|
+
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
// i18n(allProps?.locale || meta.i18n.fallbackLocale, meta.i18n.locales, meta.i18n.messages, meta.i18n.routes, meta.i18n.routeMap)
|
|
113
|
+
allProps?.locale && setLocale(allProps?.locale || meta.i18n.fallbackLocale)
|
|
114
|
+
|
|
115
|
+
const pathname = resolveStaticOutputPath(page, prop.routeParams)
|
|
116
|
+
// TODO: melhorar isso para impedir erros do wouter e afins
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
global.location = { pathname: '/'+ (pathname === 'index' ? '' : pathname) }
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
global.window = { $duto: {
|
|
121
|
+
sroutes: meta.spages,
|
|
122
|
+
navigate: {},
|
|
123
|
+
route: { pattern: page.pattern, params: prop.routeParams, path: global.location.pathname },
|
|
124
|
+
} }
|
|
125
|
+
|
|
126
|
+
let html = ''
|
|
127
|
+
|
|
128
|
+
// if (!isClientOnly) { // TODO: fazer funcionar com `use client`
|
|
129
|
+
try {
|
|
130
|
+
let el = h(mod.default, allProps as any)
|
|
131
|
+
for (const layout of layouts)
|
|
132
|
+
el = h(layout.mod.default, null, el)
|
|
133
|
+
|
|
134
|
+
html = render(el)
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error(` SSR render failed for ${filePath}: ${e}`)
|
|
137
|
+
console.warn(e)
|
|
138
|
+
html = ''
|
|
139
|
+
}
|
|
140
|
+
// }
|
|
141
|
+
|
|
142
|
+
// TODO: refatorar isso... aqui dentro já é o SSG
|
|
143
|
+
const needsClient = hasGetProps || !!isClientOnly || islands.length > 0
|
|
144
|
+
|
|
145
|
+
// console.error(page.fileName)
|
|
146
|
+
// console.error('needsClient: ', needsClient)
|
|
147
|
+
|
|
148
|
+
const assets = []
|
|
149
|
+
for (const [depPath, dep] of meta.map.get(page.fullFilePath)!.deps)
|
|
150
|
+
assets.push(normalizeId(dep.path))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
html = buildHtml(assets, html) //, needsClient, allProps, pattern, islands)
|
|
154
|
+
// html = selfCloseEmptyTags(String(minify.minify(
|
|
155
|
+
// Buffer.from(buildHtml(assets, html, needsClient, allProps, pattern, islands)),
|
|
156
|
+
// {}
|
|
157
|
+
// // options != null ? options : {},
|
|
158
|
+
// )), 'i-i')
|
|
159
|
+
|
|
160
|
+
const outFile = join(
|
|
161
|
+
config.root,
|
|
162
|
+
config.build.outDir,
|
|
163
|
+
pathname +'.html'
|
|
164
|
+
)
|
|
165
|
+
const outHTML = minify.minify(
|
|
166
|
+
Buffer.from(html),
|
|
167
|
+
{}
|
|
168
|
+
// options != null ? options : {},
|
|
169
|
+
)
|
|
170
|
+
await mkdir(dirname(outFile), {recursive: true})
|
|
171
|
+
// await writeFile(outFile, outHTML, 'utf-8')
|
|
172
|
+
await Bun.write(outFile, outHTML)
|
|
173
|
+
|
|
174
|
+
const key = '/'+ pathname
|
|
175
|
+
if (page.isDynamic) {
|
|
176
|
+
const _key = '/'+ page.fileName
|
|
177
|
+
if (!meta.summary.has(_key)) {
|
|
178
|
+
const ddata = {
|
|
179
|
+
path: _key,
|
|
180
|
+
size: page.content.length,
|
|
181
|
+
sizeBr: (await brCompress(page.content)).length,
|
|
182
|
+
sizeJS: 0, // TODO
|
|
183
|
+
}
|
|
184
|
+
meta.summary.set(_key, { type: 1, children: new Map<string, SummaryMetadata>(), ...ddata})
|
|
185
|
+
meta.summary.set(_key + 2, { type: 2, ...ddata})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const summary = meta.summary.get(_key)!
|
|
189
|
+
summary.children?.set(key, {
|
|
190
|
+
type: 0,
|
|
191
|
+
path: key,
|
|
192
|
+
size: outHTML.length,
|
|
193
|
+
sizeBr: (await brCompress(outHTML)).length,
|
|
194
|
+
sizeJS: 0,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
} else {
|
|
198
|
+
meta.summary.set(key, {
|
|
199
|
+
type: 0,
|
|
200
|
+
path: key,
|
|
201
|
+
size: outHTML.length,
|
|
202
|
+
sizeBr: (await brCompress(outHTML)).length,
|
|
203
|
+
sizeJS: 0,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildHtml(
|
|
211
|
+
_assets: string[],
|
|
212
|
+
html: string,
|
|
213
|
+
// needsClient: boolean,
|
|
214
|
+
// props?: StaticProps,
|
|
215
|
+
// routePattern?: string,
|
|
216
|
+
// islands?: Island[],
|
|
217
|
+
): string {
|
|
218
|
+
if (!(html.includes('<body') || html.includes('<html') || html.includes('<head')))
|
|
219
|
+
html = wrapHtml(html)
|
|
220
|
+
|
|
221
|
+
if (!DOCTYPE_RE.test(html))
|
|
222
|
+
html = '<!DOCTYPE html>'+ html
|
|
223
|
+
|
|
224
|
+
// const pageProps = needsClient
|
|
225
|
+
// ? `<script>
|
|
226
|
+
// window.__PROPS__ = ${JSON.stringify(props ?? {})};
|
|
227
|
+
// </script>`
|
|
228
|
+
// : ''
|
|
229
|
+
|
|
230
|
+
const assets = [
|
|
231
|
+
':duto/client/app.tsx',
|
|
232
|
+
// ..._assets,
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
const prepend = assets.map(asset => {
|
|
236
|
+
const href = getAsset(asset)
|
|
237
|
+
return href ? `<link href="${href}" rel=stylesheet>` : null
|
|
238
|
+
}).filter(Boolean).join('')
|
|
239
|
+
// + `<style>d-b,d-p,i-d,i-s{display:contents}</style>`
|
|
240
|
+
// + `<style>d,d-slot,d-s-slot{display:contents}</style>`
|
|
241
|
+
|
|
242
|
+
// d>*,d-slot>*,d-s-slot>*{display:revert}
|
|
243
|
+
|
|
244
|
+
// d {
|
|
245
|
+
// display: block;
|
|
246
|
+
// box-sizing: border-box;
|
|
247
|
+
// }
|
|
248
|
+
|
|
249
|
+
// /* Herda todos os estilos padrão de div */
|
|
250
|
+
// d:not([hidden]) {
|
|
251
|
+
// display: block;
|
|
252
|
+
// }
|
|
253
|
+
|
|
254
|
+
// /* Remove qualquer comportamento de display:contents */
|
|
255
|
+
// d, d-slot, d-s-slot {
|
|
256
|
+
// display: block !important;
|
|
257
|
+
// }
|
|
258
|
+
|
|
259
|
+
// d *,
|
|
260
|
+
// d-slot *,
|
|
261
|
+
// d-s-slot * {
|
|
262
|
+
// visibility: visible !important;
|
|
263
|
+
// opacity: 1 !important;
|
|
264
|
+
// }
|
|
265
|
+
// </style>
|
|
266
|
+
|
|
267
|
+
// <script>class DDiv extends HTMLElement {
|
|
268
|
+
// constructor() {
|
|
269
|
+
// super();
|
|
270
|
+
// }
|
|
271
|
+
// }
|
|
272
|
+
|
|
273
|
+
// if (typeof customElements !== 'undefined' && !customElements.get('d')) {
|
|
274
|
+
// customElements.define('d', DDiv);
|
|
275
|
+
// }</script>
|
|
276
|
+
|
|
277
|
+
if (html.includes('</head>')) {
|
|
278
|
+
html = html.replace('</head>', prepend +'</head>')
|
|
279
|
+
} else if (html.includes('<head>')) {
|
|
280
|
+
html = html.replace('<head>', '<head>'+ prepend)
|
|
281
|
+
} else {
|
|
282
|
+
html = prepend + html
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// const append = pageProps + assets.map(file => {
|
|
286
|
+
const append = assets.map(file => {
|
|
287
|
+
const src = getChunk(file)
|
|
288
|
+
return src ? `<script src="${src}" type=module></script>` : null
|
|
289
|
+
}).filter(Boolean).join('')
|
|
290
|
+
|
|
291
|
+
if (html.includes('</body>'))
|
|
292
|
+
return html.replace('</body>', `${append}</body>`)
|
|
293
|
+
|
|
294
|
+
return html + append
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function wrapHtml(content: string) {
|
|
298
|
+
return `<html>
|
|
299
|
+
<head>
|
|
300
|
+
<meta charset=utf-8>
|
|
301
|
+
<meta content="width=device-width,initial-scale=1" name=viewport>
|
|
302
|
+
</head>
|
|
303
|
+
<body hidden>
|
|
304
|
+
${content}
|
|
305
|
+
</body>
|
|
306
|
+
</html>`
|
|
307
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { h } from 'preact'
|
|
2
|
+
import { renderToString } from 'preact-render-to-string'
|
|
3
|
+
import { IMPORT, newID } from 't0n'
|
|
4
|
+
|
|
5
|
+
import { getEntry } from './assets'
|
|
6
|
+
import { makeByteToCharMap, extractPropsFromJSX } from './ast'
|
|
7
|
+
|
|
8
|
+
import type { ImportUsage, IslandMeta } from './types'
|
|
9
|
+
|
|
10
|
+
function serializeProps(props: Record<string, unknown>) {
|
|
11
|
+
return JSON.stringify(props)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function htmlEncodeProps(serialized: string) {
|
|
15
|
+
return serialized
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function renderIslandSSR(
|
|
23
|
+
component: unknown,
|
|
24
|
+
props: Record<string, unknown>,
|
|
25
|
+
children?: unknown,
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
try {
|
|
28
|
+
return renderToString(h(component as never, props as never, children as never))
|
|
29
|
+
} catch {
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildIslandTag(meta: IslandMeta) {
|
|
35
|
+
return `<i-s id="${meta.id}" url="${getEntry(meta.url)}"${meta.export === 'default' ? '' : ` export="${meta.export}"`}${meta.props === '{}' ? '' : ` props="${meta.props}"`}>${meta.ssrHtml}</i-s>`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replaces in "sourceCode" each occurrence of imported components
|
|
40
|
+
* from "importSource" by hydrator-compatible <island> tags.
|
|
41
|
+
*
|
|
42
|
+
* @param sourceCode TSX source code
|
|
43
|
+
* @param importSource The target module (e.g. '@/components/counter2')
|
|
44
|
+
* @param opts Resolution callbacks
|
|
45
|
+
* @returns Code with replacements applied
|
|
46
|
+
*/
|
|
47
|
+
export async function replaceIslands(
|
|
48
|
+
code: string,
|
|
49
|
+
usages: ImportUsage[],
|
|
50
|
+
) {
|
|
51
|
+
if (!usages.length) return code
|
|
52
|
+
|
|
53
|
+
const b2c = makeByteToCharMap(code)
|
|
54
|
+
const toChar = (byteOffset: number) => b2c[byteOffset - 1]
|
|
55
|
+
|
|
56
|
+
const replacements: Array<{ start: number; end: number; replacement: string }> = []
|
|
57
|
+
|
|
58
|
+
for (const importUsage of usages) {
|
|
59
|
+
const component = await IMPORT(importUsage.source, importUsage.imported)
|
|
60
|
+
|
|
61
|
+
// schedule removal of the import declaration (replacement with an empty string)
|
|
62
|
+
replacements.push({
|
|
63
|
+
start: toChar(importUsage.importStart),
|
|
64
|
+
end: toChar(importUsage.importEnd),
|
|
65
|
+
replacement: '',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// schedule the replacement of each JSX usage with `<island>`
|
|
69
|
+
for (const usage of importUsage.usages) {
|
|
70
|
+
if (usage.type !== 'jsx') continue
|
|
71
|
+
|
|
72
|
+
const props = extractPropsFromJSX(usage.node)
|
|
73
|
+
const ssrHtml = await renderIslandSSR(component, props)
|
|
74
|
+
|
|
75
|
+
const island = buildIslandTag({
|
|
76
|
+
id: newID(),
|
|
77
|
+
url: importUsage.source,
|
|
78
|
+
export: importUsage.imported,
|
|
79
|
+
props: htmlEncodeProps(serializeProps(props)),
|
|
80
|
+
ssrHtml,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
replacements.push({
|
|
84
|
+
start: toChar(usage.start),
|
|
85
|
+
end: toChar(usage.end),
|
|
86
|
+
replacement: island,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!replacements.length) return code
|
|
92
|
+
|
|
93
|
+
replacements.sort((a, b) => a.start - b.start)
|
|
94
|
+
|
|
95
|
+
const parts: string[] = []
|
|
96
|
+
let cursor = 0
|
|
97
|
+
|
|
98
|
+
for (const { start, end, replacement } of replacements) {
|
|
99
|
+
if (start < cursor) continue // overlapped, discards
|
|
100
|
+
if (start > cursor) parts.push(code.slice(cursor, start))
|
|
101
|
+
parts.push(replacement)
|
|
102
|
+
cursor = end
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cursor < code.length) parts.push(code.slice(cursor))
|
|
106
|
+
|
|
107
|
+
return parts.join('').replace(/\n{3,}/g, '\n\n')
|
|
108
|
+
}
|