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

Class fields initialized in the wrong order when useDefineForClassFields is set to true #11722

Open
7 tasks done
n1kk opened this issue Jan 17, 2023 · 6 comments
Open
7 tasks done
Labels
p2-edge-case Bug, but has workaround or limited in scope (priority)

Comments

@n1kk
Copy link

n1kk commented Jan 17, 2023

Describe the bug

When useDefineForClassFields flag is set to true in the tsconfig.json then the output produced by vite has class properties initialized in the wrong order.

Sample ts code:

export class A {
    public tag: string = "[" + this.name + "]";
    constructor(public name: string) {
    }
}
let a = new A("asd");
console.log(a.tag);

The tag property relies on the name property, which is guaranteed to be a string, but in the output that vite produces the tag property is initialized before the name is.

class A {
  constructor(name) {
    __publicField(this, "tag", "[" + this.name + "]");
    this.name = name;
  }
}
let a = new A("asd");
console.log(a.tag);

Stackblitz link

TS compiles it in the right order Playground Link

However if I just compile the code with esbuild of the same version that is specified in the vite dependencies it produces the correct order:

export class A {
  constructor(name) {
    this.name = name;
    __publicField(this, "tag", "[" + this.name + "]");
  }
}
let a = new A("asd");
console.log(a.tag);

Stackblitz link

What can be the reason for this difference in the ourput?

tsconfig:

{
  "compilerOptions": {
    "target": "ES2017",
    "useDefineForClassFields": true,
    "experimentalDecorators": true
  },
  "include": ["src"],
}

vite.config.ts:

import { defineConfig } from "vite";
import type { UserConfig } from "vite";

export default defineConfig(({ mode }) => {
    const config: UserConfig = {
        build: {
            minify: false,
            rollupOptions: {
                input: {
                    main: 'index.html',
                }, 
            }
        },
    };
    return config;
});

esbuild output produced with this command

pnpm esbuild src/index.ts --outdir=dist-es --target=es2017

Reproduction

https://stackblitz.com/edit/vitejs-vite-eidnfv?file=dist/assets/main-57fe9a19.js

Steps to reproduce

  • Open Stackblitz project
  • run 'npm run build`
  • run npm run esbuild
  • check generated code in dist and es-dist

System Info

System:
    OS: Windows 10 10.0.19044
    CPU: (12) x64 Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
    Memory: 37.62 GB / 63.73 GB
  Binaries:
    Node: 16.17.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 9.2.0 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 108.0.5359.126
    Edge: Spartan (44.19041.1266.0), Chromium (108.0.1462.76)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    vite: ^4.0.4 => 4.0.4

Used Package Manager

pnpm

Logs

❯ pnpm vite build --debug
vite:config bundled config file loaded in 322.33ms +0ms
vite:esbuild init tsconfck (root: C:/dev/_playground/esbuild_class_props_test) +0ms
vite:esbuild init tsconfck (root: C:/dev/_playground/esbuild_class_props_test) +2ms
vite:esbuild init tsconfck (root: C:/dev/_playground/esbuild_class_props_test) +1ms
vite:esbuild init tsconfck (root: C:/dev/_playground/esbuild_class_props_test) +1ms
vite:esbuild init tsconfck end +1ms
vite:esbuild init tsconfck end +1ms
vite:esbuild init tsconfck end +0ms
vite:esbuild init tsconfck end +0ms
vite:config using resolved config: {
vite:config build: {
vite:config target: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
vite:config cssTarget: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
vite:config outDir: 'dist',
vite:config assetsDir: 'assets',
vite:config assetsInlineLimit: 4096,
vite:config cssCodeSplit: true,
vite:config sourcemap: false,
vite:config rollupOptions: { input: [Object] },
vite:config minify: false,
vite:config terserOptions: {},
vite:config write: true,
vite:config emptyOutDir: null,
vite:config copyPublicDir: true,
vite:config manifest: false,
vite:config lib: false,
vite:config ssr: false,
vite:config ssrManifest: false,
vite:config reportCompressedSize: true,
vite:config chunkSizeWarningLimit: 500,
vite:config watch: null,
vite:config commonjsOptions: { include: [Array], extensions: [Array] },
vite:config dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] },
vite:config modulePreload: { polyfill: true }
vite:config },
vite:config optimizeDeps: {
vite:config disabled: 'build',
vite:config force: undefined,
vite:config esbuildOptions: { preserveSymlinks: false }
vite:config },
vite:config configFile: 'C:/dev/_playground/esbuild_class_props_test/vite.config.ts',
vite:config configFileDependencies: [ 'C:/dev/_playground/esbuild_class_props_test/vite.config.ts' ],
vite:config inlineConfig: {
vite:config root: undefined,
vite:config base: undefined,
vite:config mode: undefined,
vite:config configFile: undefined,
vite:config logLevel: undefined,
vite:config clearScreen: undefined,
vite:config optimizeDeps: { force: undefined },
vite:config build: {}
vite:config },
vite:config root: 'C:/dev/_playground/esbuild_class_props_test',
vite:config base: '/',
vite:config rawBase: '/',
vite:config resolve: {
vite:config mainFields: [ 'module', 'jsnext:main', 'jsnext' ],
vite:config browserField: true,
vite:config conditions: [],
vite:config extensions: [
vite:config '.mjs', '.js',
vite:config '.mts', '.ts',
vite:config '.jsx', '.tsx',
vite:config '.json'
vite:config ],
vite:config dedupe: [],
vite:config preserveSymlinks: false,
vite:config alias: [ [Object], [Object] ]
vite:config },
vite:config publicDir: 'C:\dev\_playground\esbuild_class_props_test\public',
vite:config cacheDir: 'C:/dev/_playground/esbuild_class_props_test/node_modules/.vite',
vite:config command: 'build',
vite:config mode: 'production',
vite:config ssr: {
vite:config format: 'esm',
vite:config target: 'node',
vite:config optimizeDeps: { disabled: true, esbuildOptions: [Object] }
vite:config },
vite:config isWorker: false,
vite:config mainConfig: null,
vite:config isProduction: true,
vite:config plugins: [
vite:config 'vite:build-metadata',
vite:config 'vite:pre-alias',
vite:config 'alias',
vite:config 'vite:modulepreload-polyfill',
vite:config 'vite:resolve',
vite:config 'vite:html-inline-proxy',
vite:config 'vite:css',
vite:config 'vite:esbuild',
vite:config 'vite:json',
vite:config 'vite:wasm-helper',
vite:config 'vite:worker',
vite:config 'vite:asset',
vite:config 'vite:wasm-fallback',
vite:config 'vite:define',
vite:config 'vite:css-post',
vite:config 'vite:build-html',
vite:config 'vite:worker-import-meta-url',
vite:config 'vite:asset-import-meta-url',
vite:config 'vite:force-systemjs-wrap-complete',
vite:config 'vite:watch-package-data',
vite:config 'commonjs',
vite:config 'vite:data-uri',
vite:config 'vite:dynamic-import-vars',
vite:config 'vite:import-glob',
vite:config 'vite:build-import-analysis',
vite:config 'vite:esbuild-transpile',
vite:config 'vite:reporter',
vite:config 'vite:load-fallback'
vite:config ],
vite:config server: {
vite:config preTransformRequests: true,
vite:config middlewareMode: false,
vite:config fs: { strict: true, allow: [Array], deny: [Array] }
vite:config },
vite:config preview: {
vite:config port: undefined,
vite:config strictPort: undefined,
vite:config host: undefined,
vite:config https: undefined,
vite:config open: undefined,
vite:config proxy: undefined,
vite:config cors: undefined,
vite:config headers: undefined
vite:config },
vite:config env: { BASE_URL: '/', MODE: 'production', DEV: false, PROD: true },
vite:config assetsInclude: [Function: assetsInclude],
vite:config logger: {
vite:config hasWarned: false,
vite:config info: [Function: info],
vite:config warn: [Function: warn],
vite:config warnOnce: [Function: warnOnce],
vite:config error: [Function: error],
vite:config clearScreen: [Function: clearScreen],
vite:config hasErrorLogged: [Function: hasErrorLogged]
vite:config },
vite:config packageCache: Map(0) { set: [Function (anonymous)] },
vite:config createResolver: [Function: createResolver],
vite:config worker: {
vite:config format: 'iife',
vite:config plugins: [
vite:config 'vite:build-metadata',
vite:config 'vite:pre-alias',
vite:config 'alias',
vite:config 'vite:modulepreload-polyfill',
vite:config 'vite:resolve',
vite:config 'vite:html-inline-proxy',
vite:config 'vite:css',
vite:config 'vite:esbuild',
vite:config 'vite:json',
vite:config 'vite:wasm-helper',
vite:config 'vite:worker',
vite:config 'vite:asset',
vite:config 'vite:wasm-fallback',
vite:config 'vite:define',
vite:config 'vite:css-post',
vite:config 'vite:build-html',
vite:config 'vite:worker-import-meta-url',
vite:config 'vite:asset-import-meta-url',
vite:config 'vite:force-systemjs-wrap-complete',
vite:config 'vite:watch-package-data',
vite:config 'commonjs',
vite:config 'vite:data-uri',
vite:config 'vite:dynamic-import-vars',
vite:config 'vite:import-glob',
vite:config 'vite:build-import-analysis',
vite:config 'vite:esbuild-transpile',
vite:config 'vite:load-fallback'
vite:config ],
vite:config rollupOptions: {},
vite:config getSortedPlugins: [Function: getSortedPlugins],
vite:config getSortedPluginHooks: [Function: getSortedPluginHooks]
vite:config },
vite:config appType: 'spa',
vite:config experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false },
vite:config getSortedPlugins: [Function: getSortedPlugins],
vite:config getSortedPluginHooks: [Function: getSortedPluginHooks]
vite:config } +20ms
vite v4.0.4 building for production...
✓ 3 modules transformed.
dist/index.html 0.17 kB
dist/assets/main-57fe9a19.js 1.79 kB │ gzip: 0.74 kB

