vitrify 0.2.0 → 0.2.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.
@@ -0,0 +1,137 @@
1
+ import type {
2
+ FastifyPluginCallback,
3
+ FastifyRequest,
4
+ FastifyReply
5
+ } from 'fastify'
6
+ import fastifyStatic from 'fastify-static'
7
+ import { readFileSync } from 'fs'
8
+ // import { injectSsrContext } from '../helpers/ssr.js'
9
+ import type { ViteDevServer } from 'vite'
10
+ import type { SsrFunction } from '../../vitrify-config.js'
11
+
12
+ export interface FastifySsrOptions {
13
+ baseUrl?: string
14
+ provide?: (
15
+ req: FastifyRequest,
16
+ res: FastifyReply
17
+ ) => Promise<Record<string, unknown>>
18
+ vite?: ViteDevServer
19
+ cliDir?: URL
20
+ appDir?: URL
21
+ productName?: string
22
+ ssrFunctions?: SsrFunction[]
23
+ }
24
+
25
+ const fastifySsrPlugin: FastifyPluginCallback<FastifySsrOptions> = async (
26
+ fastify,
27
+ options,
28
+ done
29
+ ) => {
30
+ if (import.meta.env.MODE === 'development') {
31
+ if (!options.vite) throw new Error('Option vite cannot be undefined')
32
+ const middie = (await import('middie')).default
33
+ await fastify.register(middie)
34
+ fastify.use(options.vite.middlewares)
35
+
36
+ fastify.get('*', async (req, res) => {
37
+ try {
38
+ // const url = req.originalUrl
39
+ const url = req.raw.url
40
+ const ssrContext = {
41
+ req,
42
+ res
43
+ }
44
+ // always read fresh template in dev
45
+ // template = readFileSync(resolve('index.html'), 'utf-8')
46
+ const template = readFileSync(
47
+ new URL('index.html', options.cliDir)
48
+ ).toString()
49
+
50
+ // template = await vite.transformIndexHtml(url, template)
51
+ const entryUrl = new URL('ssr/entry-server.ts', options.cliDir).pathname
52
+ const render = (await options.vite!.ssrLoadModule(entryUrl)).render
53
+ let manifest
54
+ // TODO: https://github.com/vitejs/vite/issues/2282
55
+ try {
56
+ manifest = {}
57
+ } catch (e) {
58
+ manifest = {}
59
+ }
60
+
61
+ const [appHtml, preloadLinks] = await render(url, manifest, ssrContext)
62
+ const html = template
63
+ .replace(`<!--preload-links-->`, preloadLinks)
64
+ .replace(`<!--app-html-->`, appHtml)
65
+ .replace('<!--product-name-->', options.productName || 'Product name')
66
+
67
+ res.code(200)
68
+ res.type('text/html')
69
+ res.send(html)
70
+ // res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
71
+ } catch (e: any) {
72
+ console.error(e.stack)
73
+ options.vite && options.vite.ssrFixStacktrace(e)
74
+ res.code(500)
75
+ res.send(e.stack)
76
+ }
77
+ })
78
+ } else {
79
+ options.baseUrl = options.baseUrl || '/'
80
+ fastify.register(fastifyStatic, {
81
+ root: new URL('./dist/ssr/client', options.appDir).pathname,
82
+ wildcard: false,
83
+ index: false,
84
+ prefix: options.baseUrl
85
+ })
86
+
87
+ fastify.get(`${options.baseUrl}*`, async (req, res) => {
88
+ const url = req.raw.url
89
+ const provide = options.provide ? await options.provide(req, res) : {}
90
+ const ssrContext: Record<string, any> = {
91
+ req,
92
+ res,
93
+ provide
94
+ }
95
+
96
+ // template = readFileSync(new URL('../client/index.html', import.meta.url).pathname).toString()
97
+ // manifest = JSON.parse(readFileSync(new URL('../client/ssr-manifest.json', import.meta.url)).toString())
98
+ // render = (await import(new URL('./entry-server.mjs', import.meta.url).pathname)).render
99
+ const template = readFileSync(
100
+ new URL('./dist/ssr/client/index.html', options.appDir).pathname
101
+ ).toString()
102
+ const manifest = JSON.parse(
103
+ readFileSync(
104
+ new URL('./dist/ssr/client/ssr-manifest.json', options.appDir)
105
+ ).toString()
106
+ )
107
+ const render = (
108
+ await import(
109
+ new URL('./dist/ssr/server/entry-server.mjs', options.appDir).pathname
110
+ )
111
+ ).render
112
+
113
+ const [appHtml, preloadLinks] = await render(url, manifest, ssrContext)
114
+
115
+ if (!ssrContext.initialState) ssrContext.initialState = {}
116
+ ssrContext.initialState.provide = provide
117
+
118
+ let html = template
119
+ .replace(`<!--preload-links-->`, preloadLinks)
120
+ .replace(`<!--app-html-->`, appHtml)
121
+
122
+ if (options.ssrFunctions?.length) {
123
+ for (const ssrFunction of options.ssrFunctions) {
124
+ html = ssrFunction(html, ssrContext)
125
+ }
126
+ }
127
+
128
+ res.code(200)
129
+ res.type('text/html')
130
+ res.send(html)
131
+ })
132
+ }
133
+
134
+ done()
135
+ }
136
+
137
+ export { fastifySsrPlugin }
@@ -0,0 +1,49 @@
1
+ import { promises as fs } from 'fs'
2
+ import { routesToPaths } from '../../helpers/routes.js'
3
+ import type { SsrFunction } from '../../vitrify-config.js'
4
+
5
+ export const prerender = async ({
6
+ outDir,
7
+ templatePath,
8
+ manifestPath,
9
+ entryServerPath,
10
+ ssrFunctions
11
+ }: {
12
+ outDir: string
13
+ templatePath: string
14
+ manifestPath: string
15
+ entryServerPath: string
16
+ ssrFunctions: SsrFunction[]
17
+ }) => {
18
+ const promises = []
19
+ const template = (await fs.readFile(templatePath)).toString()
20
+ const manifest = await fs.readFile(manifestPath)
21
+ const { render, getRoutes } = await import(entryServerPath)
22
+ const routes = await getRoutes()
23
+ const paths = routesToPaths(routes).filter(
24
+ (i) => !i.includes(':') && !i.includes('*')
25
+ )
26
+ for (const url of paths) {
27
+ const filename =
28
+ (url.endsWith('/') ? 'index' : url.replace(/^\//g, '')) + '.html'
29
+ console.log(`Generating ${filename}`)
30
+ const ssrContext = {
31
+ req: { headers: {}, url },
32
+ res: {}
33
+ }
34
+ const [appHtml, preloadLinks] = await render(url, manifest, ssrContext)
35
+
36
+ let html = template
37
+ .replace(`<!--preload-links-->`, preloadLinks)
38
+ .replace(`<!--app-html-->`, appHtml)
39
+
40
+ if (ssrFunctions?.length) {
41
+ for (const ssrFunction of ssrFunctions) {
42
+ html = ssrFunction(html, ssrContext)
43
+ }
44
+ }
45
+
46
+ promises.push(fs.writeFile(outDir + filename, html, 'utf-8'))
47
+ }
48
+ return Promise.all(promises)
49
+ }
@@ -0,0 +1,38 @@
1
+ import type { FastifyInstance } from 'fastify'
2
+ import fastify from 'fastify'
3
+ import type { SsrFunction } from '../../vitrify-config.js'
4
+ // import { setup } from 'virtual:fastify-setup'
5
+ import { fastifySsrPlugin } from './fastify-ssr-plugin.js'
6
+ // import { getPkgJsonDir } from '../app-urls.js'
7
+
8
+ export const createApp = ({
9
+ setup,
10
+ appDir,
11
+ baseUrl,
12
+ ssrFunctions
13
+ }: {
14
+ setup: (fastify: FastifyInstance) => any
15
+ appDir: URL
16
+ baseUrl?: string
17
+ ssrFunctions?: SsrFunction[]
18
+ }) => {
19
+ const app = fastify({
20
+ logger: true
21
+ })
22
+
23
+ app.register(fastifySsrPlugin, {
24
+ baseUrl,
25
+ appDir,
26
+ ssrFunctions
27
+ })
28
+
29
+ setup(app)
30
+
31
+ return app
32
+ }
33
+
34
+ // const app = createApp({
35
+ // setup
36
+ // })
37
+
38
+ // app.listen(process.env.PORT || 3000, process.env.HOST || '127.0.0.1')
@@ -0,0 +1,142 @@
1
+ // https://github.com/quasarframework/quasar/blob/dev/app/lib/helpers/logger.js
2
+ import chalk from 'chalk'
3
+ const { bgGreen, green, inverse, bgRed, red, bgYellow, yellow } = chalk
4
+ import readline from 'readline'
5
+ import type { AddressInfo, Server } from 'net'
6
+ import type { ResolvedConfig, Logger } from 'vite'
7
+ import os from 'os'
8
+ import type { Hostname } from '../helpers/utils.js'
9
+ import { resolveHostname } from '../helpers/utils.js'
10
+
11
+ /**
12
+ * Main approach - App CLI related
13
+ */
14
+
15
+ const dot = '•'
16
+ const banner = 'App ' + dot
17
+ const greenBanner = green(banner)
18
+ const redBanner = red(banner)
19
+ const yellowBanner = yellow(banner)
20
+
21
+ export const clearConsole = process.stdout.isTTY
22
+ ? () => {
23
+ // Fill screen with blank lines. Then move to 0 (beginning of visible part) and clear it
24
+ const blank = '\n'.repeat(process.stdout.rows)
25
+ console.log(blank)
26
+ readline.cursorTo(process.stdout, 0, 0)
27
+ readline.clearScreenDown(process.stdout)
28
+ }
29
+ : () => {}
30
+
31
+ export const log = function (msg?: string) {
32
+ console.log(msg ? ` ${greenBanner} ${msg}` : '')
33
+ }
34
+
35
+ export const warn = function (msg?: string, pill?: string) {
36
+ if (msg !== void 0) {
37
+ const pillBanner = pill !== void 0 ? bgYellow.black('', pill, '') + ' ' : ''
38
+
39
+ console.warn(` ${yellowBanner} ⚠️ ${pillBanner}${msg}`)
40
+ } else {
41
+ console.warn()
42
+ }
43
+ }
44
+
45
+ export const fatal = function (msg?: string, pill?: string) {
46
+ if (msg !== void 0) {
47
+ const pillBanner = pill !== void 0 ? errorPill(pill) + ' ' : ''
48
+
49
+ console.error(`\n ${redBanner} ⚠️ ${pillBanner}${msg}\n`)
50
+ } else {
51
+ console.error()
52
+ }
53
+
54
+ process.exit(1)
55
+ }
56
+
57
+ /**
58
+ * Extended approach - Compilation status & pills
59
+ */
60
+
61
+ export const successPill = (msg?: string) => bgGreen.black('', msg, '')
62
+ export const infoPill = (msg?: string) => inverse('', msg, '')
63
+ export const errorPill = (msg?: string) => bgRed.white('', msg, '')
64
+ export const warningPill = (msg?: string) => bgYellow.black('', msg, '')
65
+
66
+ export const success = function (msg?: string, title = 'SUCCESS') {
67
+ console.log(` ${greenBanner} ${successPill(title)} ${green(dot + ' ' + msg)}`)
68
+ }
69
+ export const getSuccess = function (msg?: string, title?: string) {
70
+ return ` ${greenBanner} ${successPill(title)} ${green(dot + ' ' + msg)}`
71
+ }
72
+
73
+ export const info = function (msg?: string, title = 'INFO') {
74
+ console.log(` ${greenBanner} ${infoPill(title)} ${green(dot)} ${msg}`)
75
+ }
76
+ export const getInfo = function (msg?: string, title?: string) {
77
+ return ` ${greenBanner} ${infoPill(title)} ${green(dot)} ${msg}`
78
+ }
79
+
80
+ export const error = function (msg?: string, title = 'ERROR') {
81
+ console.log(` ${redBanner} ${errorPill(title)} ${red(dot + ' ' + msg)}`)
82
+ }
83
+ export const getError = function (msg?: string, title = 'ERROR') {
84
+ return ` ${redBanner} ${errorPill(title)} ${red(dot + ' ' + msg)}`
85
+ }
86
+
87
+ export const warning = function (msg?: string, title = 'WARNING') {
88
+ console.log(
89
+ ` ${yellowBanner} ${warningPill(title)} ${yellow(dot + ' ' + msg)}`
90
+ )
91
+ }
92
+ export const getWarning = function (msg?: string, title = 'WARNING') {
93
+ return ` ${yellowBanner} ${warningPill(title)} ${yellow(dot + ' ' + msg)}`
94
+ }
95
+
96
+ export function printHttpServerUrls(
97
+ server: Server,
98
+ config: ResolvedConfig
99
+ ): void {
100
+ const address = server.address()
101
+ const isAddressInfo = (x: any): x is AddressInfo => x?.address
102
+ if (isAddressInfo(address)) {
103
+ const hostname = resolveHostname(config.server.host)
104
+ const protocol = config.server.https ? 'https' : 'http'
105
+ printServerUrls(
106
+ hostname,
107
+ protocol,
108
+ address.port,
109
+ config.base,
110
+ config.logger.info
111
+ )
112
+ }
113
+ }
114
+
115
+ function printServerUrls(
116
+ hostname: Hostname,
117
+ protocol: string,
118
+ port: number,
119
+ base: string,
120
+ info: Logger['info']
121
+ ): void {
122
+ if (hostname.host === '127.0.0.1') {
123
+ const url = `${protocol}://${hostname.name}:${chalk.bold(port)}${base}`
124
+ info(` > Local: ${chalk.cyan(url)}`)
125
+ if (hostname.name !== '127.0.0.1') {
126
+ info(` > Network: ${chalk.dim('use `--host` to expose')}`)
127
+ }
128
+ } else {
129
+ Object.values(os.networkInterfaces())
130
+ .flatMap((nInterface) => nInterface ?? [])
131
+ .filter((detail) => detail && detail.address && detail.family === 'IPv4')
132
+ .map((detail) => {
133
+ const type = detail.address.includes('127.0.0.1')
134
+ ? 'Local: '
135
+ : 'Network: '
136
+ const host = detail.address.replace('127.0.0.1', hostname.name)
137
+ const url = `${protocol}://${host}:${chalk.bold(port)}${base}`
138
+ return ` > ${type} ${chalk.cyan(url)}`
139
+ })
140
+ .forEach((msg) => info(msg))
141
+ }
142
+ }
@@ -0,0 +1,29 @@
1
+ // https://github.com/antfu/vite-ssg/blob/462722203dade87365a519d847fcd881ee16a7f4/src/node/utils.ts#L13
2
+ import type { RouteRecordRaw } from 'vue-router'
3
+
4
+ export const routesToPaths = (routes?: RouteRecordRaw[]) => {
5
+ if (!routes) return ['/']
6
+
7
+ const paths: Set<string> = new Set()
8
+
9
+ const getPaths = (routes: RouteRecordRaw[], prefix = '') => {
10
+ // remove trailing slash
11
+ prefix = prefix.replace(/\/$/g, '')
12
+ for (const route of routes) {
13
+ let path = route.path
14
+ // check for leading slash
15
+ if (route.path) {
16
+ path =
17
+ prefix && !route.path.startsWith('/')
18
+ ? `${prefix}/${route.path}`
19
+ : route.path
20
+
21
+ paths.add(path)
22
+ }
23
+ if (Array.isArray(route.children)) getPaths(route.children, path)
24
+ }
25
+ }
26
+
27
+ getPaths(routes)
28
+ return [...paths]
29
+ }
@@ -0,0 +1,52 @@
1
+ export const injectSsrContext = (html: string, ssrContext: Record<string, any>) => html.replace(
2
+ /(<html[^>]*)(>)/i,
3
+ (found, start, end) => {
4
+ let matches
5
+
6
+ matches = found.match(/\sdir\s*=\s*['"]([^'"]*)['"]/i)
7
+ if (matches) {
8
+ start = start.replace(matches[0], '')
9
+ }
10
+
11
+ matches = found.match(/\slang\s*=\s*['"]([^'"]*)['"]/i)
12
+ if (matches) {
13
+ start = start.replace(matches[0], '')
14
+ }
15
+
16
+ return `${start} ${ssrContext._meta.htmlAttrs || ''} ${end}`
17
+ }
18
+ )
19
+ .replace(
20
+ /(<head[^>]*)(>)/i,
21
+ (_, start, end) => `${start}${end}${ssrContext._meta.headTags || ''}`
22
+ )
23
+ .replace(
24
+ /(<\/head>)/i,
25
+ (_, tag) => `${ssrContext._meta.resourceStyles || ''}${ssrContext._meta.endingHeadTags || ''}${tag}`
26
+ )
27
+ .replace(
28
+ /(<body[^>]*)(>)/i,
29
+ (found, start, end) => {
30
+ let classes = ssrContext._meta.bodyClasses || ''
31
+
32
+ const matches = found.match(/\sclass\s*=\s*['"]([^'"]*)['"]/i)
33
+
34
+ if (matches) {
35
+ if (matches[1].length > 0) {
36
+ classes += ` ${matches[1]}`
37
+ }
38
+ start = start.replace(matches[0], '')
39
+ }
40
+
41
+ return `${start} class="${classes.trim()}" ${ssrContext._meta.bodyAttrs || ''}${end}${ssrContext._meta.bodyTags || ''}`
42
+ }
43
+ )
44
+ .replace(`<!--initial-state-->`, `<script>
45
+ window.__INITIAL_STATE__ = ${JSON.stringify(ssrContext.initialState)}
46
+ </script>
47
+ `)
48
+
49
+ export const injectInitialState = (html: string, ssrContext: Record<string, any>) =>
50
+ html.replace(`<!--initial-state-->`, `<script>
51
+ window.__INITIAL_STATE__ = ${JSON.stringify(ssrContext.initialState)}
52
+ </script>`)
@@ -0,0 +1,37 @@
1
+ // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/logger.ts
2
+ export interface Hostname {
3
+ // undefined sets the default behaviour of server.listen
4
+ host: string | undefined
5
+ // resolve to localhost when possible
6
+ name: string
7
+ }
8
+
9
+ export function resolveHostname(
10
+ optionsHost: string | boolean | undefined
11
+ ): Hostname {
12
+ let host: string | undefined
13
+ if (
14
+ optionsHost === undefined ||
15
+ optionsHost === false ||
16
+ optionsHost === 'localhost'
17
+ ) {
18
+ // Use a secure default
19
+ host = '127.0.0.1'
20
+ } else if (optionsHost === true) {
21
+ // If passed --host in the CLI without arguments
22
+ host = undefined // undefined typically means 0.0.0.0 or :: (listen on all IPs)
23
+ } else {
24
+ host = optionsHost
25
+ }
26
+
27
+ // Set host name to localhost when possible, unless the user explicitly asked for '127.0.0.1'
28
+ const name =
29
+ (optionsHost !== '127.0.0.1' && host === '127.0.0.1') ||
30
+ host === '0.0.0.0' ||
31
+ host === '::' ||
32
+ host === undefined
33
+ ? 'localhost'
34
+ : host
35
+
36
+ return { host, name }
37
+ }