Skip to content

add transforms API and migrate all addons#1001

Merged
jycouet merged 26 commits intomainfrom
feat/transforms-api
Mar 30, 2026
Merged

add transforms API and migrate all addons#1001
jycouet merged 26 commits intomainfrom
feat/transforms-api

Conversation

@jycouet
Copy link
Copy Markdown
Contributor

@jycouet jycouet commented Mar 21, 2026

Curried transforms API with injected utils

Transforms are now curried and inject relevant utilities into the callback. One import, no boilerplate.

import { transforms } from '@sveltejs/sv-utils';

// Script/JSON/CSS/HTML/text - curried, plugs directly into sv.file()
sv.file(file.viteConfig, transforms.script(({ ast, js }) => {
  js.vite.addPlugin(ast, { code: 'foo()' });
}));

// Svelte with script block guaranteed
sv.file(path, transforms.svelteScript({ language }, ({ ast, svelte, js }) => {
  js.imports.addNamed(ast.instance.content, { imports: ['foo'], from: 'foo' });
  svelte.addFragment(ast, '<Foo />');
}));

// Svelte without script (CSS/template only)
sv.file(path, transforms.svelte(({ ast, svelte }) => {
  svelte.addFragment(ast, '<style>...</style>');
}));

// Raw content still available
sv.file(path, (content) => {
  return content.replace('foo', 'bar');
});

What changed

  • transforms.* are curried: callback first, returns (content) => string
  • Utils (js, svelte, css, json, text, html) injected in callbacks - no separate imports needed
  • transforms.svelteScript({ language }, cb) ensures script block, ast.instance always non-null
  • transforms.svelte(cb) for template/style-only transforms
  • All addons migrated, docs updated

jycouet added 2 commits March 21, 2026 18:49
Typed, parser-aware `string -> string` functions that wrap
parse -> callback(ast/data) -> generateCode(). Includes transforms
for script, svelte, css, json, yaml, toml, html, and text.

The engine detects transforms via `isTransform()` and injects
workspace context (language) automatically.
Mechanical migration: every `sv.file()` callback that followed the
parse -> mutate -> generateCode pattern now uses the typed transform.

`parse` is retained only where transforms don't fit:
- prettier.ts: try/catch around JSON parsing
- sveltekit-adapter.ts: conditional parser (json vs toml) and
  standalone parse outside sv.file
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 21, 2026

🦋 Changeset detected

Latest commit: 17da24a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@sveltejs/sv-utils Patch
sv Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 21, 2026

Open in StackBlitz

npx https://pkg.pr.new/svelte-migrate@1001
npx https://pkg.pr.new/sv@1001
npx https://pkg.pr.new/@sveltejs/sv-utils@1001

commit: 17da24a

@jycouet jycouet mentioned this pull request Mar 21, 2026
12 tasks
@manuel3108
Copy link
Copy Markdown
Member

I thought I wrote that somewhere already, but I might be imagining.
Still not a huge fan of this to be honest. I think it makes the sv.file() api worse and less readable.

I would suggest something like this, because I do get what you want to abstract:

transforms.script(sv, filePath, () => {
  // some random code
}):

This would fully replace the sv.file call for all transforms that we have prepared and get rid of an indentation. And if a transform does not exist, you could still use sv.file

Alternatively you could also get me to vote for having sv.file or sv.file.raw and sv.file.script making the while experiencing more straightforward and less nested.

What do you think?

@jycouet
Copy link
Copy Markdown
Contributor Author

jycouet commented Mar 27, 2026

Thx for sharing, I'll play around with these apis 👍👍👍

@svelte-docs-bot
Copy link
Copy Markdown

@jycouet
Copy link
Copy Markdown
Contributor Author

jycouet commented Mar 28, 2026

Why I like D/ explicit content

  • Zero coupling — sv-utils is pure string → string, sv just calls edit(content)
  • Composable — mix transforms and raw string edits in one sv.file callback
  • No engine changes — no branding, no isTransform(), no context injection
  • language from run scope — addons already have it, no magic needed
  • Simpler internals — transforms are plain functions, testable standalone

Style convention

  • Multi-line transforms: (content) => { return transforms.X(content, ...); }
  • One-liners: (content) => transforms.X(content, ...)

@manuel3108 WDYT ? :D

@sacrosanctic
Copy link
Copy Markdown
Contributor

sacrosanctic commented Mar 28, 2026

Here are my off the wall ideas.

curried function

