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

(feat) enable actions to enhance typings on applied element #1553

Merged
merged 6 commits into from Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
Expand Up @@ -126,7 +126,7 @@ export function convertHtmlxToJsx(
handleStyleDirective(str, node as StyleDirective, element as Element);
break;
case 'Action':
handleActionDirective(str, node as BaseDirective, element as Element);
handleActionDirective(node as BaseDirective, element as Element);
break;
case 'Transition':
handleTransitionDirective(str, node as BaseDirective, element as Element);
Expand Down
27 changes: 2 additions & 25 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Action.ts
@@ -1,32 +1,9 @@
import MagicString from 'magic-string';
import { BaseDirective } from '../../interfaces';
import {
getDirectiveNameStartEndIdx,
rangeWithTrailingPropertyAccess,
TransformationArray
} from '../utils/node-utils';
import { Element } from './Element';

/**
* use:xxx={params} ---> __sveltets_2_ensureAction(xxx(svelte.mapElementTag('ParentNodeName'),(params)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this comment is still valid?

*/
export function handleActionDirective(
str: MagicString,
attr: BaseDirective,
element: Element
): void {
const transformations: TransformationArray = [
'__sveltets_2_ensureAction(',
getDirectiveNameStartEndIdx(str, attr),
`(${element.typingsNamespace}.mapElementTag('${element.tagName}')`
];
if (attr.expression) {
transformations.push(
',(',
rangeWithTrailingPropertyAccess(str.original, attr.expression),
')'
);
}
transformations.push('));');
element.appendToStartEnd(transformations);
export function handleActionDirective(attr: BaseDirective, element: Element): void {
element.addAction(attr);
}
172 changes: 128 additions & 44 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts
@@ -1,11 +1,13 @@
import MagicString from 'magic-string';
import { BaseNode } from '../../interfaces';
import { BaseDirective, BaseNode } from '../../interfaces';
import { surroundWithIgnoreComments } from '../../utils/ignore';
import {
transform,
TransformationArray,
sanitizePropName,
surroundWith
surroundWith,
getDirectiveNameStartEndIdx,
rangeWithTrailingPropertyAccess
} from '../utils/node-utils';

const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split(',');
Expand All @@ -30,10 +32,11 @@ const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,tra
* ```
*/
export class Element {
private startTransformation: TransformationArray = [];
private startEndTransformation: TransformationArray = ['});'];
private attrsTransformation: TransformationArray = [];
private slotLetsTransformation?: [TransformationArray, TransformationArray];
private actionsTransformation: TransformationArray = [];
private actionIdentifiers: string[] = [];
private endTransformation: TransformationArray = [];
private startTagStart: number;
private startTagEnd: number;
Expand All @@ -43,13 +46,10 @@ export class Element {

// Add const $$xxx = ... only if the variable name is actually used
// in order to prevent "$$xxx is defined but never used" TS hints
private addNameConstDeclaration?: () => void;
private referencedName = false;
private _name: string;
public get name(): string {
if (this.addNameConstDeclaration) {
this.addNameConstDeclaration();
this.addNameConstDeclaration = undefined;
}
this.referencedName = true;
return this._name;
}

Expand All @@ -73,7 +73,6 @@ export class Element {
this.isSelfclosing = this.computeIsSelfclosing();
this.startTagStart = this.node.start;
this.startTagEnd = this.computeStartTagEnd();
const createElement = `${this.typingsNamespace}.createElement`;

const tagEnd = this.startTagStart + this.node.name.length + 1;
// Ensure deleted characters are mapped to the attributes object so we
Expand All @@ -98,24 +97,10 @@ export class Element {
// remove the colon: svelte:xxx -> sveltexxx
const nodeName = `svelte${this.node.name.substring(7)}`;
this._name = '$$_' + nodeName + this.computeDepth();
this.startTransformation.push(`{ ${createElement}("${this.node.name}", {`);
this.addNameConstDeclaration = () =>
(this.startTransformation[0] = `{ const ${this._name} = ${createElement}("${this.node.name}", {`);
break;
}
case 'svelte:element': {
const nodeName = this.node.tag
? typeof this.node.tag !== 'string'
? ([this.node.tag.start, this.node.tag.end] as [number, number])
: `"${this.node.tag}"`
: '""';
this._name = '$$_svelteelement' + this.computeDepth();
this.startTransformation.push(`{ ${createElement}(`, nodeName, ', {');
this.addNameConstDeclaration = () => (
(this.startTransformation[0] = `{ const ${this._name} = ${createElement}(`),
nodeName,
', {'
);
break;
}
case 'slot': {
Expand All @@ -124,29 +109,10 @@ export class Element {
// of the slot tag are correct. The check will error if the user defined $$Slots
// and the slot definition or its attributes contradict that type definition.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
this._name = '$$_slot' + this.computeDepth();
const slotName =
this.node.attributes?.find((a: BaseNode) => a.name === 'name')?.value[0] ||
'default';
this.startTransformation.push(
'{ __sveltets_createSlot(',
typeof slotName === 'string'
? `"${slotName}"`
: surroundWith(this.str, [slotName.start, slotName.end], '"', '"'),
', {'
);
this.addNameConstDeclaration = () =>
(this.startTransformation[0] = `{ const ${this._name} = __sveltets_createSlot(`);
break;
}
default: {
this._name = '$$_' + sanitizePropName(this.node.name) + this.computeDepth();
this.startTransformation.push(
`{ ${createElement}("`,
[this.node.start + 1, this.node.start + 1 + this.node.name.length],
'", {'
);
this.addNameConstDeclaration = () =>
(this.startTransformation[0] = `{ const ${this._name} = ${createElement}("`);
break;
}
}
Expand Down Expand Up @@ -183,6 +149,28 @@ export class Element {
this.slotLetsTransformation[1].push(...transformation, ',');
}

addAction(attr: BaseDirective) {
const id = `$$action_${this.actionIdentifiers.length}`;
this.actionIdentifiers.push(id);
if (!this.actionsTransformation.length) {
this.actionsTransformation.push('{');
}

this.actionsTransformation.push(
`const ${id} = __sveltets_2_ensureAction(`,
getDirectiveNameStartEndIdx(this.str, attr),
`(${this.typingsNamespace}.mapElementTag('${this.tagName}')`
);
if (attr.expression) {
this.actionsTransformation.push(
',(',
rangeWithTrailingPropertyAccess(this.str.original, attr.expression),
')'
);
}
this.actionsTransformation.push('));');
}

/**
* Add something right after the start tag end.
*/
Expand Down Expand Up @@ -216,21 +204,27 @@ export class Element {
this.endTransformation.push('}');
}

if (this.actionIdentifiers.length) {
this.endTransformation.push('}');
}

if (this.isSelfclosing) {
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
// Named slot transformations go first inside a outer block scope because
// <div let:xx {x} /> means "use the x of let:x", and without a separate
// block scope this would give a "used before defined" error
...slotLetTransformation,
...this.startTransformation,
...this.actionsTransformation,
...this.getStartTransformation(),
...this.attrsTransformation,
...this.startEndTransformation,
...this.endTransformation
]);
} else {
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
...slotLetTransformation,
...this.startTransformation,
...this.actionsTransformation,
...this.getStartTransformation(),
...this.attrsTransformation,
...this.startEndTransformation
]);
Expand All @@ -244,6 +238,96 @@ export class Element {
}
}

private getStartTransformation(): TransformationArray {
const createElement = `${this.typingsNamespace}.createElement`;
const addActions = () => {
if (this.actionIdentifiers.length) {
return `, __sveltets_2_union(${this.actionIdentifiers.join(',')})`;
} else {
return '';
}
};

switch (this.node.name) {
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
// Although not everything that is possible to add to Element
// is valid on the special svelte elements,
// we still also handle them here and let the Svelte parser handle invalid
// cases. For us it doesn't make a difference to a normal HTML element.
case 'svelte:options':
case 'svelte:head':
case 'svelte:window':
case 'svelte:body':
case 'svelte:fragment': {
if (!this.referencedName) {
return [`{ ${createElement}("${this.node.name}"${addActions()}, {`];
} else {
return [
`{ const ${this._name} = ${createElement}("${
this.node.name
}"${addActions()}, {`
];
}
}
case 'svelte:element': {
const nodeName = this.node.tag
? typeof this.node.tag !== 'string'
? ([this.node.tag.start, this.node.tag.end] as [number, number])
: `"${this.node.tag}"`
: '""';
if (!this.referencedName) {
return [`{ ${createElement}(`, nodeName, `${addActions()}, {`];
} else {
return [
`{ const ${this._name} = ${createElement}(`,
nodeName,
`${addActions()}, {`
];
}
}
case 'slot': {
// If the element is a <slot> tag, create the element with the createSlot-function
// which is created inside createRenderFunction.ts to check that the name and attributes
// of the slot tag are correct. The check will error if the user defined $$Slots
// and the slot definition or its attributes contradict that type definition.
const slotName =
this.node.attributes?.find((a: BaseNode) => a.name === 'name')?.value[0] ||
'default';
if (!this.referencedName) {
return [
'{ __sveltets_createSlot(',
typeof slotName === 'string'
? `"${slotName}"`
: surroundWith(this.str, [slotName.start, slotName.end], '"', '"'),
', {'
];
} else {
return [
`{ const ${this._name} = __sveltets_createSlot(`,
typeof slotName === 'string'
? `"${slotName}"`
: surroundWith(this.str, [slotName.start, slotName.end], '"', '"'),
', {'
];
}
}
default: {
if (!this.referencedName) {
return [
`{ ${createElement}("`,
[this.node.start + 1, this.node.start + 1 + this.node.name.length],
`"${addActions()}, {`
];
} else {
return [
`{ const ${this._name} = ${createElement}("`,
[this.node.start + 1, this.node.start + 1 + this.node.name.length],
`"${addActions()}, {`
];
}
}
}
}

private computeStartTagEnd() {
if (this.node.children?.length) {
return this.node.children[0].start;
Expand Down
5 changes: 4 additions & 1 deletion packages/svelte2tsx/svelte-jsx.d.ts
Expand Up @@ -17,7 +17,10 @@ declare namespace svelteHTML {
// "undefined | null" because of <svelte:element>
element: Key | undefined | null, attrs: Elements[Key]
): Key extends keyof ElementTagNameMap ? ElementTagNameMap[Key] : Key extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[Key] : any;

function createElement<Elements extends IntrinsicElements, Key extends keyof Elements, T>(
// "undefined | null" because of <svelte:element>
element: Key | undefined | null, attrsEnhancers: T, attrs: Elements[Key] & T
): Key extends keyof ElementTagNameMap ? ElementTagNameMap[Key] : Key extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[Key] : any;

type NativeElement = HTMLElement;

Expand Down
6 changes: 4 additions & 2 deletions packages/svelte2tsx/svelte-shims.d.ts
Expand Up @@ -246,9 +246,11 @@ declare function __sveltets_2_ensureAnimation(animationCall: __sveltets_2_Svelte

type __sveltets_2_SvelteActionReturnType = {
update?: (args: any) => void,
destroy?: () => void
destroy?: () => void,
attributes?: Record<string, any>,
events?: Record<string, any>
} | void
declare function __sveltets_2_ensureAction(actionCall: __sveltets_2_SvelteActionReturnType): {};
declare function __sveltets_2_ensureAction<T extends __sveltets_2_SvelteActionReturnType>(actionCall: T): T extends {attributes?: any, events?: any} ? T['attributes'] & {[Key in keyof T['events'] as `on:${Key & string}`]?: T['events'][Key]} : {};

type __sveltets_2_SvelteTransitionConfig = {
delay?: number,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.