Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Environment-specific imports #4172

bluwy started this conversation in Ideas
Jul 8, 2021 · 12 comments · 16 replies
Discussion options

Summary

Allow tagging imports as environment-specific and only import them when the environment matches.

Basic example

// Only loads if not ssr
import { foo } from '$lib/client-module?client';
// Only loads in ssr
import { hey } from '$lib/server-module?server';

Motivation

In most SSR frameworks, a route view is usually executed in the browser and node context. This causes browser-only modules and node-only modules to fail if they're executed in the opposite intended context. The current pattern is to use dynamic imports to load these modules conditionally, which generates some amount of boilerplate. For example:

let foo
if (!import.meta.env.SSR) {
 foo = await import('foo-browser-library') // foo-browser-library could call DOM APIs when importing
}
// now foo doesn't cause errors in the node context

Detailed design

Introduce new ?client and ?server suffix. An experimental plugin is created and tested in a SvelteKit app.

For example, in SSR mode,

  1. Any imports with the ?client suffix will be stripped of the final bundled code as if the import never existed.
  2. Any imports with the ?server suffix, the import id will be stripped of the ?server suffix as if a normal import is written. e.g. "./foo?server" => "./foo"

The opposite would be true for non-SSR mode.

Drawbacks

  1. Imports being removed requires users to wrap if (import.env.meta.SSR) to avoid runtime unreferenced/undefined variables, which can sometimes be overlooked.
  2. Import with ?client or ?server suffix loses intellisense (A custom typescript plugin can fix this)

Alternative

  1. @patak-js suggested on discord of using ?only=client and ?only=server, which could future-proof the API if more environments are needed to be supported.
  2. [Different solution] A suffix like ?pure to aggressively treeshake a bundle without accounting for side-effects, since side-effects are the issue window is not defined errors pop up in the first place when importing browser-only module, for example.
You must be logged in to vote

Replies: 12 comments 16 replies

Comment options

That's a very neat feature I would like to see!
I'm using vite-ssr withfirebase (client context) + firebase-admin (server context) and the await import() workaround makes the structure of the code a bit unclear. It would be really cool to have the ?only= suffix. 💪

You must be logged in to vote
0 replies
Comment options

@bluwy we discussed this idea with the team and we think it is an interesting approach. As you already showcased in your plugin this can be implemented for the moment in userland without a commitment from core, so it is better to let people explore this path using your plugin.

There is a chance that we may not end up adding new URL query suffixes in core, if build tools start to embrace the syntax from import assertions. So it is good to have more time before adding something like this in core.