Validations

@sapphi-red
Copy link
Member

It seems this is happening because Vite transpiles TS to JS (ESNext) first and then transpiles to lower target.

There's a inconsistency in TypeScript with different target: microsoft/TypeScript#45995, microsoft/TypeScript#50971

class A {
    public tag: string = "[" + this.name + "]";
    constructor(public name: string) {
    }
}
let a = new A("asd");
console.log(a.tag);

does work for ES2022<. But doesn't work with ES2022+/ESNext.

@sapphi-red sapphi-red added the p2-edge-case Bug, but has workaround or limited in scope (priority) label Jan 18, 2023
@n1kk
Copy link
Author

n1kk commented Jan 18, 2023

Hey @sapphi-red , thanks for reply. But I have target set to es2017, so why it doesn't work for me then? Or do I have to set it somewhere else besides tsconfig?

I tried setting target to es2017 in the vote.config.ts explicitly in the stackblitz example but it still produces same output.

@sapphi-red
Copy link
Member

I mean Vite first transpiles your TS code to JS with target: 'esnext'. It's not configurable.

The only workaround I can think of is to move the assignment inside the constructor.

export class A {
    public tag: string;
    constructor(public name: string) {
        this.tag = "[" + this.name + "]";
    }
}
let a = new A("asd");
console.log(a.tag);

@n1kk
Copy link
Author

n1kk commented Jan 18, 2023

Ah, I see. Yes, moving it to a constructor is a workaround we're currently utilizing. The problem is that it's not being shown as an error by TS, so we have to make everyone aware of this and hope no one will do it.

Would be nice to have eslint rule for this till it's fixed, but I haven't found any that can suit this case.

@branko-d
Copy link

branko-d commented Nov 29, 2023

This is a huge bug! It essentially eliminates the usage of MobX in any non-trivial codebase.

In our case, we would have to change thousands of places in our codebase (despite being correct according to TypeScript syntax) and afterwards risk mistakes that are invisible until run-time.

For what it's worth, Vite docs recognize the significance of useDefineForClassFields:

https://vitejs.dev/guide/features.html#typescript-compiler-options

--- EDIT ---

Looks like putting the following in vite.config.js solves the problem:

import { defineConfig } from 'vite';

export default defineConfig(
    {
        esbuild: {
            target: "es2020"
        },
        // ...
    }
)

This is the same target as in our tsconfig.json.

@sagarpanchal
Copy link

@branko-d
@yyx990803

How do I fix this for scripts with #!/usr/bin/env vite-node --script?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p2-edge-case Bug, but has workaround or limited in scope (priority)
Projects
None yet
Development

No branches or pull requests

4 participants