Skip to content

add transforms API and migrate all addons#1001

Open
jycouet wants to merge 18 commits intomainfrom
feat/transforms-api
Open

add transforms API and migrate all addons#1001
jycouet wants to merge 18 commits intomainfrom
feat/transforms-api

Conversation

@jycouet
Copy link
Copy Markdown
Contributor

@jycouet jycouet commented Mar 21, 2026

transforms API — Options explored

Problem

Every addon repeats the same boilerplate: parse → manipulate AST → return generateCode().

Constraint

sv (engine) and sv-utils must stay independent; separate packages, separate versioning.

Options

Option Shape Pros Cons
A/ Branded functions sv.file(path, transforms.script((ast) => {...})) Flattest DX, addon never sees content Branding machinery, isTransform() in engine, engine needs to know sv-utils types
B/ Methods on sv.file sv.file.script(path, (ast) => {...}) Very clean Puts transform logic in sv, can't version independently
C/ Pass sv to transforms transforms.script(sv, path, (ast) => {...}) Flat, no nesting sv-utils needs to know sv.file() contract
D/ Explicit content sv.file(path, (content) => { return transforms.script(content, (ast) => {...}); }) Zero coupling, composable, mix transforms + raw edits Slightly more verbose

Prep for community add-on improvements.

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

⚠️ No Changeset found

Latest commit: 2593cb9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a 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: 2593cb9

@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>');                                                                                                                     
}));   

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