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

Hooks to manipulate the Svelte compiler #11611

Open
adiguba opened this issue May 14, 2024 · 5 comments
Open

Hooks to manipulate the Svelte compiler #11611

adiguba opened this issue May 14, 2024 · 5 comments

Comments

@adiguba
Copy link
Contributor

adiguba commented May 14, 2024

Describe the problem

As Svelte is a compiler, it would be nice to allow developper to enhance it by providing some hooks.
This will allow to add some specific requirement at compile time, and specific warning/error.

For exemple, I wrote a component MyComp and I want to handle some specific warning/error :

<MyComp format="small" data={data} /> <!-- Warning : 'data' is deprecated, use 'value' instead -->

<MyComp format="mini" /> <!-- Warning : Bad format.  -->

<MyComp data={data} /> <!-- Error : format is required -->

Or another FancyList component that should only accept <li> or <FancyItem> :

<FancyList>
    <li>One item</li>
    <FancyItem>Another Item</FancyItem>
</FancyList> <!-- OK -->

<FancyList>
    One text <!-- ERROR : text is not allowed here -->
    <li>One item</li>
    <div> <!--ERROR : <div> is not allowed here -->
    <FancyItem>Another Item</FancyItem>
    <FancyList>Content</FancyList> <!-- ERROR : <FancyList> is not allowed here -->
</FancyList> <!-- OK -->

I would be nice if Svelte (6 ?) allow us to add these sort of hooks into the compiler.

I know that some checks can be done via TypeScript, but this could allow more control.

Describe the proposed solution

We could use a <script context="compiler"> for that (or a specific method in context="module").

The code inside this tag will only be used at compile time by the compiler, and will not be included in the generated code (client or server).

A special variable $$tag will allow us to manipulate the compiler, in order to display messages/errors :

// MyComp.svelte
<script context="compiler">

	$$tag.error("Display an error message on the entire component, and make the compilation fail");
	$$tag.warn("Display an warning message on the entire component");
	$$tag.info("Display an info message on the entire component");

	$$tag.error("Display an error message on the prop 'data', and make the compilation fail", 'data');
	$$tag.warn("Display an warning message on the prop 'data'", 'data');
	$$tag.info("Display an info message on the prop 'data'", 'data');

</script>

The $$tag should also contain utility methods to check the status of the props :

// MyComp.svelte
<script context="compiler" lang="ts">

	// Get the name of all the props passed
	const propsNames : string[] = $$tags.props();

	if ($$tag.is_present('data')) {
		// true if the prop 'data' is present on the component's tag
		// Ex:
		// 	<MyComp data="data" />
		// 	<MyComp data={data} />
		// 	<MyComp {data} />
		// 	<MyComp data />
		// 	<MyComp data="data" {...props}/>
		// 	<MyComp {...props} data="data" />
	}

	if ($$tag.is_missing('data')) {
		// true if the prop 'data' is not directly present on the component's tag
		// Ex:
		// 	<MyComp />
		// 	<MyComp {...props} />
	}

	if ($$tag.has_spread()) {
		// true if the component's tag has at least one spread
		// Ex:
		//	<MyComp {...props} />
	}

	if ($$tag.is_spreadable('data')) {
		// true if the component's tag has at least on spread,
		// and the prop 'data' can be set by the spread
		// Ex:
		//	<MyComp {...props} />
		//	<MyComp data="data" {...props} />
		//
		// But false for :
		//	<MyComp {...props} data="data" />
	}

	if ($$tag.is_constant('data')) {
		// true if the prop 'data' is present and has a constant value
		// Ex:
		// 	<MyComp data="data" />
		// 	<MyComp data={false} />
		// 	<MyComp data={true} />
		// 	<MyComp data={15} />
		// 	<MyComp data={null} />
		// 	<MyComp data={undefined} />
		// 	<MyComp data={"data"} />
		//
		// But false for :
		//	<MyComp data={data} />
		//	<MyComp bind:data={data} />
		//	<MyComp data="text with {data}" />
	}


	if ($$tag.is_constant('data', ['a', 'b', 'c'])) {
		// true if the prop 'data' is present and has a constant value
		// which corresponds to one of the values
		// Ex:
		// 	<MyComp data="a" />
		// 	<MyComp data="b" />
		// 	<MyComp data="c" />
		//
		// But false for :
		//	<MyComp data="d" />
		//	<MyComp data={data} />
	}

	if ($$tag.is_bind('data')) {
		// true if the prop 'data' is binded
		// Ex:
		// 	<MyComp bind:data={data} />
		// 	<MyComp bind:data />
	}

	if ($$tag.is_snippet('data')) {
		// true if the prop 'data' is a snippet
		// Ex:
		// 	<MyComp> {#snippet data()} XXX {/snippet} </MyComp>
	}

</script>

It should also include some function that make basic and classic checks :

// MyComp.svelte
<script context="compiler">

	/** Show an error if the props is missng */
	$$tag.require('format', 'The props "format" is required');

	/** If the props is a constant, verify that his value match one of the provideds values */
	$$tag.verify('format', ['full', 'medium', 'small'], "Bad format");

	/** Same thing, but with a regexp : */
	$$tag.verify('format', /full|medium|small/g, "Bad format");

	/** Or using a callback to verify the value : */
	$$tag.verify('format', (v) => {
		return v === 'full' || v === 'medium' || v === 'small'
	}, "Bad format");

	/** Same thing, but using an Error as message */
	$$tag.verify('pattern', (v) => {
		return new Regexp(v, "g"); // the catched error will be used as error message
	});

</script>

Snippets

And a verify_snippet() to check the format of the snippets.

Some examples :

// UList.svelte
<script context="compiler">

	$$tags.verify_snippet('children', {
		text: false,		// disallow text on the root of the snippet
		html: false,		// disallow {@html} on the root of the snippet
		allows: [ 'li', Li ],	// Accept allow only tag <li> or component <Li>
	});

<script>
// Dialog.svelte
<script context="compiler">

	$$tags.verify_snippet('children', {
		// snippet must have exactly one Header, one Content and one Footer :
		match: [ Header, Content, Footer ]
	});

<script>
// Table.svelte
<script context="compiler">

	$$tags.verify_snippet('children', {
		// snippet must have : 
		match: [ 
			[ THead, 0, 1],	// zero or one THead
			[ TBody, 1, 1],	// one TBody
			[ TFoot, 0, 1]  // zero or one TFoot
		]
	});

<script>

Importance

nice to have

@7nik
Copy link

7nik commented May 14, 2024

TS: am I joke for you?

<MyComp format="mini" /> <!-- Warning : Bad format.  -->

What does it even mean? Unacceptable value of format? Use type like "big" | "medium" | "small".

FancyList component that should only accept <li> or <FancyItem>

Can you elaborate on why you want to restrict the content of snippets?

@adiguba
Copy link
Contributor Author

adiguba commented May 14, 2024

What does it even mean? Unacceptable value of format? Use type like "big" | "medium" | "small".

Yes, like I say I known that some things can be done with Typescript...

Ok here it's just a basic example, but we can imagine some more complex validation where it's harder to check in Typescript.
For example if the format can be one of "big" | "medium" | "small" OR any date pattern like 'YYYY-MM-DD hh:mm:ss".

Validating the format with code will allow to detect problems during compilation.

	$$tag.verify('format', (format) => {
		if (format === 'full' || format === 'medium' || format === 'small') {
			return true;
		}
		
		if (/* code that validate the format */) {
			return true;
		}
		return false;
	}, "Bad format");

Can you elaborate on why you want to restrict the content of snippets?

Just to avoid mistakes when using the tag.

A lot of components require respecting a specific tree structure.
It would be nice to be able to say that to the compiler.

Ex a <FancyList> could be an <ul> internally, and therefore only require <li> tags on his children, as other tags can break the rendering.

<FancyList>
    item <!-- Error : tag <li> is required ! -->
</FancyList>

Or a <Tabs> would only accept Tab components :

<Tabs>
    <Tab title="Tab one">
         Content of the panel
    </Tab>
    <MyOtherComponent /> <!-- Error : <MyOtherComponent> is not allowed here -->
</Tabs>

Or a Dialog that require a specific structure :

<Dialog>
    <DialogCloseButton />
    <DialogHeader> ... </DialogHeader>
    <DialogContent> ... </DialogContent>
    <DialogFooter> ... </DialogFooter>
</Tabs>

Any help that helps prevent an error is always a good thing.

@7nik
Copy link

7nik commented May 14, 2024

Validating the format with code will allow to detect problems during compilation.

And it won't work with variables. If the lib authors care, they can do

if (!isValid(propX)) {
  if (DEV) {
    throw new Error(`${propX} is not valid value for propX!`);
  }
  propX = defaultPropXValue; // or other way to ignore the new value
}

Regarding a specific tree structure, this is what slots/snippets are for - the required tree is completely inside the component and user content is inserted into it.

<FancyList {items}><!-- uses <ul> and <li> internally -->
  {#snippet listItem(item, i)}
    {i+1}. {item}
  {/snippet}
</FancyList>

<Tabs tabs={[{
  title="Tab one",
  content=tab1,
},{
  title="Tab two",
  content=tab2,
}]} />
{#snippet tab1(title)}
  Content of the panel {title}
{/snippet}
{#snippet tab2(title, active)}
  <b class:fancy={active}>My fancy tab</b>
{/snippet}

Also, actions can be used to validate the DOM tree in the DEV runtime only.

@paoloricciuti
Copy link
Contributor

Also also you can actually do most of this with a custom eslint plugin

@adiguba
Copy link
Contributor Author

adiguba commented May 15, 2024

Regarding a specific tree structure, this is what slots/snippets are for - the required tree is completely inside the component and user content is inserted into it.

Snippet are great to pass portion of code, but verbose and limited on some aspect.
And this sort of code with multiples snippets passed as prop is not really readable :(

I prefer to have something that looks more like HTML, much clearer.

Also also you can actually do most of this with a custom eslint plugin

Maybe, but when I design a Svelte component, I find it logical to define rules there, and not in a plugin from another tool which may not be used or configured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants