From 8b080920613a569239ddada7842939a95ff1bff5 Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Fri, 10 Feb 2023 15:54:17 -0700 Subject: [PATCH] feat: add taglib extensions and type definitions for typescript support --- .changeset/gentle-foxes-knock.md | 7 + packages/babel-utils/index.d.ts | 4 +- packages/compiler/src/taglib/loader/Tag.js | 1 + packages/compiler/src/taglib/loader/Taglib.js | 1 + .../src/taglib/loader/loadTagFromProps.js | 11 + .../src/taglib/loader/loadTaglibFromProps.js | 6 + packages/marko/index.d.ts | 316 ++++++++++++++++++ packages/marko/package.json | 3 +- .../src/core-tags/core/await/index.marko | 13 + .../src/taglib/core/index.js | 1 + 10 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 .changeset/gentle-foxes-knock.md create mode 100644 packages/marko/index.d.ts create mode 100644 packages/marko/src/core-tags/core/await/index.marko diff --git a/.changeset/gentle-foxes-knock.md b/.changeset/gentle-foxes-knock.md new file mode 100644 index 0000000000..761710dcfc --- /dev/null +++ b/.changeset/gentle-foxes-knock.md @@ -0,0 +1,7 @@ +--- +"@marko/translator-default": patch +"@marko/compiler": patch +"marko": patch +--- + +Add taglib extensions and type definitions for typescript support. diff --git a/packages/babel-utils/index.d.ts b/packages/babel-utils/index.d.ts index 9ba8f036f6..9a1f693dc6 100644 --- a/packages/babel-utils/index.d.ts +++ b/packages/babel-utils/index.d.ts @@ -70,8 +70,8 @@ export interface TagDefinition { export interface TaglibLookup { getTagsSorted(): TagDefinition[]; - getTag(tagName: string): TagDefinition; - getAttribute(tagName: string, attrName: string): AttributeDefinition; + getTag(tagName: string): undefined | TagDefinition; + getAttribute(tagName: string, attrName: string): undefined | AttributeDefinition; forEachAttribute( tagName: string, callback: (attr: AttributeDefinition, tag: TagDefinition) => void diff --git a/packages/compiler/src/taglib/loader/Tag.js b/packages/compiler/src/taglib/loader/Tag.js index cff4491b27..de8bb7d296 100644 --- a/packages/compiler/src/taglib/loader/Tag.js +++ b/packages/compiler/src/taglib/loader/Tag.js @@ -14,6 +14,7 @@ class Tag { this.attributes = {}; this.transformers = []; this.patternAttributes = []; + this.types = undefined; } addAttribute(attr) { diff --git a/packages/compiler/src/taglib/loader/Taglib.js b/packages/compiler/src/taglib/loader/Taglib.js index b5d4f0d2d4..5c3f81d003 100644 --- a/packages/compiler/src/taglib/loader/Taglib.js +++ b/packages/compiler/src/taglib/loader/Taglib.js @@ -30,6 +30,7 @@ class Taglib { ok(filePath, '"filePath" expected'); this.filePath = this.path /* deprecated */ = this.id = filePath; this.dirname = path.dirname(this.filePath); + this.scriptLang = undefined; this.tags = {}; this.migrators = []; this.transformers = []; diff --git a/packages/compiler/src/taglib/loader/loadTagFromProps.js b/packages/compiler/src/taglib/loader/loadTagFromProps.js index d8b4f59130..0e65a19a77 100644 --- a/packages/compiler/src/taglib/loader/loadTagFromProps.js +++ b/packages/compiler/src/taglib/loader/loadTagFromProps.js @@ -322,6 +322,17 @@ class TagLoader { } } + /** + * This property is used by @marko/language-tools (editor tooling) + * to override the Marko file used when generating the tags exposed + * typescript / jsdoc types. + */ + types(value) { + var tag = this.tag; + var dirname = this.dirname; + tag.types = nodePath.resolve(dirname, value); + } + /** * An Object where each property maps to an attribute definition. * The property key will be the attribute name and the property value diff --git a/packages/compiler/src/taglib/loader/loadTaglibFromProps.js b/packages/compiler/src/taglib/loader/loadTaglibFromProps.js index 656f6bd65d..9ed09af513 100644 --- a/packages/compiler/src/taglib/loader/loadTaglibFromProps.js +++ b/packages/compiler/src/taglib/loader/loadTaglibFromProps.js @@ -210,6 +210,12 @@ class TaglibLoader { } } } + scriptLang(lang) { + // The "script-lang" property is used to specify the language of embedded scripts (either "js" or "ts"). + // The language tools will prefer the language specified by the "script-lang" if specified. + // If unspecified the language tools will check for a tsconfig, if one is found then "ts", otherwise we use "js". + this.taglib.scriptLang = lang; + } tagsDir(dir) { // The "tags-dir" property is used to supporting scanning // of a directory to discover custom tags. Scanning a directory diff --git a/packages/marko/index.d.ts b/packages/marko/index.d.ts new file mode 100644 index 0000000000..c114f3fb4b --- /dev/null +++ b/packages/marko/index.d.ts @@ -0,0 +1,316 @@ +declare module "*.marko" { + const template: Marko.Template; + export default template; +} + +declare namespace NodeJS { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface ReadableStream { } +} + +declare namespace Marko { + /** A mutable global object for the current render. */ + export interface Global { + serializedGlobals?: Record; + [attr: PropertyKey]: unknown; + } + + export type TemplateInput = Input & { + $global?: Global; + }; + + export interface Out + extends PromiseLike> { + /** The underlying ReadableStream Marko is writing into. */ + stream: unknown; + /** A mutable global object for the current render. */ + global: Global; + /** Disable all async rendering. Will error if something beings async. */ + sync(): void; + /** Returns true if async rendering is disabled. */ + isSync(): boolean; + /** Write unescaped text at the current stream position. */ + write(val: string | void): this; + /** Write javascript content to be merged with the scripts Marko sends out on the next flush. */ + script(val: string | void): this; + /** Returns the currently rendered html content. */ + toString(): string; + /** Starts a new async/forked stream. */ + beginAsync(options?: { + name?: string; + timeout?: number; + last?: boolean; + }): Out; + /** Marks the current stream as complete (async streams may still be executing). */ + end(val?: string | void): this; + emit(eventName: PropertyKey, ...args: any[]): boolean; + on(eventName: PropertyKey, listener: (...args: any[]) => any): this; + once(eventName: PropertyKey, listener: (...args: any[]) => any): this; + prependListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + removeListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + /** Register a callback executed when the last async out has completed. */ + onLast(listener: (next: () => void) => unknown): this; + /** Pipe Marko's stream to another stream. */ + pipe(stream: unknown): this; + /** Emits an error on the stream. */ + error(e: Error): this; + /** Schedules a Marko to flush buffered html to the underlying stream. */ + flush(): this; + /** Creates a detached out stream (used for out of order flushing). */ + createOut(): Out; + /** Write escaped text at the current stream position. */ + text(val: string | void): void; + } + + /** Body content created from by a component, typically held in an object with a renderBody property. */ + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Body< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + in Params extends readonly any[] = [], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + out Return = void + > { } + + /** Valid data types which can be passed in as a <${dynamic}/> tag name. */ + export type DynamicTagName = + | { + renderBody?: Body | Template | string | void | false; + } + | Body + | Template + | string + | void + | false; + + /** Extract the return tag type from a renderBody. */ + export type BodyReturnType = B extends Body + ? Return + : never; + + /** Extract the tag parameter types received by a renderBody. */ + export type BodyParamaters = B extends Body + ? Params + : never; + + export abstract class Component< + Input extends Record = Record + > implements Emitter { + /** A unique id for this instance. */ + public readonly id: string; + /** The top level element rendered by this instance. */ + public readonly el: Element | void; + /** The attributes passed to this instance. */ + public readonly input: Input; + /** @deprecated */ + public readonly els: Element[]; + /** Mutable state that when changed causes a rerender. */ + abstract state: undefined | null | Record; + + /** Returns the amount of event handlers listening to a specific event. */ + listenerCount(eventName: PropertyKey): number; + /** + * Used to wrap an existing event emitted and ensure that all events are + * cleaned up once this component is destroyed + * */ + subscribeTo( + emitter: unknown + ): Omit; + /** Emits an event on the component instance. */ + emit(eventName: PropertyKey, ...args: any[]): boolean; + /** Listen to an event on the component instance. */ + on(eventName: PropertyKey, listener: (...args: any[]) => any): this; + /** Listen to an event on the component instance once. */ + once(eventName: PropertyKey, listener: (...args: any[]) => any): this; + /** Listen to an event on the component instance before all other listeners. */ + prependListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + /** Remove a listener from the component instance. */ + removeListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + /** Remove all listeners from the component instance. */ + removeAllListeners(eventName?: PropertyKey): this; + /** Removes the component instance from the DOM and cleans up all active event handlers including all children. */ + destroy(): void; + /** Schedule an update (similar to if a state had been changed). */ + forceUpdate(): void; + /** Generates a unique id derived from the current unique instance id (similar to :scoped in the template). */ + elId(key?: string, index?: number): string; + /** @alias elId */ + getElId(key?: string, index?: number): string; + /** Gets an element reference by its `key` attribute in the template. */ + getEl( + key?: string, + index?: number + ): T; + /** Gets all element references by their `key` attribute in the template. */ + getEls(key: string): T; + /** Gets a component reference by its `key` attribute in the template. */ + getComponent( + key: string, + index?: number + ): T; + /** Gets all component references by their `key` attribute in the template. */ + getComponents(key: string): T; + /** True if this instance has been removed from the dom. */ + /** True if this instance is scheduled to rerender. */ + isDestroyed(): boolean; + /** Replace the entire state object with a new one, removing old properties. */ + replaceState(state: this["state"]): void; + /** + * Update a property on this.state (should prefer mutating this.state directly). + * When passed an object as the first argument, it will be merged into the state. + */ + setState( + name: Key & keyof this["state"], + value: (this["state"] & Record)[Key] + ): void; + setState(value: Partial): void; + + /** Schedules an update related to a specific state property and optionally updates the value. */ + setStateDirty( + name: Key & keyof this["state"], + value?: (this["state"] & Record)[Key] + ): void; + /** Synchronously flush any scheduled updates. */ + update(): void; + /** Appends the dom for the current instance to a parent DOM element. */ + appendTo(target: ParentNode): this; + /** Inserts the dom for the current instance after a sibling DOM element. */ + insertAfter(target: ChildNode): this; + /** Inserts the dom for the current instance before a sibling DOM element. */ + insertBefore(target: ChildNode): this; + /** Prepends the dom for the current instance to a parent DOM element. */ + prependTo(target: ParentNode): this; + /** Replaces an existing DOM element with the dom for the current instance. */ + replace(target: ChildNode): this; + /** Replaces the children of an existing DOM element with the dom for the current instance. */ + replaceChildrenOf(target: ParentNode): this; + /** Called when the component is firsted created. */ + abstract onCreate?(input: this["input"], out: Marko.Out): void; + /** Called every time the component receives input from it's parent. */ + abstract onInput?( + input: this["input"], + out: Marko.Out + ): void | this["input"]; + /** Called after a component has successfully rendered, but before it's update has been applied to the dom. */ + abstract onRender?(out: Marko.Out): void; + /** Called after the first time the component renders and is attached to the dom. */ + abstract onMount?(): void; + /** Called when a components render has been applied to the DOM (excluding when it is initially mounted). */ + abstract onUpdate?(): void; + /** Called when a component is destroyed and removed from the dom. */ + abstract onDestroy?(): void; + } + + /** The top level api for a Marko Template. */ + export abstract class Template { + /** Creates a Marko compatible output stream. */ + createOut(): Out; + + /** + * The folowing types are processed up by the @marko/language-tools + * and inlined into the compiled template. + * + * This is done to support generics on each of these methods + * until TypeScript supports higher kinded types. + * + * https://github.com/microsoft/TypeScript/issues/1213 + */ + + /** @marko-overload-start */ + /** Asynchronously render the template. */ + abstract render( + input: Marko.TemplateInput, + stream?: { + write: (chunk: string) => void; + end: (chunk?: string) => void; + } + ): Marko.Out; + + /** Synchronously render the template. */ + abstract renderSync( + input: Marko.TemplateInput + ): Marko.RenderResult; + + /** Synchronously render a template to a string. */ + abstract renderToString(input: Marko.TemplateInput): string; + + /** Render a template and return a stream.Readable in nodejs or a ReadableStream in a web worker environment. */ + abstract stream( + input: Marko.TemplateInput + ): ReadableStream & NodeJS.ReadableStream; + /** @marko-overload-end */ + } + + export interface RenderResult< + out Component extends Marko.Component = Marko.Component + > { + /** Returns the component created as a result of rendering the template. */ + getComponent(): Component; + getComponents(selector?: any): any; + /** Triggers the mount lifecycle of a component without necessarily attaching it to the DOM. */ + afterInsert(host?: any): this; + /** Gets the DOM node rendered by a template. */ + getNode(host?: any): Node; + /** Gets the HTML output of the rendered template. */ + toString(): string; + /** Appends the dom of the rendered template to a parent DOM element. */ + appendTo(target: ParentNode): this; + /** Inserts the dom of the rendered template after a sibling DOM element. */ + insertAfter(target: ChildNode): this; + /** Inserts the dom of the rendered template before a sibling DOM element. */ + insertBefore(target: ChildNode): this; + /** Prepends the dom of the rendered template to a parent DOM element. */ + prependTo(target: ParentNode): this; + /** Replaces an existing DOM element with the dom of the rendered template. */ + replace(target: ChildNode): this; + /** Replaces the children of an existing DOM element with the dom of the rendered template. */ + replaceChildrenOf(target: ParentNode): this; + out: Out; + /** @deprecated */ + document: any; + /** @deprecated */ + getOutput(): string; + /** @deprecated */ + html: string; + /** @deprecated */ + context: Out; + } + + export interface Emitter { + listenerCount(eventName: PropertyKey): number; + emit(eventName: PropertyKey, ...args: any[]): boolean; + on(eventName: PropertyKey, listener: (...args: any[]) => any): this; + once(eventName: PropertyKey, listener: (...args: any[]) => any): this; + prependListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + removeListener( + eventName: PropertyKey, + listener: (...args: any[]) => any + ): this; + removeAllListeners(eventName?: PropertyKey): this; + } + + export type Repeated = [T, T, ...T[]]; + export type Repeatable = T | Repeated; + export type MaybeRepeatable = undefined | Repeatable; + + export interface NativeTags { + [name: string]: { + input: Record; + return: unknown; + }; + } +} diff --git a/packages/marko/package.json b/packages/marko/package.json index aa6da7b2b6..536c6f4003 100644 --- a/packages/marko/package.json +++ b/packages/marko/package.json @@ -71,5 +71,6 @@ "index-browser.marko", "index.js", "node-require.js" - ] + ], + "types": "index.d.ts" } diff --git a/packages/marko/src/core-tags/core/await/index.marko b/packages/marko/src/core-tags/core/await/index.marko new file mode 100644 index 0000000000..59a550ad7a --- /dev/null +++ b/packages/marko/src/core-tags/core/await/index.marko @@ -0,0 +1,13 @@ +/** + * @template T + * @typedef {{ + * value?: T; + * then?: Marko.Body<[Awaited], void>; + * catch?: Marko.Body<[unknown], void>; + * placeholder?: Marko.Body<[], void>; + * client-reorder?: boolean; + * name?: string; + * timeout?: number; + * show-after?: string; + * }} Input + */ diff --git a/packages/translator-default/src/taglib/core/index.js b/packages/translator-default/src/taglib/core/index.js index 3cea99fcf9..d2a557504d 100644 --- a/packages/translator-default/src/taglib/core/index.js +++ b/packages/translator-default/src/taglib/core/index.js @@ -284,6 +284,7 @@ export default { }, "": { renderer: "marko/src/core-tags/core/await/renderer.js", + types: "marko/src/core-tags/core/await/index.marko", "code-generator": translateAwait, "@_provider": "expression", "@_name": "string",