Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally add compiled/bundled version of library when using the package command #2572

Open
dummdidumm opened this issue Oct 8, 2021 · 13 comments
Labels
feature request New feature or request p2-nice-to-have SvelteKit cannot be used by a small number of people, quality of life improvements, etc. pkg:svelte-package Issues related to svelte-package
Milestone

Comments

@dummdidumm
Copy link
Member

dummdidumm commented Oct 8, 2021

Describe the problem

Right now sveltekit package only preprocesses TS/Svelte files to JS/"vanilla Svelte" files and exports these in their raw form. This is good when you want to use the library within another Svelte project, which is probably the 80% use case.
However, if you want to use the component library as-is, for example on another project not using Svelte, you need to add this functionality on your own currently.

Describe the proposed solution

Add an option to also provide a ESM-style bundled version of the component library. Provide that as an export for the "main" or "module" export. What I'm not sure about here is how to then differentiate between the compiled and raw version when generating the "exports" field. Should we double down on the "special svelte field" and do

"exports": {
  ".": {
     "import": "...",
     "svelte": "..."
  }
}

?

If so, which tooling packages need updating for this to work correctly?

Another idea: Provide the bundle inside a special folder called standalone, so people using the bundle would do import { SomeComponent } from 'some-library/standalone';. This would make it easier for bundlers/tooling because they wouldn't have to know about the "svelte" field. I think this is my prefered solution.

Alternatives considered

Some docs on how to achieve this

Importance

would make my life easier

Additional Information

Continuation of #518

@dummdidumm dummdidumm added pkg:svelte-package Issues related to svelte-package p2-nice-to-have SvelteKit cannot be used by a small number of people, quality of life improvements, etc. labels Oct 8, 2021
@dummdidumm dummdidumm added the feature request New feature or request label Oct 21, 2021
@lokimckay
Copy link

For future readers who want to generate separate UMD/IIFE/ES outputs for each component in your library, here is a minimal example of the setup I chose
(uses Svelte + Rollup but not Svelte Kit)

https://github.com/lokimckay-references/svelte-web-component-library-example

@ignatiusmb
Copy link
Member

There would be cases where there's only one index.js in the root of some-library that re-exports all the Svelte components, that would be the most straightforward one and package can just point that file and leave the rest to the bundler. But, it gets tricky when there's only one or multiple .svelte files without an entry file (and maybe the author only wants to bundle some of them), or when there's some-library along with its modules like some-library/core, some-library/icons, some-library/utils, etc.

I can't think of an easy way to scan and compile the Svelte files as there's a whole lot of possibilities on how the author wants the compiled version to look like. Rather than guessing and making a lot of assumptions, I think it would be better if we provide some options for the author to fill in themselves, that way we also don't need to reserve /standalone in case the library itself has its own standalone module

With the use cases above, the options that currently comes to mind would be something like

interface Config {
  kit?: {
    package?: {
      /** input path relative to 'src/lib' and output path relative to 'package' or whatever dir is */
      bundle?: Array<[input: string, output: string]>; // or maybe make it an object with key as input and value as output?

      /** will enable customElement option and use this string to register the element name */
      customElementTag?: string; // or maybe just 'tag'?

      // and other options, theoretically speaking...
      // namespace?: 'html' | 'mathml' | 'svg' | 'foreign';
    }
  }
}

Everything else should be left as default values, and the example config would look like

export default {
  kit: {
    package: {
      bundle: [
        ['index.js', 'standalone/index.js'],
        ['core/index.js', 'standalone/core.js'],
        ['icons/index.js', 'standalone/icons.js'],
        ['utils/index.js', 'standalone/utils.js'],
      ],

      // or as an object, but then one input can't generate two different outputs
      // not sure why anyone wants that, but it's something to consider
      bundle: {
        'index.js': 'standalone/index.js',
        'core/index.js': 'standalone/core.js',
        'icons/index.js': 'standalone/icons.js',
        'utils/index.js': 'standalone/utils.js',
      },

      /** cannot be used when bundle has multiple inputs? */
      // customElementTag?: 'stacked-up';
    }
  }
}

@NazimHAli
Copy link

For future readers who want to generate separate UMD/IIFE/ES outputs for each component in your library, here is a minimal example of the setup I chose (uses Svelte + Rollup but not Svelte Kit)

https://github.com/lokimckay-references/svelte-web-component-library-example

Thanks for sharing, going to try this out. One of my projects stalled after realizing that bundling isn't supported yet.

@pngwn
Copy link
Member

pngwn commented Jan 14, 2022

Would a package entry that takes an object or array of objects be more future proof here:

export default {
  kit: {
    package: {
      format: 'esm',
      entry: "index.js",
      output: "standalone.js" // or ["standalone.js", "standalone_two.js"]
      customElementTag?: 'stacked-up';
    }
  }
}

and

export default {
  kit: {
    package: [
      {
        format: 'esm',
        entry: "index.js",
        output: "esm/index.js" // or ["standalone.js", "standalone_two.js"]
        customElementTag?: 'stacked-up';
      },
      {
        format: 'umd',
        entry: "index.js",
        output: "umd/index.js" // or ["standalone.js", "standalone_two.js"]
        customElementTag?: 'stacked-up';
      },
      {
        format: 'esm',
        entry: "utils/index.js",
        output: "utils/index.js" // or ["standalone.js", "standalone_two.js"]
      }
    ]
  }
}

Actual options TBD, but this feels like the safest way to support multiple entry/output files/ folders with potentially custom config for each bundle'

@tytusplanck
Copy link

tytusplanck commented Mar 31, 2022

Out of curiosity would building a Svelte package via kit instead of vanilla Svelte + Rollup technically be more performant (speed and bundle size) since it uses Vite under the covers?

@Rich-Harris Rich-Harris added this to the post-1.0 milestone Apr 26, 2022
@oneezy
Copy link

oneezy commented Dec 16, 2022

Would love to see this come to fruition. Currently creating a Sveltekit/ Tailwind/ Histoire ui lib and it's ready for npm. Went to publish and realized I needed to somehow process the Tailwind classes which sent me down a very long rabbit hole that eventually led here (and about 10 other places). Seems like there's some ways with svelte-preprocess but still researching the best way.

@dominikg
Copy link
Member

@oneezy there is a way that could work using an experimental feature of vite-plugin-svelte and it may work better in the future.

You can use a vite devserver and ssrLoadModule with custom queries to get preprocessed output and write that to disk if you want. If you combine that with vitePreprocess in your svelte config it should work out the tailwind classes.

https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts#L125

but you'd have to distribute a css file alongside your lib so it actually works then. Generating that is not going to happen with this, you'd have to use a regular vite build for it.

The above could be turned into an option to vite-plugin-svelte to also emit preprocessed output during build, which svelte package could then use more easily. I mentioned it briefly at the end of our release party stream, there will be improvements around svelte package.

@oneezy
Copy link

oneezy commented Dec 16, 2022

Yo @dominikg ! You're exactly the man I wanted to speak to about this. I watched the Sveltekit 1.0.0 release party stream and heard the comments you made around svelte-package so figured you had some deeper insight. I'll try your suggestions mentioned above and report back.

As a side note, I really like the direction you guys have written up here. Your proposal @pngwn feels pretty clean and would love to see something like this in the near future.

@Garito
Copy link

Garito commented Jul 13, 2023

Hi!
I'm super interested on this feature
Any advance already?
Thanks

@Metehan-Altuntekin
Copy link

This is already possible with using and configuring Rollup as @lokimckay suggested and it works well. An adapter to handle this could be a nice idea

@MrWaip
Copy link

MrWaip commented Mar 22, 2024

It is possible to assemble a .svelte component first as a client-side component, and then as an SSR (server-side rendering) component. Then create an index.js file in which, depending on the SSR variable, export either the SSR bundle or the client bundle.

import IconClient from "./components/IcLAboutUncirculatedDoc.client";
import IconSsr from "./components/IcLAboutUncirculatedDoc.ssr";

const SSR = import.meta.env.SSR;

let module;

if (SSR) {
  module = IconSsr;
} else {
  module = IconClient;
}

export { module as IcLAboutUncirculatedDoc };

In this case, such a component can be rendered as usual. And it will work for both SSR and the browser. And it will be properly tree-shaken during the build.

<script>
import { IcLAboutUncirculatedDoc } from './icons'
</script>

<IcLAboutUncirculatedDoc />

But the solution turns out to be complex.

await build({
  configFile: false,
  plugins: [
    svelte({
      compilerOptions: {
        hydratable: true,
        // dev: true,
      },
    }),
  ],
  build: {
    minify: true,
    lib: {
      entry: {
        "icons/index.client": "src/icons/index.ts",
      },
      formats: ["es"],
      name: "lib",
    },
    rollupOptions: {
      external: ["svelte/internal"],
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
        manualChunks: (id, { getModuleInfo }) => {
          if (id.includes("/icons/components/")) {
            let [, name] = id.split("/icons/components/");

            name = name.split("/").at(-1);

            name = name.replace(".svelte", ".client");

            return "icons/components/" + name;
          }
        },
      },
    },
  },
});

await build({
  configFile: false,
  plugins: [
    svelte({
      compilerOptions: {
        hydratable: true,
        // dev: true,
      },
    }),
  ],
  build: {
    emptyOutDir: false,
    minify: true,
    ssr: true,
    lib: {
      entry: {
        "icons/index.ssr": "src/icons/index.ts",
      },
      formats: ["es"],
      name: "lib",
    },
    rollupOptions: {
      external: ["svelte/internal"],
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
        manualChunks: (id, { getModuleInfo }) => {
          if (id.includes("/icons/components/")) {
            let [, name] = id.split("/icons/components/");

            name = name.split("/").at(-1);

            name = name.replace(".svelte", ".ssr");

            return "icons/components/" + name;
          }
        },
      },
    },
  },
});

@ryanbas21
Copy link

It is possible to assemble a .svelte component first as a client-side component, and then as an SSR (server-side rendering) component. Then create an index.js file in which, depending on the SSR variable, export either the SSR bundle or the client bundle.

import IconClient from "./components/IcLAboutUncirculatedDoc.client";
import IconSsr from "./components/IcLAboutUncirculatedDoc.ssr";

const SSR = import.meta.env.SSR;

let module;

if (SSR) {
  module = IconSsr;
} else {
  module = IconClient;
}

export { module as IcLAboutUncirculatedDoc };

In this case, such a component can be rendered as usual. And it will work for both SSR and the browser. And it will be properly tree-shaken during the build.

<script>
import { IcLAboutUncirculatedDoc } from './icons'
</script>

<IcLAboutUncirculatedDoc />

But the solution turns out to be complex.

await build({
  configFile: false,
  plugins: [
    svelte({
      compilerOptions: {
        hydratable: true,
        // dev: true,
      },
    }),
  ],
  build: {
    minify: true,
    lib: {
      entry: {
        "icons/index.client": "src/icons/index.ts",
      },
      formats: ["es"],
      name: "lib",
    },
    rollupOptions: {
      external: ["svelte/internal"],
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
        manualChunks: (id, { getModuleInfo }) => {
          if (id.includes("/icons/components/")) {
            let [, name] = id.split("/icons/components/");

            name = name.split("/").at(-1);

            name = name.replace(".svelte", ".client");

            return "icons/components/" + name;
          }
        },
      },
    },
  },
});

await build({
  configFile: false,
  plugins: [
    svelte({
      compilerOptions: {
        hydratable: true,
        // dev: true,
      },
    }),
  ],
  build: {
    emptyOutDir: false,
    minify: true,
    ssr: true,
    lib: {
      entry: {
        "icons/index.ssr": "src/icons/index.ts",
      },
      formats: ["es"],
      name: "lib",
    },
    rollupOptions: {
      external: ["svelte/internal"],
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
        manualChunks: (id, { getModuleInfo }) => {
          if (id.includes("/icons/components/")) {
            let [, name] = id.split("/icons/components/");

            name = name.split("/").at(-1);

            name = name.replace(".svelte", ".ssr");

            return "icons/components/" + name;
          }
        },
      },
    },
  },
});

But if i understand correctly - this would still need to be used within a svelte app, correct? Or is this solution somehow framework agnostic?

@MrWaip
Copy link

MrWaip commented Apr 24, 2024

this would still need to be used within a svelte app, correct

Not necessary. The compiled Svelte component can be used programmatically, framework agnostic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request p2-nice-to-have SvelteKit cannot be used by a small number of people, quality of life improvements, etc. pkg:svelte-package Issues related to svelte-package
Projects
None yet
Development

No branches or pull requests