-
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
⬆️ Add simple upgrade/downgrade package for MyST AST #1802
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: d01c5a7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
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 think the upgrade downgrade should be on the output of what is written to json in the site.
{
version: 1,
sha: "",
mdast: {},
citations: {order:} ... etc,
}
That way we can execute and move around.
Currently the upgrade/downgrade is on the mdast only (likely the bulk of the changes, but citations were a poor choice to do outside of the tree as an example...!).
import { downgrade } from './version_2_1.js'; | ||
import type { Parent } from 'mdast'; | ||
|
||
const SIMPLE_AST: Parent = { |
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 don't think it matters for now (as in we can get this in asap), however, I think this would be easier to manage with separate test dir with json files and then a global test runner.
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.
This is looking good to me - appreciate how it is simple and explicit.
import { upgrade as upgrade1To2 } from './version_1_2.js'; | ||
|
||
export function upgrade(from: string, to: string, ast: Parent): void { | ||
if (from === '1' && to === '2') { |
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.
This means we need to be careful and explicit about bumping version
in the myst metadata.
An alternative could be having tests against the content to determine if the upgrade is required (e.g. "if footnoteReference numbering is defined") - that could allow more flexibility to introduce compatibility updates that missed a version bump on the myst side.
I think the explicit version numbering is good, just musing on alternatives.
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.
Maybe a slightly different thought - do we need/want a minor_version
field here? Where we can have a bit more intentionality around what constitutes a major change (outputs) vs something that is pretty minor and can be dealt with with fallbacks in the theme, etc.
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.
@fwkoch right now we need to do some work to ensure that we're talking about the same "version" (being the schema version), certainly.
I have flipped between thinking about using a minor version vs major version. These kind of changes are hard to categorise. Ultimately, my experience with Packaging leads me to feel that version numbers are not very good at communication compatibility when "compatibility" is not well-defined. And for MyST's AST schema, I think that's the case (it's used in so many different ways with differing assumptions).
The angle I've been working of late is to prefer handling compatibility explicitly by downgrading/upgrading, rather than implicitly encoding compatibility in the version number semantics.
Do either of you see a case where this might be a problem?
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 agree that for the schema, major/minor versioning are less useful. Any change to the schema could be breaking for some client I think. So then a minor bump would only mean something more like "this change isn't breaking for us"
It gets extra fuzzy with how we've used myst-spec-ext
- because we have that package, the entire MyST development has happened with a single, unchanged version of myst-spec
despite the AST extending and evolving. Even further, the site JSON Rowan mentioned in his review is defined by myst-cli
only. So - does the upgrade/downgrade logic consider all those aspects and extended fields, but only is able to apply when the spec version changes?
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.
@fwkoch yes, this reminds me why I originally scoped the package to the AST; we don't export a promise about our page types.
Here's my view of where we go.
- At some point in future, we move to 🔄 Redefine schema in terms of
@types/mdast
myst-spec#67 - We add types for
$.json
files tomyst-spec
myst-spec-ext
becomes a shim that exports frommyst-spec
- We define a single version number for
myst-spec
that changes whenever we make an AST or auxiliary change. - We define a breaking change to be one that removes fields or changes types (in this case, "change" excludes "new fields").
This means that the MyST "spec" version is not just the MyST AST. I think that is probably a good thing, because the AST and the auxiliary state are intimately connected. This is especially pertinent now that MyST can consume .myst.json
files generated by other tools.
Consequently, we have the myst.xref.json
and myst.search.json
files which are separately versioned.
In the meantime, we could redefine the interface of this package to be
interface File {
mdast: Parent,
};
export function upgrade(file: File, from: Version, to: Version) {
}
This means that at some point we can grow the File
interface.
@rowanc1 @fwkoch do you have any feelings about this approach?
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.
We can not strongly type the interface. The file type will change over time, so that upgrade File
type will also change (e.g. citations is in there now, and won't be in the future). The types are known after the version is known, and then we can cast to the right type, which can be File_v1
in this package, or something.
For the options I would prefer them in a dict, so that we can add to it over time { throwError: false, inPlace: true }
etc. The upgrade options, should ideally be optional, and then the function should just upgrade to the latest.
upgrade(file);
upgrade(file, { from: 1, to: 15 });
The upgrade process should stamp the version into the file if it isn't there (where we are right now, which is 1
).
The mdast version and the file version should be the same, and I agree that myst.xref.json
and myst.search.json
have different version numbers.
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 think we can strongly type the interface; we already do that here for the AST. The idea is not to exhaustively type the AST, though; only the types that you actually need to work with during the upgrade step.
The upgrade options, should ideally be optional, and then the function should just upgrade to the latest.
Yes, I think if we add the proper version field, we can do-away with the from
version arg. We'll still want to
if the version consuming the package is older than the latest, though.
I've been toying with the same idea too. Doing that would require that "version" here refers to both the AST version and the version of the page data. We probably can tie the two together. |
@@ -0,0 +1,17 @@ | |||
import type { IFile } from '../types/index.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.
The idea here is that we accept unvalidated inputs, and enforce strict types inside the upgrade/downgrade routines
export interface IFile { | ||
version: any; | ||
mdast: any; | ||
} |
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.
The generic "untyped input" interface.
export interface IFile { | ||
version: '1'; | ||
mdast: any; | ||
} |
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 "more typed" per-version interface.
I don't imagine that we'll actually type mdast
— this would require bundling the entire MyST JSON spec with each iteration, which is probably overkill. Ultimately, we're still mostly going to need to just tell the type system that what we did was allowed. Let's not drown in types along the way.
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.
Agree that we don't need to do strong typing here at all.
import { upgrade } from './upgrade/index.js'; | ||
import type { IFile } from './types/index.js'; | ||
|
||
export function makeCompatible(src: IFile, to: string) { |
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.
Change this to opts: {to: string }
?
export function makeCompatible(src: IFile, to: string) { | |
export function migrate(src: IFile, to: string) { |
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.
In meeting the version is required. Can add options as third arg.
import { upgrade as upgrade1To2 } from './version_1_2.js'; | ||
|
||
export function upgrade(src: IFile, to: string): void { | ||
const { version: from } = src; |
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.
const { version: from } = src; | |
const { version: from = '1' } = src; |
I will take a pass through this and get it merged @agoose77. |
This PR adds a simple package that sets an API for upgrading and downgrading an AST. Over time, we can use this to define imperative motions between AST schema versions.
For now, this PR implements a new "version 2" of the MyST schema which drops the use of
number
in favour ofenumerator
in footnotes.Next Steps
mystmd