-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Automatically generate tutorial app from Markdown #2732
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
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Mihovil Ilakovac <mihovil@ilakovac.com>
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 that, for consistency, I'd also have write
actions take in a patch.
Deploying wasp-docs-on-main with
|
Latest commit: |
6c3935d
|
Status: | ✅ Deploy successful! |
Preview URL: | https://672e0989.wasp-docs-on-main.pages.dev |
Branch Preview URL: | https://miho-tutorial-app-from-docs.wasp-docs-on-main.pages.dev |
Signed-off-by: Mihovil Ilakovac <mihovil@ilakovac.com>
a260193
to
1348c89
Compare
|
||
Tutorial actions are defined in MDX files using JSX components: | ||
|
||
````markdown |
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.
````markdown | |
````mdx |
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 use this on any code block with MDX.
@FranjoMindek @Martinsos ready for another look |
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.
@infomiho I reviewed once again, but I have to admit I didn't go into TS code -> there is just too much of it. I would say we leave it as it is, it will be good enough.
Only thing I wonder, if it is missing, is e2e test(s). I commented on it before, maybe having a single test would be valuable. I wonder how do you know test if tacte works? You run it manually? If so, we wnat to have a test for it. Let's discuss it a bit at least, any ideas? How hard are they, would it make sense to do it now? if not, why not?
@@ -0,0 +1,120 @@ | |||
import { useState } from "react"; | |||
/* | |||
`TutorialAction` component is related to the Tutorial Actions Executor (TACTE) which you can find in the `web/tutorial-actions-executor` folder. |
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.
Btw I see we keep using step and action interchangeably. I think you did make it uniform in the code, so it is Actions, and that is good, but it is very natural to say Step, and we do sometimes say it in the docs I think. I wonder if "step" is really the word we should be using all the time, and not "action". How did we end up with "action", and what do you thikn about that? Isn't it only introducing confusion? Because "Tutorial Actions Executor" makes me wonder, ok what are these actions? While "Tutorial Step Executor" wouldn't make me wonder about what step is, it is clear. Or even just "Tutorial Executor", then I would just assume it executes tutorial steps.
I know this question sounds blah, because who will rename all that, but on the other hand, much better to take care of this now then to have to do it later, and I am already thinking about open sourcing.
|
||
Wasp docs have a tutorial that walks users through building a complete Wasp application step-by-step. | ||
|
||
Next to the text that explains each step, we added `<TutorialAction>` components that define machine-executable steps, |
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.
Just an example, notice how you used step 5 times here, and then you have TutorialAction, which again you say is a step. So whyd o we call it an action is the question.
a patch fails due to conflicts, `generate-app` command pauses and allows you | ||
to resolve the conflicts manually. | ||
|
||
### 2. Edit Action (`npm run edit-action`) |
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.
Should it be called edit-patch-action
?
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.
Updated the name 👍
- Executes all actions before the target action | ||
- Moves all the changes from the target action to the Git staging area | ||
- Allows you to edit the code in your editor | ||
- Updates the patch based on your changes | ||
- Reapplies all subsequent actions |
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.
Either periods at the ends, or no capital letter at the start. You have more bullet points lists like this one for same goes for those.
import type { AppDirPath } from "../tutorialApp"; | ||
import type { Action, ApplyPatchAction } from "./actions"; | ||
|
||
export function commitActionChanges({ |
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.
Doesn't sem like his function does much? It just strips id from action? Is it worth having it for that? I guess types. But is it wroth having comitAllChanges then? Why not combine these two?
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've built this in layers:
commitActionChanges -> commitAllChanges -> git command
commitAllChanges
expects astring
commit messagecommitActionChanges
expects anAction
-> which callscommitAllChanges
with the action ID as 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.
I went over changes since last review.
); | ||
} | ||
|
||
export async function getActionsFromMdxContent( |
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'm not sure we need this function? Why not keep it inline?
I don't feel we gain anything?
Worse yet we still pass the sourceTutorialFilePath
even if it is named getActionsFromMdxContent
?
Why do we need file path if it works on content?
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 totally forgot to add a unit test for this function. I wanted to do IO for reading the content in one function and then do the actual parsing of the content in getActionsFromMdxContent
.
I've added the unit tests now 👍
Also, created an issue to run the unit tests in the CI: #3247
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 wanted to do IO for reading the content in one function and then do the actual parsing of the content in getActionsFromMdxContent.
But why?
That is my question. What's the motivation here? What do we gain?
We don't abstract anything useful in my opinion.
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 getActionsFromMdxFile
function reads a file from the disk and I didn't want to mock reading file from the disk to be able to test the core logic. This way I have a pure function which I test by passing in a string and see if the output is okay 🤷
Would you rather mock fs.readFile
?
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.
Hm I agree that purity is great.
However, I think that the way we separate them is not great currently.
Something is off about it to me.
I tried to follow my senses a bit, but I still didn't mange to do it to be happy enough.
Maybe this inspires something in you?
I'll leave it up to your decision.
async function getActionsFromMdxFile(
sourceTutorialFilePath: MdxFilePath,
tutorialApp: TutorialApp,
): Promise<Action[]> {
try {
const mdxFileContent = await fs.readFile(
path.resolve(sourceTutorialFilePath),
);
const tutorialActionAttributes =
await parseTutorialActionNodesFromMdxContent(mdxFileContent);
return tutorialActionAttributes.map((tutorialActionNode) =>
createTutorialAction(
tutorialApp,
sourceTutorialFilePath,
tutorialActionNode,
),
);
} catch (err) {
throw new Error(
`Error processing tutorial file '${sourceTutorialFilePath}'`,
{ cause: err },
);
}
}
export async function parseTutorialActionNodesFromMdxContent(
fileContent: Buffer<ArrayBufferLike>,
): Promise<TutorialActionAttributes[]> {
const nodes: TutorialActionAttributes[] = [];
const ast = fromMarkdown(fileContent, {
extensions: [mdxJsx({ acorn, addResult: true })],
mdastExtensions: [mdxJsxFromMarkdown()],
});
const tutorialComponentName = "TutorialAction";
visit(ast, "mdxJsxFlowElement", (node) => {
if (node.name !== tutorialComponentName) {
return;
}
nodes.push(parseTutorialActionNode(node));
});
return nodes;
}
// would be better if it was some discriminated union
// but I don't know your types too well
// it should be easily inferred somewhere in the type system
// or I think we are doing something wrong
interface TutorialActionAttributes {
actionId: ActionId;
actionKind: Action["kind"];
starterTemplateName: string | null;
}
function parseTutorialActionNode(
tutorialActionNode: MdxJsxFlowElement,
): TutorialActionAttributes {
const actionId = getAttributeValue(
tutorialActionNode,
"id",
) as ActionId | null;
if (!actionId) {
throw new Error(
// TODO: give meaningful location where this error happened
`TutorialAction component requires the 'id' attribute. ${tutorialActionNode.position}`,
);
}
const actionKind = getAttributeValue(tutorialActionNode, "action") as
| Action["kind"]
| null;
if (!actionKind) {
throw new Error(
// TODO: give meaningful location where this error happened
`TutorialAction component requires the 'action' attribute. ${tutorialActionNode.position}`,
);
}
const starterTemplateName = getAttributeValue(
tutorialActionNode,
"starterTemplateName",
);
if (actionKind === "INIT_APP" && !starterTemplateName) {
// TODO: give meaningful location where this error happened
throw new Error(
`TutorialAction component with action 'INIT_APP' requires the 'starterTemplateName' attribute. ${tutorialActionNode.position}`,
);
}
return {
actionId,
actionKind,
starterTemplateName,
};
}
function createTutorialAction(
tutorialApp: TutorialApp,
sourceTutorialFilePath: MdxFilePath,
{ actionKind, actionId, starterTemplateName }: TutorialActionAttributes,
): Action {
const commonData: BaseAction = {
id: actionId,
sourceTutorialFilePath,
};
switch (actionKind) {
case "INIT_APP":
// if we make TutorialActionAttributes a discriminated union
// we wouldn't need the ! here
return createInitAppAction(commonData, starterTemplateName!);
case "APPLY_PATCH":
return createApplyPatchAction(
commonData,
tutorialApp.docsTutorialPatchesPath,
);
case "MIGRATE_DB":
return createMigrateDbAction(commonData);
default:
assertUnreachable(
actionKind,
`Unknown action '${actionKind}' in TutorialAction component`,
);
}
}
export function sortTutorialFileNames(filePaths: string[]): string[] { | ||
/** | ||
* Sorts a list of files which are named "01-something.md" by their numeric prefix. | ||
* @param filePaths |
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.
@param filePaths
is unnecessary I think with typescript.
We don't really want to add any types to this, so why even have it at all?
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.
It was auto-generated and I left it here, I'll delete it 👍
This PR implements running all the steps from the tutorial in the docs. We encode all the code changes into Git patches which we apply one by one.
We implement tooling for working with the patches when something breaks:
There are two commands:
generate-app
which handles creating and apply patchesedit-step
which is used to edit a patch and it rebases the changesCheck out the
README.md
for more info on using theNote for reviewers: please try it out and think of any process that might be useful to cover with the CLI
Closes #2814