Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve for tag types and codegen #322

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading