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

const enum in a component always generates a runtime object #281

Closed
dkzlv opened this issue Nov 20, 2020 · 6 comments
Closed

const enum in a component always generates a runtime object #281

dkzlv opened this issue Nov 20, 2020 · 6 comments

Comments

@dkzlv
Copy link

dkzlv commented Nov 20, 2020

Describe the bug
If you use a const enum in the <script> tag it will still generate a runtime object.

To Reproduce

  1. use official template and run the official script for TS support
  2. disable terser rollup plugin (to have readable output). Also, even though the default state of preserveConstEnums is false, you can set it just to be sure.
  3. replace App.svelte contents with this:
<script lang='ts'>
  const enum State {
    opened,
    closed,
  }
  let state = State.opened;
  // We add this so it is not moved into module context
  state++
</script>
  1. run npm build or just look into IDE tab with the pre-built version of the component. It will look like this:
/* generated by Svelte v3.29.7 */
import { SvelteComponent, init, safe_not_equal } from "svelte/internal";

function instance($$self) {
	var State;

	(function (State) {
		State[State["opened"] = 0] = "opened";
		State[State["closed"] = 1] = "closed";
	})(State || (State = {}));

	let state = State.opened;

	// We add this so it is not moved into module context
	state++;

	return [];
}

class Component extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, null, safe_not_equal, {});
	}
}

export default Component;

Expected behavior
Should be built into something like this:

/* generated by Svelte v3.29.7 */
import { SvelteComponent, init, safe_not_equal } from "svelte/internal";

function instance($$self) {
	let state = 0;

	// We add this so it is not moved into module context
	state++;

	return [];
}

class Component extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, null, safe_not_equal, {});
	}
}

export default Component;

Additional context
Copied from here.

@dummdidumm suggested that the reason is the isolatedModules setting. I tried to set it into false, no luck. Also it seems to me that if you do not export the const enum isolatedModules doesn't seem to affect it at all (based mostly on this article).

I tried to compile the following TS file with and without the isolatedModules setting:

const enum State {
  closed,
  opened,
}

let state = State.closed;

export const fn = () => {}

The result seems to be the same (and correct):

"use strict";
exports.__esModule = true;
exports.fn = void 0;
var state = 0 /* closed */;
exports.fn = function () { };
@harvey-k
Copy link

harvey-k commented Mar 6, 2021

I ran into this as well. I believe this is due to a bug in TypeScript, not svelte-preprocess:
microsoft/TypeScript#16671

Further discussion in recent design meeting notes:
microsoft/TypeScript#42991

@dkzlv
Copy link
Author

dkzlv commented Mar 7, 2021

@harvey-k I think you're referring to some other problem.

The issues you linked to say that const enum is not inlined in other modules with isolatedModules setting turned on, and it results in runtime error, because the enum itself disappears from the module.

My issue is that const enum defined inside the component is not inlined and behaves like you explicitly set "preserveConstEnum": true — so it has the whole map of options that cannot be minified in any way.

So you do something like

<script>
  const enum States {
    loading,
    error,
    data,
  }

  let state: States = States.loading;
</script>

{#if state == States.loading}
  ...
{:else if state == States.error}
  ...
{:else}
  ...
{/if}

and you would expect it to be inlined like let state = 0, if state == 1, etc., but instead you get the whole unminifiable inefficient map generated in the bundle.

(function (States) {
	States[States["loading"] = 0] = "loading";
	States[States["error"] = 1] = "error";
	States[States["data"] = 2] = "data";
})(States || (States = {}));

@harvey-k
Copy link

harvey-k commented Mar 7, 2021

@dkzlv Sorry, I might have linked an issue only partially related to this complicated mix.

Here's a standalone example:

index.ts

const enum State {
  Loading = 1,
  Error = 2,
  Data = 3
};

export function load() {
  let s: State = State.Loading;
  switch (+s) {
    case State.Loading:
      console.log("loading");
      break;
    case State.Error:
      console.log("error");
      break;
    case State.Data:
      console.log("data");
      break;
    default:
      console.log("unknown");
      break;
  }
}

Using typescript 4.2.3 produces the following index.js

npx tsc --isolatedModules true index.ts
"use strict";
exports.__esModule = true;
exports.load = void 0;
var State;
(function (State) {
    State[State["Loading"] = 1] = "Loading";
    State[State["Error"] = 2] = "Error";
    State[State["Data"] = 3] = "Data";
})(State || (State = {}));
;
function load() {
    var s = State.Loading;
    switch (+s) {
        case State.Loading:
            console.log("loading");
            break;
        case State.Error:
            console.log("error");
            break;
        case State.Data:
            console.log("data");
            break;
        default:
            console.log("unknown");
            break;
    }
}
exports.load = load;

Not using isolatedModules produces our hoped for output

npx tsc index.ts
"use strict";
exports.__esModule = true;
exports.load = void 0;
;
function load() {
    var s = 1 /* Loading */;
    switch (+s) {
        case 1 /* Loading */:
            console.log("loading");
            break;
        case 2 /* Error */:
            console.log("error");
            break;
        case 3 /* Data */:
            console.log("data");
            break;
        default:
            console.log("unknown");
            break;
    }
}
exports.load = load;

@kaisermann
Copy link
Member

kaisermann commented Mar 7, 2021

@harvey-k I think you're referring to some other problem.

The issues you linked to say that const enum is not inlined in other modules with isolatedModules setting turned on, and it results in runtime error, because the enum itself disappears from the module.

My issue is that const enum defined inside the component is not inlined and behaves like you explicitly set "preserveConstEnum": true — so it has the whole map of options that cannot be minified in any way.

So you do something like

<script>
  const enum States {
    loading,
    error,
    data,
  }

  let state: States = States.loading;
</script>

{#if state == States.loading}
  ...
{:else if state == States.error}
  ...
{:else}
  ...
{/if}

and you would expect it to be inlined like let state = 0, if state == 1, etc., but instead you get the whole unminifiable inefficient map generated in the bundle.

Unfortunately, this really won't work because the preprocessor only handles typescript inside the script tags. It does not have access to the expressions in the markup. We need a way to transpile the ts inside the curly markup expressions and transform them considering the script context. This is not exactly related to #318, but there might be an intersection between their solutions.

@harvey-k
Copy link

harvey-k commented Mar 7, 2021

Also, it appears that preserveConstEnums is implicit when using isolatedModules, which is part of the default svelte tsconfig.

tsc --isolatedModules true --preserveConstEnums false index.ts
error TS5091: Option 'preserveConstEnums' cannot be disabled when 'isolatedModules' is enabled.

@dkzlv
Copy link
Author

dkzlv commented Mar 8, 2021

@harvey-k That is eye opening, thanks.

@kaisermann Yeah, I see. Don't really think it would be worth the effort to start working with TS inside the template. I've only come up with two cases when it's useful (non-null assertion and const enums), and there's always a way to do mostly the same but without the problems for the compiler and library.

Tried to find a way around this limitation, but no luck. Added a related feature request in the main svelte repo, that would help terser inline objects like this in the context=module block of the component:

const States = {
  error: 0,
  loading: 1,
  data: 2
}

It would fix the issue for me entirely. For now it's not inlined due to the order of declarations in the compiled Svelte code, more on this here: sveltejs/svelte#6062.

For now I think it's reasonable to close the issue as it cannot be dealt with in any way.

@dkzlv dkzlv closed this as completed Mar 8, 2021
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

No branches or pull requests

3 participants