Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions adex/src/plugins/client-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { dirname, join } from 'path'
import { readFileSync } from 'fs'
import { mergeConfig, build } from 'vite'
import preact from '@preact/preset-vite'
import { existsSync } from 'fs'
import { rm } from 'fs/promises'

/**
* Create a plugin for building the client in SSR mode
*
* @param {object} options - Options for client builder
* @param {boolean} [options.ssr=true] - Whether to enable SSR
* @param {boolean} [options.islands=false] - Whether to enable islands architecture
* @returns {import("vite").Plugin}
*/
export function createClientBuilder({ ssr = true, islands = false } = {}) {
let baseUrl = '/'
return {
name: 'adex-client-builder',
config(cfg) {
const out = cfg.build.outDir ?? 'dist'
return {
appType: 'custom',
build: {
write: !islands,
manifest: 'manifest.json',
outDir: join(out, 'client'),
rollupOptions: {
input: 'virtual:adex:client',
},
output: {
entryFileNames: '[name]-[hash].js',
format: 'esm',
},
},
}
},
configResolved(cfg) {
baseUrl = cfg.base
return
},
generateBundle(opts, bundle) {
let clientEntryPath
for (const key in bundle) {
if (
['_virtual_adex_client', '_app'].includes(bundle[key].name) &&
'isEntry' in bundle[key] &&
bundle[key].isEntry
) {
clientEntryPath = key
}
}

const links = [
// @ts-expect-error Vite types don't include viteMetadata but it exists at runtime
...(bundle[clientEntryPath]?.viteMetadata?.importedCss ?? new Set()),
].map(d => {
return `<link rel="stylesheet" href="${join(baseUrl, d)}" />`
})

if (!ssr) {
this.emitFile({
type: 'asset',
fileName: 'index.html',
source: `<html>
<head>
${links.join('\n')}
</head>
<body>
<div id="app"></div>
<script src="${join(baseUrl, clientEntryPath)}" type="module"></script>
</body>
</html>`,
})
}
},
}
}

/**
* Create a build preparation plugin
*
* @param {object} options - Configuration options
* @param {boolean} [options.islands=false] - Whether to enable islands architecture
* @returns {import("vite").Plugin}
*/
export function createBuildPrep({ islands = false }) {
return {
name: 'adex-build-prep',
apply: 'build',
async configResolved(config) {
if (!islands) return

// Making it order safe
const outDirNormalized = config.build.outDir.endsWith('/server')
? dirname(config.build.outDir)
: config.build.outDir

// remove the `client` dir if islands are on,
// we don't generate the client assets and
// their existence adds the client entry into the bundle
const clientDir = join(outDirNormalized, 'client')
if (!existsSync(clientDir)) return
await rm(clientDir, {
recursive: true,
force: true,
})
},
}
}
99 changes: 99 additions & 0 deletions adex/src/plugins/dev-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { resolve } from 'path'

/**
* Create a development server plugin
*
* @param {object} options - Configuration options
* @param {boolean} [options.islands=false] - Whether to enable islands architecture
* @returns {import("vite").Plugin}
*/
export function createDevServer({ islands = false } = {}) {
const devCSSMap = new Map()
let cfg

return {
name: 'adex-dev-server',
apply: 'serve',
enforce: 'pre',

config() {
return {
ssr: {
noExternal: ['adex/app'],
},
}
},

configResolved(_cfg) {
cfg = _cfg
},

async resolveId(id, importer, meta) {
if (id.endsWith('.css')) {
if (!importer) return
const importerFromRoot = importer.replace(resolve(cfg.root), '')
const resolvedCss = await this.resolve(id, importer, meta)
if (resolvedCss) {
devCSSMap.set(
importerFromRoot,
(devCSSMap.get(importer) ?? []).concat(resolvedCss.id)
)
}
return
}
},

configureServer(server) {
return () => {
server.middlewares.use(async function (req, res, next) {
const module = await server.ssrLoadModule('virtual:adex:handler')
if (!module) {
return next()
}

try {
const { html, serverHandler, pageRoute } = await module.handler(
req,
res
)

if (serverHandler) {
return serverHandler(req, res)
}

const cssLinks = devCSSMap.get(pageRoute) ?? []
let renderedHTML = html.replace(
'</head>',
`
<link rel="preload" href="/virtual:adex:global.css" as="style" onload="this.rel='stylesheet'" />
${cssLinks.map(d => {
return `<link rel="preload" href="/${d}" as="style" onload="this.rel='stylesheet'"/>`
})}
</head>
`
)

if (!islands) {
renderedHTML = html.replace(
'</body>',
`<script type='module' src="/virtual:adex:client"></script></body>`
)
}

const finalRenderedHTML = await server.transformIndexHtml(
req.url,
renderedHTML
)

res.setHeader('content-type', 'text/html')
res.write(finalRenderedHTML)
return res.end()
} catch (err) {
server.ssrFixStacktrace(err)
next(err)
}
})
}
},
}
}
65 changes: 65 additions & 0 deletions adex/src/plugins/guard-plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { resolve, dirname, join } from 'path'
import { fileURLToPath } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const cwd = process.cwd()

/**
* Create guard plugins to prevent misuse of environment variables
*
* @returns {import("vite").Plugin[]}
*/
export function createGuardPlugins() {
return [
{
name: 'adex-guard-env',
enforce: 'pre',
async transform(code, id) {
// ignore usage of `process.env` in node_modules
// Still risky but hard to do anything about
const nodeMods = resolve(cwd, 'node_modules')
if (id.startsWith(nodeMods)) return

// ignore usage of `process.env` in `adex/env`
const envLoadId = await this.resolve('adex/env')
if (id === envLoadId?.id) return

if (code.includes('process.env')) {
this.error(
'Avoid using `process.env` to access environment variables and secrets. Use `adex/env` instead'
)
}
},
writeBundle() {
const pagesPath = resolve(cwd, 'src/pages')
const info = this.getModuleInfo('adex/env')
const viteRef = this

/**
* Check the import tree to ensure env is not used on client-side
* @param {string} importPath - Path of the module to check
* @param {string[]} importStack - Stack of imports leading to this module
*/
function checkTree(importPath, importStack = []) {
if (importPath.startsWith(pagesPath)) {
throw new Error(
`Cannot use/import \`adex/env\` on the client side, importerStack: ${importStack.join(' -> ')}`
)
}

// Get all importers of this module and check recursively
const moduleInfo = viteRef.getModuleInfo(importPath)
if (moduleInfo && moduleInfo.importers) {
moduleInfo.importers.forEach(d =>
checkTree(d, [...importStack, importPath, d])
)
}
}

if (info) {
info.importers.forEach(i => checkTree(i))
}
},
},
]
}
Loading
Loading