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

chore(TS): Add declare and remove useless methods from Object. #8574

Merged
merged 28 commits into from Jan 10, 2023

Conversation

asturur
Copy link
Member

@asturur asturur commented Jan 7, 2023

Motivation

Finishing up basic object migration.

Description

  • change the public properties to declare so that any transpiler understand those should be removed.
  • added the default values in the constructor, while still making possible to change them globally before initialization of objects failed / scope blown up
  • cacheProperties and stateProperties needs to stay on prototype, having them as static is not really possible since it would require duplicating them for each class and would add another constrain when subclassing. Apparently static properties aren't usable with inheritance failed / scope blown up
  • Fixed as much as possible TS complains, a couple have been ignored
  • Object._removeTransformMatrix moved as utility since is used only from the svg parser

Changes

  • removed Object.centerObject and similar. The methods were aliases for canvas counterparts with the added variable that they would be no-op if there wasn't a canvas. Seems jus confusing. Developers should call those methods from the canvas instance instead of relying on the fact that the canvas is maybe there maybe not and the code won't do nothing but won't fail

Gist

In Action

@asturur
Copy link
Member Author

asturur commented Jan 7, 2023

Messed up a bunch of things with cacheProperties i need to fix yet.
adding all the declare everywhere wil let us exports or class syntax normally.

@asturur
Copy link
Member Author

asturur commented Jan 7, 2023

This is going to be another painful PR on top of certifying that class were a mistake and are less powerful and useful than functions. now i m sure.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2023

Build Stats

file / KB (diff) bundled minified
fabric 928.366 (-1.966) 299.110 (-0.726)

@asturur
Copy link
Member Author

asturur commented Jan 7, 2023

@ShaMan123 this thing is becoming more complicated than planned.

Using public properties or defaults in the constructor is triggering the set calls. set calls executes side effects in different order depending on the order of the keys of the options and results changes.
We could update tests but it won't be stable anyway since order of iteration on keys is not guaranteed.

I already tried to assign defaults outside of set ( as we were doing before because they were on the prototype ) but that can't be done because they can't be assigned on this before the constructor run, and the constructor of the super classes expect them to be set already.

We could stop executing side effects on the constructor entirely but that would mean we would need to find what breaks and fix it manually.

@asturur
Copy link
Member Author

asturur commented Jan 7, 2023

Reducing the scope of the pr to unlock exports

@ShaMan123
Copy link
Member

I have to say this PR seems a step back rather than forward.
Something to discuss.
All this declare mess, did you try to alter tsconfig#strictPropertyInitialization beforehand? Is it related? Well, I guess it is easy to revert, a simple search & replace. But a lot of work for a transient step.
I still don't understand where you set the default values in the constructor. Are we still hacking it via the prototype? If so I guess that is what is causing all this mess. Am I right?
Regarding set, it is something we discussed and a fault in design as I see it. I read that class properties are initialized in the constructor with Object#defineProperty and after that via Object#set. That is the spec. And it makes sense. No side effects from the constructor block.
We should align to that. #8372 suggest a way to manage effects but I think we need a robust and simple way such as setter. I know you dislike it, I do too. You suggested proxy. I read about that. I have no idea how to incorporate with the class new command (can only think of static initializers which is a turn off).
We are at the finish line which is always the hardest part in projects, right?
We made amazing progress. Let's decide how to manage this. It is a crucial decision.
All in all my point is that we decided to align to modern js but this PR is doing the opposite.
What do you think?

@ShaMan123
Copy link
Member

ShaMan123 commented Jan 8, 2023

take a look at this POC for circle and ellipse.
https://github.com/fabricjs/fabric.js/compare/init-define-props-poc
Circle and ellipse tests pass.

@ShaMan123
Copy link
Member

ShaMan123 commented Jan 8, 2023

I prefer a chat if you are willing
Let's solve this and plan the end of the migration, tackling remaining concerns.

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

Some premise, adding items to the prototype isn't hacking.

declare is needed because otherwise those are public properties, so if you don't assign a value, you need to declare them.
The fact that the current TS plugin is fixing them ( but the new one and babel aren't ) is a hint of something wrong.

Babel transpiler either errors because there is a declare or they are actual undefined properties, but existing.

image

Looks like the above code is valid, so when the code gets transpiled and the type definition removed, those are undefined props, without declare ( and a removal from the transpiler ) the code is not correct because those undefined will remove values you add in constructor.

Putting the defaults in the contructor as i did in the previous commits doesn't work for many classes that have set initializators that move up and down between the sub and super classes.

Circle and ellipses works for me too, group and active selection do not.

The reason why i m not handling it in this PR anymore is because every class will need ad hoc changes to work with setting defaults in the constructor, test changes too, so we will need more PRs.

So i m descoping this PR to adding declare only, and limiting the diff to that as much as possible, so at least we can try again to enable exports from the main index.

19aa171 this is the commit where i moved all defaults in constructors and then i strated roll back.

@ShaMan123
Copy link
Member

Some premise, adding items to the prototype isn't hacking.

I thought class properties do not belong to the prototype, only to the instance. Methods belong to the prototype.

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

I also separated cache properties and state properties from default values because those will either need to be in some config or stay in the prototype. Their usage is unfit for static, static does not inherit with the subclasses apparently.

@ShaMan123
Copy link
Member

static field do inherit

class A {
  static method() {
    return 1;
  }
  static arr = [1,2,3]
  static type = 'A'
}

class B extends A {
  static {
    this.arr.push(4)
    this.type = 'B'
  }
  static method() {
    return super.method() + 2;
  }
}

console.log(B.method(), B.arr, B.type)
Output
"use strict";
var _a;
class A {
    static method() {
        return 1;
    }
}
A.arr = [1, 2, 3];
A.type = 'A';
class B extends A {
    static method() {
        return super.method() + 2;
    }
}
_a = B;
(() => {
    _a.arr.push(4);
    _a.type = 'B';
})();
console.log(B.method(), B.arr, B.type);
Compiler Options
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2017",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "node"
  }
}

Playground Link: Provided

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

Some premise, adding items to the prototype isn't hacking.

I thought class properties do not belong to the prototype, only to the instance. Methods belong to the prototype.

Sure for pure state properties that sounds a thumb rule to which you can everyone agree with.
That does not work if you don't want to define the same properties along many classes or refactor entirely your code to have it work with this rule.

@ShaMan123
Copy link
Member

I need an example.
Static init seems to bridge that gap, does it not?

@ShaMan123
Copy link
Member

BTW in the example the static arr property is shared between classes. Pushing 4 to arr in B mutated it for A as well

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

They inerhit just on static things.
You can't put cacheProperties on static in a class, and then grab them in a subclass in a convenient way outside of static.

@ShaMan123
Copy link
Member

Thanks for elaborating.
I modified the example you linked. It had a typo, that is why there were js errors.
Regarding 19aa171 I would argue that declare ruined your effort but as you said. Next effort.

class A {
  static method() {
    return 1;
  }
  set(key, value) {
    // TS stop complaining
    if (this.constructor.arr.includes(key)) {
      console.log('ack', key, value);
    }
  }
  static arr = [1,2,3]
  static type = 'A'
}

class B extends A {
  static {
    this.arr.push(4)
    this.type = 'B'
  }
  static arr = [...A.arr, 'key', 'key2'];
  static method() {
    return super.method() + 2;
  }
}

class C extends B {

}

console.log('a', A.method(), A.arr, A.type);
console.log('b', B.method(), B.arr, B.type);
console.log('c', C.method(), C.arr, C.type);

const c = new C();
const b = new B();

C.method = () => {
  console.log('modified static method');
  return 5;
}

c.set('key', 4);
console.log(b.constructor.type)

C.arr = [...C.arr, 'foo'];
c.set('foo', 'bar')
console.log(c.constructor.method())
Output
"use strict";
var _a;
class A {
    static method() {
        return 1;
    }
    set(key, value) {
        // TS stop complaining
        if (this.constructor.arr.includes(key)) {
            console.log('ack', key, value);
        }
    }
}
A.arr = [1, 2, 3];
A.type = 'A';
class B extends A {
    static method() {
        return super.method() + 2;
    }
}
_a = B;
(() => {
    _a.arr.push(4);
    _a.type = 'B';
})();
B.arr = [...A.arr, 'key', 'key2'];
class C extends B {
}
console.log('a', A.method(), A.arr, A.type);
console.log('b', B.method(), B.arr, B.type);
console.log('c', C.method(), C.arr, C.type);
const c = new C();
const b = new B();
C.method = () => {
    console.log('modified static method');
    return 5;
};
c.set('key', 4);
console.log(b.constructor.type);
C.arr = [...C.arr, 'foo'];
c.set('foo', 'bar');
console.log(c.constructor.method());
Compiler Options
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2017",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "node"
  }
}

Playground Link: Provided

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

I m not sure declare are doing anything to the code rather than claryfing for tools that those are types and not actual code.
Do you think are doing something else?

@ShaMan123
Copy link
Member

ShaMan123 commented Jan 8, 2023

I don't know. It is so unclear what happens under the hood. What is taken care by rollup, the plugins and native classes.
Looking at the build file:

image

Copy link
Member

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks fine
a few small comments

if (
this === (target as InteractiveFabricObject) &&
action.slice &&
action.slice(0, 5) === 'scale'
Copy link
Member

@ShaMan123 ShaMan123 Jan 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startsWith?

        action.startsWith('scale')

Comment on lines 130 to 144
_updateCacheCanvas() {
const targetCanvas = this.canvas;
if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) {
const target = targetCanvas._currentTransform.target,
action = targetCanvas._currentTransform.action;
if (
this === (target as InteractiveFabricObject) &&
action.slice &&
action.slice(0, 5) === 'scale'
) {
return false;
}
}
return super._updateCacheCanvas();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this!
It is well scoped.

Comment on lines +809 to 811
protected setOptions(options: Record<string, any> = {}) {
this._setOptions(options);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this alias?
It is called only once in fabric from this contrusctor

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and _setOptions is an alias for set so that can go away as well.
too many methods that do the same shit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not this should go because no strange stuf in the constructor. It will go when we move the defaults

Comment on lines +1713 to +1714
// static canvas and canvas have both an array of InteractiveObjects
// @ts-ignore this needs to be fixed somehow, or ignored globally
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think static canvas should have the non interactive object as a type while canvas should have the interactive object
Wondering how complicated and dumb that might turn out

Comment on lines -30 to -31
this.set('rx', (options && options.rx) || 0);
this.set('ry', (options && options.ry) || 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised this doesn't break width and height values

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because constructor({...}) is doing that up in the chain with setOptions.
The issue could be if you pass with after rx,ry. But those are exactly the kind of things that needs to addressed when we setup the defaults.
I would also not bother, i prefer to say you have to set both of them, and zero is not a good value if not for both. I wouldn't try autofixing for the developer that have to understand this once.

Same issue with polygon bounding box, line points, radius and witdh/height. there are so many things we try to take care of we probably shouldn't.

If it wasn't late now i woudl say that width and height should be internal prop for geometry and the one we set for rect and text should be some sort of public width/height that nothing have to do with the geometry calculcations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it wasn't late now i woudl say that width and height should be internal prop for geometry and the one we set for rect and text should be some sort of public width/height that nothing have to do with the geometry calculcations.

Sounds good

Same issue with polygon bounding box, line points, radius and witdh/height. there are so many things we try to take care of we probably shouldn't.

we should draft topics for a team meeting

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this whole how to use set/ set and _set, side effects, will be a recurring theme

Comment on lines +104 to +108
if (isFiller(this.stroke)) {
ctx.strokeStyle = this.stroke.toLive(ctx);
} else {
ctx.strokeStyle = this.stroke ?? ctx.fillStyle;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code looks like a util
I think it is duplicated a few times

@@ -1,3 +1,4 @@
import { IPoint } from '../point.class';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably a leftover of a previous thing.

Copy link
Member Author

@asturur asturur Jan 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, when it comes to assigning defaults this will have a constructor too. So it will need the IType definititon

Comment on lines 467 to 469
// with the new controls this noScaleCache here shouldn't be needed anymore
lockScalingFlip: true,
// with the new controls this noScaleCache here shouldn't be needed anymore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

care to elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i was thinking at the width behaviour and that scaling does not affect textbox because we usually change the width. but i think i forgot of both axis scaling

Comment on lines 16 to 39
export const _assignTransformMatrixProps = (
object: FabricObjectWithTransformMatrix
) => {
if (object.transformMatrix) {
const { scaleX, scaleY, angle, skewX } = qrDecompose(
object.transformMatrix
);
object.flipX = false;
object.flipY = false;
object.set('scaleX', scaleX);
object.set('scaleY', scaleY);
object.angle = angle;
object.skewX = skewX;
object.skewY = 0;
}
};

/**
* This function is an helper for svg import. it removes the transform matrix
* and set to object properties that fabricjs can handle
* @private
* @param {Object} preserveAspectRatioOptions
*/
export const _removeTransformMatrix = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't know pick one. I think exportin only one is good enough

src/util/types.ts Outdated Show resolved Hide resolved
asturur and others added 3 commits January 8, 2023 15:41
Co-authored-by: Shachar <34343793+ShaMan123@users.noreply.github.com>
@ShaMan123
Copy link
Member

@asturur don't have an option in VSCODE that it formats on save running prettier?

@ShaMan123
Copy link
Member

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

@asturur don't have an option in VSCODE that it formats on save running prettier?

Works sporadically for me, both here and at work prettier does not kick in for me.

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

I have mixed the code with the dependency update branch with export and type declaration and i have a local branch with only 101 failing test from 174 i started after merging.
That means the declare are avoiding side effects we didn't plan for.
As soon as i m down to 0 fails it means that this work, i ll put all the declare here and then we can merge it

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

ok down to 9 failures but i have to stop now

@asturur
Copy link
Member Author

asturur commented Jan 8, 2023

ok so those additional declare changes ( no test changes, no code changed ) should give us support for export and types emitting, using babel OR the other rollup plugin ( That does not support browserlist )

@asturur
Copy link
Member Author

asturur commented Jan 9, 2023

let's give priority to this today if possible, i want to move asap to discussing browerlist file and what we support and transpile on which tool.
Practicing with the new import strategy and see if tree shaking is possible is another big chunk of work that i would like to be available for whoever wants to try it

@ShaMan123
Copy link
Member

how can i help?

Copy link
Member

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect TS to expose a flag in the config controlling this instead of this shitty declare business

@@ -31,7 +31,7 @@ export class Textbox extends IText {
* Cached array of text wrapping.
* @type Array
*/
declare __cachedLines: Array<any> | null = null;
declare __cachedLines: Array<any> | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a dead prop

@asturur
Copy link
Member Author

asturur commented Jan 9, 2023

how can i help?

Usually just approve if the PR is ok, or block if there is something you really don't like

@asturur asturur merged commit fa697ac into master Jan 10, 2023
@asturur asturur deleted the default-values-object branch January 10, 2023 01:18
@ShaMan123
Copy link
Member

and microsoft/TypeScript#33509

@asturur
Copy link
Member Author

asturur commented Jan 10, 2023

Yes i didn't read all the links deeply, but exactly the part of initializing as undefined.
The older ts-plugin was shielding us from that.

We will have to assign our own values with define properties too since we are doing it manually if we want to do that on spec.
Babel does that, and we can take their small initialization function.

@ShaMan123
Copy link
Member

ShaMan123 commented Jan 10, 2023

So a babel plugin will save us from declare?

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

Successfully merging this pull request may close these issues.

None yet

2 participants