From b371d66ee69cbcf8562e51e23d11b6891c82332d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benny=20Powers=20-=20=D7=A2=D7=9D=20=D7=99=D7=A9=D7=A8?= =?UTF-8?q?=D7=90=D7=9C=20=D7=97=D7=99!?= Date: Thu, 14 Nov 2024 16:18:01 +0200 Subject: [PATCH] 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 99b12554 * 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 --- .gitignore | 7 +- .nvmrc | 2 +- docs/11ty-types.d.ts | 72 ++- docs/_plugins/lit-ssr/lit-css-node.ts | 27 ++ docs/_plugins/lit-ssr/lit.ts | 176 ++++---- docs/_plugins/lit-ssr/worker.ts | 96 ++++ docs/_plugins/lit-ssr/worker/worker.js | 73 --- docs/_plugins/rhds.ts | 125 ++++-- docs/_plugins/typescript-assets.ts | 99 ++++ .../iconography/iconography.11ty.js | 6 +- docs/patterns/tabs/patterns/code-tabs.html | 2 +- docs/theming/developers.md | 2 +- docs/tsconfig.json | 3 +- elements/tsconfig.json | 6 +- eleventy.config.ts | 107 ++--- eslint.config.js | 2 + lib/ssr-controller.ts | 4 +- package-lock.json | 422 +++++++++++++++++- package.json | 37 +- patches/@jspm+generator+2.3.1.patch | 50 +++ patches/@lit+context+1.1.3.patch | 11 + patches/@patternfly+pfe-core+4.0.4.patch | 46 ++ patches/@patternfly+pfe-tools+4.0.1.patch | 13 + tsconfig.settings.json | 36 +- uxdot/uxdot-pattern-ssr-controller-client.ts | 11 +- uxdot/uxdot-pattern-ssr-controller-server.ts | 25 +- uxdot/uxdot-pattern.ts | 7 +- uxdot/uxdot-sidenav.ts | 10 +- web-dev-server.config.js | 17 +- 29 files changed, 1116 insertions(+), 378 deletions(-) create mode 100644 docs/_plugins/lit-ssr/lit-css-node.ts create mode 100644 docs/_plugins/lit-ssr/worker.ts delete mode 100644 docs/_plugins/lit-ssr/worker/worker.js create mode 100644 docs/_plugins/typescript-assets.ts create mode 100644 patches/@jspm+generator+2.3.1.patch create mode 100644 patches/@lit+context+1.1.3.patch create mode 100644 patches/@patternfly+pfe-core+4.0.4.patch create mode 100644 patches/@patternfly+pfe-tools+4.0.1.patch diff --git a/.gitignore b/.gitignore index e0fa50cb9c..badc7da136 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.nvmrc b/.nvmrc index 751f4c9f38..0f576a16ca 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.7.0 +v23.2.0 diff --git a/docs/11ty-types.d.ts b/docs/11ty-types.d.ts index e15f30385d..421cf491a5 100644 --- a/docs/11ty-types.d.ts +++ b/docs/11ty-types.d.ts @@ -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; @@ -40,6 +42,7 @@ declare module '@11ty/eleventy/src/UserConfig.js' { } interface FilterContext extends Context { } + interface TransformContext extends Context { inputPath: string; outputPath: string; @@ -76,7 +79,7 @@ declare module '@11ty/eleventy/src/UserConfig.js' { compile(inputContent: string): (this: CompileContext, data: unknown) => string | Promise; } - interface EleventyBeforeEventHandlerOptions { + interface BeforeEvent { directories: EleventyData['directories'], /** @deprecated */ dir: {input: string; output: string; includes: string, data: string; layouts: string;}; @@ -84,16 +87,26 @@ declare module '@11ty/eleventy/src/UserConfig.js' { 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; + urlToInputPath: Record; + } + + type EleventyEvent = + | BeforeEvent + | AfterEvent + | ContentMapEvent + | string[] + | UserConfig; + type TransformCallback = (this: TransformContext, content: string) => string | Promise; type AddCollectionCallback = (api: CollectionApi) => CollectionItem[] | Promise; - type OnCallback = (opts: O) => void | Promise; - export type PluginFunction = (config: UserConfig, opts?: Opts) => void | Promise type FilterFunction = (this: FilterContext, data: T) => R | Promise; @@ -107,6 +120,45 @@ declare module '@11ty/eleventy/src/UserConfig.js' { | (() => unknown) | (() => Promise); + 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; + }) => + | 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): void; @@ -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): void; - on(event: 'eleventy.beforeWatch', callback: (changedFiles: string[]) => void | Promise): void; - on(event: 'eleventy.contentMap', callback: (opts: ({ inputPathToUrl: Record, urlToInputPath: Record })) => void | Promise): void; - on(event: 'eleventy.beforeConfig', callback: (config: UserConfig) => void | Promise): void; + on(event: 'eleventy.before', callback: (event: BeforeEvent) => void | Promise): void; + on(event: 'eleventy.after', callback: (event: AfterEvent) => void | Promise): void; + on(event: 'eleventy.contentMap', callback: (event: ContentMapEvent) => void | Promise): void; + on(event: 'eleventy.beforeWatch', callback: (changedFiles: string[]) => void | Promise): void; + on(event: 'eleventy.beforeConfig', callback: (config: UserConfig) => void | Promise): void; + on(event: string, callback: (event: any) => void | Promise): void; setQuietMode(quiet: boolean): void; + setServerOptions(options: Partial): void javascriptFunctions: Record any>; watchIgnores: Set; } diff --git a/docs/_plugins/lit-ssr/lit-css-node.ts b/docs/_plugins/lit-ssr/lit-css-node.ts new file mode 100644 index 0000000000..94d462eb8e --- /dev/null +++ b/docs/_plugins/lit-ssr/lit-css-node.ts @@ -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; + +const cache = new Map(); + +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); + } +} diff --git a/docs/_plugins/lit-ssr/lit.ts b/docs/_plugins/lit-ssr/lit.ts index 543c59ef14..d1268dae18 100644 --- a/docs/_plugins/lit-ssr/lit.ts +++ b/docs/_plugins/lit-ssr/lit.ts @@ -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; +} - const resolvedComponentModules = componentModules.map(module => - pathToFileURL(resolve(process.cwd(), module)).href); +export interface RenderResponseMessage { + page: Pick; + 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)+$/, ''); +} diff --git a/docs/_plugins/lit-ssr/worker.ts b/docs/_plugins/lit-ssr/worker.ts new file mode 100644 index 0000000000..d073e7e64c --- /dev/null +++ b/docs/_plugins/lit-ssr/worker.ts @@ -0,0 +1,96 @@ +import type { LitElement, ReactiveController } from 'lit'; +import type { RenderInfo, RenderResult } from '@lit-labs/ssr'; +import type { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.ts'; +import type { RenderRequestMessage, RenderResponseMessage } from './lit.js'; + +import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js'; + +import { register } from 'node:module'; +import { register as registerTS } from 'tsx/esm/api'; + +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { html } from 'lit'; +import { render } from '@lit-labs/ssr'; +import { collectResult } from '@lit-labs/ssr/lib/render-result.js'; +import Piscina from 'piscina'; + +interface WorkerInitData { + imports: string[]; + tsconfig: string; +} + +const { imports, tsconfig } = Piscina.workerData as WorkerInitData; + +registerTS({ tsconfig }); +register('./lit-css-node.ts', import.meta.url); + +async function importModule(bareSpec: string) { + const spec = pathToFileURL(resolve(process.cwd(), bareSpec)).href.replace('.js', '.ts'); + await import(spec); +} + +await Promise + .allSettled(imports.map(importModule)) + // eslint-disable-next-line no-console + .catch(console.error); + +class RHDSSSRableRenderer extends LitElementRenderer { + static isRHDSSSRController(ctrl: ReactiveController): ctrl is RHDSSSRController { + return !!(ctrl as RHDSSSRController).isRHDSSSRController; + } + + getControllers() { + const element = (this.element as LitElement & { _$EO: Set }); + return Array.from(element._$EO ?? new Set()) + .filter(RHDSSSRableRenderer.isRHDSSSRController); + } + + async setupController(controller: RHDSSSRController, renderInfo: RenderInfo) { + if (controller.ssrSetup) { + await controller.ssrSetup(renderInfo); + } + return ''; + } + + override* renderShadow(renderInfo: RenderInfo): RenderResult { + for (const controller of this.getControllers()) { + yield this.setupController(controller, renderInfo); + } + yield* super.renderShadow(renderInfo); + } +} + +const elementRenderers = [ + RHDSSSRableRenderer, +]; + +class UnsafeHTMLStringsArray extends Array { + public raw: readonly string[]; + constructor(string: string) { + super(); + this.push(string); + this.raw = [string]; + } +} + +/** + * Render a page using lit-ssr + * + * @param opts + * @param opts.page + * @param opts.content + */ +export default async function renderPage({ + page, + content, +}: RenderRequestMessage): Promise { + const start = performance.now(); + const tpl = html(new UnsafeHTMLStringsArray(content)); + const result = render(tpl, { elementRenderers, page } as unknown as RenderInfo); + const rendered = await collectResult(result); + const end = performance.now(); + return { page, rendered, durationMs: end - start }; +} + diff --git a/docs/_plugins/lit-ssr/worker/worker.js b/docs/_plugins/lit-ssr/worker/worker.js deleted file mode 100644 index f7992027fa..0000000000 --- a/docs/_plugins/lit-ssr/worker/worker.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ - -/** @import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js' */ -/** @import { ReactiveController } from 'lit' */ - -import { parentPort } from 'worker_threads'; -import { render } from '@lit-labs/ssr'; -import { unsafeHTML } from 'lit/directives/unsafe-html.js'; -import { collectResult } from '@lit-labs/ssr/lib/render-result.js'; - -import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js'; - -import { ssrControllerMap } from '@rhds/elements/lib/ssr-controller.js'; - -if (parentPort === null) { - throw new Error('worker.js must only be run in a worker thread'); -} - -let initialized = false; - -/** - * @param {ReactiveController} controller - * @returns {controller is RHDSSSRController} - */ -function isRHDSSSRController(controller) { - return !!controller.isRHDSSSRController; -} - -parentPort.on('message', async message => { - switch (message.type) { - case 'initialize-request': { - if (!initialized) { - await Promise.all(message.imports.map(module => import(module))); - parentPort.postMessage({ type: 'initialize-response' }); - } - initialized = true; - break; - } - - case 'render-request': { - const { id, content, page } = message; - const result = render(unsafeHTML(content), { - elementRenderers: [ - class RHDSSSRableRenderer extends LitElementRenderer { - * renderShadow(renderInfo) { - const controllers = ssrControllerMap.get(this.element); - yield controllers?.map(async x => { - if (isRHDSSSRController(x)) { - x.page = page; - await x.ssrSetup(); - return []; - } - }) ?? []; - yield* super.renderShadow(renderInfo); - } - }, - ], - }); - const rendered = await collectResult(result); - parentPort.postMessage({ - type: 'render-response', - id, - rendered, - }); - break; - } - } -}); - diff --git a/docs/_plugins/rhds.ts b/docs/_plugins/rhds.ts index eb60a5c5f8..aff4119313 100644 --- a/docs/_plugins/rhds.ts +++ b/docs/_plugins/rhds.ts @@ -1,25 +1,23 @@ import type { UserConfig } from '@11ty/eleventy'; -import * as ChildProcess from 'node:child_process'; -import { join, dirname } from 'node:path'; -import { promisify } from 'node:util'; -import { readdir, writeFile, mkdir } from 'node:fs/promises'; +import { join, dirname, relative } from 'node:path'; +import { cp, glob, readdir, writeFile, mkdir } from 'node:fs/promises'; import { makeDemoEnv } from '#scripts/environment.js'; +import { $ } from 'execa'; + import yaml from 'js-yaml'; import slugify from 'slugify'; import RHDSAlphabetizeTagsPlugin from '#11ty-plugins/alphabetize-tags.js'; import RHDSElementDocsPlugin from '#11ty-plugins/element-docs.js'; import RHDSElementDemosPlugin from '#11ty-plugins/element-demos.js'; +import LitSSRPlugin from '#11ty-plugins/lit-ssr/lit.js'; import { getPfeConfig } from '@patternfly/pfe-tools/config.js'; import { capitalize } from '#11ty-plugins/tokensHelpers.js'; import repoStatus from '#11ty-data/repoStatus.js'; -const exec = promisify(ChildProcess.exec); -const cwd = process.cwd(); - /** * EleventyTransformContext the `this` binding for transform functions * outputPath the path the page will be written to @@ -57,6 +55,8 @@ const COPY_CONTENT_EXTENSIONS = [ 'd.ts', ]; +const cwd = process.cwd(); + /** * Generate a map of files per package which should be copied to the site dir */ @@ -79,10 +79,47 @@ async function getFilesToCopy() { interface Options { tagsToAlphabetize: string[]; + componentModules: string[]; + tsconfig: string; +} + + +async function clean() { + await $`npx tspc -b elements --clean`; + await $`npx tspc -b lib --clean`; } export default async function(eleventyConfig: UserConfig, options?: Options) { - eleventyConfig.on('eleventy.before', async ({ runMode, directories }) => { + /** add the normalized pfe-tools config to global data */ + eleventyConfig.addGlobalData('pfeconfig', getPfeConfig()); + + eleventyConfig.addGlobalData('sideNavDropdowns', [ + { title: 'About', url: '/about', collection: 'about' }, + { title: 'Get started', url: '/get-started', collection: 'getstarted' }, + { title: 'Foundations', url: '/foundations', collection: 'foundations' }, + { title: 'Tokens', url: '/tokens', collection: 'tokenCategory' }, + { title: 'Elements', url: '/elements', collection: 'elementDocs' }, + { title: 'Theming', url: '/theming', collection: 'theming' }, + { title: 'Patterns', url: '/patterns', collection: 'pattern' }, + { title: 'Accessibility', url: '/accessibility', collection: 'accessibility' }, + ]); + + eleventyConfig.addDataExtension('yml, yaml', (contents: string) => yaml.load(contents)); + + eleventyConfig.addPassthroughCopy('docs/demo.{js,map,ts}'); + eleventyConfig.addPassthroughCopy('docs/theming/**/*.css'); + + eleventyConfig.addPassthroughCopy({ + 'node_modules/element-internals-polyfill': '/assets/packages/element-internals-polyfill', + // ensure icons are copied to the assets dir. + 'node_modules/@patternfly/icons/': '/assets/packages/@patternfly/icons/', + }); + + eleventyConfig.addPassthroughCopy(await getFilesToCopy(), { + filter: (path: string) => !path.endsWith('.html'), + }); + + eleventyConfig.on('eleventy.before', async ({ directories, runMode }) => { const outPath = join(directories.output, 'assets/javascript/repoStatus.json'); switch (runMode) { case 'watch': @@ -92,13 +129,23 @@ export default async function(eleventyConfig: UserConfig, options?: Options) { } }); - /** add the normalized pfe-tools config to global data */ - eleventyConfig.addGlobalData('pfeconfig', getPfeConfig()); + let hasCleanedSinceWatchStarted = false; + eleventyConfig.on('eleventy.before', async function({ runMode }) { + switch (runMode) { + case 'build': + return clean(); + default: + if (!hasCleanedSinceWatchStarted) { + hasCleanedSinceWatchStarted = true; + return await clean(); + } + } + }); /** custom-elements.json */ eleventyConfig.on('eleventy.before', async function({ runMode }) { if (runMode === 'watch') { - await exec('npx cem analyze'); + await $`npx cem analyze`; } }); @@ -108,26 +155,41 @@ export default async function(eleventyConfig: UserConfig, options?: Options) { await writeFile(outPath, await makeDemoEnv(), 'utf8'); }); - eleventyConfig.addDataExtension('yml, yaml', (contents: string) => yaml.load(contents)); - - eleventyConfig.addPlugin(RHDSAlphabetizeTagsPlugin, options); - - eleventyConfig.addPlugin(RHDSElementDocsPlugin); - eleventyConfig.addPlugin(RHDSElementDemosPlugin); - - eleventyConfig.addPassthroughCopy('docs/demo.{js,map,ts}'); - - eleventyConfig.addPassthroughCopy({ - 'node_modules/element-internals-polyfill': '/assets/packages/element-internals-polyfill', - }); - - // ensure icons are copied to the assets dir. - eleventyConfig.addPassthroughCopy({ - 'node_modules/@patternfly/icons/': '/assets/packages/@patternfly/icons/', + eleventyConfig.on('eleventy.after', async function({ runMode }) { + const cwd = process.cwd(); + const pkgsDir = join(cwd, '_site/assets/packages'); + switch (runMode) { + case 'build': + await $`npx tspc -b elements`; + await mkdir(join(pkgsDir, '@rhds/elements/elements'), { recursive: true }); + await mkdir(join(pkgsDir, '@rhds/elements/lib'), { recursive: true }); + for await (const file of glob('./{elements,lib}/**/*.{js,d.ts,map,css}')) { + const rel = relative(cwd, file); + const outDir = join(pkgsDir, '@rhds/elements'); + const out = join(outDir, dirname(rel)); + const from = join(cwd, rel); + const to = join(outDir, rel); + await mkdir(out, { recursive: true }); + await cp(from, to); + } + } }); - eleventyConfig.addPassthroughCopy(await getFilesToCopy(), { - filter: (path: string) => !path.endsWith('.html'), + eleventyConfig.on('eleventy.after', async function({ runMode }) { + const cwd = process.cwd(); + const pkgsDir = join(cwd, '_site/assets/packages'); + switch (runMode) { + case 'build': + await $`npx tspc -b uxdot`; + await mkdir(join(pkgsDir, '@uxdot/elements'), { recursive: true }); + for await (const file of glob('./uxdot/*.{js,d.ts,map,css}')) { + const rel = relative(join(cwd, 'uxdot'), file); + const outDir = join(pkgsDir, '@uxdot/elements'); + const from = join(cwd, 'uxdot', rel); + const to = join(outDir, rel); + await cp(from, to); + } + } }); eleventyConfig.addJavaScriptFunction('getTagNameSlug', getTagNameSlug); @@ -194,4 +256,9 @@ export default async function(eleventyConfig: UserConfig, options?: Options) { eleventyConfig.addWatchTarget(dir); } } + + eleventyConfig.addPlugin(RHDSAlphabetizeTagsPlugin, options); + eleventyConfig.addPlugin(RHDSElementDocsPlugin); + eleventyConfig.addPlugin(RHDSElementDemosPlugin); + eleventyConfig.addPlugin(LitSSRPlugin, options); }; diff --git a/docs/_plugins/typescript-assets.ts b/docs/_plugins/typescript-assets.ts new file mode 100644 index 0000000000..87ccbe0d47 --- /dev/null +++ b/docs/_plugins/typescript-assets.ts @@ -0,0 +1,99 @@ +import type { UserConfig } from '@11ty/eleventy'; + +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { transform as esbuildTransform } from 'esbuild'; +import { transform } from '@pwrs/lit-css'; + +const cwd = process.cwd(); + +const tsconfigRaw = await readFile(new URL( + '../../tsconfig.settings.json', + import.meta.url, +), 'utf8'); + +async function transformTypescriptSource(sourcefile: string) { + try { + const { code: body, map } = await esbuildTransform(await readFile(sourcefile, 'utf8'), { + loader: 'ts', + sourcemap: true, + target: 'es2022', + sourcefile, + tsconfigRaw, + }); + + return { + body, + status: 200, + headers: { + 'Content-Type': 'text/javascript', + 'SourceMap': `data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString('base64')}`, + }, + }; + } catch (e) { + // eslint-disable-next-line no-console + console.log(`Error transforming ${sourcefile}`); + // eslint-disable-next-line no-console + console.log(e); + const body = JSON.stringify(e); + return { + body, + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }; + } +} + +export default function(eleventyConfig: UserConfig) { + eleventyConfig.addExtension('11ty.ts', { + key: '11ty.js', + compile() { + return async function(this, data) { + return this.defaultRenderer(data); + }; + }, + }); + + eleventyConfig.setServerOptions({ + onRequest: { + '/assets/packages/@:scope/elements/:path*.css': async function transformCss( + args: { patternGroups: Record }, + ) { + const filePath = join(cwd, args.patternGroups.scope === 'uxdot' ? 'uxdot' : '.', `${args.patternGroups.path}.css`); + const css = await readFile(filePath, 'utf8'); + if (args.patternGroups.path.includes('lightdom')) { + return { body: css, status: 200 }; + } else { + return { + body: await transform({ css, filePath }), + status: 200, + headers: { + 'Content-Type': 'text/javascript', + }, + }; + } + }, + + '/assets/packages/@:scope/:pkg/:path*.js': async function({ + patternGroups: { scope, path, pkg }, + }) { + switch (scope) { + case 'rhds': + switch (pkg) { + case 'elements': return transformTypescriptSource(join(cwd, `${path}.ts`)); + default: return; + } + case 'uxdot': + return transformTypescriptSource(join(cwd, 'uxdot', `${path}.ts`)); + default: + return; + } + }, + }, + }); +} + + diff --git a/docs/foundations/iconography/iconography.11ty.js b/docs/foundations/iconography/iconography.11ty.js index ca41633a0c..f2fb60522c 100644 --- a/docs/foundations/iconography/iconography.11ty.js +++ b/docs/foundations/iconography/iconography.11ty.js @@ -18,7 +18,7 @@ export default class IconsPage { async render(ctx) { const { icons } = ctx; return html` - +