├── .changeset
├── README.md
└── config.json
├── .eslintrc.cjs
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── component.md
│ ├── documentation.md
│ ├── feature_request.md
│ └── tech-debt.md
└── workflows
│ ├── ci.yaml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── README.md
├── package.json
├── pnpm-lock.yaml
├── src
├── helpers.ts
├── index.ts
├── load-svelte-config.ts
├── sequence.ts
├── traverse
│ ├── AwaitBlock.ts
│ ├── Block.ts
│ ├── ComponentBlock.ts
│ ├── EachBlock.ts
│ └── index.ts
└── types.ts
├── tests
├── await_block
│ ├── index.svelte.ts
│ └── index.test.ts
├── component_block
│ ├── index.svelte.ts
│ └── index.test.ts
├── each_block
│ ├── index.svelte.ts
│ └── index.test.ts
├── expression_builder
│ ├── index.svelte.ts
│ └── index.test.ts
├── runes
│ ├── index.svelte.ts
│ ├── index.test.ts
│ ├── svelte.config.disabled.js
│ └── svelte.config.enabled.js
└── simple_builder
│ ├── index.svelte.ts
│ └── index.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['@typescript-eslint'],
6 | ignorePatterns: ['*.cjs', 'dist/**/*'],
7 | overrides: [],
8 | env: {
9 | es2017: true,
10 | node: true,
11 | },
12 | rules: {
13 | 'no-empty-function': 'off',
14 | '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [tglide]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: thomasglopes
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "\U0001F41BBug:"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
40 | To reproduce using Stackblitz, use this [template](https://stackblitz.com/edit/node-rvtbvk?file=package.json) and modify the `src/routes/+page.svelte` file.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/component.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Builder
3 | about: Builder creation request
4 | title: "✨ Builder:"
5 | labels: builder
6 | assignees: ''
7 |
8 | ---
9 |
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation
3 | about: Additions or changes to the documentation site
4 | title: "\U0001F4C1Docs:"
5 | labels: documentation
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "\U0001F31FFeature request:"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/tech-debt.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Tech Debt
3 | about: Technical debt, proposed architecture changes, etc.
4 | title: "⚙Tech Debt:"
5 | labels: tech debt
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 | pull_request:
9 |
10 | # cancel in-progress runs on new commits to same PR (gitub.event.number)
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
13 | cancel-in-progress: true
14 |
15 | permissions:
16 | contents: read # to fetch code (actions/checkout)
17 |
18 | jobs:
19 | Lint:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v3
23 | - uses: pnpm/action-setup@v2
24 | with:
25 | version: 8.6.3
26 | run_install: true
27 | - run: pnpm run lint
28 |
29 | Check:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v3
33 | - uses: pnpm/action-setup@v2
34 | with:
35 | version: 8.6.3
36 | run_install: true
37 | - run: pnpm run check
38 |
39 | Svelte-4-Tests:
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: actions/checkout@v3
43 | - uses: pnpm/action-setup@v2
44 | with:
45 | version: 8.6.3
46 | run_install: true
47 | - run: pnpm run test
48 |
49 | Svelte-5-Tests:
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v3
53 | - uses: pnpm/action-setup@v2
54 | with:
55 | version: 8.6.3
56 | run_install: true
57 | - run: pnpm add -D svelte@next
58 | - run: pnpm run test
59 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 |
9 | concurrency: ${{ github.workflow }}-${{ github.ref }}
10 |
11 | jobs:
12 | release:
13 | permissions:
14 | contents: write # to create release (changesets/action)
15 | pull-requests: write # to create pull request (changesets/action)
16 | name: Release
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout Repo
20 | uses: actions/checkout@v3
21 | with:
22 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
23 | fetch-depth: 0
24 | - uses: pnpm/action-setup@v2.2.4
25 | with:
26 | version: 8.6.3
27 | - name: Setup Node.js
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: '20.x'
31 | cache: pnpm
32 |
33 | - run: pnpm install --frozen-lockfile
34 |
35 | - name: Create Release Pull Request or Publish to npm
36 | id: changesets
37 | uses: changesets/action@v1
38 | with:
39 | publish: pnpm release
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | build
4 | .svelte-kit
5 | dist
6 | .env
7 | .env.*
8 | !.env.example
9 | *.tgz
10 | *.log
11 | dist
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.15.0
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | .env
6 | .env.*
7 | pnpm-lock.yaml
8 | .vscode
9 | .github
10 | .changeset
11 | /dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "es5",
5 | "printWidth": 90
6 | }
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @melt-ui/pp
2 |
3 | ## 0.3.2
4 |
5 | ### Patch Changes
6 |
7 | - f15475e: fix: Existing `@const` tag declarations in the template are now considered
8 |
9 | ## 0.3.1
10 |
11 | ### Patch Changes
12 |
13 | - 939b6fe: chore: Removed required engine restrictions
14 |
15 | ## 0.3.0
16 |
17 | ### Minor Changes
18 |
19 | - 685f181: feat: Added support for Svelte 5 Runes Mode
20 |
21 | ## 0.2.0
22 |
23 | ### Minor Changes
24 |
25 | - 978fd50: feat: Added a built-in sequential preprocessor
26 |
27 | ### Patch Changes
28 |
29 | - 978fd50: fix: Fixed source maps
30 |
31 | ## 0.1.4
32 |
33 | ### Patch Changes
34 |
35 | - 9bbd746: fix: Corrected `module` and `types` entry points
36 |
37 | ## 0.1.3
38 |
39 | ### Patch Changes
40 |
41 | - e73cc40: chore: Use `estree-walker` instead of Svelte's `walk`
42 | - eb41557: chore: Added Svelte 5 as a supported peer dependency
43 |
44 | ## 0.1.2
45 |
46 | ### Patch Changes
47 |
48 | - 3c4b53a: chore: Simplified internal logic for injecting `{@const}` tags
49 |
50 | ## 0.1.1
51 |
52 | ### Patch Changes
53 |
54 | - 8acd92f: fix: Clarify the use of the `alias` config option
55 |
56 | ## 0.1.0
57 |
58 | ### Minor Changes
59 |
60 | - 0b6b380: refactor: Replaces all instances of `use:melt` instead of `melt`
61 |
62 | ### Patch Changes
63 |
64 | - 6646560: fix: `{@const}` tags are now injected into the direct parent block of a qualified action node.
65 |
66 | ## 0.1.0-next.1
67 |
68 | ### Patch Changes
69 |
70 | - 6646560: fix: `{@const}` tags are now injected into the direct parent block of a qualified action node.
71 |
72 | ## 0.1.0-next.0
73 |
74 | ### Minor Changes
75 |
76 | - 0b6b380: refactor: Replaces all instances of `use:melt` instead of `melt`
77 |
78 | ## 0.0.7
79 |
80 | ### Patch Changes
81 |
82 | - b2dea2c: fix: Handle the shorthand notation for the `melt` attribute (ex: `
`)
83 |
84 | ## 0.0.6
85 |
86 | ### Patch Changes
87 |
88 | - e7111f6: Fixed peer dependency for `@melt-ui/svelte`
89 |
90 | ## 0.0.5
91 |
92 | ### Patch Changes
93 |
94 | - e71d27d: simplifies the transformed output
95 |
96 | ## 0.0.4
97 |
98 | ### Patch Changes
99 |
100 | - f15c55c: fix: Don't process `melt` props for Svelte Components
101 |
102 | ## 0.0.3
103 |
104 | ### Patch Changes
105 |
106 | - 249f0ea: added `@melt-ui/svelte` as a peer dependency
107 |
108 | ## 0.0.2
109 |
110 | ### Patch Changes
111 |
112 | - c774d7f: finds and replaces a `melt` attribute instead of a `use:melt` action
113 |
114 | ## 0.0.1
115 |
116 | ### Patch Changes
117 |
118 | - fcf712d: initial release
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @melt-ui/pp
2 |
3 | https://www.melt-ui.com/docs/preprocessor
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@melt-ui/pp",
3 | "version": "0.3.2",
4 | "description": "Preprocessor for Melt UI",
5 | "module": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "type": "module",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/index.d.ts",
11 | "default": "./dist/index.js"
12 | }
13 | },
14 | "files": [
15 | "dist"
16 | ],
17 | "scripts": {
18 | "test": "vitest",
19 | "dev": "tsup --watch",
20 | "build": "tsup",
21 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
22 | "lint:write": "prettier --plugin-search-dir . --write . && eslint . --fix",
23 | "format": "prettier --plugin-search-dir . --write .",
24 | "check": "tsc --noEmit",
25 | "release": "pnpm run build && changeset publish"
26 | },
27 | "keywords": [],
28 | "author": "",
29 | "license": "MIT",
30 | "devDependencies": {
31 | "@changesets/cli": "^2.27.1",
32 | "@types/estree": "^1.0.5",
33 | "@types/node": "^20.10.5",
34 | "@typescript-eslint/eslint-plugin": "^5.60.0",
35 | "@typescript-eslint/parser": "^5.60.0",
36 | "eslint": "^8.56.0",
37 | "eslint-config-prettier": "^8.8.0",
38 | "prettier": "^2.8.8",
39 | "svelte": "^4.0.0",
40 | "tsup": "^8.0.1",
41 | "typescript": "^5.1.3",
42 | "vitest": "^1.1.0"
43 | },
44 | "peerDependencies": {
45 | "@melt-ui/svelte": ">= 0.29.0",
46 | "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0-next.1"
47 | },
48 | "dependencies": {
49 | "estree-walker": "^3.0.3",
50 | "magic-string": "^0.30.5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { walk as estree_walk } from 'estree-walker';
2 | import { loadSvelteConfig } from './load-svelte-config.js';
3 |
4 | import type { Ast, TemplateNode } from 'svelte/types/compiler/interfaces';
5 | import type { CallExpression } from 'estree';
6 | import type { Node } from './types.js';
7 | import type { PreprocessOptions } from './index.js';
8 |
9 | export function isAliasedAction(name: string, alias: string | string[]): boolean {
10 | if (typeof alias === 'string') {
11 | return name === alias;
12 | }
13 | return alias.includes(name);
14 | }
15 |
16 | const RUNES = [
17 | '$derived',
18 | '$effect',
19 | '$effect.active',
20 | '$effect.pre',
21 | '$effect.root',
22 | '$inspect',
23 | '$inspect.with',
24 | '$props',
25 | '$state',
26 | '$state.frozen',
27 | ];
28 |
29 | /**
30 | * There are 3 ways to determine if a component is in 'runes mode':
31 | * 1. If `` is set
32 | * 2. If `svelte-config.compilerOptions.runes` === `true`
33 | * 3. If a rune is present in the component (`$state`, `$derived`, `$effect`, etc.)
34 | */
35 | export async function isRuneMode(
36 | ast: Ast,
37 | options?: PreprocessOptions
38 | ): Promise {
39 | // check if the component has ``
40 | for (const element of ast.html.children ?? []) {
41 | if (element.type !== 'Options' || element.name !== 'svelte:options') continue;
42 |
43 | const attributes = element.attributes;
44 | for (const attr of attributes) {
45 | if (attr.name !== 'runes') continue;
46 | // ``
47 | if (typeof attr.value === 'boolean') {
48 | return attr.value;
49 | }
50 | // `` or ``
51 | if (typeof attr.value[0].expression.value === 'boolean') {
52 | return attr.value[0].expression.value;
53 | }
54 | }
55 | }
56 |
57 | // `svelte-config.compilerOptions.runes`
58 | const svelteConfig = await loadSvelteConfig(options?.svelteConfigPath);
59 | if (typeof svelteConfig?.compilerOptions?.runes === 'boolean') {
60 | return svelteConfig.compilerOptions.runes;
61 | }
62 |
63 | // a rune is present in the component
64 | let hasRunes = false;
65 | if (ast.module) {
66 | hasRunes = containsRunes(ast.module);
67 | }
68 |
69 | if (ast.instance) {
70 | hasRunes = hasRunes || containsRunes(ast.instance);
71 | }
72 |
73 | return hasRunes;
74 | }
75 | type Script = NonNullable;
76 | function containsRunes(script: Script): boolean {
77 | let containsRunes = false;
78 |
79 | walk(script, {
80 | enter(node) {
81 | // already found a rune, don't need to check the rest
82 | if (containsRunes) {
83 | this.skip();
84 | return;
85 | }
86 |
87 | if (node.type !== 'CallExpression') return;
88 | const callExpression = node as unknown as CallExpression;
89 |
90 | // $inspect(item)
91 | const callee = callExpression.callee;
92 | if (callee.type === 'Identifier') {
93 | containsRunes = RUNES.some((rune) => rune === callee.name);
94 | }
95 | // $inspect.with(item)
96 | if (callee.type === 'MemberExpression') {
97 | if (callee.computed) return;
98 | if (callee.object.type !== 'Identifier') return;
99 | if (callee.property.type !== 'Identifier') return;
100 |
101 | const name = callee.object.name + '.' + callee.property.name;
102 | containsRunes = RUNES.some((rune) => rune === name);
103 | }
104 | },
105 | });
106 |
107 | return containsRunes;
108 | }
109 |
110 | export function getMeltBuilderName(i: number) {
111 | return `__MELTUI_BUILDER_${i}__`;
112 | }
113 |
114 | // excuse the mess...
115 | type Enter = Parameters[1]['enter'];
116 | type EnterParams = Parameters>;
117 | type Leave = Parameters[1]['leave'];
118 | type WalkerContext = {
119 | skip: () => void;
120 | remove: () => void;
121 | replace: (node: Node) => void;
122 | };
123 | type WalkerArgs = {
124 | enter?: (
125 | this: WalkerContext,
126 | node: Node,
127 | parent: Node | null,
128 | key: EnterParams[2],
129 | index: EnterParams[3]
130 | ) => void;
131 | leave?: Leave;
132 | };
133 | /**
134 | * Enhances the param types of the estree-walker's `walk` function
135 | * as it doesn't want to accept Svelte's provided `TemplateNode` type.
136 | */
137 | export function walk, Node extends TemplateNode>(
138 | ast: AST,
139 | args: WalkerArgs
140 | ) {
141 | // @ts-expect-error do this once so i don't have to keep adding these ignores
142 | return estree_walk(ast, args);
143 | }
144 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import MagicString from 'magic-string';
2 | import { parse, type PreprocessorGroup, VERSION } from 'svelte/compiler';
3 | import { getMeltBuilderName, isRuneMode, walk } from './helpers.js';
4 | import { traverse } from './traverse/index.js';
5 |
6 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
7 | import type { Config, Node } from './types.js';
8 |
9 | export * from './sequence.js';
10 |
11 | export type PreprocessOptions = {
12 | /**
13 | * For aliasing the name of the `melt` action.
14 | *
15 | * When configured, the PP will __only__ process action names
16 | * that are passed to this field.
17 | *
18 | * @example
19 | * ```ts
20 | * // ONLY process actions named `_melt`
21 | * preprocessMeltUI({ alias: ["_melt"] })
22 | *
23 | * // process actions named `_melt` or `melt`
24 | * preprocessMeltUI({ alias: ["melt", "_melt"] })
25 | *
26 | * ```
27 | *
28 | * @default "melt"
29 | */
30 | alias?: string | string[];
31 | /**
32 | * Path to a svelte config file, either absolute or relative to `process.cwd()`.
33 | *
34 | * Set to `false` to ignore the svelte config file.
35 | */
36 | svelteConfigPath?: string | false;
37 | };
38 |
39 | /**
40 | * A preprocessor for Melt UI.
41 | *
42 | * Intelligently replaces all instances of `use:melt={$builder}` with the correct spread syntax,
43 | * providing a sleeker developer experience.
44 | *
45 | * Simply add it to the end of your array of preprocessors.
46 | * @example
47 | * ```js
48 | * // svelte.config.js
49 | * import { preprocessMeltUI } from '@melt-ui/pp';
50 | *
51 | * const config = {
52 | * // ... other svelte config options
53 | * preprocess: [
54 | * // ... other preprocessors
55 | * preprocessMeltUI() // add to the end!
56 | * ]
57 | * // ...
58 | * };
59 | * ```
60 | */
61 | export function preprocessMeltUI(options?: PreprocessOptions): PreprocessorGroup {
62 | const isSvelte5 = VERSION.startsWith('5');
63 | return {
64 | name: 'MeltUI Preprocess',
65 | markup: async ({ content, filename }) => {
66 | const config: Config = {
67 | alias: options?.alias ?? 'melt',
68 | markup: new MagicString(content, { filename }),
69 | builders: [],
70 | builderCount: 0,
71 | content,
72 | };
73 |
74 | let scriptContentNode: { start: number; end: number } | undefined;
75 | const ast = parse(content, { css: false, filename });
76 | const runesMode = isSvelte5 && (await isRuneMode(ast, options));
77 |
78 | // Grab the Script node so we can inject any hoisted expressions later
79 | if (ast.instance) {
80 | walk(ast.instance, {
81 | enter(node) {
82 | if (node.type === 'Script' && node.context === 'default') {
83 | scriptContentNode = node.content as { start: number; end: number };
84 | }
85 | },
86 | });
87 | }
88 |
89 | const leftOverActions = traverse({ baseNode: ast.html, config });
90 |
91 | // Any action that couldn't find a home within a scoped block will
92 | // bubble up to the top, indicating that these actions need
93 | // their expressions hoisted.
94 | leftOverActions.forEach((action) => {
95 | handleTopLevelAction({ actionNode: action, config });
96 | });
97 |
98 | // Build the identifiers to later inject into the script tag
99 | let identifiersToInsert = '';
100 | for (const builder of config.builders) {
101 | let identifier = '';
102 | if ('identifierName' in builder) {
103 | // if the user just passed in an identifier, just use that
104 | identifier = builder.identifierName;
105 | } else {
106 | // otherwise, we'll take the expression and hoist it into the script node
107 | identifier = getMeltBuilderName(config.builderCount++);
108 | if (runesMode) {
109 | identifiersToInsert += `\tlet ${identifier} = $derived(${builder.expression.contents});\n`;
110 | } else {
111 | identifiersToInsert += `\t$: ${identifier} = ${builder.expression.contents};\n`;
112 | }
113 | }
114 |
115 | const attributes = `{...${identifier}} use:${identifier}.action`;
116 |
117 | // replace the `use:melt={...}` with the attributes
118 | config.markup.overwrite(builder.startPos, builder.endPos, attributes, {
119 | storeName: true,
120 | });
121 | }
122 |
123 | // inject the hoisted expressions into the script node
124 | if (identifiersToInsert) {
125 | if (scriptContentNode) {
126 | // insert the new identifiers into the end of the script tag
127 | config.markup.appendRight(scriptContentNode.end, '\n' + identifiersToInsert);
128 | } else {
129 | // incase they don't already have a script tag...
130 | config.markup.prepend('\n');
131 | }
132 | }
133 |
134 | return {
135 | code: config.markup.toString(),
136 | };
137 | },
138 | };
139 | }
140 |
141 | type HandleTopLevelActionArgs = {
142 | actionNode: TemplateNode;
143 | config: Config;
144 | };
145 | /**
146 | * Constructs the Builder and adds it to its list.
147 | */
148 | function handleTopLevelAction(args: HandleTopLevelActionArgs) {
149 | const { actionNode, config } = args;
150 | let identifierName: string | undefined;
151 | const expression = actionNode.expression as Node;
152 |
153 | if (expression.type === 'Identifier') {
154 | // only an identifier was passed
155 | // i.e. use:melt={$builder}
156 | identifierName = expression.name;
157 | config.builders.push({
158 | identifierName,
159 | startPos: actionNode.start,
160 | endPos: actionNode.end,
161 | });
162 | } else {
163 | // any other expression type...
164 | // i.e. use:melt={$builder({ arg1: '', arg2: '' })}
165 | config.builders.push({
166 | expression: {
167 | startPos: expression.start,
168 | endPos: expression.end,
169 | contents: config.content.substring(expression.start, expression.end),
170 | },
171 | startPos: actionNode.start,
172 | endPos: actionNode.end,
173 | });
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/load-svelte-config.ts:
--------------------------------------------------------------------------------
1 | // Originally sourced and modified from https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/src/utils/load-svelte-config.js
2 |
3 | import { createRequire } from 'node:module';
4 | import { pathToFileURL } from 'node:url';
5 | import path from 'node:path';
6 | import fs from 'node:fs';
7 |
8 | // used to require cjs config in esm.
9 | // NOTE dynamic import() cjs technically works, but timestamp query cache bust
10 | // have no effect, likely because it has another internal cache?
11 | let esmRequire: NodeRequire;
12 |
13 | const svelteConfigNames = ['svelte.config.js', 'svelte.config.cjs', 'svelte.config.mjs'];
14 |
15 | async function dynamicImportDefault(filePath: string, timestamp: number) {
16 | return await import(filePath + '?t=' + timestamp).then((m) => m.default);
17 | }
18 |
19 | type SvelteConfig = {
20 | compilerOptions?: { runes?: boolean };
21 | };
22 |
23 | export async function loadSvelteConfig(
24 | svelteConfigPath?: string | false
25 | ): Promise {
26 | if (svelteConfigPath === false) {
27 | return;
28 | }
29 | const configFile = findConfigToLoad(svelteConfigPath);
30 | if (configFile) {
31 | let err;
32 | // try to use dynamic import for svelte.config.js first
33 | if (configFile.endsWith('.js') || configFile.endsWith('.mjs')) {
34 | try {
35 | const result = await dynamicImportDefault(
36 | pathToFileURL(configFile).href,
37 | fs.statSync(configFile).mtimeMs
38 | );
39 | if (result != null) {
40 | return {
41 | ...result,
42 | configFile,
43 | };
44 | } else {
45 | throw new Error(`invalid export in ${configFile}`);
46 | }
47 | } catch (e) {
48 | console.error(`failed to import config ${configFile}`, e);
49 | err = e;
50 | }
51 | }
52 | // cjs or error with dynamic import
53 | if (!configFile.endsWith('.mjs')) {
54 | try {
55 | // identify which require function to use (esm and cjs mode)
56 | const _require = import.meta.url
57 | ? esmRequire ?? (esmRequire = createRequire(import.meta.url))
58 | : require;
59 |
60 | // avoid loading cached version on reload
61 | delete _require.cache[_require.resolve(configFile)];
62 | const result = _require(configFile);
63 | if (result != null) {
64 | return {
65 | ...result,
66 | configFile,
67 | };
68 | } else {
69 | throw new Error(`invalid export in ${configFile}`);
70 | }
71 | } catch (e) {
72 | console.error(`failed to require config ${configFile}`, e);
73 | if (!err) {
74 | err = e;
75 | }
76 | }
77 | }
78 | // failed to load existing config file
79 | throw err;
80 | }
81 | }
82 |
83 | function findConfigToLoad(svelteConfigPath?: string): string | undefined {
84 | const root = process.cwd();
85 | if (svelteConfigPath) {
86 | const absolutePath = path.isAbsolute(svelteConfigPath)
87 | ? svelteConfigPath
88 | : path.resolve(root, svelteConfigPath);
89 | if (!fs.existsSync(absolutePath)) {
90 | throw new Error(`failed to find svelte config file ${absolutePath}.`);
91 | }
92 | return absolutePath;
93 | } else {
94 | const existingKnownConfigFiles = svelteConfigNames
95 | .map((candidate) => path.resolve(root, candidate))
96 | .filter((file) => fs.existsSync(file));
97 | if (existingKnownConfigFiles.length === 0) {
98 | console.debug(`no svelte config found at ${root}`, undefined, 'config');
99 | return;
100 | } else if (existingKnownConfigFiles.length > 1) {
101 | console.warn(
102 | `found more than one svelte config file, using ${existingKnownConfigFiles[0]}. you should only have one!`,
103 | existingKnownConfigFiles
104 | );
105 | }
106 | return existingKnownConfigFiles[0];
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/sequence.ts:
--------------------------------------------------------------------------------
1 | // Originally sourced and modified from https://github.com/pchynoweth/svelte-sequential-preprocessor
2 |
3 | import { preprocess } from 'svelte/compiler';
4 | import { PreprocessorGroup, Processed } from 'svelte/types/compiler/preprocess';
5 |
6 | /**
7 | * A Svelte preprocessor that wraps other preprocessors and forces them to run sequentially.
8 | *
9 | * @example
10 | * ```js
11 | * // svelte.config.js
12 | * import { preprocessMeltUI, sequence } from '@melt-ui/pp';
13 | *
14 | * const config = {
15 | * // ... other svelte config options
16 | * preprocess: sequence([
17 | * // ... other preprocessors (e.g. `vitePreprocess()`)
18 | * preprocessMeltUI()
19 | * ])
20 | * // ...
21 | * };
22 | * ```
23 | */
24 | export function sequence(preprocessors: PreprocessorGroup[]): PreprocessorGroup {
25 | return {
26 | async markup({ content, filename }): Promise {
27 | let code = content;
28 | let map: Processed['map'];
29 | let attributes: Processed['attributes'];
30 | let toString: Processed['toString'];
31 | const dependencies: Processed['dependencies'] = [];
32 |
33 | for (const pp of preprocessors) {
34 | const processed = await preprocess(code, pp, { filename });
35 | if (processed && processed.dependencies) {
36 | dependencies.push(...processed.dependencies);
37 | }
38 | code = processed ? processed.code : code;
39 | map = processed.map ?? map;
40 | attributes = processed.attributes ?? attributes;
41 | toString = processed.toString ?? toString;
42 | }
43 |
44 | return {
45 | code,
46 | dependencies,
47 | map,
48 | attributes,
49 | toString,
50 | };
51 | },
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/traverse/AwaitBlock.ts:
--------------------------------------------------------------------------------
1 | import { traverseBlock } from './Block.js';
2 |
3 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
4 | import type { Config } from '../types.js';
5 |
6 | type TraverseAwaitBlockArgs = {
7 | awaitBlockNode: TemplateNode;
8 | config: Config;
9 | };
10 | export function traverseAwaitBlock({ awaitBlockNode, config }: TraverseAwaitBlockArgs) {
11 | if (awaitBlockNode.type !== 'AwaitBlock') throw Error('This node is not an AwaitBlock');
12 |
13 | /* determine if those identifiers are being used in the melt action's expression */
14 |
15 | // then block
16 | traverseBlock({
17 | blockNode: awaitBlockNode.then,
18 | config,
19 | });
20 |
21 | // catch block
22 | traverseBlock({
23 | blockNode: awaitBlockNode.catch,
24 | config,
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/src/traverse/Block.ts:
--------------------------------------------------------------------------------
1 | import { getMeltBuilderName, isAliasedAction, walk } from '../helpers.js';
2 | import { traverse } from './index.js';
3 |
4 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
5 | import type { Config, Node } from '../types.js';
6 |
7 | type BlockArgs = {
8 | blockNode: TemplateNode;
9 | config: Config;
10 | };
11 |
12 | /**
13 | * Traverses any given block and checks if there are any identifiers
14 | * that exist in it's child `melt` action's expression.
15 | *
16 | * If there are, we'll inject an `{@const}` block into the provided block
17 | * with it's corresponding identifiers.
18 | */
19 | export function traverseBlock({ blockNode, config }: BlockArgs) {
20 | if (blockNode.children === undefined) return;
21 |
22 | // walk the children to determine if the block's provided identifiers are
23 | // being used in the melt action's expression
24 | walk(blockNode.children, {
25 | enter(node) {
26 | if (
27 | node.type === 'Action' &&
28 | isAliasedAction(node.name, config.alias) &&
29 | node.expression !== null // assigned to something
30 | ) {
31 | handleActionNode({
32 | actionNode: node,
33 | blockNode,
34 | config,
35 | });
36 |
37 | // we don't have to walk the Action's children
38 | this.skip();
39 | return;
40 | }
41 |
42 | // if it's anything else, walk again
43 | const returnedActions = traverse({ baseNode: node, config });
44 |
45 | for (const actionNode of returnedActions) {
46 | handleActionNode({
47 | actionNode,
48 | blockNode,
49 | config,
50 | });
51 | }
52 |
53 | // only want to walk over the direct children, so we'll skip the rest
54 | this.skip();
55 | },
56 | });
57 | }
58 |
59 | type HandleActionNodeArgs = {
60 | blockNode: TemplateNode;
61 | actionNode: TemplateNode;
62 | config: Config;
63 | };
64 |
65 | /**
66 | * Injects the `{@const}` tag as a child of the provided block
67 | * node if the expression is anything but an `Identifier`.
68 | */
69 | function handleActionNode({ config, actionNode, blockNode }: HandleActionNodeArgs) {
70 | const expression = actionNode.expression as Node;
71 |
72 | // any other expression type...
73 | // i.e. use:melt={$builder({ arg1: '', arg2: '' })}
74 | if (expression.type !== 'Identifier') {
75 | const expressionContent = config.content.substring(expression.start, expression.end);
76 |
77 | // extract the indent of the block such that the indentation of the injected
78 | // {@const} tag is in line with the rest of the block
79 | const blockContent = config.content.substring(blockNode.start, blockNode.end);
80 | const blockLines = blockContent.split('\n');
81 | const indent = blockLines.at(1)?.match(/\s*/);
82 |
83 | // a weird quirk with Await and Component blocks where the first child
84 | // is a Text node, so we'll ignore them and take the 2nd child instead
85 | let firstChild = blockNode.children?.at(0);
86 | if (firstChild?.type === 'Text') {
87 | firstChild = blockNode.children?.at(1);
88 | }
89 |
90 | // checks if there are any existing `{@const}` tags. if there are, we want to
91 | // append the injected block at the end
92 | let lastConst: TemplateNode | undefined;
93 | for (const child of blockNode.children ?? []) {
94 | if (child.type === 'ConstTag') lastConst = child;
95 | }
96 |
97 | // convert this into a {@const} block
98 | const pos = lastConst?.end ?? firstChild?.start;
99 | const constIdentifier = getMeltBuilderName(config.builderCount++);
100 | if (!pos) throw Error('This is unreachable');
101 |
102 | // we'll add the indent and new line depending on where we're injecting it
103 | const constTag = lastConst
104 | ? `\n${indent}{@const ${constIdentifier} = ${expressionContent}}`
105 | : `{@const ${constIdentifier} = ${expressionContent}}\n${indent}`;
106 |
107 | config.builders.push({
108 | identifierName: constIdentifier,
109 | startPos: actionNode.start,
110 | endPos: actionNode.end,
111 | });
112 |
113 | config.markup.prependRight(pos, constTag);
114 | } else {
115 | // if it's just an identifier, add it to the list of builders so that it can
116 | // later be transformed into the correct syntax
117 | // i.e. use:melt={$builder}
118 | config.builders.push({
119 | identifierName: expression.name,
120 | startPos: actionNode.start,
121 | endPos: actionNode.end,
122 | });
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/traverse/ComponentBlock.ts:
--------------------------------------------------------------------------------
1 | import { traverseBlock } from './Block.js';
2 |
3 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
4 | import type { Config } from '../types.js';
5 |
6 | type TraverseEachBlockArgs = {
7 | compBlockNode: TemplateNode;
8 | config: Config;
9 | };
10 | export function traverseComponentBlock({ compBlockNode, config }: TraverseEachBlockArgs) {
11 | if (compBlockNode.type !== 'InlineComponent' && compBlockNode.type !== 'SlotTemplate')
12 | throw Error('This node is not an InlineComponent or a SlotTemplate');
13 |
14 | // determine if those identifiers are scoped to this block
15 | traverseBlock({
16 | blockNode: compBlockNode,
17 | config,
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/traverse/EachBlock.ts:
--------------------------------------------------------------------------------
1 | import { traverseBlock } from './Block.js';
2 |
3 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
4 | import type { Config } from '../types.js';
5 |
6 | type TraverseEachBlockArgs = {
7 | eachBlockNode: TemplateNode;
8 | config: Config;
9 | };
10 | export function traverseEachBlock({ eachBlockNode, config }: TraverseEachBlockArgs) {
11 | if (eachBlockNode.type !== 'EachBlock') throw Error('This node is not an EachBlock');
12 |
13 | // determine if those identifiers are being used in the melt action's expression
14 | traverseBlock({
15 | blockNode: eachBlockNode,
16 | config,
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/traverse/index.ts:
--------------------------------------------------------------------------------
1 | import { traverseEachBlock } from './EachBlock.js';
2 | import { traverseAwaitBlock } from './AwaitBlock.js';
3 | import { traverseComponentBlock } from './ComponentBlock.js';
4 | import { isAliasedAction, walk } from '../helpers.js';
5 |
6 | import type { TemplateNode } from 'svelte/types/compiler/interfaces';
7 | import type { Config } from '../types.js';
8 |
9 | type TraverseArgs = {
10 | baseNode: TemplateNode;
11 | config: Config;
12 | };
13 | export function traverse({ baseNode, config }: TraverseArgs) {
14 | const actions: TemplateNode[] = [];
15 |
16 | walk(baseNode, {
17 | enter(node) {
18 | // if there's an each block that contains an expression,
19 | // add a {@const identifier = expression}
20 | if (node.type === 'EachBlock') {
21 | traverseEachBlock({ eachBlockNode: node, config });
22 |
23 | // don't need to traverse the rest of the Each Block
24 | this.skip();
25 | }
26 |
27 | // components with a let:identifier
28 | if (
29 | (node.type === 'InlineComponent' || node.type === 'SlotTemplate') &&
30 | node.children &&
31 | node.children.length > 0
32 | ) {
33 | traverseComponentBlock({ compBlockNode: node, config });
34 |
35 | // don't need to traverse the rest of the Component Block
36 | this.skip();
37 | }
38 |
39 | // {#await} blocks
40 | if (node.type === 'AwaitBlock') {
41 | // check identifiers in the then and catch block, if present
42 | traverseAwaitBlock({ awaitBlockNode: node, config });
43 |
44 | // don't need to traverse the rest of the Await Block
45 | this.skip();
46 | }
47 |
48 | // top level Actions
49 | if (
50 | node.type === 'Action' &&
51 | isAliasedAction(node.name, config.alias) &&
52 | node.expression !== null // assigned to something
53 | ) {
54 | actions.push(node);
55 |
56 | // we don't have to walk the Action's children
57 | this.skip();
58 | }
59 | },
60 | });
61 |
62 | // return all the leftover actions
63 | return actions;
64 | }
65 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Node as ESTreeNode } from 'estree';
2 | import type MagicString from 'magic-string';
3 |
4 | export type Builder = BuilderIdentifier | BuilderExpression;
5 |
6 | type BuilderIdentifier = {
7 | identifierName: string;
8 | startPos: number;
9 | endPos: number;
10 | };
11 | type BuilderExpression = {
12 | startPos: number;
13 | endPos: number;
14 | expression: ExpressionContent;
15 | };
16 |
17 | type ExpressionContent = {
18 | startPos: number;
19 | endPos: number;
20 | contents: string;
21 | };
22 |
23 | export type Node = ESTreeNode & {
24 | start: number;
25 | end: number;
26 | };
27 |
28 | export type Config = {
29 | alias: string | string[];
30 | builders: Builder[];
31 | markup: MagicString;
32 | content: string;
33 | builderCount: number;
34 | };
35 |
--------------------------------------------------------------------------------
/tests/await_block/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const basicAwait = `
2 |
13 |
14 | {#await promise}
15 |
16 | {:then item}
17 |
18 | {:catch error}
19 |
20 | {/await}
21 | `;
22 |
23 | export const basicAwaitExpected = `
24 |
35 |
36 | {#await promise}
37 |
38 | {:then item}
39 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
40 |
41 | {:catch error}
42 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: error, arg2: '' })}
43 |
44 | {/await}
45 | `;
46 |
47 | export const basicShorthandAwait = `
48 |
59 |
60 | {#await promise then item}
61 |
62 | {:catch error}
63 |
64 | {/await}
65 | `;
66 |
67 | export const basicShorthandAwaitExpected = `
68 |
79 |
80 | {#await promise then item}
81 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
82 |
83 | {:catch error}
84 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: error, arg2: '' })}
85 |
86 | {/await}
87 | `;
88 |
89 | export const controlAwait = `
90 |
101 |
102 | {#await promise}
103 |
104 | {:then item}
105 |
106 | {:catch error}
107 |
108 | {/await}
109 | `;
110 |
111 | export const controlAwaitExpected = `
112 |
123 |
124 | {#await promise}
125 |
126 | {:then item}
127 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: 1, arg2: '' })}
128 |
129 | {:catch error}
130 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: 1, arg2: '' })}
131 |
132 | {/await}
133 | `;
134 |
135 | export const basicIdentifierAwait = `
136 |
145 |
146 | {#await promise}
147 |
148 | {:then item}
149 |
150 | {:catch error}
151 |
152 | {/await}
153 | `;
154 |
155 | export const basicIdentifierAwaitExpected = `
156 |
165 |
166 | {#await promise}
167 |
168 | {:then item}
169 |
170 | {:catch error}
171 |
172 | {/await}
173 | `;
174 |
175 | export const duplicateIdentifierAwait = `
176 |
187 |
188 | {#await promise}
189 |
190 | {:then item}
191 |
192 | {:catch error}
193 |
194 | {/await}
195 | `;
196 |
197 | export const duplicateIdentifierAwaitExpected = `
198 |
209 |
210 | {#await promise}
211 |
212 | {:then item}
213 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: item })}
214 |
215 | {:catch error}
216 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: error, arg2: error })}
217 |
218 | {/await}
219 | `;
220 |
221 | export const destructuredAwait = `
222 |
233 |
234 | {#await promise}
235 |
236 | {:then {item1, item2}}
237 |
238 | {:catch {error1, error2}}
239 |
240 | {/await}
241 | `;
242 |
243 | export const destructuredAwaitExpected = `
244 |
255 |
256 | {#await promise}
257 |
258 | {:then {item1, item2}}
259 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
260 |
261 | {:catch {error1, error2}}
262 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: error1, arg2: error2 })}
263 |
264 | {/await}
265 | `;
266 |
267 | export const scopedAwait = `
268 |
279 |
280 | {#await promise then item}
281 |
282 | {#await promise then item}
283 |
284 | {:catch error}
285 |
286 | {/await}
287 | {:catch error}
288 |
289 | {#await promise2 then item}
290 |
291 | {:catch error}
292 |
293 | {/await}
294 | {/await}
295 | `;
296 |
297 | export const scopedAwaitExpected = `
298 |
309 |
310 | {#await promise then item}
311 | {@const __MELTUI_BUILDER_2__ = $builder({ arg1: item, arg2: '' })}
312 |
313 | {#await promise then item}
314 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
315 |
316 | {:catch error}
317 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: error, arg2: '' })}
318 |
319 | {/await}
320 | {:catch error}
321 | {@const __MELTUI_BUILDER_5__ = $builder({ arg1: error, arg2: '' })}
322 |
323 | {#await promise2 then item}
324 | {@const __MELTUI_BUILDER_3__ = $builder({ arg1: item, arg2: '' })}
325 |
326 | {:catch error}
327 | {@const __MELTUI_BUILDER_4__ = $builder({ arg1: error, arg2: '' })}
328 |
329 | {/await}
330 | {/await}
331 | `;
332 |
333 | export const nestedAwaitUpper = `
334 |
345 |
346 | {#await promise then item}
347 | {#await promise then item2}
348 |
349 | {:catch error}
350 |
351 | {/await}
352 | {:catch error1}
353 | {#await promise then item}
354 |
355 | {:catch error2}
356 |
357 | {/await}
358 | {/await}
359 | `;
360 |
361 | export const nestedAwaitUpperExpected = `
362 |
373 |
374 | {#await promise then item}
375 | {#await promise then item2}
376 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
377 |
378 | {:catch error}
379 | {@const __MELTUI_BUILDER_1__ = $builder({ arg1: item, arg2: '' })}
380 |
381 | {/await}
382 | {:catch error1}
383 | {#await promise then item}
384 | {@const __MELTUI_BUILDER_2__ = $builder({ arg1: error1, arg2: '' })}
385 |
386 | {:catch error2}
387 | {@const __MELTUI_BUILDER_3__ = $builder({ arg1: error1, arg2: '' })}
388 |
389 | {/await}
390 | {/await}
391 | `;
392 |
393 | export const nestedAwaitLower = `
394 |
405 |
406 | {#await promise then item}
407 | {#await promise then item2}
408 |
409 | {/await}
410 | {/await}
411 | `;
412 |
413 | export const nestedAwaitLowerExpected = `
414 |
425 |
426 | {#await promise then item}
427 | {#await promise then item2}
428 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item2, arg2: '' })}
429 |
430 | {/await}
431 | {/await}
432 | `;
433 |
434 | export const nestedAwaitBoth = `
435 |
446 |
447 | {#await promise then item}
448 | {#await promise then item2}
449 |
450 | {/await}
451 | {/await}
452 | `;
453 |
454 | export const nestedAwaitBothExpected = `
455 |
466 |
467 | {#await promise then item}
468 | {#await promise then item2}
469 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
470 |
471 | {/await}
472 | {/await}
473 | `;
474 |
--------------------------------------------------------------------------------
/tests/await_block/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { preprocessMeltUI } from '$pkg/index';
3 | import * as t from './index.svelte';
4 |
5 | describe('Await Block', () => {
6 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
7 | if (!markup) throw new Error('Should always exist');
8 |
9 | test('basic await', async () => {
10 | const processed = await markup({
11 | content: t.basicAwait,
12 | });
13 |
14 | expect(processed?.code).toBe(t.basicAwaitExpected);
15 | });
16 |
17 | test('basic shorthand await', async () => {
18 | const processed = await markup({
19 | content: t.basicShorthandAwait,
20 | });
21 |
22 | expect(processed?.code).toBe(t.basicShorthandAwaitExpected);
23 | });
24 |
25 | test('basic identifier await', async () => {
26 | const processed = await markup({
27 | content: t.basicIdentifierAwait,
28 | });
29 |
30 | expect(processed?.code).toBe(t.basicIdentifierAwaitExpected);
31 | });
32 |
33 | test('control await', async () => {
34 | const processed = await markup({
35 | content: t.controlAwait,
36 | });
37 |
38 | expect(processed?.code).toBe(t.controlAwaitExpected);
39 | });
40 |
41 | test('duplicate args await', async () => {
42 | const processed = await markup({
43 | content: t.duplicateIdentifierAwait,
44 | });
45 |
46 | expect(processed?.code).toBe(t.duplicateIdentifierAwaitExpected);
47 | });
48 |
49 | test('destructured value and error await', async () => {
50 | const processed = await markup({
51 | content: t.destructuredAwait,
52 | });
53 |
54 | expect(processed?.code).toBe(t.destructuredAwaitExpected);
55 | });
56 |
57 | test('nested await - lexical shadowing', async () => {
58 | const processed = await markup({
59 | content: t.scopedAwait,
60 | });
61 |
62 | expect(processed?.code).toBe(t.scopedAwaitExpected);
63 | });
64 |
65 | test('nested await - upper identifier only', async () => {
66 | const processed = await markup({
67 | content: t.nestedAwaitUpper,
68 | });
69 |
70 | expect(processed?.code).toBe(t.nestedAwaitUpperExpected);
71 | });
72 |
73 | test('nested await - lower identifier only', async () => {
74 | const processed = await markup({
75 | content: t.nestedAwaitLower,
76 | });
77 |
78 | expect(processed?.code).toBe(t.nestedAwaitLowerExpected);
79 | });
80 |
81 | test('nested await - both identifiers', async () => {
82 | const processed = await markup({
83 | content: t.nestedAwaitBoth,
84 | });
85 |
86 | expect(processed?.code).toBe(t.nestedAwaitBothExpected);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/tests/component_block/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const basicComponent = `
2 |
13 |
14 |
15 |
16 |
17 | `;
18 |
19 | export const basicComponentExpected = `
20 |
31 |
32 |
33 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
34 |
35 |
36 | `;
37 |
38 | export const basicShorthand = `
39 |
50 |
51 |
52 |
53 |
54 | `;
55 |
56 | export const basicShorthandExpected = `
57 |
68 |
69 |
70 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
71 |
72 |
73 | `;
74 |
75 | export const control = `
76 |
87 |
88 |
89 |
90 |
91 | `;
92 |
93 | export const controlExpected = `
94 |
105 |
106 |
107 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: 1, arg2: '' })}
108 |
109 |
110 | `;
111 |
112 | export const basicIdentifier = `
113 |
114 |
115 |
116 | `;
117 |
118 | export const basicIdentifierExpected = `
119 |
120 |
121 |
122 | `;
123 |
124 | export const duplicateIdentifier = `
125 |
136 |
137 |
138 |
139 |
140 | `;
141 |
142 | export const duplicateIdentifierExpected = `
143 |
154 |
155 |
156 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: item })}
157 |
158 |
159 | `;
160 |
161 | export const destructured = `
162 |
173 |
174 |
175 |
176 |
177 | `;
178 |
179 | export const destructuredExpected = `
180 |
191 |
192 |
193 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
194 |
195 |
196 | `;
197 |
198 | export const scoped = `
199 |
210 |
211 |
212 |
213 |
214 |
215 |
216 | `;
217 |
218 | export const scopedExpected = `
219 |
230 |
231 |
232 |
233 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
234 |
235 |
236 |
237 | `;
238 |
239 | export const nestedUpper = `
240 |
251 |
252 |
253 |
254 |
255 |
256 |
257 | `;
258 |
259 | export const nestedUpperExpected = `
260 |
271 |
272 |
273 |
274 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: '' })}
275 |
276 |
277 |
278 | `;
279 |
280 | export const nestedLower = `
281 |
292 |
293 |
294 |
295 |
296 |
297 |
298 | `;
299 |
300 | export const nestedLowerExpected = `
301 |
312 |
313 |
314 |
315 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item2, arg2: '' })}
316 |
317 |
318 |
319 | `;
320 |
321 | export const nestedBoth = `
322 |
333 |
334 |
335 |
336 |
337 |
338 |
339 | `;
340 |
341 | export const nestedBothExpected = `
342 |
353 |
354 |
355 |
356 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
357 |
358 |
359 |
360 | `;
361 |
362 | export const slotTemplate = `
363 |
374 |
375 |
376 |
377 |
378 |
379 |
380 | `;
381 |
382 | export const slotTemplateExpected = `
383 |
394 |
395 |
396 |
397 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
398 |
399 |
400 |
401 | `;
402 |
--------------------------------------------------------------------------------
/tests/component_block/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { preprocessMeltUI } from '$pkg/index';
3 | import * as t from './index.svelte';
4 |
5 | describe('Component Block', () => {
6 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
7 | if (!markup) throw new Error('Should always exist');
8 |
9 | test('basic component', async () => {
10 | const processed = await markup({
11 | content: t.basicComponent,
12 | });
13 |
14 | expect(processed?.code).toBe(t.basicComponentExpected);
15 | });
16 |
17 | test('basic shorthand', async () => {
18 | const processed = await markup({
19 | content: t.basicShorthand,
20 | });
21 |
22 | expect(processed?.code).toBe(t.basicShorthandExpected);
23 | });
24 |
25 | test('basic identifier', async () => {
26 | const processed = await markup({
27 | content: t.basicIdentifier,
28 | });
29 |
30 | expect(processed?.code).toBe(t.basicIdentifierExpected);
31 | });
32 |
33 | test('control', async () => {
34 | const processed = await markup({
35 | content: t.control,
36 | });
37 |
38 | expect(processed?.code).toBe(t.controlExpected);
39 | });
40 |
41 | test('duplicate args', async () => {
42 | const processed = await markup({
43 | content: t.duplicateIdentifier,
44 | });
45 |
46 | expect(processed?.code).toBe(t.duplicateIdentifierExpected);
47 | });
48 |
49 | test('destructured value and error', async () => {
50 | const processed = await markup({
51 | content: t.destructured,
52 | });
53 |
54 | expect(processed?.code).toBe(t.destructuredExpected);
55 | });
56 |
57 | test('nested - lexical shadowing', async () => {
58 | const processed = await markup({
59 | content: t.scoped,
60 | });
61 |
62 | expect(processed?.code).toBe(t.scopedExpected);
63 | });
64 |
65 | test('nested - upper identifier only', async () => {
66 | const processed = await markup({
67 | content: t.nestedUpper,
68 | });
69 |
70 | expect(processed?.code).toBe(t.nestedUpperExpected);
71 | });
72 |
73 | test('nested - lower identifier only', async () => {
74 | const processed = await markup({
75 | content: t.nestedLower,
76 | });
77 |
78 | expect(processed?.code).toBe(t.nestedLowerExpected);
79 | });
80 |
81 | test('nested - both identifiers', async () => {
82 | const processed = await markup({
83 | content: t.nestedBoth,
84 | });
85 |
86 | expect(processed?.code).toBe(t.nestedBothExpected);
87 | });
88 |
89 | test('slot template - both identifiers', async () => {
90 | const processed = await markup({
91 | content: t.slotTemplate,
92 | });
93 |
94 | expect(processed?.code).toBe(t.slotTemplateExpected);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/tests/each_block/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const basicEach = `
2 |
13 |
14 | {#each [1, 2, 3] as item}
15 |
16 | {/each}
17 | `;
18 |
19 | export const basicEachExpected = `
20 |
31 |
32 | {#each [1, 2, 3] as item}
33 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
34 |
35 | {/each}
36 | `;
37 |
38 | export const controlEach = `
39 |
50 |
51 | {#each [1, 2, 3] as item}
52 |
53 | {/each}
54 | `;
55 |
56 | export const controlEachExpected = `
57 |
68 |
69 | {#each [1, 2, 3] as item}
70 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: 1, arg2: '' })}
71 |
72 | {/each}
73 | `;
74 |
75 | export const basicIdentifierEach = `
76 |
85 |
86 | {#each [1, 2, 3] as item}
87 |
88 | {/each}
89 | `;
90 |
91 | export const basicIdentifierEachExpected = `
92 |
101 |
102 | {#each [1, 2, 3] as item}
103 |
104 | {/each}
105 | `;
106 |
107 | export const duplicateEach = `
108 |
119 |
120 | {#each [1, 2, 3] as item}
121 |
122 | {/each}
123 | `;
124 |
125 | export const duplicateEachExpected = `
126 |
137 |
138 | {#each [1, 2, 3] as item}
139 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: item })}
140 |
141 | {/each}
142 | `;
143 |
144 | export const destructuredEach = `
145 |
156 |
157 | {#each [{item1: 1, item2: 1}, {item1: 2, item2: 2}, {item1: 3, item2: 3}] as {item1, item2}}
158 |
159 | {/each}
160 | `;
161 |
162 | export const destructuredEachExpected = `
163 |
174 |
175 | {#each [{item1: 1, item2: 1}, {item1: 2, item2: 2}, {item1: 3, item2: 3}] as {item1, item2}}
176 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item1, arg2: item2 })}
177 |
178 | {/each}
179 | `;
180 |
181 | export const scopedEach = `
182 |
193 |
194 | {#each [1, 2, 3] as item}
195 | {#each [4, 5, 6] as item}
196 |
197 | {/each}
198 | {/each}
199 | `;
200 |
201 | export const scopedEachExpected = `
202 |
213 |
214 | {#each [1, 2, 3] as item}
215 | {#each [4, 5, 6] as item}
216 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
217 |
218 | {/each}
219 | {/each}
220 | `;
221 |
222 | export const nestedEachUpper = `
223 |
234 |
235 | {#each [1, 2, 3] as item}
236 | {#each [4, 5, 6] as item2}
237 |
238 | {/each}
239 | {/each}
240 | `;
241 |
242 | export const nestedEachUpperExpected = `
243 |
254 |
255 | {#each [1, 2, 3] as item}
256 | {#each [4, 5, 6] as item2}
257 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: '' })}
258 |
259 | {/each}
260 | {/each}
261 | `;
262 |
263 | export const nestedEachLower = `
264 |
275 |
276 | {#each [1, 2, 3] as item}
277 | {#each [4, 5, 6] as item2}
278 |
279 | {/each}
280 | {/each}
281 | `;
282 |
283 | export const nestedEachLowerExpected = `
284 |
295 |
296 | {#each [1, 2, 3] as item}
297 | {#each [4, 5, 6] as item2}
298 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item2, arg2: '' })}
299 |
300 | {/each}
301 | {/each}
302 | `;
303 |
304 | export const nestedEachBoth = `
305 |
316 |
317 | {#each [1, 2, 3] as item}
318 | {#each [4, 5, 6] as item2}
319 |
320 | {/each}
321 | {/each}
322 | `;
323 |
324 | export const nestedEachBothExpected = `
325 |
336 |
337 | {#each [1, 2, 3] as item}
338 | {#each [4, 5, 6] as item2}
339 | {@const __MELTUI_BUILDER_0__ = $builder({ arg1: item, arg2: item2 })}
340 |
341 | {/each}
342 | {/each}
343 | `;
344 |
345 | export const thumbEach = `
346 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | {#each $value as _}
364 |
368 | {/each}
369 |
370 | `;
371 |
372 | export const thumbEachExpected = `
373 |
384 |
385 |
386 |
387 |
388 |
389 |
390 | {#each $value as _}
391 | {@const __MELTUI_BUILDER_0__ = $thumb()}
392 |
396 | {/each}
397 |
398 | `;
399 |
400 | export const existingConst = `
401 |
412 |
413 |
414 |
415 |
416 |
417 |
418 | {#each $value as { id }, i}
419 | {@const itemId = id}
420 | {@const itemId2 = id}
421 |
425 | {/each}
426 |
427 | `;
428 |
429 | export const existingConstExpected = `
430 |
441 |
442 |
443 |
444 |
445 |
446 |
447 | {#each $value as { id }, i}
448 | {@const itemId = id}
449 | {@const itemId2 = id}
450 | {@const __MELTUI_BUILDER_0__ = $item(itemId)}
451 |
455 | {/each}
456 |
457 | `;
458 |
--------------------------------------------------------------------------------
/tests/each_block/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { preprocessMeltUI } from '$pkg/index';
3 | import * as t from './index.svelte';
4 |
5 | describe('Each Block', () => {
6 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
7 | if (!markup) throw new Error('Should always exist');
8 |
9 | test('basic each', async () => {
10 | const processed = await markup({
11 | content: t.basicEach,
12 | });
13 |
14 | expect(processed?.code).toBe(t.basicEachExpected);
15 | });
16 |
17 | test('basic identifier each', async () => {
18 | const processed = await markup({
19 | content: t.basicIdentifierEach,
20 | });
21 |
22 | expect(processed?.code).toBe(t.basicIdentifierEachExpected);
23 | });
24 |
25 | test('control each', async () => {
26 | const processed = await markup({
27 | content: t.controlEach,
28 | });
29 |
30 | expect(processed?.code).toBe(t.controlEachExpected);
31 | });
32 |
33 | test('duplicate args each', async () => {
34 | const processed = await markup({
35 | content: t.duplicateEach,
36 | });
37 |
38 | expect(processed?.code).toBe(t.duplicateEachExpected);
39 | });
40 |
41 | test('destructured context each', async () => {
42 | const processed = await markup({
43 | content: t.destructuredEach,
44 | });
45 |
46 | expect(processed?.code).toBe(t.destructuredEachExpected);
47 | });
48 |
49 | test('nested each - lexical shadowing', async () => {
50 | const processed = await markup({
51 | content: t.scopedEach,
52 | });
53 |
54 | expect(processed?.code).toBe(t.scopedEachExpected);
55 | });
56 |
57 | test('nested each - upper identifier only', async () => {
58 | const processed = await markup({
59 | content: t.nestedEachUpper,
60 | });
61 |
62 | expect(processed?.code).toBe(t.nestedEachUpperExpected);
63 | });
64 |
65 | test('nested each - lower identifier only', async () => {
66 | const processed = await markup({
67 | content: t.nestedEachLower,
68 | });
69 |
70 | expect(processed?.code).toBe(t.nestedEachLowerExpected);
71 | });
72 |
73 | test('nested each - both identifiers', async () => {
74 | const processed = await markup({
75 | content: t.nestedEachBoth,
76 | });
77 |
78 | expect(processed?.code).toBe(t.nestedEachBothExpected);
79 | });
80 |
81 | test('with no reference to the context', async () => {
82 | const processed = await markup({
83 | content: t.thumbEach,
84 | });
85 |
86 | expect(processed?.code).toBe(t.thumbEachExpected);
87 | });
88 |
89 | test('with existing const declaration', async () => {
90 | const processed = await markup({
91 | content: t.existingConst,
92 | });
93 |
94 | expect(processed?.code).toBe(t.existingConstExpected);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/tests/expression_builder/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const callExpression = `
2 |
13 |
14 |
15 | `;
16 |
17 | export const callExpressionExpected = `
18 |
31 |
32 |
33 | `;
34 |
35 | export const objExpression = `
36 |
47 |
48 |
49 | `;
50 |
51 | export const objExpressionExpected = `
52 |
65 |
66 |
67 | `;
68 |
69 | export const multiExpressions = `
70 |
81 |
82 |
83 |
84 |
85 | `;
86 |
87 | export const multiExpressionsExpected = `
88 |
103 |
104 |
105 |
106 |
107 | `;
108 |
--------------------------------------------------------------------------------
/tests/expression_builder/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { preprocessMeltUI } from '$pkg/index';
3 | import * as t from './index.svelte';
4 |
5 | describe('Expression Builder', () => {
6 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
7 | if (!markup) throw new Error('Should always exist');
8 |
9 | test('CallExpression', async () => {
10 | const processed = await markup({
11 | content: t.callExpression,
12 | });
13 |
14 | expect(processed?.code).toBe(t.callExpressionExpected);
15 | });
16 |
17 | test('ObjectExpression', async () => {
18 | const processed = await markup({
19 | content: t.objExpression,
20 | });
21 |
22 | expect(processed?.code).toBe(t.objExpressionExpected);
23 | });
24 |
25 | test('Multi CallExpression', async () => {
26 | const processed = await markup({
27 | content: t.multiExpressions,
28 | });
29 |
30 | expect(processed?.code).toBe(t.multiExpressionsExpected);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tests/runes/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const inferredRunes = `
2 |
15 |
16 |
17 | `;
18 |
19 | export const inferredRunesExpected = `
20 |
35 |
36 |
37 | `;
38 |
39 | export const inferredRunesMemberExpression = `
40 |
54 |
55 |
56 | `;
57 |
58 | export const inferredRunesMemberExpressionExpected = `
59 |
75 |
76 |
77 | `;
78 |
79 | export const svelteOptionsExplicit = `
80 |
81 |
92 |
93 |
94 | `;
95 |
96 | export const svelteOptionsExplicitExpected = `
97 |
98 |
111 |
112 |
113 | `;
114 |
115 | export const svelteOptionsImplicit = `
116 |
117 |
128 |
129 |
130 | `;
131 |
132 | export const svelteOptionsImplicitExpected = `
133 |
134 |
147 |
148 |
149 | `;
150 |
151 | export const svelteOptionsExplicitDisabled = `
152 |
153 |
164 |
165 |
166 | `;
167 |
168 | export const svelteOptionsExplicitDisabledExpected = `
169 |
170 |
183 |
184 |
185 | `;
186 |
187 | export const svelteConfigExplicitEnabled = `
188 |
199 |
200 |
201 | `;
202 |
203 | export const svelteConfigExplicitEnabledExpected = `
204 |
217 |
218 |
219 | `;
220 |
221 | export const svelteConfigExplicitDisabled = `
222 |
233 |
234 |
235 | `;
236 |
237 | export const svelteConfigExplicitDisabledExpected = `
238 |
251 |
252 |
253 | `;
254 |
255 | export const ignoredConfig = `
256 |
269 |
270 |
271 | `;
272 |
273 | export const ignoredConfigExpected = `
274 |
289 |
290 |
291 | `;
292 |
--------------------------------------------------------------------------------
/tests/runes/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { VERSION } from 'svelte/compiler';
3 | import { preprocessMeltUI } from '$pkg/index';
4 | import * as t from './index.svelte';
5 |
6 | const isSvelte5 = VERSION.startsWith('5');
7 | describe.skipIf(!isSvelte5)('Runes mode', () => {
8 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
9 | if (!markup) throw new Error('Should always exist');
10 |
11 | // Runes mode set via inference
12 | test('inferred runes - Identifiers', async () => {
13 | const processed = await markup({
14 | content: t.inferredRunes,
15 | });
16 |
17 | expect(processed?.code).toBe(t.inferredRunesExpected);
18 | });
19 |
20 | test('inferred runes - MemberExpression', async () => {
21 | const processed = await markup({
22 | content: t.inferredRunesMemberExpression,
23 | });
24 |
25 | expect(processed?.code).toBe(t.inferredRunesMemberExpressionExpected);
26 | });
27 |
28 | // Runes mode set via
29 | test('runes mode set with ', async () => {
30 | const processed = await markup({
31 | content: t.svelteOptionsExplicit,
32 | });
33 |
34 | expect(processed?.code).toBe(t.svelteOptionsExplicitExpected);
35 | });
36 |
37 | test('runes mode set with ', async () => {
38 | const processed = await markup({
39 | content: t.svelteOptionsImplicit,
40 | });
41 |
42 | expect(processed?.code).toBe(t.svelteOptionsImplicitExpected);
43 | });
44 |
45 | test('runes mode disabled with ', async () => {
46 | const processed = await markup({
47 | content: t.svelteOptionsExplicitDisabled,
48 | });
49 |
50 | expect(processed?.code).toBe(t.svelteOptionsExplicitDisabledExpected);
51 | });
52 |
53 | // Runes mode set via svelte config
54 | test('runes mode enabled with svelte config `compilerOptions.runes` set to `true`', async () => {
55 | const { markup } = preprocessMeltUI({
56 | svelteConfigPath: './tests/runes/svelte.config.enabled.js',
57 | });
58 | if (!markup) throw new Error('Should always exist');
59 |
60 | const processed = await markup({
61 | content: t.svelteConfigExplicitEnabled,
62 | });
63 |
64 | expect(processed?.code).toBe(t.svelteConfigExplicitEnabledExpected);
65 | });
66 |
67 | test('runes mode disabled with svelte config `compilerOptions.runes` set to `false`', async () => {
68 | const { markup } = preprocessMeltUI({
69 | svelteConfigPath: './tests/runes/svelte.config.disabled.js',
70 | });
71 | if (!markup) throw new Error('Should always exist');
72 |
73 | const processed = await markup({
74 | content: t.svelteConfigExplicitDisabled,
75 | });
76 |
77 | expect(processed?.code).toBe(t.svelteConfigExplicitDisabledExpected);
78 | });
79 |
80 | test('ignore svelte config', async () => {
81 | const { markup } = preprocessMeltUI({
82 | svelteConfigPath: false,
83 | });
84 | if (!markup) throw new Error('Should always exist');
85 |
86 | const processed = await markup({
87 | content: t.svelteConfigExplicitDisabled,
88 | });
89 |
90 | expect(processed?.code).toBe(t.svelteConfigExplicitDisabledExpected);
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/tests/runes/svelte.config.disabled.js:
--------------------------------------------------------------------------------
1 | export default {
2 | compilerOptions: {
3 | runes: false,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/tests/runes/svelte.config.enabled.js:
--------------------------------------------------------------------------------
1 | export default {
2 | compilerOptions: {
3 | runes: true,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/tests/simple_builder/index.svelte.ts:
--------------------------------------------------------------------------------
1 | export const simple = `
2 |
11 |
12 |
13 | `;
14 |
15 | export const simpleExpected = `
16 |
25 |
26 |
27 | `;
28 |
29 | export const aliasedExpression = `
30 |
49 |
50 |
51 |
52 | `;
53 |
54 | export const aliasedExpressionExpected = `
55 |
74 |
75 |
76 |
77 | `;
78 |
79 | export const aliasedMelt = `
80 |
89 |
90 |
91 |
92 | `;
93 |
94 | export const aliasedMeltExpected = `
95 |
104 |
105 |
106 |
107 | `;
108 |
--------------------------------------------------------------------------------
/tests/simple_builder/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { preprocessMeltUI } from '$pkg/index';
3 | import * as t from './index.svelte';
4 |
5 | describe('Simple Builder - Identifiers', () => {
6 | const { markup } = preprocessMeltUI({ svelteConfigPath: false });
7 | if (!markup) throw new Error('Should always exist');
8 |
9 | test('simple', async () => {
10 | const processed = await markup({
11 | content: t.simple,
12 | });
13 |
14 | expect(processed?.code).toBe(t.simpleExpected);
15 | });
16 |
17 | test('aliased expression', async () => {
18 | const processed = await markup({
19 | content: t.aliasedExpression,
20 | });
21 |
22 | expect(processed?.code).toBe(t.aliasedExpressionExpected);
23 | });
24 |
25 | test('aliased melt action', async () => {
26 | const { markup: aliasMarkup } = preprocessMeltUI({
27 | alias: ['melt', '_melt'],
28 | svelteConfigPath: false,
29 | });
30 | if (!aliasMarkup) throw new Error('Should always exist');
31 |
32 | const processed = await aliasMarkup({
33 | content: t.aliasedMelt,
34 | });
35 |
36 | expect(processed?.code).toBe(t.aliasedMeltExpected);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "checkJs": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "resolveJsonModule": true,
8 | "skipLibCheck": true,
9 | "sourceMap": true,
10 | "strict": true,
11 | "lib": ["ES2022"],
12 | "target": "ES2022",
13 | "module": "ES2022",
14 | "moduleResolution": "bundler",
15 | "paths": {
16 | "$pkg/*": ["./src/*"]
17 | }
18 | },
19 | "include": ["src", "tests"]
20 | }
21 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: ['src/index.ts'],
5 | outDir: 'dist',
6 | sourcemap: true,
7 | format: ['esm'],
8 | dts: true,
9 | clean: true,
10 | target: 'es2022',
11 | });
12 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | alias: {
6 | $pkg: new URL('./src/', import.meta.url).pathname,
7 | },
8 | },
9 | });
10 |
--------------------------------------------------------------------------------