Alternative to option D, reads like option A. If transforms.svelte is curried...

type transforms = {
  // svelte: (content, ast) => ast
  svelte: (ast) => (content) => ast
}

you could do something like this.

sv.file(path, transforms.svelte((ast) => {
  svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
}));

but if you need content, it would look a bit funky

sv.file(path, content => transforms.svelte((ast) => {
    svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
  })(content);
);

two APIs

More or less what @manuel3108 said but keeping them similar looking for conhesiveness.

One for the common case, the other for a comprehensive case.

sv.file.svelte(path, (ast) => {...})

sv.file(path, (content) => {
  const step1 = transform.svelte(content, (ast) => {...})
  const step2 = transform.text(content, (ast) => {...})
  return step2
}
transforms.svelte(sv, path, (ast) => {...})
transforms.custom(sv, path, ...)

Might as well give me the util

I'm already calling transforms.svelte, but I still need to the util separately?

// original
import { transforms, svelte } from '@sveltejs/sv-utils';

sv.file(path, (content) => {
  return transforms.svelte(content, (ast) => {
    svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
  });
});
// proposal
-import { transforms, svelte } from '@sveltejs/sv-utils';
+import { transforms } from '@sveltejs/sv-utils';

sv.file(path, (content) => {
-  return transforms.svelte(content, (ast) => {
+  return transforms.svelte(content, (ast, svelte) => {
    svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
  });
});

some kind of injection

run: ({ isKit, cancel, sv, options, directory }) => {
if (!isKit) return cancel('SvelteKit is required');

const file = sv.load(transforms)

// Add "Hello [who]!" to the root page
file.svelte(path, (content) => {
  return transforms.svelte(content, (ast) => {...});
});

@manuel3108
Copy link
Copy Markdown
Member

@jycouet You wrote in the PR description

Puts transform logic in sv, can't version independently

But is that actually true? Because each addon & community addon should receive sv: SvApi either way right? So you are never going to be able to to version them independently. Otherwise we should probably also rethink all of the other sv methods for similar problems.

If I would need to make the decision right now only based on my personal preferences and dx, I would combine my suggestion with one from @sacrosanctic

sv.file.svelte(path, ({ ast, utils }) => {
  utils.addFragment(ast, `<p>Hello ${options.who}!</p>`)
})

this

  • avoids adding more nesting levels
  • is a really direct and clean way of declaring your intentions
  • let's us remove 2-4 lines of code from every file manipulation
  • let's us get rid of a bunch of imports that have been bothering me for a long time

potential cons:

  • naming the utils the same in every method might be cumbersome. As you would either need to hover utils or check the file type you are currently transforming (sv.file.svelte())

But given my recent activity in this project im totally open if you guys decide on continue with another solution. Just adding my two cents here :D

@jycouet
Copy link
Copy Markdown
Contributor Author

jycouet commented Mar 28, 2026

But is that actually true? Because each addon & community addon should receive sv: SvApi either way right? So you are never going to be able to to version them independently. Otherwise we should probably also rethink all of the other sv methods for similar problems.

This is the most import IMO ^^.
So, sv has sv-utils for it's official add-ons. But the idea is that community add-ons bundles everything, so their transforms are internal (and here can can support any version)
I have this in my mind:
image

official add-ons are an "exception" of sv... (I don't know how to say it)

I wanted to write this here quickly ^^, will now look closely all your option @sacrosanctic & @manuel3108 :D

@jycouet
Copy link
Copy Markdown
Contributor Author

jycouet commented Mar 28, 2026

@manuel3108 @sacrosanctic , another round of comments with this style?

let's call it option J (because why not ^^)

sv.file(path, transforms.svelte(({ast, content, svelte, js, ... }) => {                                                                                                                      
  svelte.addFragment(ast, '<p>Hello!</p>');                                                                                                                     
}));   

@sacrosanctic sacrosanctic mentioned this pull request Mar 30, 2026
@sacrosanctic
Copy link
Copy Markdown
Contributor

sacrosanctic commented Mar 30, 2026

  • should we use path.join?
  • rename onParseError -> onError
  • should we use transform.text, if we're not text.upsert
  • a helper to abstract (props) => (content:string) => string
    to make typing the curried fn easier

@jycouet jycouet merged commit 504bce9 into main Mar 30, 2026
8 checks passed
@jycouet jycouet deleted the feat/transforms-api branch March 30, 2026 18:19
@github-actions github-actions bot mentioned this pull request Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants