Skip to content

Commit f1efd70

Browse files
authored
feat: add taglib extensions and type definitions for typescript support (#1885)
* feat: add taglib extensions and type definitions for typescript support * [ci] format --------- Co-authored-by: DylanPiercey <[email protected]>
1 parent aaa1a91 commit f1efd70

File tree

10 files changed

+364
-3
lines changed

10 files changed

+364
-3
lines changed

.changeset/gentle-foxes-knock.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@marko/translator-default": patch
3+
"@marko/compiler": patch
4+
"marko": patch
5+
---
6+
7+
Add taglib extensions and type definitions for typescript support.

packages/babel-utils/index.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ export interface TagDefinition {
7070

7171
export interface TaglibLookup {
7272
getTagsSorted(): TagDefinition[];
73-
getTag(tagName: string): TagDefinition;
74-
getAttribute(tagName: string, attrName: string): AttributeDefinition;
73+
getTag(tagName: string): undefined | TagDefinition;
74+
getAttribute(
75+
tagName: string,
76+
attrName: string
77+
): undefined | AttributeDefinition;
7578
forEachAttribute(
7679
tagName: string,
7780
callback: (attr: AttributeDefinition, tag: TagDefinition) => void

packages/compiler/src/taglib/loader/Tag.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Tag {
1414
this.attributes = {};
1515
this.transformers = [];
1616
this.patternAttributes = [];
17+
this.types = undefined;
1718
}
1819

1920
addAttribute(attr) {

packages/compiler/src/taglib/loader/Taglib.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Taglib {
3030
ok(filePath, '"filePath" expected');
3131
this.filePath = this.path /* deprecated */ = this.id = filePath;
3232
this.dirname = path.dirname(this.filePath);
33+
this.scriptLang = undefined;
3334
this.tags = {};
3435
this.migrators = [];
3536
this.transformers = [];

packages/compiler/src/taglib/loader/loadTagFromProps.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ class TagLoader {
322322
}
323323
}
324324

325+
/**
326+
* This property is used by @marko/language-tools (editor tooling)
327+
* to override the Marko file used when generating the tags exposed
328+
* typescript / jsdoc types.
329+
*/
330+
types(value) {
331+
var tag = this.tag;
332+
var dirname = this.dirname;
333+
tag.types = nodePath.resolve(dirname, value);
334+
}
335+
325336
/**
326337
* An Object where each property maps to an attribute definition.
327338
* The property key will be the attribute name and the property value

packages/compiler/src/taglib/loader/loadTaglibFromProps.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ class TaglibLoader {
210210
}
211211
}
212212
}
213+
scriptLang(lang) {
214+
// The "script-lang" property is used to specify the language of embedded scripts (either "js" or "ts").
215+
// The language tools will prefer the language specified by the "script-lang" if specified.
216+
// If unspecified the language tools will check for a tsconfig, if one is found then "ts", otherwise we use "js".
217+
this.taglib.scriptLang = lang;
218+
}
213219
tagsDir(dir) {
214220
// The "tags-dir" property is used to supporting scanning
215221
// of a directory to discover custom tags. Scanning a directory

packages/marko/index.d.ts

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
declare module "*.marko" {
2+
const template: Marko.Template;
3+
export default template;
4+
}
5+
6+
declare namespace NodeJS {
7+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
8+
interface ReadableStream {}
9+
}
10+
11+
declare namespace Marko {
12+
/** A mutable global object for the current render. */
13+
export interface Global {
14+
serializedGlobals?: Record<string, boolean>;
15+
[attr: PropertyKey]: unknown;
16+
}
17+
18+
export type TemplateInput<Input = { [attr: PropertyKey]: any }> = Input & {
19+
$global?: Global;
20+
};
21+
22+
export interface Out<Component extends Marko.Component = Marko.Component>
23+
extends PromiseLike<RenderResult<Component>> {
24+
/** The underlying ReadableStream Marko is writing into. */
25+
stream: unknown;
26+
/** A mutable global object for the current render. */
27+
global: Global;
28+
/** Disable all async rendering. Will error if something beings async. */
29+
sync(): void;
30+
/** Returns true if async rendering is disabled. */
31+
isSync(): boolean;
32+
/** Write unescaped text at the current stream position. */
33+
write(val: string | void): this;
34+
/** Write javascript content to be merged with the scripts Marko sends out on the next flush. */
35+
script(val: string | void): this;
36+
/** Returns the currently rendered html content. */
37+
toString(): string;
38+
/** Starts a new async/forked stream. */
39+
beginAsync(options?: {
40+
name?: string;
41+
timeout?: number;
42+
last?: boolean;
43+
}): Out;
44+
/** Marks the current stream as complete (async streams may still be executing). */
45+
end(val?: string | void): this;
46+
emit(eventName: PropertyKey, ...args: any[]): boolean;
47+
on(eventName: PropertyKey, listener: (...args: any[]) => any): this;
48+
once(eventName: PropertyKey, listener: (...args: any[]) => any): this;
49+
prependListener(
50+
eventName: PropertyKey,
51+
listener: (...args: any[]) => any
52+
): this;
53+
removeListener(
54+
eventName: PropertyKey,
55+
listener: (...args: any[]) => any
56+
): this;
57+
/** Register a callback executed when the last async out has completed. */
58+
onLast(listener: (next: () => void) => unknown): this;
59+
/** Pipe Marko's stream to another stream. */
60+
pipe(stream: unknown): this;
61+
/** Emits an error on the stream. */
62+
error(e: Error): this;
63+
/** Schedules a Marko to flush buffered html to the underlying stream. */
64+
flush(): this;
65+
/** Creates a detached out stream (used for out of order flushing). */
66+
createOut(): Out;
67+
/** Write escaped text at the current stream position. */
68+
text(val: string | void): void;
69+
}
70+
71+
/** Body content created from by a component, typically held in an object with a renderBody property. */
72+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
73+
export interface Body<
74+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
75+
in Params extends readonly any[] = [],
76+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
77+
out Return = void
78+
> {}
79+
80+
/** Valid data types which can be passed in as a <${dynamic}/> tag name. */
81+
export type DynamicTagName =
82+
| {
83+
renderBody?: Body<any, any> | Template | string | void | false;
84+
}
85+
| Body<any, any>
86+
| Template
87+
| string
88+
| void
89+
| false;
90+
91+
/** Extract the return tag type from a renderBody. */
92+
export type BodyReturnType<B> = B extends Body<any, infer Return>
93+
? Return
94+
: never;
95+
96+
/** Extract the tag parameter types received by a renderBody. */
97+
export type BodyParamaters<B> = B extends Body<infer Params, any>
98+
? Params
99+
: never;
100+
101+
export abstract class Component<
102+
Input extends Record<PropertyKey, any> = Record<PropertyKey, any>
103+
> implements Emitter
104+
{
105+
/** A unique id for this instance. */
106+
public readonly id: string;
107+
/** The top level element rendered by this instance. */
108+
public readonly el: Element | void;
109+
/** The attributes passed to this instance. */
110+
public readonly input: Input;
111+
/** @deprecated */
112+
public readonly els: Element[];
113+
/** Mutable state that when changed causes a rerender. */
114+
abstract state: undefined | null | Record<PropertyKey, any>;
115+
116+
/** Returns the amount of event handlers listening to a specific event. */
117+
listenerCount(eventName: PropertyKey): number;
118+
/**
119+
* Used to wrap an existing event emitted and ensure that all events are
120+
* cleaned up once this component is destroyed
121+
* */
122+
subscribeTo(
123+
emitter: unknown
124+
): Omit<Emitter, "listenerCount" | "prependListener" | "emit">;
125+
/** Emits an event on the component instance. */
126+
emit(eventName: PropertyKey, ...args: any[]): boolean;
127+
/** Listen to an event on the component instance. */
128+
on(eventName: PropertyKey, listener: (...args: any[]) => any): this;
129+
/** Listen to an event on the component instance once. */
130+
once(eventName: PropertyKey, listener: (...args: any[]) => any): this;
131+
/** Listen to an event on the component instance before all other listeners. */
132+
prependListener(
133+
eventName: PropertyKey,
134+
listener: (...args: any[]) => any
135+
): this;
136+
/** Remove a listener from the component instance. */
137+
removeListener(
138+
eventName: PropertyKey,
139+
listener: (...args: any[]) => any
140+
): this;
141+
/** Remove all listeners from the component instance. */
142+
removeAllListeners(eventName?: PropertyKey): this;
143+
/** Removes the component instance from the DOM and cleans up all active event handlers including all children. */
144+
destroy(): void;
145+
/** Schedule an update (similar to if a state had been changed). */
146+
forceUpdate(): void;
147+
/** Generates a unique id derived from the current unique instance id (similar to :scoped in the template). */
148+
elId(key?: string, index?: number): string;
149+
/** @alias elId */
150+
getElId(key?: string, index?: number): string;
151+
/** Gets an element reference by its `key` attribute in the template. */
152+
getEl<T extends Element | void = Element | void>(
153+
key?: string,
154+
index?: number
155+
): T;
156+
/** Gets all element references by their `key` attribute in the template. */
157+
getEls<T extends Element[] = Element[]>(key: string): T;
158+
/** Gets a component reference by its `key` attribute in the template. */
159+
getComponent<T extends Component | void = Component | void>(
160+
key: string,
161+
index?: number
162+
): T;
163+
/** Gets all component references by their `key` attribute in the template. */
164+
getComponents<T extends Component[] = Component[]>(key: string): T;
165+
/** True if this instance has been removed from the dom. */
166+
/** True if this instance is scheduled to rerender. */
167+
isDestroyed(): boolean;
168+
/** Replace the entire state object with a new one, removing old properties. */
169+
replaceState(state: this["state"]): void;
170+
/**
171+
* Update a property on this.state (should prefer mutating this.state directly).
172+
* When passed an object as the first argument, it will be merged into the state.
173+
*/
174+
setState<Key extends PropertyKey>(
175+
name: Key & keyof this["state"],
176+
value: (this["state"] & Record<PropertyKey, unknown>)[Key]
177+
): void;
178+
setState(value: Partial<this["state"]>): void;
179+
180+
/** Schedules an update related to a specific state property and optionally updates the value. */
181+
setStateDirty<Key extends PropertyKey>(
182+
name: Key & keyof this["state"],
183+
value?: (this["state"] & Record<PropertyKey, unknown>)[Key]
184+
): void;
185+
/** Synchronously flush any scheduled updates. */
186+
update(): void;
187+
/** Appends the dom for the current instance to a parent DOM element. */
188+
appendTo(target: ParentNode): this;
189+
/** Inserts the dom for the current instance after a sibling DOM element. */
190+
insertAfter(target: ChildNode): this;
191+
/** Inserts the dom for the current instance before a sibling DOM element. */
192+
insertBefore(target: ChildNode): this;
193+
/** Prepends the dom for the current instance to a parent DOM element. */
194+
prependTo(target: ParentNode): this;
195+
/** Replaces an existing DOM element with the dom for the current instance. */
196+
replace(target: ChildNode): this;
197+
/** Replaces the children of an existing DOM element with the dom for the current instance. */
198+
replaceChildrenOf(target: ParentNode): this;
199+
/** Called when the component is firsted created. */
200+
abstract onCreate?(input: this["input"], out: Marko.Out): void;
201+
/** Called every time the component receives input from it's parent. */
202+
abstract onInput?(
203+
input: this["input"],
204+
out: Marko.Out
205+
): void | this["input"];
206+
/** Called after a component has successfully rendered, but before it's update has been applied to the dom. */
207+
abstract onRender?(out: Marko.Out): void;
208+
/** Called after the first time the component renders and is attached to the dom. */
209+
abstract onMount?(): void;
210+
/** Called when a components render has been applied to the DOM (excluding when it is initially mounted). */
211+
abstract onUpdate?(): void;
212+
/** Called when a component is destroyed and removed from the dom. */
213+
abstract onDestroy?(): void;
214+
}
215+
216+
/** The top level api for a Marko Template. */
217+
export abstract class Template {
218+
/** Creates a Marko compatible output stream. */
219+
createOut(): Out;
220+
221+
/**
222+
* The folowing types are processed up by the @marko/language-tools
223+
* and inlined into the compiled template.
224+
*
225+
* This is done to support generics on each of these methods
226+
* until TypeScript supports higher kinded types.
227+
*
228+
* https://github.com/microsoft/TypeScript/issues/1213
229+
*/
230+
231+
/** @marko-overload-start */
232+
/** Asynchronously render the template. */
233+
abstract render(
234+
input: Marko.TemplateInput,
235+
stream?: {
236+
write: (chunk: string) => void;
237+
end: (chunk?: string) => void;
238+
}
239+
): Marko.Out<Marko.Component>;
240+
241+
/** Synchronously render the template. */
242+
abstract renderSync(
243+
input: Marko.TemplateInput
244+
): Marko.RenderResult<Marko.Component>;
245+
246+
/** Synchronously render a template to a string. */
247+
abstract renderToString(input: Marko.TemplateInput): string;
248+
249+
/** Render a template and return a stream.Readable in nodejs or a ReadableStream in a web worker environment. */
250+
abstract stream(
251+
input: Marko.TemplateInput
252+
): ReadableStream<string> & NodeJS.ReadableStream;
253+
/** @marko-overload-end */
254+
}
255+
256+
export interface RenderResult<
257+
out Component extends Marko.Component = Marko.Component
258+
> {
259+
/** Returns the component created as a result of rendering the template. */
260+
getComponent(): Component;
261+
getComponents(selector?: any): any;
262+
/** Triggers the mount lifecycle of a component without necessarily attaching it to the DOM. */
263+
afterInsert(host?: any): this;
264+
/** Gets the DOM node rendered by a template. */
265+
getNode(host?: any): Node;
266+
/** Gets the HTML output of the rendered template. */
267+
toString(): string;
268+
/** Appends the dom of the rendered template to a parent DOM element. */
269+
appendTo(target: ParentNode): this;
270+
/** Inserts the dom of the rendered template after a sibling DOM element. */
271+
insertAfter(target: ChildNode): this;
272+
/** Inserts the dom of the rendered template before a sibling DOM element. */
273+
insertBefore(target: ChildNode): this;
274+
/** Prepends the dom of the rendered template to a parent DOM element. */
275+
prependTo(target: ParentNode): this;
276+
/** Replaces an existing DOM element with the dom of the rendered template. */
277+
replace(target: ChildNode): this;
278+
/** Replaces the children of an existing DOM element with the dom of the rendered template. */
279+
replaceChildrenOf(target: ParentNode): this;
280+
out: Out<Component>;
281+
/** @deprecated */
282+
document: any;
283+
/** @deprecated */
284+
getOutput(): string;
285+
/** @deprecated */
286+
html: string;
287+
/** @deprecated */
288+
context: Out<Component>;
289+
}
290+
291+
export interface Emitter {
292+
listenerCount(eventName: PropertyKey): number;
293+
emit(eventName: PropertyKey, ...args: any[]): boolean;
294+
on(eventName: PropertyKey, listener: (...args: any[]) => any): this;
295+
once(eventName: PropertyKey, listener: (...args: any[]) => any): this;
296+
prependListener(
297+
eventName: PropertyKey,
298+
listener: (...args: any[]) => any
299+
): this;
300+
removeListener(
301+
eventName: PropertyKey,
302+
listener: (...args: any[]) => any
303+
): this;
304+
removeAllListeners(eventName?: PropertyKey): this;
305+
}
306+
307+
export type Repeated<T> = [T, T, ...T[]];
308+
export type Repeatable<T> = T | Repeated<T>;
309+
export type MaybeRepeatable<T> = undefined | Repeatable<T>;
310+
311+
export interface NativeTags {
312+
[name: string]: {
313+
input: Record<string, unknown>;
314+
return: unknown;
315+
};
316+
}
317+
}

packages/marko/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,6 @@
7171
"index-browser.marko",
7272
"index.js",
7373
"node-require.js"
74-
]
74+
],
75+
"types": "index.d.ts"
7576
}

0 commit comments

Comments
 (0)