-
Notifications
You must be signed in to change notification settings - Fork 75
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
fix(marshal): remove ambient types, generate declarations from JSDoc #1025
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool stuff! I'd be so much more confident in this kind of changes if we had noImplicitAny
.
Requesting change as I believe the default convert slot got flipped.
import { isPromise } from '@endo/promise-kit'; | ||
import { getTag, isObject } from './helpers/passStyle-helpers.js'; | ||
import { makeTagged } from './makeTagged.js'; | ||
import { passStyleOf } from './passStyleOf.js'; | ||
|
||
/** @typedef {import('./types').Passable} Passable */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: while tsc doesn't care as much, I think we should keep the habit of using fully qualified names, aka with extensions, for imports
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reasons to leave it this way:
- There should never be any ambiguity what file it refers to. (There must not be a
types.ts
next to atypes.js
.) - We can change the syntax of the imported file between
.ts
and.js
without touching everywhere it's imported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if there is a .d.ts
or .ts
file, the import should always be of .js
file per TypeScript conventions. TypeScript will internally use the .d.ts
file if it exists. We use explicit .js
extensions for our imports as automatic extension resolution is something we left behind when switching to native ESM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the correction on the second bullet about changing extensions. I didn't know that TypeScript conventions are that if the file is foo.ts
you should still import it as foo.js
.
I neglected to include what I think is the most compelling reason to leave it this way: less to type and read. I think simplicity wins unless there is a compelling reason to add the extension. Is it to be consistent with the JSM module imports? I think consistency is important when it reduces confusion or misunderstanding, but I have a hard time imagining someone being confused by the absence of .js
here in the JSDoc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right I was looking for consistency. I've never found terseness to be very compelling. We require extensions for ESM imports (static, and in the future dynamic), and since TS style jsdoc type imports use a dynamic-like ESM notion, I figure we should stay consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A worthy experiment. It is much less painful than I expected. Enough so that, assuming this is representative, I'm ok moving in this direction in general.
vscode has a very nice feature that when I use a name I have not yet imported but should, it often (not always) prompts me with the correct choice and writes the import
itself. If it did so for this /** @typedef {import('../types').Foo} Foo */
pattern as well, then I wouldn't even hesitate. Maybe eventually we'll have such tooling.
Thanks for pushing ahead with this. A very pleasant surprise. LGTM!!!
export function makeMarshal( | ||
convertValToSlot = defaultValToSlotFn, | ||
convertSlotToVal = defaultSlotToValFn, | ||
export const makeMarshal = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed that one. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have we considered a global code mod to convert functions? https://github.com/facebook/jscodeshift works great
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, but maybe sometime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to see this done automatically!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an attempt: Agoric/agoric-sdk#4608
It does for One drawback I believe is that going to the type definition would take 2 clicks, first one going to the |
packages/marshal/exported.js
Outdated
// eslint-disable-next-line import/export | ||
export * from './src/types.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to this pattern of explicit type exports over implicit.
We shouldn't have to disable the linter to do the right thing. https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/export.md says it prevents "funny business"; doesn't TypeScript cover that? I propose we consider disabling this rule in our eslint config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michaelfig, could we not include this export * from './src/types.js';
in the index.js
, so that we can get rid of the exported.js
? I believe the only purpose it served was for making sure marshal ambient types were loaded, which we want to get rid of, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have gone ahead and done that. It reads much better.
/** @typedef {import('../types').Checker} Checker */ | ||
/** @typedef {import('../types').PassStyle} PassStyle */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're sticking with JSDoc, maybe we can submit a PR for TypeScript to have a nicer syntax for this.
In regular TS this is clean:
import type {Checker, PassStyle} from '../types`
In JSDoc TS maybe,
/**
* @typeimport '../types' {Checker, PassStyle}
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the first to want this :) microsoft/TypeScript#22160
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I left some non-blocking comments.
Glad to see support for type imports, which came up in Agoric/agoric-sdk#4410 (comment)
export function makeMarshal( | ||
convertValToSlot = defaultValToSlotFn, | ||
convertSlotToVal = defaultSlotToValFn, | ||
export const makeMarshal = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have we considered a global code mod to convert functions? https://github.com/facebook/jscodeshift works great
f7345c8
to
b55a2be
Compare
I took over the PR and extended it with generation of declaration files for marshal, and rebased on latest master + preliminary TS 4.5 compat PR I opened in #1043. I've checked that with the appropriate updates to Agoric/agoric-sdk#4413, all seem to be working well. This shows how we can move away from ambient types, and stop requiring TS from trying to parse the JS of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some non-blocking suggestions and questions.
4ecf8ab
to
2c0bff0
Compare
c5f2ef7
to
b0d11fa
Compare
Reverted some of the changes so this PR is now purely type changes (and a couple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving away from ambient types is a good but breaking change. This will require some ceremony.
In each affected NEWS.md
, we need a note for # Next release
of what end-users will need to do when they upgrade.
At least one commit must have !
in the conventional commit and a *BREAKING CHANGE*: ...
paragraph that will get rolled into the changelog and bump the version appropriately.
When integrating into Agoric SDK, I use scripts/sync-versions.sh ../endo
and that rolls over all the semver bumps, so all of the alignment will have to occur in the Sync Endo PR.
2bbf300
to
8513cfb
Compare
4a62453
to
be9d5ac
Compare
In the commit message, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address my comment about the commit message having *BREAKING CHANGE:*
verbatim. Otherwise, this is great progress.
be9d5ac
to
ac69e63
Compare
Done. @michaelfig could you re-review as a comment? I'll carry your review as mine (since you are the original author...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! Just a few minor cleanups I'd recommend, but not blocking.
Keep using JSdoc, remove exported.js *BREAKING CHANGE*: This change switches to exported types and removes the `exported.js` file which provided ambient types.
Also add NEWS.md in artifact
1e0194b
to
5235b7f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Carrying @michaelfig's approval
I just noticed that building the d.ts files (e.g. by running prepack) spits them out adjacent to the .js files, which aren't gitignored and could end up being checked in. Any reasons not to output the build into a |
Because we'd need to also copy the .js source, which would be incompatible with non-publish same repo usage |
We'd only need to copy the .js source if we pointed A problem with how it is now (besides the gitignore) is that |
I am not ok with anything that requires same repo build step to have anything work. It's a foot gun. How can they be out of sync? They're only generated when publishing a package, and not needed same repo as tsc will happily parse the JS to get type definitions when the dependency is not in node_modules |
I'm not familiar with the foot injuries you have in mind. In my experience watch mode suffices to keep things up to date.
They're generated when publishing a package, but don't go away on their own. Then the source .js can be edited and is out of sync. Not "expected", but perhaps a "foot gun". |
True, we need a clean step after publish, and / or move publishing to CI. Watch step requires developer environment setup. I think the fact these build artifacts are not ignored is probably a good thing as it'll remind a clean is needed. |
I would very much prefer not to need a watch mode. |
@turadg, there's a |
After my last publish, the |
I find with the following, things work as they should (at least wrt. clean). Explain again why we're including diff --git a/packages/marshal/jsconfig.json b/packages/marshal/jsconfig.json
index b455fcf0d..0611838b4 100644
--- a/packages/marshal/jsconfig.json
+++ b/packages/marshal/jsconfig.json
@@ -15,5 +15,5 @@
"strictNullChecks": true,
"moduleResolution": "node",
},
- "include": ["*.js", "*.ts", "src/**/*.js", "src/**/*.ts", "test/**/*.js"]
+ "include": ["*.js", "src/**/*.js", "test/**/*.js"]
} |
That’s good to know. That patch is acceptable. I made the change to cover |
Maybe the correct solution then is to do: diff --git a/packages/marshal/jsconfig.build.json b/packages/marshal/jsconfig.build.json
index 13018509f..ea9465e90 100644
--- a/packages/marshal/jsconfig.build.json
+++ b/packages/marshal/jsconfig.build.json
@@ -6,5 +6,5 @@
"emitDeclarationOnly": true,
"declarationMap": true
},
- "exclude": ["test/"]
+ "exclude": ["test/", "**/*.ts"]
} |
Experiment to remove dependency on ambient types. The process:
export {};
near the top of every JSDoctypes.js
file. This switches it from an ambient file to a module with exported types.exported.js
so that ambient type users break hard.export * from './src/types.js';
toindex.js
so that upstream consumers can import types from the package identifier. Season with eslint-ignores to taste.import './types.js';
,import './internal-types.js';
etc./** @typedef {import('./types.js').MemberName} MemberName */
or for template types/** @template T @typedef {import('./types.js').MemberName2<T>} MemberName2 */
. as needed to the top of.js
sources.package.json
to include generated files./** @typedef {import('@agoric/marshal').MyType} MyType */
, and remove anyimport '@agoric/marshal/exported';
.--maxNodeModulesJsDepth
hack that used to be needed with JSDoc ambient type innode_modules
.