stackblitz / tutorialkit

TutorialKit by StackBlitz - Create interactive tutorials powered by the WebContainer API
https://tutorialkit.dev
MIT License
465 stars 42 forks source link

External scripts don't work #306

Open iliakan opened 2 months ago

iliakan commented 2 months ago

Describe the bug

Adding an external script to a lesson doesn't work.

Steps to reproduce

  1. Add <script src="landing.js"></script> to content.md of a lesson.
  2. Create landing.js file with console.log(1) in the lesson folder.
  3. Run npm run dev and open the lesson in the browser.
  4. The script is not executed - the corresponding HTTP request returns 404.

Expected behavior

Astro supports scripts. I'd expect the script to work.

Platform

P.S.

Adding image.png to the lesson and including it as ![](ball.png) works.

Perhaps, there's an alternative "proper" way to add an external script?

AriPerkkio commented 2 months ago

I'm not sure if Astro supports this out-of-the-box (cc. @Nemikolh, ???). But one thing you could do is convert the content.md into content.mdx, import the script contents as raw string and add it to markup:

---
type: lesson
title: Example
---

import landing from "./_files/landing.js?raw"

# Test

<script>
  {landing}
</script>

But if you just want to add some interactivity to lesson markdown, I would recommend to check Astro docs: https://docs.astro.build/en/basics/astro-components/ and https://docs.astro.build/en/guides/client-side-scripts/

Nemikolh commented 2 months ago

With Astro's content collection, I would advise against having code that is part of your app inside your content/* folders (the only exception being content/config.ts which is treated specially by Astro).

I find that it's usually nicer to have the following structure:

src
├── utils
│   └── script.ts
├── content
│   ├── config.ts
│   └── tutorial
│       ├── meta.md
│       └── ...
...

Which then let you import your script like this in any content.mdx:

---
type: lesson
title: Example
---

import "@utils/script";

# My lesson

The nice thing is that you can import your script using the same import in any lesson.

If you want to add a ui element, you might find it useful to have a component instead which let's you access and modify the state of the tutorial:

With a src/components/UpdateFileButton.tsx file:

import tutorialStore from 'tutorialkit:store';

interface Props {
  path: string;
  content: string;
}

export function UpdateFileButton({ path, content }: Props) {
  function writeFile() {
    tutorialStore.updateFile(path, content);
  }

  return (
    <button onClick={writeFile}>
      Update file
    </button>
  );
}

You can then use it like this in your lessons:

---
type: lesson
title: Example
---

import { UpdateFileButton } from "@components/UpdateFileButton";

# My lesson

<UpdateFileButton client:load path="/index.js" content="console.log('Hello world!')" />
iliakan commented 1 month ago

So it's ok to put images in content, but not scripts? Even if the script is for a particular lesson only?

AriPerkkio commented 1 month ago

@iliakan could you describe your use case a bit more? What does the landing.js do? Why does it need to be run outside webcontainer as well?

iliakan commented 1 month ago

@AriPerkkio for this particular case I was going to create a landing script to nicely show callouts above the webcontainer and the editor, to introduce the environment in a clear visual style.

Nemikolh commented 1 month ago

So it's ok to put images in content, but not scripts? Even if the script is for a particular lesson only?

I mean you can also have them there but it won't be as nice as having them outside in terms of DX with vite because we exclude those folders from vite's optimizeDeps entry: https://github.com/stackblitz/tutorialkit/blob/1eb41fd917ceff08fe9484bd07e732cb673a7a1f/packages/astro/src/index.ts#L87-L89

So IIRC, it means that vite won't be scanning those scripts and they might load a bit slower in development.

Note that they are excluded for a reason: we do not want vite to try to resolve imports for file that ends in WebContainer as it leads to confusing errors