Skip to content

Automatically create JS versions of our TS code in the docs #2638

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

Merged
merged 29 commits into from
May 13, 2025

Conversation

cprecioso
Copy link
Member

@cprecioso cprecioso commented Apr 8, 2025

Description

Part of #2455

This PR allows to just write TS code directly, and automatically transform it and appear with the JS/TS selector.

It does so by adding two plugins to Docusaurus' MDX handling:

  • autoJSCode will find any code block with the auto-js meta, transform it with ts-blank-space, and remove the extra blank space with prettier.

    Example transformation

    Input:

     ```ts auto-js title="hello.ts"
     const foo = (n: number) => 8 as string
     ```

    Equivalent output (actual output is done in AST):

     <Tabs groupId="js-ts">
     <TabItem value="js" label="JavaScript">
     
     ```js title="hello.js"
     const foo = (n) => 8
     ```
     
     </TabItem>
     <TabItem value="ts" label="TypeScript">
     
     ```ts title="hello.ts"
     const foo = (n: number) => 8 as string
     ```
     
     </TabItem>
     </Tabs>
    Why use `ts-blank-space`?

    Basically, it's the transformer that does the fewest modifications to the input code, which I think is important to keep the overall visual structure of the code in the examples. It also helps to not introduce TS-only constructs like enums that, when automatically converted, introduce distracting noise.

    I also feel validated in its approach since it uses the TypeScript compiler directly to do its job, and not a custom parser that might get outdated. As well, the blank-space-replacing method is used by SWC and Node.js in their TypeScript support.

  • codeWithHole is made to deal with some of our examples, that just omit some syntactically needed parts of the code with .... In order for the TypeScript transformation to work correctly, this plugin allows us to use an identifier named hole that will be replaced by ..., while still being syntactically correct.

    Example transformation

    Input:

     ```ts with-hole
     const foo = (n: number) => {
     	return {
     		name: "bar",
     		hole
     	}
     }
     ```

    Equivalent output (actual output is done in AST):

     ```ts
     const foo = (n: number) => {
     	return {
     		name: "bar",
     		/* ... */
     	}
     }
     ```

With these two plugins, we can transform our examples to be less complex to author while keeping the option to read our docs in JS or TS. If there are any examples that can't be automatically converted, or that shouldn't be converted, we still retain the option to do it manually as we've been doing until now.

I converted two examples so you can see the output, please check them out.

@cprecioso cprecioso requested review from Martinsos and infomiho April 8, 2025 09:32
@cprecioso cprecioso changed the title Automatically create JS versions of our TS code Automatically create JS versions of our TS code in the docs Apr 8, 2025
Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

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

Very cool! Seems like an awesome thing to me, much less duplication! And we can still do it manually if we really need to (we can right? Would we then use those Tabs again?).

I will let others check the details of the PR and approve it, to make sure it works fully with how we do things currently (e.g. if you toggle to JS, does it remmeber the toggle? Mostly checking if we lost anything with not using our Tab system anymore). Aha but I see now that we actually do still use Tabs, they are produced by the plugin -> ok that sounds good then!

One th ing that would certainly be valuable is more documentation on this -> we should mention in docs README.md that we are using this mechanism because it is a bit magic, so having a central place with some discoverability where we describe it would be valuable.

Also, these remark/ files, we should provide some top level header comments in them, let's say one at the top of each file, to explain a bit the motivation and what are they here for, as they are a bit unexpected.
Also I didn't review the code in them, I will let the others do that.

Awesome all together!

@cprecioso
Copy link
Member Author

we should mention in docs README.md

top level header comments

Yes! I wanted to get this out to get some comments on the approach but I wanted to do this, I'll get on it

@Martinsos
Copy link
Member

we should mention in docs README.md

top level header comments

Yes! I wanted to get this out to get some comments on the approach but I wanted to do this, I'll get on it

No worries, I assumed so!
Advice: If you want the people to take a look at the PR, but not do a deep review -> make a draft PR. That basically communicates exactly that, that you are looking for comments, but you not done yet. While real PR means it is ready for proper reviewing.

@cprecioso cprecioso self-assigned this Apr 8, 2025
@cprecioso
Copy link
Member Author

Can I get a review? 🙏🏼

Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

I love this idea. It will make our lives much easier.

Testing

I tried with some more complex examples:

<Tabs groupId="js-ts">

  • here the tricky part are the TS specific comments (which stay in the JS version), we'd probably need to rewrite those code blocks not to use those comments and move that info somewhere outside of the code block

Another example:

<Tabs groupId="js-ts">

We show Prisma code with the language switch for completeness sake - it toggles between different variants of the text below. So I guess, here we'll just use the tabs directly, you addressed it in the README, that's great 👍

In this example:

<Tabs groupId="js-ts">

The JS version still has the import statement (without anything imported):
Screenshot 2025-04-11 at 13 57 10

For me, the next step would be - try to convert all the code blocks to see where the current approach fails and try to address those cases. For some of the cases we'll need to modify the code (e.g. to exclude Typescript specific comments), that's okay for me.

@cprecioso
Copy link
Member Author

@infomiho

try to convert all the code blocks

Are we sure converting 230 occurrences of this pattern is a good use of the team's time? 😅
image

I was planning on us leaving stuff as it is; writing new stuff with this plugin, and converting on the go whenever we touch a file.

@infomiho
Copy link
Contributor

I think it's a valid use of team time since it will speed us the docs authoring process. It took me a couple of minutes per code block to do it, so it might be a few hours max IMHO.

What I looking to achieve: find the the edge cases for which the plugin might not produce 1:1 JS code. While this is still hot, while you have the context, I think it'll be good to figure out and write down the process/limits of the plugin.

Let's do it like this: invest one hour to convert as many examples as possible and try going for as many unique examples as possible 🙂

@cprecioso cprecioso marked this pull request as draft April 16, 2025 15:01
@cprecioso
Copy link
Member Author

Turning to draft until #2658 is merged

@cprecioso cprecioso changed the title Automatically create JS versions of our TS code in the docs WIP: Automatically create JS versions of our TS code in the docs Apr 25, 2025
@infomiho
Copy link
Contributor

infomiho commented May 5, 2025

I suppose this PR can go forward since #2658 is merged 🎉

@cprecioso
Copy link
Member Author

@infomiho ready for review

