Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: typescript assets in 11ty dev server and ssr #1992

Merged
merged 46 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
99b1255
chore: prepare decorators for standard decorators
bennypowers Oct 15, 2024
2067482
chore: wip 11ty dev server ts transform
bennypowers Oct 15, 2024
f7290c9
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Oct 16, 2024
48e3599
perf: try to improve render perf
bennypowers Oct 16, 2024
f5209d5
docs: dev server transform typescript
bennypowers Oct 20, 2024
d53f20a
docs: try to fix lit-ssr render
bennypowers Oct 20, 2024
2b1973d
docs: try to fix ssr render
bennypowers Oct 22, 2024
fc7a27f
docs: try to fix ssr render
bennypowers Oct 22, 2024
c6db830
chore: revert 99b12554
bennypowers Oct 22, 2024
1ee9791
docs: fix ssr stuff
bennypowers Oct 22, 2024
225d332
docs: fix ssr hydration mismatches via brute force
bennypowers Oct 22, 2024
fbd64ae
fix: ssr controller
bennypowers Oct 22, 2024
d12149d
docs: mollify tsc
bennypowers Oct 22, 2024
3bab444
docs: order of ops
bennypowers Oct 22, 2024
8ca66c7
docs: ooo
bennypowers Oct 22, 2024
a104d33
docs: order of operations
bennypowers Oct 22, 2024
02d6c72
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Oct 22, 2024
6272989
style: lint
bennypowers Oct 22, 2024
9952ac3
style: lint
bennypowers Oct 22, 2024
05264f1
chore: wireit scripts
bennypowers Oct 22, 2024
3073734
docs: uxdot els
bennypowers Oct 22, 2024
3bf45ba
docs: uxdot els
bennypowers Oct 22, 2024
dfb850d
docs: fix fresh build
bennypowers Oct 23, 2024
d9bc89f
style: minor refactor
bennypowers Oct 23, 2024
65ffc37
docs: remove unused dependency
bennypowers Oct 23, 2024
0602821
docs: repeat builds
bennypowers Oct 23, 2024
2986c3f
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Oct 27, 2024
9cd8407
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Oct 29, 2024
666d667
docs: move ssr plugin into rhds plugin
bennypowers Oct 29, 2024
8745a39
chore: juggle tsconfig settings
bennypowers Oct 29, 2024
a7d288b
chore: fix linter
bennypowers Oct 29, 2024
2b5790c
Merge remote-tracking branch 'origin/main' into chore/ts-assets-11ty-dev
bennypowers Oct 30, 2024
d044afe
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Nov 3, 2024
d59180d
chore: patch jspm to fix dev server issues
bennypowers Nov 3, 2024
16da0e1
fix: better patches, config
bennypowers Nov 3, 2024
e58f092
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Nov 4, 2024
8c9be4d
docs: fix config
bennypowers Nov 4, 2024
ab75745
chore: appease syntax linter?
bennypowers Nov 4, 2024
fd7dad4
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Nov 11, 2024
b78391a
docs: prevent errors in watch
bennypowers Nov 11, 2024
d65bd3a
docs: replace execa with piscina for ssr
bennypowers Nov 11, 2024
53f7a6f
chore: linter
bennypowers Nov 11, 2024
dce7085
style: lint
bennypowers Nov 12, 2024
eedd19e
chore: node version
bennypowers Nov 12, 2024
189ce69
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Nov 12, 2024
1006297
Merge branch 'main' into chore/ts-assets-11ty-dev
bennypowers Nov 14, 2024
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
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@ docs/_data/*.js
docs/assets/javascript/elements/playground-elements.js
docs/assets/javascript/elements/assets/playground-typescript-worker-*.js
docs/assets/javascript/environment.js
!docs/11ty-types.d.ts
!docs/_plugins/lit-ssr/worker/*.js

# Build artifacts
elements.js
elements/*/*.js
elements/*/test/*.js
!elements/**/demo/*.css
uxdot/*.js
react
lib/**/*.js
!elements/**/demo/*.css
*.tsbuildinfo
*.map
*.d.ts
!declaration.d.ts
!docs/11ty-types.d.ts
!./docs/11ty-types.d.ts
!*/11ty-types.d.ts
custom-elements.json
test-results.xml
rhds.min.js
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22.7.0
v23.2.0
72 changes: 63 additions & 9 deletions docs/11ty-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ declare module '@11ty/eleventy-plugin-syntaxhighlight/src/getAttributes.js' {

declare module '@11ty/eleventy/src/UserConfig.js' {
import type MarkdownIt from 'markdown-it';
import type { URLPattern } from 'urlpattern-polyfill';

interface EleventyPage {
url: string;
fileSlug: string;
Expand Down Expand Up @@ -40,6 +42,7 @@ declare module '@11ty/eleventy/src/UserConfig.js' {
}

interface FilterContext extends Context { }

interface TransformContext extends Context {
inputPath: string;
outputPath: string;
Expand Down Expand Up @@ -76,24 +79,34 @@ declare module '@11ty/eleventy/src/UserConfig.js' {
compile(inputContent: string): (this: CompileContext, data: unknown) => string | Promise<string>;
}

interface EleventyBeforeEventHandlerOptions {
interface BeforeEvent {
directories: EleventyData['directories'],
/** @deprecated */
dir: {input: string; output: string; includes: string, data: string; layouts: string;};
outputMode: 'js'|'json'|'ndjson';
runMode: 'build'|'serve'|'watch';
}

interface EleventyAfterEventHandlerOptions extends EleventyBeforeEventHandlerOptions {
interface AfterEvent extends BeforeEvent {
results?: {inputPath:string;outputPath:string; url:string;content:string}[];
}

interface ContentMapEvent {
inputPathToUrl: Record<string, string>;
urlToInputPath: Record<string, string>;
}

type EleventyEvent =
| BeforeEvent
| AfterEvent
| ContentMapEvent
| string[]
| UserConfig;

type TransformCallback = (this: TransformContext, content: string) => string | Promise<string>;

type AddCollectionCallback = (api: CollectionApi) => CollectionItem[] | Promise<CollectionItem[]>;

type OnCallback<O = EleventyBeforeEventHandlerOptions> = (opts: O) => void | Promise<void>;

export type PluginFunction<Opts = unknown> = (config: UserConfig, opts?: Opts) => void | Promise<void>

type FilterFunction<T = string, R = string> = (this: FilterContext, data: T) => R | Promise<R>;
Expand All @@ -107,6 +120,45 @@ declare module '@11ty/eleventy/src/UserConfig.js' {
| (() => unknown)
| (() => Promise<unknown>);

interface EleventyDevServerResponse {
body: string;
status: number;
headers?: {
[key: string]: string | undefined,
};
}

interface ServerOptions {
liveReload: boolean;
domDiff: boolean;
port: number;
watch: string[];
showAllHosts: boolean
https: { key: string; cert: string; }
encoding: string;
showVersion: boolean;
indexFileName: string;
injectedScriptsFolder: string;
portReassignmentRetryCount: number;
folder: string;
/** @deprecated */
enabled: boolean;
/** @deprecated use domDiff */
domdiff: boolean;
onRequest: Record<string, (opts: {
url: URL;
pattern: URLPattern;
patternGroups: Record<string|number, string>;
}) =>
| undefined
| string
| EleventyDevServerResponse
| Promise<
| undefined
| string
| EleventyDevServerResponse>>;
}

export default class UserConfig {
addCollection(name: string, callback: AddCollectionCallback): void;
addDataExtension(names: string, processor: (content: string) => unknown | Promise<unknown>): void;
Expand All @@ -127,12 +179,14 @@ declare module '@11ty/eleventy/src/UserConfig.js' {
getFilter(name: string): FilterFunction;
getFilter(name: string): FilterFunctionWithArgs;
globalData: { [key: string]: DataEntry };
on(event: 'eleventy.before', callback: OnCallback): void;
on(event: 'eleventy.after', callback: OnCallback<EleventyAfterEventHandlerOptions>): void;
on(event: 'eleventy.beforeWatch', callback: (changedFiles: string[]) => void | Promise<void>): void;
on(event: 'eleventy.contentMap', callback: (opts: ({ inputPathToUrl: Record<string, string>, urlToInputPath: Record<string, string> })) => void | Promise<void>): void;
on(event: 'eleventy.beforeConfig', callback: (config: UserConfig) => void | Promise<void>): void;
on(event: 'eleventy.before', callback: (event: BeforeEvent) => void | Promise<void>): void;
on(event: 'eleventy.after', callback: (event: AfterEvent) => void | Promise<void>): void;
on(event: 'eleventy.contentMap', callback: (event: ContentMapEvent) => void | Promise<void>): void;
on(event: 'eleventy.beforeWatch', callback: (changedFiles: string[]) => void | Promise<void>): void;
on(event: 'eleventy.beforeConfig', callback: (config: UserConfig) => void | Promise<void>): void;
on(event: string, callback: (event: any) => void | Promise<void>): void;
setQuietMode(quiet: boolean): void;
setServerOptions(options: Partial<ServerOptions>): void
javascriptFunctions: Record<string, (...args: unknown[]) => any>;
watchIgnores: Set<string>;
}
Expand Down
27 changes: 27 additions & 0 deletions docs/_plugins/lit-ssr/lit-css-node.ts
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);
}
}
176 changes: 83 additions & 93 deletions docs/_plugins/lit-ssr/lit.ts
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)+$/, '');
}

Loading
Loading