-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: typescript assets in 11ty dev server and ssr (#1992)
* chore: prepare decorators for standard decorators * chore: wip 11ty dev server ts transform * perf: try to improve render perf * docs: dev server transform typescript * docs: try to fix lit-ssr render * docs: try to fix ssr render * docs: try to fix ssr render * chore: revert 99b1255 * docs: fix ssr stuff * docs: fix ssr hydration mismatches via brute force * fix: ssr controller * docs: mollify tsc * docs: order of ops * docs: ooo * docs: order of operations * style: lint * style: lint * chore: wireit scripts * docs: uxdot els * docs: uxdot els * docs: fix fresh build * style: minor refactor * docs: remove unused dependency we can use esbuild instead of swc * docs: repeat builds * docs: move ssr plugin into rhds plugin correct the order of operations when cleaning / building * chore: juggle tsconfig settings * chore: fix linter * chore: patch jspm to fix dev server issues
- Loading branch information
1 parent
b684e15
commit b371d66
Showing
29 changed files
with
1,116 additions
and
378 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v22.7.0 | ||
v23.2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { transform } from '@pwrs/lit-css'; | ||
import { readFile } from 'node:fs/promises'; | ||
import { fileURLToPath } from 'node:url'; | ||
|
||
interface HookContext { | ||
source: string; | ||
format: 'module' | 'commonjs' | 'wasm' | 'json'; | ||
} | ||
|
||
type LoadFunction = (url: string, context: HookContext) => Promise<HookContext>; | ||
|
||
const cache = new Map<string, string>(); | ||
|
||
export async function load(url: string, context: HookContext, nextLoad: LoadFunction) { | ||
if (url.endsWith('.css')) { | ||
if (!cache.has(url)) { | ||
const filePath = fileURLToPath(new URL(url)); | ||
const css = await readFile(filePath, 'utf8'); | ||
cache.set(url, await transform({ css, filePath })); | ||
} | ||
const format = 'module'; | ||
const source = cache.get(url); | ||
return { source, shortCircuit: true, format }; | ||
} else { | ||
return nextLoad(url, context); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,113 +1,103 @@ | ||
/** | ||
* @license based on code from eleventy-plugin-lit | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
import type { EleventyPage } from '@11ty/eleventy/src/UserConfig.js'; | ||
import type { UserConfig } from '@11ty/eleventy'; | ||
import { dirname, resolve } from 'node:path'; | ||
import { fileURLToPath, pathToFileURL } from 'node:url'; | ||
import { Worker } from 'node:worker_threads'; | ||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
import { readFile, writeFile } from 'node:fs/promises'; | ||
|
||
interface Options { | ||
componentModules?: string[]; | ||
} | ||
import { Piscina } from 'piscina'; | ||
import tsBlankSpace from 'ts-blank-space'; | ||
import chalk from 'chalk'; | ||
|
||
// Lit SSR includes comment markers to track the outer template from | ||
// the template we've generated here, but it's not possible for this | ||
// outer template to be hydrated, so they serve no purpose. | ||
function trimOuterMarkers(renderedContent: string) { | ||
return renderedContent | ||
.replace(/^((<!--[^<>]*-->)|(<\?>)|\s)+/, '') | ||
.replace(/((<!--[^<>]*-->)|(<\?>)|\s)+$/, ''); | ||
} | ||
import { register } from 'node:module'; | ||
|
||
export default function(eleventyConfig: UserConfig, opts?: Options) { | ||
const { componentModules } = opts ?? {}; | ||
if (componentModules === undefined || componentModules.length === 0) { | ||
// If there are no component modules, we could never have anything to | ||
// render. | ||
return; | ||
} | ||
export interface RenderRequestMessage { | ||
content: string; | ||
page: Pick<EleventyPage, 'inputPath' | 'outputPath'>; | ||
} | ||
|
||
const resolvedComponentModules = componentModules.map(module => | ||
pathToFileURL(resolve(process.cwd(), module)).href); | ||
export interface RenderResponseMessage { | ||
page: Pick<EleventyPage, 'inputPath' | 'outputPath'>; | ||
rendered?: string; | ||
durationMs: number; | ||
} | ||
|
||
let worker: Worker; | ||
interface Options { | ||
componentModules?: string[]; | ||
/** path from project root to tsconfig */ | ||
tsconfig?: string; | ||
} | ||
|
||
const requestIdResolveMap = new Map(); | ||
let requestId = 0; | ||
async function redactTSFileInPlace(path: string) { | ||
const inURL = new URL(path, import.meta.url); | ||
const outURL = new URL(path.replace('.ts', '.js'), import.meta.url); | ||
await writeFile(outURL, tsBlankSpace(await readFile(inURL, 'utf8')), 'utf8'); | ||
} | ||
|
||
eleventyConfig.on('eleventy.before', async function() { | ||
worker = new Worker(resolve(__dirname, './worker/worker.js')); | ||
register('./lit-css-node.ts', import.meta.url); | ||
|
||
worker.on('error', err => { | ||
// eslint-disable-next-line no-console | ||
console.error('Unexpected error while rendering lit component in worker thread', err); | ||
throw err; | ||
/** | ||
* Eleventy plugin to server-render lit elements directly from typescript source | ||
* @param eleventyConfig | ||
* @param opts | ||
*/ | ||
export default async function(eleventyConfig: UserConfig, opts?: Options) { | ||
const imports = opts?.componentModules ?? []; | ||
const tsconfig = opts?.tsconfig ?? './tsconfig.json'; | ||
|
||
let pool: Piscina; | ||
|
||
// If there are no component modules, we could never have anything to | ||
// render. | ||
if (imports?.length) { | ||
eleventyConfig.on('eleventy.before', async function() { | ||
await redactTSFileInPlace('./worker.ts'); | ||
const filename = new URL('worker.js', import.meta.url).pathname; | ||
pool = new Piscina({ | ||
filename, | ||
workerData: { | ||
imports, | ||
tsconfig, | ||
}, | ||
}); | ||
}); | ||
|
||
let requestResolve: (v?: unknown) => void; | ||
const requestPromise = new Promise(_resolve => { | ||
requestResolve = _resolve; | ||
eleventyConfig.on('eleventy.after', async function() { | ||
return pool.close(); | ||
}); | ||
|
||
worker.on('message', message => { | ||
switch (message.type) { | ||
case 'initialize-response': { | ||
requestResolve(); | ||
break; | ||
} | ||
eleventyConfig.addTransform('render-lit', async function(this, content) { | ||
const { outputPath, inputPath } = this.page; | ||
|
||
case 'render-response': { | ||
const { id, rendered } = message; | ||
const _resolve = requestIdResolveMap.get(id); | ||
if (_resolve === undefined) { | ||
throw new Error( | ||
'@lit-labs/eleventy-plugin-lit received invalid render-response message' | ||
); | ||
} | ||
_resolve(rendered); | ||
requestIdResolveMap.delete(id); | ||
break; | ||
} | ||
if (!outputPath.endsWith('.html')) { | ||
return content; | ||
} | ||
}); | ||
|
||
const message = { | ||
type: 'initialize-request', | ||
imports: resolvedComponentModules, | ||
}; | ||
|
||
worker.postMessage(message); | ||
await requestPromise; | ||
}); | ||
|
||
eleventyConfig.on('eleventy.after', async () => { | ||
await worker.terminate(); | ||
}); | ||
|
||
eleventyConfig.addTransform('render-lit', async function(this, content) { | ||
const { outputPath, inputPath, fileSlug } = this.page; | ||
if (!outputPath.endsWith('.html')) { | ||
return content; | ||
} | ||
|
||
const renderedContent: string = await new Promise(_resolve => { | ||
requestIdResolveMap.set(requestId, _resolve); | ||
const message = { | ||
type: 'render-request', | ||
id: requestId++, | ||
content, | ||
page: JSON.parse(JSON.stringify({ outputPath, inputPath, fileSlug })), | ||
}; | ||
worker.postMessage(message); | ||
const page = { outputPath, inputPath }; | ||
const message = await pool.run({ page, content }); | ||
if (message.rendered) { | ||
const { durationMs, rendered, page } = message; | ||
if (durationMs > 1000) { | ||
const color = | ||
durationMs > 5000 ? chalk.red | ||
: durationMs > 1000 ? chalk.yellow | ||
: durationMs > 100 ? chalk.blue | ||
: chalk.green; | ||
// eslint-disable-next-line no-console | ||
console.log(`${color(durationMs.toFixed(2).padEnd(8))} Rendered ${page.outputPath} in`); | ||
} | ||
return trimOuterMarkers(rendered); | ||
} else { | ||
return content; | ||
} | ||
}); | ||
} | ||
} | ||
|
||
const outerMarkersTrimmed = trimOuterMarkers(renderedContent); | ||
return outerMarkersTrimmed; | ||
}); | ||
}; | ||
// Lit SSR includes comment markers to track the outer template from | ||
// the template we've generated here, but it's not possible for this | ||
// outer template to be hydrated, so they serve no purpose. | ||
function trimOuterMarkers(renderedContent: string) { | ||
return renderedContent | ||
.replace(/^((<!--[^<>]*-->)|(<\?>)|\s)+/, '') | ||
.replace(/((<!--[^<>]*-->)|(<\?>)|\s)+$/, ''); | ||
} | ||
|
Oops, something went wrong.