diff --git a/.gitignore b/.gitignore index 12ac647..01547cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.DS_Store \ No newline at end of file +.DS_Store +packages/*/dist-types/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5bfd037..7b892fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ ], "devDependencies": { "esbuild": "^0.19.0", - "lit": "^3.1.2" + "lit": "^3.1.2", + "typescript": "^5.4.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -641,6 +642,19 @@ "node": ">=6" } }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -651,20 +665,20 @@ }, "packages/core": { "name": "swtl", - "version": "0.3.1", + "version": "0.3.2", "license": "ISC", "devDependencies": { - "@swtl/lit": "^0.1.3" + "@swtl/lit": "^0.1.4" } }, "packages/lit": { "name": "@swtl/lit", - "version": "0.1.3", + "version": "0.1.4", "license": "ISC", "dependencies": { "@lit-labs/ssr": "^3.2.2", "@lit-labs/ssr-dom-shim": "^1.2.0", - "swtl": "^0.3.0" + "swtl": "^0.3.2" }, "devDependencies": {} } diff --git a/package.json b/package.json index 342c853..105e6ff 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "url": "https://github.com/thepassle/swtl.git" }, "scripts": { - "test": "npm run test --workspaces" + "test": "npm run test --workspaces", + "lint:types": "npm run lint:types --workspaces" }, - "dependencies": {}, "devDependencies": { "esbuild": "^0.19.0", - "lit": "^3.1.2" + "lit": "^3.1.2", + "typescript": "^5.4.3" }, "workspaces": [ "packages/*" ] -} \ No newline at end of file +} diff --git a/packages/core/await.js b/packages/core/await.js index 1ceff7f..db3e035 100644 --- a/packages/core/await.js +++ b/packages/core/await.js @@ -1,14 +1,35 @@ import { AWAIT_SYMBOL } from './symbol.js'; +/** + * @typedef {import('./types.js').Children} Children + * @typedef {import('./html.js').html} html + */ + +/** + * @param {{ + * promise: () => Promise, + * children: Children + * }} args + * @returns {{ + * promise: () => Promise, + * template: () => ReturnType + * }} + */ function Await({promise, children}) { return { promise, - template: children.find(c => typeof c === 'function') + template: /** @type {() => ReturnType} */ (children.find(c => typeof c === 'function')) }; } Await.kind = AWAIT_SYMBOL; +/** + * + * @param {boolean} condition + * @param {() => ReturnType} template + * @returns + */ const when = (condition, template) => condition ? template() : ''; export { Await, when }; \ No newline at end of file diff --git a/packages/core/html.js b/packages/core/html.js index 48336c1..48d259a 100644 --- a/packages/core/html.js +++ b/packages/core/html.js @@ -1,5 +1,11 @@ import { COMPONENT_SYMBOL, CUSTOM_ELEMENT_SYMBOL } from './symbol.js'; +/** + * @typedef {import('./types.js').Component} Component + * @typedef {import('./types.js').CustomElement} CustomElement + * @typedef {import('./types.js').HtmlResult} HtmlResult + */ + const TEXT = 'TEXT'; const COMPONENT = 'COMPONENT'; const TAG_OPEN = 'TAG_OPEN'; @@ -12,13 +18,31 @@ const SET_PROP = 'SET_PROP'; const PROP_VAL = 'PROP_VAL'; const customElementTagRegex = /^([a-z0-9]+-[a-z0-9-]*)/; +/** @param {string} t */ const noSelfClosing = t => `Custom elements cannot be self-closing: "${t}"`; +/** + * @param {TemplateStringsArray} statics + * @param {...unknown} dynamics + * @returns {HtmlResult} + */ export function* html(statics, ...dynamics) { + /** + * @type {TEXT | COMPONENT | TAG_OPEN} + */ let MODE = TEXT; + /** + * @type {NONE | PROP | CHILDREN} + */ let COMPONENT_MODE = NONE; + /** + * @type {SET_PROP | PROP_VAL | NONE} + */ let PROP_MODE = NONE; + /** + * @type {Array} + */ const componentStack = []; /** @@ -30,6 +54,9 @@ export function* html(statics, ...dynamics) { for (let i = 0; i < statics.length; i++) { let result = ""; let tag = ""; + /** + * @type {Component} + */ const component = { kind: COMPONENT_SYMBOL, slots: {}, @@ -59,7 +86,7 @@ export function* html(statics, ...dynamics) { !statics[i][j + 1] && typeof dynamics[i] === "function" ) { MODE = COMPONENT; - component.fn = dynamics[i]; + component.fn = /** @type {Component["fn"]} */ (dynamics[i]); componentStack.push(component); } else { result += c; @@ -126,6 +153,7 @@ export function* html(statics, ...dynamics) { if (COMPONENT_MODE === PROP) { const component = componentStack[componentStack.length - 1]; const attrOrProp = component?.kind === COMPONENT_SYMBOL ? 'properties' : 'attributes'; + /** @ts-expect-error */ const property = component?.[`${attrOrProp}`][component[`${attrOrProp}`].length - 1]; if (PROP_MODE === SET_PROP) { let property = ""; @@ -155,7 +183,7 @@ export function* html(statics, ...dynamics) { } else if (statics[i][j] === "/" && COMPONENT_MODE === PROP) { COMPONENT_MODE = NONE; PROP_MODE = NONE; - const component = componentStack.pop(); + const component = /** @type {Component} */ (componentStack.pop()); if (!componentStack.length) { result = ''; yield component; @@ -170,8 +198,10 @@ export function* html(statics, ...dynamics) { } if (property === '...') { + /** @ts-expect-error */ component[`${attrOrProp}`].push(...Object.entries(dynamics[i]).map(([name,value])=> ({name, value}))); } else if (property) { + /** @ts-expect-error */ component[`${attrOrProp}`].push({name: property, value: true}); } } else if (PROP_MODE === PROP_VAL) { @@ -228,8 +258,8 @@ export function* html(statics, ...dynamics) { * Yield if we finished the component * Swtl Component only, custom elements can't be self-closing */ - } else if (statics[i][j] === '/' && componentStack.at(-1).kind === COMPONENT_SYMBOL) { - const component = componentStack.pop(); + } else if (statics[i][j] === '/' && componentStack.at(-1)?.kind === COMPONENT_SYMBOL) { + const component = /** @type {Component} */ (componentStack.pop()); if (!componentStack.length) { PROP_MODE = NONE; COMPONENT_MODE = NONE; @@ -266,8 +296,8 @@ export function* html(statics, ...dynamics) { * Yield if we finished the component * Swtl Component only, custom elements can't be self-closing */ - if (statics[i][j] === '/' && componentStack.at(-1).kind === COMPONENT_SYMBOL) { - const component = componentStack.pop(); + if (statics[i][j] === '/' && componentStack.at(-1)?.kind === COMPONENT_SYMBOL) { + const component = /** @type {Component} */ (componentStack.pop()); if (!componentStack.length) { yield component; } @@ -275,8 +305,9 @@ export function* html(statics, ...dynamics) { * @example * ^ */ - } else if (statics[i][j] === '/' && componentStack.at(-1).kind === CUSTOM_ELEMENT_SYMBOL) { - throw new Error(noSelfClosing(componentStack.at(-1).tag)); + } else if (statics[i][j] === '/' && componentStack.at(-1)?.kind === CUSTOM_ELEMENT_SYMBOL) { + // @ts-expect-error we already know its a custom element because of the symbol + throw new Error(noSelfClosing(componentStack.at(-1)?.tag)); } else if (statics[i][j] === '>') { result = ""; COMPONENT_MODE = CHILDREN; @@ -302,7 +333,7 @@ export function* html(statics, ...dynamics) { * If there are no components on the stack, this is a top level * component, and we can yield */ - const component = componentStack.pop(); + const component = /** @type {Component} */ (componentStack.pop()); if (!componentStack.length) { MODE = TEXT; COMPONENT_MODE = NONE; @@ -318,7 +349,7 @@ export function* html(statics, ...dynamics) { } COMPONENT_MODE = PROP; PROP_MODE = SET_PROP; - component.fn = dynamics[i]; + component.fn = /** @type {Component["fn"]} */ (dynamics[i]); componentStack.push(component); } else if (!statics[i][j+1]) { /** @@ -346,7 +377,7 @@ export function* html(statics, ...dynamics) { * If there are no components on the stack, this is a top level * component, and we can yield */ - const component = componentStack.pop(); + const component = /** @type {Component | CustomElement} */ (componentStack.pop()); if (!componentStack.length) { MODE = TEXT; COMPONENT_MODE = NONE; @@ -356,7 +387,9 @@ export function* html(statics, ...dynamics) { * Otherwise we need to add the component to the parent's children */ const parentComponent = componentStack[componentStack.length - 1]; - parentComponent.children.push(component); + if (component) { + parentComponent.children.push(component); + } } } } else if (statics[i][j] === '<') { @@ -399,14 +432,14 @@ export function* html(statics, ...dynamics) { } else if (c === " ") { COMPONENT_MODE = PROP; PROP_MODE = SET_PROP; - } else if (c === "/" && statics[i][j + 1] === ">" && componentStack.at(-1).kind === COMPONENT_SYMBOL) { + } else if (c === "/" && statics[i][j + 1] === ">" && componentStack.at(-1)?.kind === COMPONENT_SYMBOL) { MODE = TEXT; COMPONENT_MODE = NONE; /** * If there are no components on the stack, this is a top level * component, and we can yield */ - const component = componentStack.pop(); + const component = /** @type {Component} */ (componentStack.pop()); if (!componentStack.length) { result = ''; yield component; diff --git a/packages/core/index.js b/packages/core/index.js index d2bf363..bed75c9 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -9,3 +9,19 @@ export { CacheOnly, NetworkOnly, } from './strategies.js'; + +/** + * @typedef {import('./types.js').Attribute} Attribute + * @typedef {import('./types.js').Property} Property + * @typedef {import('./types.js').HtmlValue} HtmlValue + * @typedef {import('./types.js').Children} Children + * @typedef {import('./types.js').HtmlResult} HtmlResult + * @typedef {import('./types.js').Component} Component + * @typedef {import('./types.js').CustomElement} CustomElement + * @typedef {import('./types.js').CustomElementRenderer} CustomElementRenderer + * @typedef {import('./types.js').RouteResult} RouteResult + * @typedef {import('./types.js').RouteArgs} RouteArgs + * @typedef {import('./types.js').Plugin} Plugin + * @typedef {import('./types.js').Route} Route + * @typedef {import('./types.js').MatchedRoute} MatchedRoute + */ \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 26afe7c..6b8fad1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "swtl", - "version": "0.3.1", + "version": "0.3.2", "description": "", "main": "index.js", "type": "module", @@ -8,17 +8,43 @@ "dev": "esbuild ./demo/sw.js --bundle --outfile=./demo/bundled-sw.js --watch --format=iife --servedir=demo", "start": "node --watch dev.js", "test": "node --test test/*.test.js", - "test:watch": "node --watch --test tests/tests.js" + "test:watch": "node --watch --test tests/tests.js", + "lint:types": "tsc", + "lint:types:watch": "tsc --watch" }, "exports": { - ".": "./index.js", - "./html.js": "./html.js", - "./await.js": "./await.js", - "./render.js": "./render.js", - "./router.js": "./router.js", - "./strategies.js": "./strategies.js", - "./slot.js": "./slot.js", - "./ssr/*.js": "./ssr/*.js", + ".": { + "types": "./dist-types/index.d.ts", + "default": "./index.js" + }, + "./html.js": { + "types": "./dist-types/html.d.ts", + "default": "./html.js" + }, + "./await.js": { + "types": "./dist-types/await.d.ts", + "default": "./await.js" + }, + "./render.js": { + "types": "./dist-types/render.d.ts", + "default": "./render.js" + }, + "./router.js": { + "types": "./dist-types/router.d.ts", + "default": "./router.js" + }, + "./strategies.js": { + "types": "./dist-types/strategies.d.ts", + "default": "./strategies.js" + }, + "./slot.js": { + "types": "./dist-types/slot.d.ts", + "default": "./slot.js" + }, + "./ssr/*.js": { + "types": "./dist-types/ssr/*.d.ts", + "default": "./ssr/*.js" + }, "./package.json": "./package.json" }, "files": [ @@ -37,7 +63,7 @@ "author": "", "license": "ISC", "devDependencies": { - "@swtl/lit": "^0.1.3" + "@swtl/lit": "^0.1.4" }, "dependencies": {} } diff --git a/packages/core/render.js b/packages/core/render.js index 7c072b0..3933bca 100644 --- a/packages/core/render.js +++ b/packages/core/render.js @@ -2,10 +2,32 @@ import { html } from './html.js'; import { defaultRenderer } from './ssr/default.js'; import { SLOT_SYMBOL, AWAIT_SYMBOL, COMPONENT_SYMBOL, CUSTOM_ELEMENT_SYMBOL, DEFAULT_RENDERER_SYMBOL } from "./symbol.js"; +/** + * @typedef {import('./types.js').CustomElementRenderer} CustomElementRenderer + * @typedef {import('./types.js').HtmlResult} HtmlResult + * @typedef {import('./types.js').HtmlValue} HtmlValue + * @typedef {Promise<{ + * id: number, + * template: (params: { + * pending: boolean, + * error: boolean, + * success: boolean + * }, + * data: unknown, + * error: typeof Error) => HtmlResult + * }>} OOOPromise + */ + +/** + * @param {ReadableStream} obj + */ function hasGetReader(obj) { return typeof obj.getReader === "function"; } +/** + * @param {ReadableStream} stream + */ export async function* streamAsyncIterator(stream) { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); @@ -21,6 +43,9 @@ export async function* streamAsyncIterator(stream) { } } +/** + * @param {any} iterable + */ async function* handleIterator(iterable) { if (hasGetReader(iterable)) { for await (const chunk of streamAsyncIterator(iterable)) { @@ -33,7 +58,13 @@ async function* handleIterator(iterable) { } } -export async function* handle(chunk, promises, customElementRenderers) { +/** + * @param {any} chunk + * @param {OOOPromise[]} promises + * @param {CustomElementRenderer[]} customElementRenderers + * @returns {AsyncGenerator} + */ +async function* handle(chunk, promises, customElementRenderers) { if (typeof chunk === "string") { yield chunk; } else if (typeof chunk === "function") { @@ -49,16 +80,19 @@ export async function* handle(chunk, promises, customElementRenderers) { yield* _render(chunk, promises, customElementRenderers); } else if (chunk?.fn?.kind === AWAIT_SYMBOL) { const { promise, template } = chunk.fn({ + // @ts-ignore ...chunk.properties.reduce((acc, prop) => ({...acc, [prop.name]: prop.value}), {}), children: chunk.children, }); const id = promises.length; promises.push( promise() + // @ts-ignore .then(data => ({ id, template: template({pending: false, error: false, success: true}, data, null) })) + // @ts-ignore .catch(error => { console.error(error.stack); return { @@ -79,9 +113,13 @@ export async function* handle(chunk, promises, customElementRenderers) { } } else if (chunk?.kind === COMPONENT_SYMBOL) { const children = []; + /** + * @type {Record} + */ const slots = {}; for (const child of chunk.children) { if (child?.fn?.kind === SLOT_SYMBOL) { + // @ts-ignore const name = child.properties.find(prop => prop.name === 'name')?.value || 'default'; slots[name] = child.children; } else { @@ -91,6 +129,7 @@ export async function* handle(chunk, promises, customElementRenderers) { yield* handle( await chunk.fn({ + // @ts-ignore ...chunk.properties.reduce((acc, prop) => ({...acc, [prop.name]: prop.value}), {}), children, slots @@ -108,13 +147,28 @@ export async function* handle(chunk, promises, customElementRenderers) { } } +/** + * + * @param {AsyncIterable | Iterable} template + * @param {Array} promises + * @param {CustomElementRenderer[]} customElementRenderers + * @returns {AsyncGenerator} + */ async function* _render(template, promises, customElementRenderers) { for await (const chunk of template) { yield* handle(chunk, promises, customElementRenderers); } } +/** + * @param {AsyncIterable | Iterable} template + * @param {CustomElementRenderer[]} customElementRenderers + * @returns {AsyncGenerator} + */ export async function* render(template, customElementRenderers = []) { + /** + * @type {Array} + */ let promises = []; if (!customElementRenderers.find(({name}) => name === DEFAULT_RENDERER_SYMBOL)) { customElementRenderers.push(defaultRenderer); @@ -147,14 +201,19 @@ export async function* render(template, customElementRenderers = []) { } } -export async function renderToString(renderResult, customElementRenderers = []) { +/** + * @param {HtmlResult} htmlResult + * @param {CustomElementRenderer[]} customElementRenderers + * @returns {Promise} + */ +export async function renderToString(htmlResult, customElementRenderers = []) { if (!customElementRenderers.find(({name}) => name === DEFAULT_RENDERER_SYMBOL)) { customElementRenderers.push(defaultRenderer); } let result = ""; - for await (const chunk of render(renderResult, customElementRenderers)) { + for await (const chunk of render(htmlResult, customElementRenderers)) { result += chunk; } diff --git a/packages/core/router.js b/packages/core/router.js index b425ce9..1ed3e62 100644 --- a/packages/core/router.js +++ b/packages/core/router.js @@ -1,6 +1,25 @@ import { render } from './render.js'; +/** + * @typedef {import('./types.js').CustomElementRenderer} CustomElementRenderer + * @typedef {import('./types.js').Route} Route + * @typedef {import('./types.js').MatchedRoute} MatchedRoute + * @typedef {import('./types.js').RouteArgs} RouteArgs + * @typedef {import('./types.js').RouteResult} RouteResult + * @typedef {import('./types.js').Plugin} Plugin + * @typedef {import('./types.js').HtmlResult} HtmlResult + */ + export class Router { + /** + * @param {{ + * routes: Route[], + * fallback: (args: RouteArgs) => RouteResult, + * plugins?: Plugin[], + * baseHref?: string, + * customElementRenderers?: CustomElementRenderer[] + * }} params + */ constructor({ routes, fallback, @@ -17,6 +36,7 @@ export class Router { }; this.routes = routes.map(route => ({ ...route, + // @ts-expect-error urlPattern: new URLPattern({ pathname: `${baseHref}${route.path}`, search: '*', @@ -25,6 +45,10 @@ export class Router { })); } + /** + * @param {MatchedRoute} route + * @returns {Plugin[]} + */ _getPlugins(route) { return [ ...(this.plugins ?? []), @@ -32,6 +56,9 @@ export class Router { ] } + /** + * @param {Request} request + */ async handleRequest(request) { const url = new URL(request.url); let matchedRoute; @@ -39,7 +66,7 @@ export class Router { for (const route of this.routes) { const match = route.urlPattern.exec(url); - if(match) { + if (match) { matchedRoute = { options: route.options, render: route.render, @@ -56,15 +83,15 @@ export class Router { const query = Object.fromEntries(new URLSearchParams(url.search)); const params = matchedRoute?.params; - const plugins = this._getPlugins(matchedRoute); - for (const plugin of plugins) { + const plugins = this._getPlugins(/** @type {MatchedRoute} */ (matchedRoute)); + for (const {name, beforeResponse} of plugins) { try { - const result = await plugin?.beforeResponse({url, query, params, request}); + const result = await beforeResponse?.({url, query, params, request}); if (result) { return result; } } catch(e) { - console.log(`Plugin "${plugin.name}" error on beforeResponse hook`, e); + console.log(`Plugin "${name}" error on beforeResponse hook`, e); throw e; } } @@ -81,7 +108,15 @@ export class Router { } export class HtmlResponse { + /** + * + * @param {unknown} template + * @param {*} routeOptions + * @param {*} renderOptions + * @returns + */ constructor(template, routeOptions = {}, renderOptions = {}) { + // @ts-expect-error const iterator = render(template, renderOptions.renderers); const encoder = new TextEncoder(); const stream = new ReadableStream({ @@ -95,7 +130,7 @@ export class HtmlResponse { controller.enqueue(encoder.encode(value)); } } catch(e) { - console.error(e.stack); + console.error(/** @type {Error} */ (e).stack); throw e; } } diff --git a/packages/core/ssr/default.js b/packages/core/ssr/default.js index 927de05..7540df8 100644 --- a/packages/core/ssr/default.js +++ b/packages/core/ssr/default.js @@ -1,6 +1,22 @@ import { render as swtlRender } from '../render.js'; import { DEFAULT_RENDERER_SYMBOL } from '../symbol.js'; +/** + * @typedef {import('../types.js').CustomElementRenderer} CustomElementRenderer + * @typedef {import('../types.js').HtmlResult} HtmlResult + * @typedef {import('../types.js').HtmlValue} HtmlValue + * @typedef {import('../types.js').Children} Children + * @typedef {import('../types.js').Attribute} Attribute + */ + +/** + * @param {{ + * tag: string, + * children: Children, + * attributes: Attribute[], + * renderers: CustomElementRenderer[] + * }} args + */ async function* render({ tag, children, attributes, renderers }) { const attrs = attributes.reduce((acc, { name, value }, index) => { const attribute = typeof value === 'boolean' && value ? name : `${name}="${value}"`; @@ -11,6 +27,9 @@ async function* render({ tag, children, attributes, renderers }) { yield ``; } +/** + * @type {CustomElementRenderer} + */ export const defaultRenderer = { name: DEFAULT_RENDERER_SYMBOL, match() { diff --git a/packages/core/strategies.js b/packages/core/strategies.js index 1752edf..098c275 100644 --- a/packages/core/strategies.js +++ b/packages/core/strategies.js @@ -1,15 +1,27 @@ +/** + * @typedef {import('./types.js').Children} Children + * @typedef {{ + * file: string, + * children: Children + * }} StrategyParams + */ + +/** @param {StrategyParams} params */ export function NetworkFirst({file, children}) { return fetch(file).catch(() => caches.match(file).then(r => r || children)); } +/** @param {StrategyParams} params */ export function CacheFirst({file, children}) { return caches.match(file).then(r => r || fetch(file).catch(() => children)); } +/** @param {StrategyParams} params */ export function CacheOnly({file, children}) { return caches.match(file).then(r => r || children); } +/** @param {StrategyParams} params */ export function NetworkOnly({file, children}) { return fetch(file).catch(() => children); } \ No newline at end of file diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..b1c3539 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "index.js", + "await.js", + "html.js", + "router.js", + "render.js", + "symbol.js", + "slot.js", + "strategies.js", + "ssr/*.js", + "types.ts" + ], + "compilerOptions": { + "composite": true, + "outDir": "./dist-types" + } +} diff --git a/packages/core/types.ts b/packages/core/types.ts new file mode 100644 index 0000000..da15254 --- /dev/null +++ b/packages/core/types.ts @@ -0,0 +1,65 @@ +import { COMPONENT_SYMBOL, CUSTOM_ELEMENT_SYMBOL } from './symbol.js'; + +export interface Attribute { + name: string; + value: unknown; +} +export interface Property extends Attribute {} + +export type HtmlValue = unknown | string | Component | CustomElement | HtmlResult; +export type Children = Array; +export type HtmlResult = Generator; + +export interface Component { + kind: typeof COMPONENT_SYMBOL; + slots: Record; + properties: Array; + children: Children; + fn?: (props: Record, children: Children) => Generator; +} + +export interface CustomElement { + tag: string; + kind: typeof CUSTOM_ELEMENT_SYMBOL; + attributes: Array; + children: Children; +} + +export interface CustomElementRenderer { + name: string | symbol; + match: (customElement: CustomElement) => boolean; + render: (params: { + tag: CustomElement["tag"], + children: Children, + attributes: Attribute[], + renderers: CustomElementRenderer[] + }) => AsyncGenerator; +} + +export type RouteResult = void | Promise | Response | Promise | HtmlResult | Promise; + +export interface RouteArgs { + url: URL; + params: Record; + query: Record; + request: Request; +} + +export interface Plugin { + name: string; + beforeResponse?: (params: RouteArgs) => RouteResult; +} + +export interface Route { + path: string, + render: (params: RouteArgs) => RouteResult, + plugins?: Plugin[], + options?: RequestInit +} + +export interface MatchedRoute { + params: Record; + render: (params: RouteArgs) => RouteResult; + plugins?: Plugin[]; + options?: RequestInit; +} \ No newline at end of file diff --git a/packages/lit/index.js b/packages/lit/index.js index 70c60c2..fced152 100644 --- a/packages/lit/index.js +++ b/packages/lit/index.js @@ -3,11 +3,23 @@ import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js"; import { getElementRenderer } from "@lit-labs/ssr/lib/element-renderer.js"; import { render as swtlRender } from 'swtl/render.js'; + /** - * @TODO - * I have to pass any renderers down to the litRenderer.render (as well as the default renderer) - * swtlRender(children, renderers) + * @typedef {import('swtl').CustomElementRenderer} CustomElementRenderer + * @typedef {import('swtl').HtmlResult} HtmlResult + * @typedef {import('swtl').HtmlValue} HtmlValue + * @typedef {import('swtl').Children} Children + * @typedef {import('swtl').Attribute} Attribute */ + +/** + * @param {{ +* tag: string, +* children: Children, +* attributes: Attribute[], +* renderers: CustomElementRenderer[] +* }} args +*/ async function* render({ tag, children, attributes, renderers }) { const renderInfo = { elementRenderers: [LitElementRenderer], @@ -20,19 +32,21 @@ async function* render({ tag, children, attributes, renderers }) { if (name.startsWith('.')) { renderer.setProperty(name.slice(1), value); } else { - renderer.attributeChangedCallback(name, null, value); + renderer.attributeChangedCallback(name, null, /** @type {string} */ (value)); } }); renderer.connectedCallback(); yield `<${tag}>`; yield ``; yield* swtlRender(children, renderers); yield ``; } +/** @type {CustomElementRenderer} */ export const litRenderer = { name: 'lit', match({tag}) { diff --git a/packages/lit/package.json b/packages/lit/package.json index 7187d12..d327371 100644 --- a/packages/lit/package.json +++ b/packages/lit/package.json @@ -1,11 +1,13 @@ { "name": "@swtl/lit", - "version": "0.1.3", + "version": "0.1.4", "description": "", "type": "module", "main": "index.js", "scripts": { - "test": "node --test test/*.test.js" + "test": "node --test test/*.test.js", + "lint:types": "tsc", + "lint:types:watch": "tsc --watch" }, "keywords": [], "author": "", @@ -13,7 +15,7 @@ "dependencies": { "@lit-labs/ssr": "^3.2.2", "@lit-labs/ssr-dom-shim": "^1.2.0", - "swtl": "^0.3.0" + "swtl": "^0.3.2" }, "devDependencies": {} } diff --git a/packages/lit/tsconfig.json b/packages/lit/tsconfig.json new file mode 100644 index 0000000..fc82805 --- /dev/null +++ b/packages/lit/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["*.js"], + "compilerOptions": { + "composite": true, + "outDir": "./dist-types" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4bd5338 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "NodeNext", + "moduleResolution": "node16", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist-types", + "strict": true, + "noImplicitThis": true, + "alwaysStrict": true, + "skipLibCheck": true + }, + "files": [], + "references": [ + { + "path": "./packages/core" + }, + { + "path": "./packages/lit" + }, + ] +}