-
Notifications
You must be signed in to change notification settings - Fork 151
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
TypeScript: Better preprocessing to make importsNotUsedAsValues unnecessary #318
Comments
I also prefer solution 1.
Moving ts to the markup step would be not only a big endeavour, but we would also need a way to know if we're going to deal with TS or not, complicating things even more. Edit: That won't work. I forgot that Svelte runs all markup preprocessors first 😭 |
What if we use this knowledge and tell people if they chain multiple preprocessors and they want the better DX they need to add Solution 2 would not work I think, I thought about this some more and there could be the situation where an import is only used as a type in the script content but used as a value within the template, marking it a false positive for removal. So only a revival of #164 would work in all cases, which would mean slower compilation. |
Hello, I'm investigating this issue because it's a real problem on big svelte/Typescript projects. Maybe I can help on the implementation. I did some tests on my local installation:
My preference goes to solution 2 if it's not too complex. The simplest way (but not the cleanest) is solution 1. Do you think it's possible to add this feature in svelte compiler? The idea of appending variables used in markup seems the only viable solution to let Typescript knows which imports it can safely remove. The problem is that it forces to compile ts and svelte twice. It could be problematic on large projects. Is there a safe way to extract js expressions from markup without full svelte compile (maybe ast parser)? If possible, it could allow us to avoid the double compilation and directly append expressions at the end of the script. |
Any updates? |
In my previous message, I ask two questions:
If we can address this two questions, we can build a reasonable roadmap:
Note: I will try to investigate on question 1 and make a PR on svelte repository. |
I'm fairly certain question 1 can be answered with "yes" |
I'm too, that's why I will try to open a PR on this. |
I created a PR on the This PR will allow us to avoid introducing the breaking change of making the I'm still looking for a safe way to extract JS expressions from markup. |
I just found that if we remove the If think that we will need to walk the AST and generate a javascript/typescript equivalent of svelte expressions. eg: {#each list as item (item.id)}
<p>{item.title}</p>
{/each} should become: for (const item of list) {
let var0 = item.title;
} What do you think? I think that this solution allows to avoid the double compilation and allow to safely extract JS expressions from svelte. |
Summary of the discussion from the svelte PR to avoid polluting the bad repo:
Update: The above argument is erroneous as explained by @dummdidumm. |
Regarding the expensiveness: I don't think it is that expensive since it's a per-file-compilation, the Svelte compiler does not need to look at other files to compile the given file. Moreover, if you add
|
Thank you for this clarification. I tried running the svelte compiler on a component with stripped The problem is that it doesn't find the variables when there is no script. So it seems that to get
Maybe I'm wrong and I over-estimate the compilers process but it still seems very expensive to me on large projects:
On my (very simple) component, it takes 75ms to compile typescript (using svelte-preprocess) and 50ms to compile svelte. So if we consider that it takes the same time for the second phase, compiling this component will take 250ms to build. On a medium/large-size project, we quickly have more than 100 components (and many of them are more complex than the one I'm testing above). If we consider that all comonents are taking the same time to compile as above, building a 100 components project would take 25sec instead of 12.5sec. Here is my test: const svelte = require("svelte");
const fs = require("fs");
const component_path = "path/to/Component.svelte";
const component_source = fs.readFileSync(component_path, "utf8");
const markup = component_source
.replace(/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi, "")
.replace(/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi, "");
const result = svelte.compile(markup, { generate: false });
console.log(result.vars); Am I wrong on my test or my estimations? |
I tried to make an implementation using the svelte AST parser: repo here. It seems to work well, imports are correctly removed and it pass all existing tests (and new ones). This implementation does the following:
It seems to be a reliable implementation. However it presents some drawbacks:
Also, this is experimental and it misses some things:
@dummdidumm, It could be interesting to implement the process you suggested to compare and see if the cost of performance is worth the cost of maintenance. If you want to test it on your projects, you need to link both
Can you please give me your reviews and feedbacks so I can see if it's interesting to continue this way? |
why so complicated? ; )
see microsoft/TypeScript#4717 (comment)
microsoft/TypeScript#4717 (comment) provides a proof-of-concept preprocess-ts.json (used in webpack.config.js) in ts.transpileModule isolatedModules is always true... but this is still a workaround there is tsconfig isolatedModules (issue) which should keep unused imports
using tsc for single file compilation:
microsoft/TypeScript#5858 (comment) says
but we can create a new project with only one file, no? # bash pseudo code
mkdir /tmp/tsc-onefile
cp some-file.ts /tmp/tsc-onefile/index.ts
cp some-tsconfig.json /tmp/tsc-onefile/tsconfig.json
( cd /tmp/tsc-onefile/; tsc ) |
|
@milahu, the solution above is too complex I agree. We switched to the solution explained in PR #342 which uses As @dummdidumm explained, the current transformer is creating wrong build output (types are kept) and TypeScript cannot determine if an import is a value or a type in That's why, the best solution we found was to inject vars from template in the compiler so it could determine if imports are used as values or as types and strip them accordingly. |
thanks for clarify im still looking for a solution that avoids double-parsing the svelte markup ...
make typescript compile interfaces to "dummy exports"? (also for module.exports = {
MyInterface: null,
}; also seen here. (could this cause identifier collisions?)
i guess you mean https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exports
but as babel/babel#8361 (comment) says
|
This is a no-go for me as it means messing with the user's code and not just transpiling it.
That's why we need We've been through all this thinking already, I don't see any other way than double compile, and it's not the end of the world. The first step hardly does a real compilation, it just walks the AST and extracts relevant info for the preprocessing, with |
edit: type imports are removed -> no need for dummy exports edit 2: something is wrong with the dummy exports: are they a thought crime?
sounds like a moral argument, since technically,
can be answered with "no". in typescript, an identifier is either a type, or a value looks like adding such a feature to typescript would make this use case much easier new problem: we must process the imports to remove
|
Your code snippets are missing the actual problem: consider you do |
sounds like we need a new ts compiler option, like ts does know the difference between values and types/interfaces, and value IDs must not collide with type IDs one edge-case i can think of is |
This won't work either, because if TS never removes anything from imports, then you need to tediously separate your interface imports from your value imports, which exactly the situation we have now and which is not nice DX. TS will merge All these steps of thinking you go through right now we've already been through, trust me, the best way is to double-compile + append the variables referenced in the template before compilation. |
@milahu Also, the problem with the example you showed above is that you are in a context where TypeScript knows the entire file. However, in Svelte, TypeScript does not know the content of the template. This leads to unexpected behaviors where TypeScript cannot determine what is really used in a Svelte file. As mentionned by @dummdidumm, we already tried a lot of different possibilities but all of them have a lot of drawbacks (in terms of maintence, performance, complexity or responsability). I was also against using the svelte compilation but it's the safest way to handle all cases and to ensure that it will not break in the future. |
pardon my stubbornness, but, as i said
so ts should be able to remove all type imports but i assume that adding this feature to typescript |
That's not true in Ref:
In
Here again, this compiler option could not work in |
yes, i phrased that too simple. there are three cases: 1. an import is used as type ( to solve case 3, we need "dummy exports": export type SomeType = number;
// ... is transpiled to ...
export const SomeType = null; |
This could work but this feature should be added on Typescript side (if I'm correct). If Typescript allows this kind of transpilation, we would be able to remove the double compilation (which is not really a double compilation since in preprocessor, it only parse and walk the AST). The drawback here is that, in this case, we won't remove unused imports which is (imho) a great feature of Typescript. Update: Instead of waiting Typescript to be enhanced, I suggest that we implement the solution in #342. I thought that it will drastically impact performances but in fact the impact is acceptable considering the gain in DX and the minimal impact on maintenance. If we want to mitigate the performance impact, we could use Of course, this should be an option so the developer is able to select his favorite transpiler. What do you think? |
dang, you found the weak spot : / tree-shaking should solve both problems, but only in production mode development mode could work with fault-tolerant dynamic imports var importedModule = await (async () => {
try { return await import('./some-module.js'); }
catch (error) { console.warn(error.message); }
return {};
})(); these could be generated by the typescript compiler with zero cost con: top level await only works in modules |
Quick info: I (almost) completed an implementation in #342. Could someone help? |
👋 Hey all, I work on the TypeScript team and am the primary author of the
@dummdidumm, I admit I just stumbled onto this conversation, so hopefully you don’t mind catching me up, but have you already discussed potential solutions on the TypeScript side with the team? I wasn’t aware of these issues when working on the aforementioned features, and it feels like they are just shy of being useful for Svelte. We would definitely be open to feedback if it seems like some small tweaks could make life much easier for the Svelte ecosystem. In particular, reading this:
feels like we ought to be able to do better. We had a request recently, for a distinct but not totally dissimilar reason, to have a way for imported names that appear to be unused not to be elided in the emit. @dummdidumm, and anyone else interested, would you mind taking a look at microsoft/TypeScript#43393 and weighing in? Thanks to @milahu for bringing this to my attention. |
@andrewbranch Thank you for joining this discussion. Your experience with TS compiler may be very useful here. The main issue we are facing is that Typescript does not know what is used in the markup part of a Svelte file so it tries to strip actually used imports. The current solution is the custom transformer mentioned above but this leads to unexpected behaviors when trying to import types and values in the same statement (types are preserved in the output which leads to runtime error). Please note that we are using Typescript in Am I correct? I looked the issue you mentioned but it does not solve this issue it just enforces that we could not use the mixed import feature anymore. The only good thing I see with this solution is that it avoid the runtime error and move it to compile time... However doubling all mixed imports is not a good idea (IMHO):
Of course, we want the best of both worlds 😅. I understand that the workaround we are actually developing (#342) is impacting performance and is involving a double parsing of markup but we did not find a safer way to make Typescript knows which imports it can safely remove. I would love to find a solution that is handled directly by the Typescript compiler but I don't know how it could be safe without knowing external modules nor statements from markup. It's a lot of missing information for a compiler! Do you have any ideas? |
This is not quite correct, though I’m not sure it changes things for you—in
This is basically the value proposition of using TypeScript 😄 The problem with a super-magical solution where we can give you “the best of both worlds” is that it sounds extremely tailored to Svelte (and Svelte’s current preprocesing pipeline). I was hoping my proposal would be a good way to meet in the middle here, because having a mode where we remove all I understand if you prefer to implement magic yourself, but thought I should jump in before a bunch of work was invested in something that we could work together on. |
Hey @andrewbranch , thank you so much for weighing in 👋 I actually saw this issue back then and thought "this would solve our problems at authoring time" but then I saw it was closed so I thought "ok they don't want to implement that". Now I feel stupid for not having asked anyway 😄 Right now one has to split types and values "by hand" because TS always wants to merge them as soon as there's a value and a type imported from the same location. In my mind a combination of strictly separating type and value imports and always preserving imports would get us the desired output (although the latter could still be handled by a transform step in I'll weigh in on both mentioned issues 👍 Re <script lang="ts">
const foo: string = "bar";
</script>
{foo} Then |
another argument for mixed imports: the issue can be solved by using fault-tolerant dynamic imports just let my computer do this boring job, TS already has enough opionions ... this workaround (dynamic imports) is only needed for development mode |
Another goal of TS is to be as much in line with what regular JS outputs as possible and not introduce JS artifacts from type-only-artifacts. Replacing type imports with dummy imports is against that goal, you can't rely on people using a bundler which does tree shaking afterwards. Moreover your suggested solution involves using dynamic await which cant not be assumed to be available in all environments yet and it also changes the semantics of the generated code too much in my opinion. |
@dummdidumm, @andrewbranch, I would like to share some of my thoughts. I'm not a Svelte maintainer and not part of the core team so my opinion is only based on the few projects I work on with my team. We will follow your decision. Concerning DX, @andrewbranch solution will improve DX since we will not have to do the separation by hand. But I think DX is not only tooling, it's also readability, maintenance and keeping habits. While having tooling is a good thing, I still think that having separated imports impacts the DX because it's more difficult to understand imports for a newcomers and it impacts readability. Our team is using TypeScript since the beginning (~10 years) so they are used to hugely leverage the type system to create strict types, mapping types, enums, etc... When working on Svelte, they have to create a lot of duplicated imports. We have some components that imports dozens of files and components. This leads to components where the import section is very huge and hard to maintain. ExampleThis code: import { method1, prop1, method2 } from "$lib/module";
import type { Type1, Type2 } from "$lib/module";
import Component1 from "$lib/components/Component1";
import type { Variant } from "$lib/components/Component1";
import Component2 from "$lib/components/Component2";
import type { Theme } from "$lib/components/Component2";
// and so on Could be rewritten in: import { method1, prop1, method2, Type1, Type2 } from "$lib/module";
import Component1, { Variant } from "$lib/components/Component1";
import Component2, { Theme} from "$lib/components/Component2";
// and so on
This is especially true when you import a lot of components and modules in the same component. The import section becomes very weird (cf. Example 2). Real-world exampleThis is the import section of a page in a real-world project (using routify but applicable on sveltekit): import { seq } from "promizr";
import { goto, url } from "@roxi/routify";
import { locale, t } from "@lib/i18n";
import type { Languages } from "@lib/i18n";
import { useMutation, useQuery, useQueryClient } from "@sveltestack/svelte-query";
import type { MutationOptions, QueryOptions } from "@sveltestack/svelte-query";
import { access, user } from "@lib/auth";
import type { Acl, Features } from "@lib/auth";
import { formatDuration } from "@lib/format";
import {
buildApplication,
duplicateApplication,
getApplicationById,
listFolders,
restoreApplication,
trashApplication,
updateApplication,
} from "@lib/application";
import type { Application, Folder } from "@lib/application";
import { notify } from "@stores/notifications";
import type { Notification } from "@stores/notifications";
import { fileurl } from "@ui/actions/fileurl";
import Icon from "@components/icons";
import type { IconDefinition } from "@components/icons";
import Card from "@components/Card.svelte";
import Loading from "@components/Loading.svelte";
import Button from "@components/Button.svelte";
import type { Variant, Size } from "@components/Button.svelte";
import Message from "@components/Message.svelte";
import type { MessageType } from "@components/Message.svelte";
import ErrorMessage from "@components/ErrorMessage.svelte";
import HelpMessage from "@components/HelpMessage.svelte";
import HelpPopover from "@components/HelpPopover.svelte";
import type { PopoverPosition, PopoverSize } from "@components/HelpPopover.svelte";
import ContentEditable from "@components/ContentEditable.svelte";
import { Confirm, DockedDialog } from "@components/dialogs";
import type { DialogMode, DialogSize } from "@components/dialogs"; Could be rewritten in: import { seq } from "promizr";
import { goto, url } from "@roxi/routify";
import { locale, t, Languages } from "@lib/i18n";
import { useMutation, useQuery, useQueryClient, MutationOptions, QueryOptions } from "@sveltestack/svelte-query";
import { access, user, Acl, Features } from "@lib/auth";
import { formatDuration } from "@lib/format";
import {
buildApplication,
duplicateApplication,
getApplicationById,
listFolders,
restoreApplication,
trashApplication,
updateApplication,
Application,
Folder,
} from "@lib/application";
import { notify, Notification } from "@stores/notifications";
import { fileurl } from "@ui/actions/fileurl";
import Icon from, { IconDefinition } "@components/icons";
import Card from "@components/Card.svelte";
import Loading from "@components/Loading.svelte";
import Button, { Variant, Size } from "@components/Button.svelte";
import Message, { MessageType } from "@components/Message.svelte";
import ErrorMessage from "@components/ErrorMessage.svelte";
import HelpMessage from "@components/HelpMessage.svelte";
import HelpPopover, { PopoverPosition, PopoverSize } from "@components/HelpPopover.svelte";
import ContentEditable from "@components/ContentEditable.svelte";
import { Confirm, DockedDialog, DialogMode, DialogSize } from "@components/dialogs"; IMHO, the second version is clearer and tinier. It's also consistent with what we have on TS side and with what a TS dev would expect to read. Moreover, with the actual version, it's possible to use mixed imports in full Typescript files (eg in $lib). This means that, in the same project, we have two ways of writting imports, one for Svelte, one for TS. This is also something that impact DX IMHO. I'm also concerned about the impact of this new mode on existing projects. Indeed, as explained above, it's possible to use mixed imports in Typescript files. Introducing this new mode will be a breaking change for all existing Typescript+Svelte projects. Maybe a way to mitigate this would be to enable this new mode only in the context of Svelte files. To summarize, @andrewbranch solution: Advantages:
Drawbacks:
@dummdidumm + @SomaticIT solution (#342): Advantages:
Drawbacks:
@milahu solution (fault-tolerant dynamic imports): Advantages:
Disadvantages:
Maybe, the best would be to add an option to select the best mode depending on your project... What do you think? Also, do not hesitate to give me your feedbacks on the lists below, I will update them |
I don't share the concern that the import list becomes unreadable. I personally never look at the list of imports anyway unless there's a hickup where I accidentally imported something with the same name but from another location. Regarding breaking change: It would only be a breaking change for people who opt in to this new behavior by setting the corresponding flag in their tsconfig. It's a one-time tedious work but it's not rocket science. TS could even provide code action quick fixes for this, making the transition even easier. I agree though that the solution with double parsing would result in probably the best DX with best backwards compatibility. I would like to hold off on going further with it though before getting a definitive answer if there will be something implemented on the TS side, and if so, when. |
@andrewbranch don't want to rush you, but is there any estimate if and when we can expect the new TS compiler option? If it's discarded or will only land in about more than half a year I think we would investigate continuing the solution @SomaticIT sketched out. |
@dummdidumm I’m hoping for TypeScript 4.5. I was trying to get something into 4.4, but we decided we needed more research into exactly what Svelte’s (and Vue’s) pipelines look like and what their needs are. I’ll keep you posted, and might try to get you to chat with me and @DanielRosenwasser at some point if you’d be up for it. |
Thanks for the update! About a possible meeting at some later time, we can do that, sure. You can hit me up through Orta, he's in the Svelte Discord, my name's the same in there as it is here. |
…(and mark them as warnings) (#6194) This PR adds a new option errorMode to CompileOptions to allow continuing the compilation process when errors occured. When set to warn, this new option will indicate to Svelte that it should log errors as warnings and continue compilation. This allows (notably) preprocessors to compile the markup to detect vars in markup before preprocessing (in this case: script and style tags are stripped so it can produce a lot of errors). This PR is part of a work on the svelte-preprocess side to improve the preprocessing of TypeScript files: sveltejs/svelte-preprocess#318 - allow compiler to pass error as warnings - enforce stops after errors during compilation (for type-checking, TS doesn't know the error method throws) - should review Element.ts:302 - added a test case for errorMode - added documentation
Although this is fixed via the "append variables that are used in the template using Ast smarts", I'm still eagerly awaiting first class TS support for this, because our approach might not work for all cases (html not in a parseable state yet). |
…(and mark them as warnings) (#6194) This PR adds a new option errorMode to CompileOptions to allow continuing the compilation process when errors occured. When set to warn, this new option will indicate to Svelte that it should log errors as warnings and continue compilation. This allows (notably) preprocessors to compile the markup to detect vars in markup before preprocessing (in this case: script and style tags are stripped so it can produce a lot of errors). This PR is part of a work on the svelte-preprocess side to improve the preprocessing of TypeScript files: sveltejs/svelte-preprocess#318 - allow compiler to pass error as warnings - enforce stops after errors during compilation (for type-checking, TS doesn't know the error method throws) - should review Element.ts:302 - added a test case for errorMode - added documentation
Is your feature request related to a problem? Please describe.
Currently, TypeScript is processed on a per-file-basis. Because the script tag is only part of the truth, a transformer was written to re-add imports which to TypeScript look unused but are actually used in the template. This means that people need to be very careful about how to write their imports, which can be especially cumbersome if a type import is from the same file as a real import.
Since TypeScript itself does not do that strict autocompletion separation (and they don't plan to change it, see this issue), people have to do it by hand.
So what would be ideal is to
Describe the solution you'd like
Ideally, I don't have to be that strict with seperating type and value imports. I currently see two ways to get there.
Solution 1: The Svelte-AST-compile-route
This solution is similar to the one currently used in
eslint-plugin-svelte3
. Some logic of #155 could maybe be reused.Advantages:
Disadvantages:
Solution 2: Walk TS AST to find type only references
This solution would be similar to #164 . Basically, walk the AST and find all references of all imports, and filter those out who are type-only. Walking the AST without advanced capabilities like a type checker / TS program is likely tedious, but probably more robust.
Advantages:
Disadvantages:
How important is this feature to you?
Important in order to boost TypeScript development productivity.
The text was updated successfully, but these errors were encountered: