-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
-
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,
- Any imports with the
?client
suffix will be stripped of the final bundled code as if the import never existed. - 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
- Imports being removed requires users to wrap
if (import.env.meta.SSR)
to avoid runtime unreferenced/undefined variables, which can sometimes be overlooked. - Import with
?client
or?server
suffix loses intellisense (A custom typescript plugin can fix this)
Alternative
- @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. - [Different solution] A suffix like
?pure
to aggressively treeshake a bundle without accounting for side-effects, since side-effects are the issuewindow is not defined
errors pop up in the first place when importing browser-only module, for example.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 30
Replies: 12 comments 16 replies
-
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. 💪
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
@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=
)
Beta Was this translation helpful? Give feedback.
All reactions
-
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 :)
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 4
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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 }
Beta Was this translation helpful? Give feedback.
All reactions
-
👎 1
-
throws error: An import declaration can only be used at the top level of a namespace or module.ts(1232)
Beta Was this translation helpful? Give feedback.
All reactions
-
@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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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', },
Beta Was this translation helpful? Give feedback.
All reactions
-
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); } }, } }
Beta Was this translation helpful? Give feedback.
All reactions
-
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 🤔
Beta Was this translation helpful? Give feedback.
All reactions
-
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', } },
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
Because Vite and TypeScript now supports imports
field, we can use that.
- Add the
imports
field below,
"imports": { "#iso/*.ts": { "types": "./src/*.client.ts", "browser": "./src/*.client.ts", "node": "./src/*.server.ts" } }
- Make sure you use TypeScript 5.0+ and Vite 4.2+.
- Also make sure the following settings are set in
tsconfig.json
"moduleResolution": "bundler"
"allowImportingTsExtensions": true
- Create
foo.server.ts
andfoo.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.
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 4
-
Sorry for the question, but what is the file you are asking me to add "imports" to? vite config? package.json? tsconfig.json?
Beta Was this translation helpful? Give feedback.
All reactions
-
package.json
is.
https://nodejs.org/api/packages.html#subpath-imports
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
this is a very neat idea i hope it gets implemented
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
😕 1
-
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
Beta Was this translation helpful? Give feedback.
All reactions
-
Maybe #16089 and resolve.conditions
will help here later on.
Beta Was this translation helpful? Give feedback.
All reactions
-
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
Beta Was this translation helpful? Give feedback.
All reactions
-
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()
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 5
-
to not lose intellisense we can use with
instead: import { lib } from "lib" with { only: "server" }
Beta Was this translation helpful? Give feedback.