Regarding the API, you should go with the design that makes more sense for you. My personal opinion is to use the direct ?server and ?client form as you have originally proposed (since I don't see a good reason why that names will conflict more than ?only=)

You must be logged in to vote
2 replies
Comment options

bluwy Jul 11, 2021
Maintainer Author

Thanks! I'll try to get the plugin polished up and publish on npm at the meantime. Will update the discussion and see how the community adopts it :)

Comment options

bluwy Jul 11, 2021
Maintainer Author

Comment options

bluwy
Jul 22, 2021
Maintainer Author

If anyone's using Svelte or Vue, the custom typescript plugin from vite-plugin-iso-import to resolve intellisense for ?client and ?server won't work for Svelte or Vue files, since both vscode extension uses it's own internal Typescript language service, which doesn't load plugins. Normal ts or js files would still work.

I have not a found a way to add support to load plugins for those extensions, but based on my recent findings, it may even be impossible unless we spawn a new Project based on tsserver's implementation. vscode-ng-language-service seems to successfully implement that strategy, but it's a lot of code with minimal gain.

If any Typescript experts are able to help out, that would be greatly appreciated! Otherwise, import assertions may be the way to go that could save us from the shambles. Or just manual module declarations.

You must be logged in to vote
0 replies
Comment options

One other use for environment-specific imports is for dev-only builds. e.g. having the import dependent on import.meta.env.DEV

if (import.meta.env.DEV) {
 import mod from 'some-dev-only-module';
 // dev-only code
}
You must be logged in to vote
2 replies
Comment options

throws error: An import declaration can only be used at the top level of a namespace or module.ts(1232)

Comment options

@luckylooke the import() syntax can be used for imports outside the top level. However, an import that won't resolve in a specific environment will still throw Error: Failed to resolve import ..., even when the condition the import is nested within isn't true. I'm guessing this is due to some hoisting function in the bundler, but perhaps someone else can provide greater insight.

Comment options

This would also help me with something I am trying to achieve. I have 2 api client implementations, one that is mocked out for development, and the other that actually fetches from the server for testing against the server and production. I would like to exclude the mock code from my final bundle, and I think this would allow me to do so by conditionally loading depending on an environment variable.

You must be logged in to vote
0 replies
Comment options

This works:

// only enable solid-devtools during dev phase
if (import.meta.env.DEV) {
 await import('solid-devtools');
}

with vite config:

 optimizeDeps: {
 esbuildOptions: {
 target: 'esnext',
 },
 },
 build: {
 target: 'es2020',
 },
You must be logged in to vote
0 replies
Comment options

This is my version of @bluwy's vite-plugin-iso-import. It doesn't completely remove the import, but it is much simpler and it still works with static imports, which is needed for svelte:

const serverRE = /(.*[?&])ssr(&.*|)$/
export default function devPlugin() {
 return {
 name: 'ssr-only-import',
 enforce: 'pre',
 async resolveId(id, importer, options) {
 let m = serverRE.exec(id);
 if(! m)
 return null;
 
 if(! options.ssr)
 return {
 id: 'data:text/javascript,export default null;',
 external: true,
 }
 return this.resolve(
 m[1] + m[2].substr(1),
 importer,
 { skipSelf: true , ...options},
 );
 },
 async load(id, options) {
 if (id.startsWith('data:text/javascript,')) {
 return id.substr(21);
 }
 },
 }
}
You must be logged in to vote
3 replies
Comment options

bluwy Jan 25, 2023
Maintainer Author

Would this work for named imports? Looks like it only shims the default import, and I don't think there's a way to shim all the imports in ESM 🤔

Comment options

It cannot be done with the dev server AFAIK, but it can be done with rollup in production:

 load(id) {
 if(id === '0円NULL')
 return {
 code: 'export const __synthetic = {};',
 syntheticNamedExports: '__synthetic',
 }
 },
Comment options

Looking at the importAnalysisPlugin, if vite wanted to support environment-specific imports, it does have the named imports available, which it could use to synthesize a mock module. So it could support syntheticNamedExports during dev.

Comment options

Because Vite and TypeScript now supports imports field, we can use that.

  1. Add the imports field below,
 "imports": {
 "#iso/*.ts": {
 "types": "./src/*.client.ts",
 "browser": "./src/*.client.ts",
 "node": "./src/*.server.ts"
 }
 }
  1. Make sure you use TypeScript 5.0+ and Vite 4.2+.
  2. Also make sure the following settings are set in tsconfig.json
    • "moduleResolution": "bundler"
    • "allowImportingTsExtensions": true
  3. Create foo.server.ts and foo.client.ts in your directory and import it as #iso/foo.ts

Stackblitz demo (It seems TS 5.0 doesn't work in stackblitz yet)

Note that the types will refer foo.client.ts. So relying on a function exported from foo.client.ts but not from foo.server.ts will lead to runtime errors.

You must be logged in to vote
5 replies
Comment options

Sorry for the question, but what is the file you are asking me to add "imports" to? vite config? package.json? tsconfig.json?

Comment options

Comment options

Where can I find information about Vite's support of package.json subpath imports? I can't find anything by searching the docs site, nor issues or discussions, this post was the closest I've come so far.

For example, I need to know if extensions are required. TypeScript doesn't require them, so I'm curious if Vite is opting to be closer to node.js in this regard.

Comment options

The related issues and PRs are #7385, #7770.

What do you mean by "TypeScript doesn't require them"? TypeScript has extension substitution feature (Vite doesn't support this one correctly #8993), but I don't know about that one.

Comment options

Thanks for the response, @sapphi-red. After a bit more experimenting, I found this to be an issue when using an array, like:

"#components/*": ["./src/components/*.ts", "./src/components/*.tsx"]

TypeScript will look for any module matching any of those extensions, but Vite (& rollup) only look at the first one.

I've opened an issue with a reproduction here: #16153

Edit: turns out that TypeScript's behavior is considered a bug, and Vite is matching Node's behavior.

Comment options

this is a very neat idea i hope it gets implemented

You must be logged in to vote
1 reply
Comment options

bluwy Jul 23, 2023
Maintainer Author

Given the duration of the proposal, I don't think it'll be built-in anytime soon 😅 There's still the caveats of IDE support and client/server undefined gotchas (if not careful), that doesn't fit being built-in. But I also think sapphi's comment brings the best of both worlds that Vite supports today.

Comment options

We should consider more than just conditional imports on the client and server.

It will also have other conditions, so this should be a general function.

For example, I am using Electron to develop an application, which is also compatible with the web version.

I want the web version import foo.browser.js and Electron version import foo.electron.js and they both have the same exports.

It's easy for do this with CJS

const foo = process.env.IS_ELECTRON ? require('./foo.electron.js') : require('./foo.browser.js')

Proposal

define the condition in vite.config.js

{
 ...
 condition: {
 browser: true, // control by user
 electron: false // control by user
 }
}

use condition with import

import foo from './foo?browser=./foo.browser.js&electron=./foo.electron.js'

if condition not match, then import './foo' it'self

You must be logged in to vote
1 reply
Comment options

Maybe #16089 and resolve.conditions will help here later on.

Comment options

Or we can do it more simply.

Import two packages, but which package is empty will be determined based on the environment

eg.

import fooClient from './foo.client.js'
import fooServer from './foo.server.js'
const foo = import.meta.env.IS_CLIENT ? fooClient : fooServer

If running on client, It will be transform to

import fooClient from './foo.client.js'
const fooServer = {}
const foo = import.meta.env.IS_CLIENT ? fooClient : fooServer

otherwise

const fooClient = {}
import fooServer from './foo.server.js'
const foo = import.meta.env.IS_CLIENT ? fooClient : fooServer

In this way it solves the problem of typescript very well

You must be logged in to vote
2 replies
Comment options

I have verified the feasibility and implement, which will be publish as soon as possible

import * as testBrowser from './test.browser'
import * as testElectron from './test.electron'
const tester = import.meta.env.IS_BROWSER ? testBrowser : testElectron
tester.test()
截屏2024年04月09日 10 55 38
Comment options

Comment options

to not lose intellisense we can use with instead: import { lib } from "lib" with { only: "server" }

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /