Skip to content

Commit

Permalink
feat(vue): introduce $app/hooks and $app/stores virtual modules
Browse files Browse the repository at this point in the history
  • Loading branch information
galvez committed Oct 14, 2024
1 parent 7ea6396 commit ba02df8
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 115 deletions.
3 changes: 3 additions & 0 deletions packages/fastify-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
},
"version": "8.0.0-alpha.2",
"optionalDependencies": {
"@fastify/htmx": "workspace:^",
"@fastify/react": "workspace:^",
"@fastify/vue": "workspace:^",
"vite": "^5.4.8"
},
"dependencies": {
Expand Down
82 changes: 82 additions & 0 deletions packages/fastify-vue/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,67 @@
import { inject } from 'vue'
import { useRoute, createMemoryHistory, createWebHistory } from 'vue-router'

export const isServer = import.meta.env.SSR
export const createHistory = isServer ? createMemoryHistory : createWebHistory
export const serverRouteContext = Symbol('serverRouteContext')
export const routeLayout = Symbol('routeLayout')

export function useRouteContext () {
if (isServer) {
return inject(serverRouteContext)
}
return useRoute().meta[serverRouteContext]
}

export function createBeforeEachHandler ({ routeMap, ctxHydration, head }, layout) {
return async function beforeCreate (to) {
// The client-side route context
const ctx = routeMap[to.matched[0].path]
// Indicates whether or not this is a first render on the client
ctx.firstRender = ctxHydration.firstRender

ctx.state = ctxHydration.state
ctx.actions = ctxHydration.actions

// Update layoutRef
layout.value = ctx.layout ?? 'default'

// If it is, take server context data from hydration and return immediately
if (ctx.firstRender) {
ctx.data = ctxHydration.data
ctx.head = ctxHydration.head
// Ensure this block doesn't run again during client-side navigation
ctxHydration.firstRender = false
to.meta[serverRouteContext] = ctx
return
}

// If we have a getData function registered for this route
if (ctx.getData) {
try {
ctx.data = await jsonDataFetch(to.fullPath)
} catch (error) {
ctx.error = error
}
}
// Note that ctx.loader() at this point will resolve the
// memoized module, so there's barely any overhead
const { getMeta, onEnter } = await ctx.loader()
if (ctx.getMeta) {
head.update(await getMeta(ctx))
}
if (ctx.onEnter) {
const updatedData = await onEnter(ctx)
if (updatedData) {
if (!ctx.data) {
ctx.data = {}
}
Object.assign(ctx.data, updatedData)
}
}
to.meta[serverRouteContext] = ctx
}
}

export async function hydrateRoutes (fromInput) {
let from = fromInput
Expand Down Expand Up @@ -27,3 +91,21 @@ function memoImport (func) {
return func[kFuncValue]
}
}

export async function jsonDataFetch (path) {
const response = await fetch(`/-/data${path}`)
let data
let error
try {
data = await response.json()
} catch (err) {
error = err
}
if (data?.statusCode === 500) {
throw new Error(data.message)
}
if (error) {
throw error
}
return data
}
3 changes: 3 additions & 0 deletions packages/fastify-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
},
"dependencies": {
"@fastify/vite": "workspace:^",
"acorn": "^8.12.1",
"acorn-walk": "^8.3.4",
"devalue": "latest",
"html-rewriter-wasm": "^0.4.1",
"unihead": "^0.8.0",
"vite": "^5.4.8",
"vue": "^3.5.8",
"vue-router": "^4.4.5",
"youch": "^3.3.4"
Expand Down
68 changes: 68 additions & 0 deletions packages/fastify-vue/parsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as acorn from 'acorn'
import * as walk from 'acorn-walk'

export function parseStateKeys (code) {
const ast = acorn.parse(code, { sourceType: 'module', ecmaVersion: 2020 })

let objectKeys = []

walk.simple(ast, {
ExportNamedDeclaration(node) {
if (node.declaration.type === 'FunctionDeclaration') {
for (const subNode of node.declaration.body.body) {
if (subNode.type === 'ReturnStatement' && subNode.argument.type === 'ObjectExpression') {
objectKeys = extractObjectKeys(subNode.argument)
}
}
} else if (node.declaration.type === 'VariableDeclaration') {
for (const subNode of node.declaration.declarations) {
if (
subNode.type === 'VariableDeclarator' &&
subNode.init.type === 'ArrowFunctionExpression' &&
subNode.init.body.type === 'ObjectExpression'
) {
objectKeys = extractObjectKeys(subNode.init.body)
}
}
}
}
})

return objectKeys
}

function extractObjectKeys(node) {
const keys = []
for (const prop of node.properties) {
if (prop.key && prop.key.type === 'Identifier') {
keys.push(prop.key.name)
}
}
return keys
}

// Example usage
const code1 = `export function state () {
return {
user: {
authenticated: false,
},
todoList: null,
}
}`;

const code2 = `export const state = () => ({
user: {
authenticated: false,
},
todoList: null,
})
if (1) {
const state = () => {
}
}
`;

console.log(parseStateKeys(code1)); // ['user', 'todoList']
console.log(parseStateKeys(code2)); // ['user', 'todoList']
87 changes: 80 additions & 7 deletions packages/fastify-vue/plugin.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { fileURLToPath } = require('url')

function viteFastifyVue (config = {}) {
const prefix = /^\/:/
const nprefix = /^\$app\//
const virtualRoot = resolve(__dirname, 'virtual')
const virtualModules = [
'mount.js',
Expand All @@ -17,7 +18,9 @@ function viteFastifyVue (config = {}) {
'layouts/',
'context.js',
'core.js',
'server.js'
'server.js',
'stores',
'hooks'
]
virtualModules.includes = function (virtual) {
if (!virtual) {
Expand All @@ -44,10 +47,17 @@ function viteFastifyVue (config = {}) {
}

function loadVirtualModule (virtual) {
if (!virtual.includes('.')) {
const code = readFileSync(resolve(virtualRoot, `${virtual}.js`), 'utf8')
return {
code,
map: null,
}
}
if (!virtualModules.includes(virtual)) {
return
}
let code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
const code = readFileSync(resolve(virtualRoot, virtual), 'utf8')
return {
code,
map: null,
Expand All @@ -61,6 +71,8 @@ function viteFastifyVue (config = {}) {
.replace(/-/g, '\\x2d')
}

let parseStateKeys

return {
name: 'vite-plugin-fastify-vue',
config (config, { isSsrBuild, command }) {
Expand Down Expand Up @@ -92,20 +104,81 @@ function viteFastifyVue (config = {}) {
viteProjectRoot = config.root
},
async resolveId (id) {
const [, virtual] = id.split(prefix)
let _prefix = prefix
if (nprefix.test(id)) {
_prefix = nprefix
}
const [, virtual] = id.split(_prefix)
if (virtual) {
const override = await loadVirtualModuleOverride(virtual)
const override = loadVirtualModuleOverride(virtual)
if (override) {
return override
}
return id
}
},
load (id) {
const [, virtual] = id.split(prefix)
return loadVirtualModule(virtual)
async load (id) {
let _prefix = prefix
if (nprefix.test(id)) {
_prefix = nprefix
}
const [, virtual] = id.split(_prefix)
if (virtual) {
if (virtual === 'stores') {
if (!parseStateKeys) {
await import('./parsing.js').then((m) => {
parseStateKeys = m.parseStateKeys
})
}
const { id } = await this._container.moduleGraph.resolveId('/:context.js')
const keys = parseStateKeys(readFileSync(id, 'utf8'))
return generateStores(keys)
}
return loadVirtualModule(virtual)
}
},
}
}

function generateStores(keys) {
let code = `
import { useRouteContext } from '@fastify/vue/client'
function storeGetter (proxy, prop) {
if (!proxy.context) {
proxy.context = useRouteContext()
}
if (prop === 'state') {
return proxy.context.state
}
let method
if (method = proxy.context.actions[proxy.key][prop]) {
if (!proxy.wrappers[prop]) {
proxy.wrappers[prop] = (...args) => {
return method(proxy.context.state, ...args)
}
}
return proxy.wrappers[prop]
} else {
throw new Error('Store action \`\${prop}\` not implemented.')
}
}
`
for (const key of keys) {
code += `
export const ${key} = new Proxy({
key: '${key}',
wrappers: {},
context: null,
}, {
get: storeGetter
})
`
}
return {
code,
map: null
}
}

module.exports = viteFastifyVue
82 changes: 0 additions & 82 deletions packages/fastify-vue/virtual/core.js

This file was deleted.

Loading

0 comments on commit ba02df8

Please sign in to comment.