Skip to content

Commit

Permalink
Merge pull request #322 from marko-js/improve-for-tag-types
Browse files Browse the repository at this point in the history
fix: improve for tag types and codegen
  • Loading branch information
DylanPiercey authored Dec 18, 2024
2 parents 33c6268 + c99ca6c commit 029bb93
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 80 deletions.
8 changes: 8 additions & 0 deletions .changeset/red-pumpkins-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@marko/language-server": patch
"@marko/language-tools": patch
"@marko/type-check": patch
"marko-vscode": patch
---

Improve `<for>` tag codegen and types.
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,7 @@
```marko
1 | <${custom}>
> 2 | <for>
| ^^^ No overload matches this call.
Overload 1 of 4, '(input: { of: readonly any[] | Iterable<any>; }, renderBody: (value: any, index: number, all: readonly any[] | Iterable<any>) => unknown): {}', gave the following error.
Argument of type '{}' is not assignable to parameter of type '{ of: readonly any[] | Iterable<any>; }'.
Property 'of' is missing in type '{}' but required in type '{ of: readonly any[] | Iterable<any>; }'.
Overload 2 of 4, '(input: { in: object; }, renderBody: (key: never, value: never) => unknown): {}', gave the following error.
Argument of type '{}' is not assignable to parameter of type '{ in: object; }'.
Property 'in' is missing in type '{}' but required in type '{ in: object; }'.
Overload 3 of 4, '(input: { from?: number | void | undefined; to: number; step?: number | void | undefined; }, renderBody: (index: number) => unknown): {}', gave the following error.
Argument of type '{}' is not assignable to parameter of type '{ from?: number | void | undefined; to: number; step?: number | void | undefined; }'.
Property 'to' is missing in type '{}' but required in type '{ from?: number | void | undefined; to: number; step?: number | void | undefined; }'.
| ^^^ Argument of type '{}' is not assignable to parameter of type '{ of: false | void | readonly unknown[] | Iterable<unknown> | null; } | { in: object; } | { from?: number | undefined; to: number; step?: number | undefined; }'.
3 | <@a/>
4 | </for>
5 | </>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,7 @@
79 | }/>
80 |
> 81 | <for|index|>
| ^^^^^^^^^ No overload matches this call.
The last overload gave the following error.
Argument of type '{ renderBody: (index: any) => MarkoReturn<void>; }' is not assignable to parameter of type '({ from?: number | undefined; to: number; step?: number | undefined; } | { in: unknown; } | { of: readonly unknown[] | Iterable<unknown>; }) & { renderBody?: AnyMarkoBody | undefined; by?: ((...args: unknown[]) => string) | undefined; }'.
Type '{ renderBody: (index: any) => MarkoReturn<void>; }' is not assignable to type '{ of: readonly unknown[] | Iterable<unknown>; } & { renderBody?: AnyMarkoBody | undefined; by?: ((...args: unknown[]) => string) | undefined; }'.
Property 'of' is missing in type '{ renderBody: (index: any) => MarkoReturn<void>; }' but required in type '{ of: readonly unknown[] | Iterable<unknown>; }'.
| ^^^ Argument of type '{}' is not assignable to parameter of type '({ from?: number | undefined; to: number; step?: number | undefined; } | { in: false | void | object | null; } | { of: false | void | readonly unknown[] | Iterable<unknown> | null; }) & { by?: ((...args: unknown[]) => string) | undefined; }'.
82 | Should error
83 | </for>
84 |
Expand Down
109 changes: 58 additions & 51 deletions packages/language-tools/marko.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,72 +150,79 @@ declare global {
Input extends { value: unknown; valueChange?: (value: any) => void },
>(input: Input): Input;

export function forTag<
export function forOfTag<
Value,
Item extends Value extends
| readonly (infer Item)[]
| Iterable<infer Item>
? Item
: unknown,
RenderBody extends Marko.Body<
BodyContent extends Marko.Body<
[item: Item, index: number, all: Value],
void
>,
>(input: {
of: Value;
renderBody: RenderBody;
by?: (item: Item, index: number) => string;
}): ReturnAndScope<RenderBodyScope<RenderBody>, void>;
>(
input: {
of: Value | false | void | null;
by?: (item: Item, index: number) => string;
},
content: BodyContent,
): ReturnAndScope<BodyContentScope<BodyContent>, void>;

export function forTag<
export function forInTag<
Value,
RenderBody extends Marko.Body<
BodyContent extends Marko.Body<
[key: keyof Value, value: Value[keyof Value]],
void
>,
>(input: {
in: Value;
renderBody: RenderBody;
by?: (value: Value[keyof Value], key: keyof Value) => string;
}): ReturnAndScope<RenderBodyScope<RenderBody>, void>;
>(
input: {
in: Value | false | void | null;
by?: (value: Value[keyof Value], key: keyof Value) => string;
},
content: BodyContent,
): ReturnAndScope<BodyContentScope<BodyContent>, void>;

export function forTag<
export function forToTag<
From extends void | number,
To extends number,
Step extends void | number,
RenderBody extends Marko.Body<[index: number], void>,
>(input: {
from?: From;
to: To;
step?: Step;
renderBody: RenderBody;
by?: (index: number) => string;
}): ReturnAndScope<RenderBodyScope<RenderBody>, void>;

export function forTag<RenderBody extends AnyMarkoBody>(
BodyContent extends Marko.Body<[index: number], void>,
>(
input: {
from?: From;
to: To;
step?: Step;
by?: (index: number) => string;
},
content: BodyContent,
): ReturnAndScope<BodyContentScope<BodyContent>, void>;

export function forTag<BodyContent extends AnyMarkoBody>(
input: (
| {
from?: number;
to: number;
step?: number;
}
| {
in: unknown;
in: object | false | void | null;
}
| {
of: readonly unknown[] | Iterable<unknown>;
of: Iterable<unknown> | readonly unknown[] | false | void | null;
}
) & { renderBody?: RenderBody; by?: (...args: unknown[]) => string },
): ReturnAndScope<RenderBodyScope<RenderBody>, void>;
) & { by?: (...args: unknown[]) => string },
content: BodyContent,
): ReturnAndScope<BodyContentScope<BodyContent>, void>;

export function forAttrTag<
export function forOfAttrTag<
Value extends Iterable<any> | readonly any[],
const Return,
>(
input: {
of: Value;
of: Value | false | void | null;
},
renderBody: (
content: (
value: Value extends readonly (infer Item)[] | Iterable<infer Item>
? Item
: unknown,
Expand All @@ -230,11 +237,11 @@ declare global {
: never;
};

export function forAttrTag<Value extends object, const Return>(
export function forInAttrTag<Value extends object, const Return>(
input: {
in: Value;
in: Value | false | void | null;
},
renderBody: (key: keyof Value, value: Value[keyof Value]) => Return,
content: (key: keyof Value, value: Value[keyof Value]) => Return,
): {
[Key in keyof Return]: Return[Key] extends
| readonly (infer Item)[]
Expand All @@ -243,7 +250,7 @@ declare global {
: never;
};

export function forAttrTag<
export function forToAttrTag<
From extends void | number,
To extends number,
Step extends void | number,
Expand All @@ -254,7 +261,7 @@ declare global {
to: To;
step?: Step;
},
renderBody: (index: number) => Return,
content: (index: number) => Return,
): {
[Key in keyof Return]: Return[Key] extends
| readonly (infer Item)[]
Expand All @@ -269,21 +276,21 @@ declare global {
: never;
};

export function forAttrTag<const Return>(attrs: {
export function forAttrTag<const Return>(
input:
| {
of: any;
of: Iterable<unknown> | readonly unknown[] | false | void | null;
}
| {
in: any;
in: object;
}
| {
from?: any;
to: any;
step?: any;
};
renderBody: (index: number) => Return;
}): {
from?: number;
to: number;
step?: number;
},
content: (...args: unknown[]) => Return,
): {
[Key in keyof Return]: Return[Key] extends
| readonly (infer Item)[]
| (infer Item extends Record<PropertyKey, any>)
Expand Down Expand Up @@ -313,7 +320,7 @@ declare global {
? [Name["renderBody"]] extends [AnyMarkoBody]
? BodyRenderer<Name["renderBody"]>
: BaseRenderer<
RenderBodyInput<
BodyContentInput<
BodyParameters<Exclude<Name["renderBody"], void>>
>
>
Expand Down Expand Up @@ -349,10 +356,10 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
(): () => <__marko_internal_input extends unknown>(
input: Marko.Directives &
RenderBodyInput<BodyParameters<Body>> &
BodyContentInput<BodyParameters<Body>> &
Relate<
__marko_internal_input,
Marko.Directives & RenderBodyInput<BodyParameters<Body>>
Marko.Directives & BodyContentInput<BodyParameters<Body>>
>,
) => ReturnAndScope<
Scopes<__marko_internal_input>,
Expand Down Expand Up @@ -389,7 +396,7 @@ declare abstract class MarkoReturn<Return> {

type AnyMarkoBody = Marko.Body<any, any>;

type RenderBodyScope<RenderBody> = RenderBody extends (...params: any) => {
type BodyContentScope<BodyContent> = BodyContent extends (...params: any) => {
[Marko._.scope]: infer Scope;
}
? Scope
Expand All @@ -400,7 +407,7 @@ type ReturnAndScope<Scope, Return> = {
scope: Scope;
};

type RenderBodyInput<Args extends readonly unknown[]> = Args extends {
type BodyContentInput<Args extends readonly unknown[]> = Args extends {
length: infer Length;
}
? number extends Length
Expand Down
82 changes: 68 additions & 14 deletions packages/language-tools/src/extractors/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,18 +565,16 @@ constructor(_?: Return) {}
);
}

this.#extractor.write(`${varShared("forTag")}({\n`);
this.#extractor.write(
`${varShared(getForTagRuntime(this.#parsed, child))}({\n`,
);
this.#writeTagNameComment(child);
this.#writeAttrs(child);

// Adds a comment containing the tag name inside the renderBody key
// this causes any errors which are just for the renderBody
// to show on the tag.
this.#extractor
.write(`["renderBody"/*`)
.copy(child.name)
.write(`*/]: (`);
.write("\n}" + SEP_COMMA_NEW_LINE)
.copy(child.typeParams)
.write("(\n");
this.#writeComments(child);
this.#extractor.copy(child.typeParams).write("(\n");

if (child.params) {
this.#copyWithMutationsReplaced(child.params.value);
Expand All @@ -595,8 +593,6 @@ constructor(_?: Return) {}
body?.renderBody ? getHoistSources(child) : undefined,
);

this.#extractor.write("})");

if (renderId) {
this.#extractor.write("\n}));\n");
} else {
Expand Down Expand Up @@ -1181,9 +1177,12 @@ constructor(_?: Return) {}
break;
}
case "for": {
this.#extractor.write(`${varShared("forAttrTag")}({\n`);
if (!this.#writeAttrs(tag)) this.#writeTagNameComment(tag);
this.#extractor.write("}, \n");
this.#extractor.write(
`${varShared(getForAttrTagRuntime(this.#parsed, tag))}({\n`,
);
this.#writeTagNameComment(tag);
this.#writeAttrs(tag);
this.#extractor.write("\n}, \n");
this.#writeComments(tag);
this.#extractor
.copy(tag.typeParams)
Expand Down Expand Up @@ -1729,6 +1728,61 @@ constructor(_?: Return) {}
}
}

const enum ForTagType {
unknown,
of,
in,
to,
}
function getForTagType(parsed: Parsed, tag: Node.Tag) {
if (tag.attrs) {
for (const attr of tag.attrs) {
if (attr.type === NodeType.AttrSpread) {
return ForTagType.unknown;
}

switch (parsed.read(attr.name)) {
case "of":
return ForTagType.of;
case "in":
return ForTagType.in;
case "to":
case "from":
case "step":
return ForTagType.to;
}
}
}

return ForTagType.unknown;
}

function getForTagRuntime(parsed: Parsed, tag: Node.Tag) {
switch (getForTagType(parsed, tag)) {
case ForTagType.of:
return "forOfTag";
case ForTagType.in:
return "forInTag";
case ForTagType.to:
return "forToTag";
default:
return "forTag";
}
}

function getForAttrTagRuntime(parsed: Parsed, tag: Node.Tag) {
switch (getForTagType(parsed, tag)) {
case ForTagType.of:
return "forOfAttrTag";
case ForTagType.in:
return "forInAttrTag";
case ForTagType.to:
return "forToAttrTag";
default:
return "forAttrTag";
}
}

function varLocal(name: string) {
return VAR_LOCAL_PREFIX + name;
}
Expand Down

0 comments on commit 029bb93

Please sign in to comment.