Skip to content

Commit

Permalink
feat: Add support for Svelte 5 Runes Mode (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianGonz97 committed Dec 30, 2023
1 parent e842db1 commit 685f181
Show file tree
Hide file tree
Showing 16 changed files with 778 additions and 229 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-carpets-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@melt-ui/pp': minor
---

feat: Added support for Svelte 5 Runes Mode
12 changes: 11 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,24 @@ jobs:
run_install: true
- run: pnpm run check

Svelte-4-Tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.3
run_install: true
- run: pnpm run test

Tests:
Svelte-5-Tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.3
run_install: true
- run: pnpm add -D svelte@next
- run: pnpm run test

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"@typescript-eslint/parser": "^5.60.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.8.0",
"magic-string": "^0.30.5",
"prettier": "^2.8.8",
"svelte": "^4.0.0",
"tsup": "^8.0.1",
Expand All @@ -51,6 +50,7 @@
"pnpm": ">=8.6.3"
},
"dependencies": {
"estree-walker": "^3.0.3"
"estree-walker": "^3.0.3",
"magic-string": "^0.30.5"
}
}
32 changes: 16 additions & 16 deletions pnpm-lock.yaml

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

99 changes: 98 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { walk as estree_walk } from 'estree-walker';
import { loadSvelteConfig } from './load-svelte-config.js';

import type { TemplateNode } from 'svelte/types/compiler/interfaces';
import type { Ast, TemplateNode } from 'svelte/types/compiler/interfaces';
import type { CallExpression } from 'estree';
import type { Node } from './types.js';
import type { PreprocessOptions } from './index.js';

export function isAliasedAction(name: string, alias: string | string[]): boolean {
if (typeof alias === 'string') {
Expand All @@ -10,6 +13,100 @@ export function isAliasedAction(name: string, alias: string | string[]): boolean
return alias.includes(name);
}

const RUNES = [
'$derived',
'$effect',
'$effect.active',
'$effect.pre',
'$effect.root',
'$inspect',
'$inspect.with',
'$props',
'$state',
'$state.frozen',
];

/**
* There are 3 ways to determine if a component is in 'runes mode':
* 1. If `<svelte:options runes={true} />` is set
* 2. If `svelte-config.compilerOptions.runes` === `true`
* 3. If a rune is present in the component (`$state`, `$derived`, `$effect`, etc.)
*/
export async function isRuneMode(
ast: Ast,
options?: PreprocessOptions
): Promise<boolean> {
// check if the component has `<svelte:options runes />`
for (const element of ast.html.children ?? []) {
if (element.type !== 'Options' || element.name !== 'svelte:options') continue;

const attributes = element.attributes;
for (const attr of attributes) {
if (attr.name !== 'runes') continue;
// `<svelte:options runes />`
if (typeof attr.value === 'boolean') {
return attr.value;
}
// `<svelte:options runes={false} />` or `<svelte:options runes={true} />`
if (typeof attr.value[0].expression.value === 'boolean') {
return attr.value[0].expression.value;
}
}
}

// `svelte-config.compilerOptions.runes`
const svelteConfig = await loadSvelteConfig(options?.svelteConfigPath);
if (typeof svelteConfig?.compilerOptions?.runes === 'boolean') {
return svelteConfig.compilerOptions.runes;
}

// a rune is present in the component
let hasRunes = false;
if (ast.module) {
hasRunes = containsRunes(ast.module);
}

if (ast.instance) {
hasRunes = hasRunes || containsRunes(ast.instance);
}

return hasRunes;
}
type Script = NonNullable<Ast['instance']>;
function containsRunes(script: Script): boolean {
let containsRunes = false;

walk(script, {
enter(node) {
// already found a rune, don't need to check the rest
if (containsRunes) {
this.skip();
return;
}

if (node.type !== 'CallExpression') return;
const callExpression = node as unknown as CallExpression;

// $inspect(item)
const callee = callExpression.callee;
if (callee.type === 'Identifier') {
containsRunes = RUNES.some((rune) => rune === callee.name);
}
// $inspect.with(item)
if (callee.type === 'MemberExpression') {
if (callee.computed) return;
if (callee.object.type !== 'Identifier') return;
if (callee.property.type !== 'Identifier') return;

const name = callee.object.name + '.' + callee.property.name;
containsRunes = RUNES.some((rune) => rune === name);
}
},
});

return containsRunes;
}

export function getMeltBuilderName(i: number) {
return `__MELTUI_BUILDER_${i}__`;
}
Expand Down
18 changes: 15 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import MagicString from 'magic-string';
import { parse, type PreprocessorGroup } from 'svelte/compiler';
import { getMeltBuilderName, walk } from './helpers.js';
import { parse, type PreprocessorGroup, VERSION } from 'svelte/compiler';
import { getMeltBuilderName, isRuneMode, walk } from './helpers.js';
import { traverse } from './traverse/index.js';

import type { TemplateNode } from 'svelte/types/compiler/interfaces';
Expand Down Expand Up @@ -28,6 +28,12 @@ export type PreprocessOptions = {
* @default "melt"
*/
alias?: string | string[];
/**
* Path to a svelte config file, either absolute or relative to `process.cwd()`.
*
* Set to `false` to ignore the svelte config file.
*/
svelteConfigPath?: string | false;
};

/**
Expand All @@ -53,6 +59,7 @@ export type PreprocessOptions = {
* ```
*/
export function preprocessMeltUI(options?: PreprocessOptions): PreprocessorGroup {
const isSvelte5 = VERSION.startsWith('5');
return {
name: 'MeltUI Preprocess',
markup: async ({ content, filename }) => {
Expand All @@ -66,6 +73,7 @@ export function preprocessMeltUI(options?: PreprocessOptions): PreprocessorGroup

let scriptContentNode: { start: number; end: number } | undefined;
const ast = parse(content, { css: false, filename });
const runesMode = isSvelte5 && (await isRuneMode(ast, options));

// Grab the Script node so we can inject any hoisted expressions later
if (ast.instance) {
Expand Down Expand Up @@ -97,7 +105,11 @@ export function preprocessMeltUI(options?: PreprocessOptions): PreprocessorGroup
} else {
// otherwise, we'll take the expression and hoist it into the script node
identifier = getMeltBuilderName(config.builderCount++);
identifiersToInsert += `\t$: ${identifier} = ${builder.expression.contents};\n`;
if (runesMode) {
identifiersToInsert += `\tlet ${identifier} = $derived(${builder.expression.contents});\n`;
} else {
identifiersToInsert += `\t$: ${identifier} = ${builder.expression.contents};\n`;
}
}

const attributes = `{...${identifier}} use:${identifier}.action`;
Expand Down
Loading

0 comments on commit 685f181

Please sign in to comment.