I tried updating the tutorial and some examples (https://github.com/wasp-lang/wasp/pull/2728/files), and the main drawbacks that I found were

  • We need to remember to add import type so they don't appear in JS
  • Edge cases:
    • // highlight-next-line comment before a TS-only line will hang around and highlight the wrong line (happened twice)
    • We're running prettier so in some cases where we're putting extra spaces around some lines to make them clearer and they get collapsed (only happened once)
    • It doesn't replace file names' extensions in clarification comments (I'd say this problem is almost unique to the tutorial)

All in all, those are edge cases that might be out of scope for this, and they're not too impactful. We can always write the tabs manually for whatever edge case arises, so this is a "strictly better" improvement IMO. I'm comfortable merging.

@cprecioso cprecioso marked this pull request as ready for review May 7, 2025 10:41
@cprecioso cprecioso changed the title WIP: Automatically create JS versions of our TS code in the docs Automatically create JS versions of our TS code in the docs May 7, 2025
Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

Great stuff @cprecioso, I think we are almost there. I'm glad you tested it out on more code blocks to get a better sense of caveats. I think the benefits definitely outweigh the caveats.

I've left some more comments, let me know when I can take another look.

Comment on lines +131 to +147
```tsx title="src/pages/SomePage.tsx" auto-js with-hole
import React, { useEffect } from "react";
import { api } from "wasp/client/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}

export const Foo = () => {
useEffect(() => {
fetchCustomRoute();
}, []);

return <>{hole}</>;
};
```
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing I noticed that when I try to auto format this code in VS Code, the code block doesn't get formatted and the code in the output is of course formatted.

Screenshot 2025-05-12 at 13 17 44

Is there a way for us to configure Prettier to format the code blocks in the MDX files? I think it makes sense for code blocks in the source code to look as similar as possible to the rendered code blocks for easier searching later on.

If it's not possible to run Prettier on MDX files (this issue suggests so) should we make it a part of the docs authoring process to format the code before including it in the code blocks?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, as you found out, Prettier is stuck in MDXv1 so I'd rather not run it on our MDXv3 files, it will mess up its delicate whitespace situation 😑

should we make it a part of the docs authoring process to format the code before including it in the code blocks?

You mean just adding that to the readme? I can do that.

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'd rather not have to run Prettier at all if we don't have it at the source, but it needs to run on the JS so it removes the blank space (I didn't find another type stripper that doesn't leave the whitespace), and then on the TS for consistency. 😔

Copy link
Contributor

@infomiho infomiho May 13, 2025

Choose a reason for hiding this comment

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

Yep, I thought maybe about adding a note in the README to say, hey, format it so the source code and the rendered code look as close as possible for better searchablity.

Too bad about missing MDX 3 support, since Docusaurus moved, you'd expect it to be more demand for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe one for the future, npx remark . --ext .mdx --output should probably format MDX 3 if used with the remark-mdx plugin. You probably know more about this than me, just throwing it out there.

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 added it here BTW

- Run `prettier` on the code before pasting it in the document, as `auto-js` will enforce it.

cprecioso and others added 8 commits May 12, 2025 13:35
- Use `unist-util-visit`, no need for `unist-util-visit-parents`.
- Extract the common functions of the visitor check and the language assert to a shared file
- For the async visitor, create a thunk inside of the visitor so we keep all the type narrowing instead of having to do it again
Co-authored-by: Mihovil Ilakovac <mihovil@ilakovac.com>
Co-authored-by: Mihovil Ilakovac <mihovil@ilakovac.com>
@cprecioso
Copy link
Member Author

@infomiho ready

@cprecioso
Copy link
Member Author

@infomiho if everything is ready let's merge ahead of the sprint review? 😁

Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

Naming comments, almost there

Copy link
Contributor

@infomiho infomiho left a comment

Choose a reason for hiding this comment

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

Great addition

@cprecioso cprecioso merged commit 5420929 into main May 13, 2025
@cprecioso cprecioso deleted the auto-js-docs branch May 13, 2025 12:07
Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

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

I am writing remark plugin myself (coderef checker) and therefore got interested in this PR, so I started reviewing this PR while it was still unmerged, but it got merged in the meantime! Which is ok, I joined the party really late.

I however finished the review, so here it is. We can do initial discussion of items here, while you do the fixes in the new PR, and then we switch to that one after the first iteration of changes.

Comment on lines +28 to +32
To write docs, you can use Markdown or MDX. The docs are located in the `docs` directory.
Remember to refer to the [Writing Guide](https://wasp.sh/docs/writingguide) for an explanation
of how we like to write docs. You can check
[Docusaurus' documentation](https://docusaurus.io/docs/2.x/markdown-features) to see which special
Markdown features available (e.g. line highlighting).
Copy link
Member

Choose a reason for hiding this comment

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

You will notice that in the rest of the markdown we (me for sure, I think others also) don't stick to 80/100 lines as the max col, but take as much space as we need. That is because editors do good job of visually wrapping long lines and it makes writing text easier (you don't need to keep formatting it as you edit it).
Instead, what I do is usually start each sentences in a new line. That makes it easy to move from sentence to sentence also. I am not sure we ever communicated this at the team level though, but it might be happening organically, or maybe it is just me and I think everybody else is also doing it hah :D. But I think it has its benefits?

Suggested change
To write docs, you can use Markdown or MDX. The docs are located in the `docs` directory.
Remember to refer to the [Writing Guide](https://wasp.sh/docs/writingguide) for an explanation
of how we like to write docs. You can check
[Docusaurus' documentation](https://docusaurus.io/docs/2.x/markdown-features) to see which special
Markdown features available (e.g. line highlighting).
To write docs, you can use Markdown or MDX. The docs are located in the `docs` directory.
Remember to refer to the [Writing Guide](https://wasp.sh/docs/writingguide) for an explanation
of how we like to write docs.
You can check [Docusaurus' documentation](https://docusaurus.io/docs/2.x/markdown-features) to see which special Markdown features available (e.g. line highlighting).

@@ -23,6 +23,97 @@ $ npm start
This command starts a local development server and opens up a browser window.
Most changes are reflected live without having to restart the server.

### Writing docs
Copy link
Member

Choose a reason for hiding this comment

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

There are quite a few headers already in this README about dealing with docs. They are also level 3 headers. It somehow feels like this specific header is the entry header for docs though. Would it make more sense to make this one level 2 header, call it ## Docs, and then put this at the start and the rest of the headers under it (which is already happening actually)?

Further question is if some of this content would be better suited in writingguide.md or even docs/README.md (which doesn't exist yet), but we don't have to deal with that now (but maybe in the future we will want to move most of this content into docs/README.md).

Copy link
Member Author

@cprecioso cprecioso May 15, 2025

Choose a reason for hiding this comment

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

We have web/readme.md#writing docs, web/writing-docs.md and web/docs/writingguide.md. It seems like there's an opportunity for centralization here but tbh I don't know where to start from, the end result would be a monster doc

#### Polyglot code blocks

For examples that have a JavaScript and TypeScript version, add a `auto-js` meta attribute
to the code block, like so:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
to the code block, like so:
to the TypeScript code block, like so:

```
~~~

And it will automatically generate a JS and TS version with a selector to switch between them:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
And it will automatically generate a JS and TS version with a selector to switch between them:
And it will automatically generate a JS version based on the TS one + a selector to switch between them:

otherwise it sound slike it generates both js and ts version, which is not what we wanted to say, right?

> You can create a language switcher manually as described in
> [Docusaurus docs](https://docusaurus.io/docs/2.x/markdown-features/code-blocks#multi-language-support-code-blocks).

If you need to omit some part of the code in a code example, you can use the `with-hole` meta attribute
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 under "polyglot code blocks", but it has nothing to do with polyglot code blocks, right? It would make more sense to put explanation about "hole" under separate header. Or maybe you can rename the header to "code blocks". Even then, each of these parts, auto-js and with-hole, might benefit from their own headers.

return formatted.trim() // prettier adds a trailing newline, we remove it
}

function transformExt(inPath: string, fn: (ext: string) => string) {
Copy link
Member

Choose a reason for hiding this comment

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

Let's define better what this function does.

  • inPath -> confusing name, in is here because of internal naming (outPath), but me as a caller of this function, I don't know anything about it. I was for real trying to figure out what inPath means, till I looked into the implementation, I thought this is just some part of path, something inside path maybe. We could just name it filepath or even path, it would be clearer.
  • fn -> ok, name is short but relatively clear from the context what it does. Could be maybe transformFn, but fn also works. One thing I don't know, and it is also not defined, is if fn expects .js or js. I am guessing the second one, but I am not 100% sure, path.extname for example returns .ext, which to me doesn't make sense, but that is how it works. Would be good to have this specified in the comment of the function.
  • Would we benefit from return type? This is Haskell speaking from me, but I love return types on functions because they communicate clear intention from the developer. I know in TS that is not so clear, it makes sense sometimes to not have a return type, but I don't see anything bad about having one here, I can understand from just reading the function header what it does (does it replace path in place or does it return one? Ok, it returns one, and it is string), without having to read the implementation and guess what will TS infer.

Copy link
Member

Choose a reason for hiding this comment

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

Btw how does this function behave if:

  1. I give it path that is a directory (so has no extension)
    1.1. which doesn't end with /.
    1.2. which does end with /.
  2. Transformation function doesn't transform anything, ext remains the same -> are we ok with that, if that happens, or do we rather want to fail/throw?

Btw, I am not saying you need to handle all those case with the directory in the code, I wouldn't waste time on that if you don't need it and it is complex, but what you can do is make it clear in the comments what is your expectation from the path -> you expect it to have extension for example, and caller is to ensure that, otherwise it is UB (undefined behaviour). That is enough already, just saying that in the function header comment.

Btw, to explain why I am commenting on this stuff: I understand that this is local utility function, it's not used in any other places, so at the moment it serves its purpose well even with not all this stuff being defined, but:

  1. At some point somebody might want to use it in some other file, maybe another plugin, or even just another place in this same file. Then, they have to guess what was the intended behaviour -> because there certainly was some intended behaviour, that served well the purpose so far. But it is not clear which of that behaviour was intentional, and which was not.
  2. Similar, if somebody needs to modify the existing usage, they will have to guess what were the unwritten assumptions / expectations and think through those, instead of them being clear upfront.

Comment on lines +189 to +192
const inExt = path.extname(inPath)
const outExt = fn(inExt)
const outPath = inPath.slice(0, -inExt.length) + outExt
return outPath
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: in and out as nomenclature is a bit unusual. I guess it means in and out of function, but that is not something I usually see used in such way. Something like old and new might be a better fit, or even no prefix (just path) and new (newPath).

Copy link
Member

Choose a reason for hiding this comment

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

Some golfing, I find it more readable (less var names to reason about), but I get it if you don't, you don't have to do it:

Suggested change
const inExt = path.extname(inPath)
const outExt = fn(inExt)
const outPath = inPath.slice(0, -inExt.length) + outExt
return outPath
const ext = path.extname(path)
return path.slice(0, -ext.length) + transformExt(ext)

@@ -0,0 +1,49 @@
/*
This file defines a plugin for the unified library that processes code blocks
Copy link
Member

Choose a reason for hiding this comment

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

Also has this interesting start with "plugin for the unified library" hah :D.

This file defines a plugin for the unified library that processes code blocks
in Markdown documents. It looks for code blocks with a specific meta flag
(`with-hole`) and replaces occurrences of the word "hole" in the code with
a placeholder comment (`...`). This is useful for code examples where
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 curious, so does it replace it with a comment always, or sometimes just raw ...? Because I think I saw somewhere earler that in JS it is just ... while in TS it is /* ... */. Also the example here, in parenthesses, is just ..., so is that a comment or not hm. Ok yeah I see below now in the Example also that it is not in a comment.

node.meta = node.meta.replace(META_FLAG_REGEX, '')

// Replace hole with ellipsis
node.value = node.value.replace(HOLE_IDENTIFIER_REGEX, HOLE_REPLACEMENT)
Copy link
Member

@Martinsos Martinsos May 14, 2025

Choose a reason for hiding this comment

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

Oh wow this is very crude -> so we just take the whole code from the code block as a string and replace hole with a regex. I was expecting it does something more refined: go through the actual AST of the js/ts, and if it is a certain type of node (identifier, maybe field, ...), and its whole value equals hole, then we replace it.
Because this will trigger on stuff like `"fire in the hole!" (where hole is just a word in a string), or if it is a part of regex, or part of comment even.

But I guess getting that js/ts AST might be quite some work, is that the problem, and also putting it back together? remark doesn't go as far as to already provide the AST for the language in the code block for us, does it? And we think it is not likely that hole will appear in our code blocks in any any string/comment/regex/... ?

If that is all so, I get it, we shouldn't waste time on handling something that is likely never to happen, but should we then at least give it a longer and more specific name, so reduce the change of false positives? Instead of hole, let's go for code-hole or hole-here or $code-hole or $hole-to-be-replaced or something like that. Oh, or CODE_HOLE, that is also nicely visible in the code example then.

@cprecioso cprecioso restored the auto-js-docs branch May 15, 2025 13:35
@cprecioso cprecioso deleted the auto-js-docs branch May 15, 2025 13:37
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.

3 participants