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

generate addon using Ember CLI v4.1.1 + add RouteInfoMock #1

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

arazfar
Copy link

@arazfar arazfar commented Mar 30, 2022

This change generates foundational addon files using Ember CLI v4.1.1.

@arazfar arazfar changed the title generate addon using Ember CLI v4.1.1 generate addon using Ember CLI v4.1.1 + add RouteInfoMock Apr 7, 2022
Copy link
Collaborator

@chriskrycho chriskrycho left a comment

Choose a reason for hiding this comment

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

Awesome first pass! Thanks so much! (Sorry it took so long!)

Comment on lines +1 to +14
interface RouteInfo {
readonly name: string;
readonly parent: RouteInfo | null;
readonly child: RouteInfo | null;
readonly localName: string;
readonly params: { [name: string]: unknown } | undefined;
readonly paramNames: string[];
readonly queryParams: { [name: string]: string };
readonly metadata: unknown;
find(
predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean,
thisArg?: any
): RouteInfo | undefined;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here we should import the type from its current location in @ember/routing:

Suggested change
interface RouteInfo {
readonly name: string;
readonly parent: RouteInfo | null;
readonly child: RouteInfo | null;
readonly localName: string;
readonly params: { [name: string]: unknown } | undefined;
readonly paramNames: string[];
readonly queryParams: { [name: string]: string };
readonly metadata: unknown;
find(
predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean,
thisArg?: any
): RouteInfo | undefined;
}
import type RouteInfo from '@ember/routing/-private/route-info';

That way we don't have to worry about keeping this in sync, other than to update the import location when we have a public API import for that type (soon!).

Comment on lines +59 to +60
predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean,
thisArg?: any
Copy link
Collaborator

Choose a reason for hiding this comment

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

A couple notes:

  • We want to not have any here, and the this must always be a RouteInfo.
  • We should need to actually match the original type for the predicate, so:
Suggested change
predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean,
thisArg?: any
predicate: (this: RouteInfo, routeInfo: RouteInfo, idx: number, array?: RouteInfo[]) => boolean,
thisArg?: RouteInfo

(I know the actual implementation has anys there, but 😭.)

predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean,
thisArg?: any
): RouteInfo | undefined {
let current = thisArg || this;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's usually preferable to use the ?? nullish coalescing operator for these:

Suggested change
let current = thisArg || this;
let current = thisArg ?? this;

parent: RouteInfo | null = null;
child: RouteInfo | null = null;
localName: string;
params: { [name: string]: unknown } = {};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Making the above change to use the type from Ember highlights that this isn't quite right and should instead be:

Suggested change
params: { [name: string]: unknown } = {};
params: Record<string, string> = {};

return this;
}

withParams(params: { [name: string]: unknown }): RouteInfoMock {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This also needs updating for params' type:

Suggested change
withParams(params: { [name: string]: unknown }): RouteInfoMock {
withParams(params: Record<string, string>): RouteInfoMock {


return undefined;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should add a finalize method which just returns this but cast as RouteInfo so it can be treated as an immutable RouteInfo from that point forward.

Suggested change
}
finalize(): RouteInfo {
return this;
}
}

That has the effect of "removing" the builder methods from the public type signature of the thing after that call.

Comment on lines +63 to +73
let i = 0;

while (current) {
if (predicate(current, i++)) {
return current;
}

current = current.parent;
}

return undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking about this led me to realize that we actually want to separate out the mock content from the builder. The idea is: by separating out the mock (and, critically, not exposing it) you can do a few things:

  1. You can make it so that the public interface is just "how to build a RouteInfo" and then "finalize it", per my comment below, which prevents people from mucking directly with it. If we add in some module-local access (using symbols), we can actually make it so the resulting RouteInfo mocks have the same runtime (not just type-level) read-only behavior as the real version does.

  2. It lets us actually build up an appropriate list for this, and do it only once per finalized RouteInfo instance. Since that is only a thing users have access to once they finalize the builder, it is safe to cache it per-instance.

I have a version of that (along with the other changes suggested for the class) here—the comments on that walk through the changes! Note that in this design, as you can see at the bottom, you can build up mocks iteratively over time (as you basically have to be able to do) because the builder remains mutable—but the resulting mocks are not directly mutable.

@@ -0,0 +1,56 @@
{
"compilerOptions": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's either—

  • update to the latest version of ember-cli-typescript and re-run ember generate ember-cli-typescript
  • or manually copy over the blueprint's compilerOptions for everything but paths

—that will give us a much better version of this tsconfig.json!

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.

2 participants