├── .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 | --------------------------------------------------------------------------------