feat(nx-dev): tutorial landing page and improvements (#30757)

Updates the online tutorial experience

- Adds a landing page at
[/tutorials](https://nx-dev-git-docs-tutorial-landing-page-nrwl.vercel.app/tutorials)
- Terminal code blocks get a "run in terminal" button
- Code blocks get an "Apply file changes" button
- The apply file changes button currently only works for code blocks
that are showing the new file results (not showing the old file with
lines marked for deletion). There is nothing technical blocking this,
just time.
- Previous and next buttons do not go between tutorials
- The preview panel can be completely minimized
- Git is stubbed out
This commit is contained in:
Isaac Mann 2025-04-23 15:08:51 -04:00 committed by GitHub
parent 53ef31e18f
commit 0e16f98c27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 828 additions and 73 deletions

View File

@ -0,0 +1,136 @@
'use client';
import { DefaultLayout, SectionHeading } from '@nx/nx-dev/ui-common';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { contactButton } from '../lib/components/headerCtaConfigs';
import { cx } from '@nx/nx-dev/ui-primitives';
import { Framework, frameworkIcons } from '@nx/graph/legacy/icons';
export default function Tutorials(): JSX.Element {
const router = useRouter();
return (
<>
<NextSeo
title="Nx Tutorials"
description="Get started with Nx by following along with one of these tutorials"
openGraph={{
url: 'https://nx.dev' + router.asPath,
title: 'Nx Tutorials',
description:
'Get started with Nx by following along with one of these tutorials',
images: [
{
url: 'https://nx.dev/socials/nx-media.png',
width: 800,
height: 421,
alt: 'Nx: Smart Monorepos · Fast CI',
type: 'image/jpeg',
},
],
siteName: 'Nx',
type: 'website',
}}
/>
<DefaultLayout headerCTAConfig={[contactButton]}>
<section>
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-4xl text-center">
<SectionHeading
as="h1"
variant="display"
className="pt-4 text-4xl sm:text-5xl md:text-6xl"
>
Nx Tutorials
</SectionHeading>
<SectionHeading
as="p"
variant="subtitle"
className="mt-6 text-center sm:text-lg"
>
Get started with Nx by following along with one of these
tutorials.
</SectionHeading>
</div>
</div>
</section>
<section className="mt-16">
<div className="col-span-2 border-y border-slate-200 bg-slate-50 px-6 py-8 md:col-span-4 lg:py-16 dark:border-slate-800 dark:bg-slate-900">
<div className="mx-auto max-w-7xl text-center">
<dl className="grid grid-cols-1 justify-between gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<TutorialCard
title="TypeScript Monorepo"
type="Tutorial"
url="/tutorials/1-ts-packages/1-introduction/1-welcome"
icon="jsMono"
/>
<TutorialCard
title="React Monorepo"
type="Tutorial"
url="/tutorials/2-react-monorepo/1r-introduction/1-welcome"
icon="reactMono"
/>
<TutorialCard
title="Angular Monorepo"
type="Tutorial"
url="/tutorials/3-angular-monorepo/1a-introduction/1-welcome"
icon="angularMono"
/>
<TutorialCard
title="Gradle Monorepo"
type="Tutorial"
url="/getting-started/tutorials/gradle-tutorial"
icon="gradle"
/>
</dl>
</div>
</div>
</section>
</DefaultLayout>
</>
);
}
function TutorialCard({
title,
type,
icon,
url,
}: {
title: string;
type: string;
icon: string; // Can be either a component name or a direct image URL
url: string;
}) {
return (
<a
key={title}
href={url}
className="no-prose relative col-span-1 mx-auto flex w-full max-w-md flex-col items-center rounded-md border border-slate-200 bg-slate-50/40 p-4 text-center font-semibold shadow-sm transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-slate-100 dark:border-slate-800/40 dark:bg-slate-800/60 dark:hover:bg-slate-800"
style={{ textDecorationLine: 'none' }}
>
{icon && (
<div className="mb-2 flex h-24 w-24 items-center justify-center rounded-lg">
{icon.startsWith('/') ? (
<img
src={icon}
alt={title}
className="h-full w-full object-contain"
/>
) : (
frameworkIcons[icon as Framework]?.image
)}
</div>
)}
<div className={cx({ 'pt-4': !!icon })}>
<div className="mb-1 text-xs font-medium uppercase text-slate-600 dark:text-slate-300">
{type}
</div>
<h3 className="m-0 text-lg font-semibold text-slate-900 dark:text-white">
{title}
</h3>
</div>
</a>
);
}

View File

@ -1,6 +1,8 @@
import tutorialkit from '@tutorialkit/astro';
import { defineConfig, envField } from 'astro/config';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { runInTerminalPlugin } from './src/code-block-button/run-in-terminal-plugin';
import { applyFileChangesPlugin } from './src/code-block-button/apply-file-changes-plugin';
export const config = defineConfig({
base: '/tutorials',
@ -45,6 +47,8 @@ export const config = defineConfig({
HeadTags: './src/components/HeadTags.astro',
TopBar: './src/components/TopBar.astro',
},
defaultRoutes: 'tutorial-only',
expressiveCodePlugins: [runInTerminalPlugin(), applyFileChangesPlugin()],
}),
],
});

View File

@ -0,0 +1,21 @@
import * as esbuild from 'esbuild';
import { writeFileSync, readFileSync } from 'fs';
const output = esbuild
.transformSync(readFileSync('./src/code-block-button/js-module.ts'), {
loader: 'ts',
minify: true,
})
.code.replaceAll('\n', '\\n');
writeFileSync(
'./src/code-block-button/js-module.min.ts',
`/**
* GENERATED FILE - DO NOT EDIT
* This JS module code was built from the source file "js-module.ts".
* To change it, modify the source file and then re-run the build script.
*/
export default '${output}';
`
);

View File

@ -6,8 +6,21 @@
"tags": [],
"// targets": "to see all targets run: nx show project tutorial --web",
"targets": {
"build-code-block-button": {
"command": "node ./compile-js-module.mjs",
"inputs": [
"{projectRoot}/src/code-block-button/compile-js-module.mjs",
"{projectRoot}/src/code-block-button/js-module.ts",
{ "externalDependencies": ["esbuild"] }
],
"outputs": ["{projectRoot}/src/code-block-button/js-module.min.ts"],
"options": {
"cwd": "{projectRoot}"
}
},
"build": {
"inputs": ["{projectRoot}/src/**/**"]
"inputs": ["{projectRoot}/src/**/**"],
"outputs": ["{projectRoot}/dist"]
},
"lint": {
"command": "echo no linting",

View File

@ -0,0 +1,30 @@
import { PluginTexts, type ExpressiveCodePlugin } from '@expressive-code/core';
import { pluginCodeBlockButton } from './base-plugin';
const svg = `<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='2 2 20 20' stroke-width='1.5' stroke='currentColor' class='size-6'><path stroke-linecap='round' stroke-linejoin='round' d='M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z' /></svg>`;
export const applyFileChangesTexts = new PluginTexts({
buttonTooltip: 'Apply file changes',
buttonExecuted: 'File updated...',
});
export function applyFileChangesPlugin(): ExpressiveCodePlugin {
return pluginCodeBlockButton(
'applyFileChanges',
svg,
applyFileChangesTexts,
(codeBlock, isTerminal) =>
!isTerminal &&
['solution:', 'file:'].some((prefix) =>
codeBlock.metaOptions.getString('path')?.startsWith(prefix)
),
(codeBlock, _) => {
return {
'data-filepath': codeBlock.metaOptions
.getString('path')
.replace('solution:', '')
.replace('file:', ''),
};
}
);
}

View File

@ -0,0 +1,305 @@
import type {
ExpressiveCodeBlock,
ExpressiveCodePlugin,
ResolverContext,
} from '@expressive-code/core';
import { codeLineClass, PluginTexts } from '@expressive-code/core';
import { h } from '@expressive-code/core/hast';
import codeBlockButtonJsModule from './js-module.min';
const terminalLanguageGroups = [
'ansi',
'bash',
'bat',
'batch',
'cmd',
'console',
'nu',
'nushell',
'powershell',
'ps',
'ps1',
'psd1',
'psm1',
'sh',
'shell',
'shellscript',
'shellsession',
'zsh',
];
export function isTerminalLanguage(language: string) {
return terminalLanguageGroups.includes(language);
}
export const frameTypes = ['code', 'terminal', 'none', 'auto'] as const;
export type FrameType = (typeof frameTypes)[number];
export function getFramesBaseStyles(
name: string,
svg: string,
{ cssVar }: ResolverContext
) {
const escapedSvg = svg.replace(/</g, '%3C').replace(/>/g, '%3E');
const svgUrl = `url("data:image/svg+xml,${escapedSvg}")`;
const buttonStyles = `.${name} {
display: flex;
gap: 0.25rem;
flex-direction: row;
position: absolute;
inset-block-start: calc(${cssVar('borderWidth')} + var(--button-spacing));
inset-inline-end: calc(${cssVar('borderWidth')} + ${cssVar(
'uiPaddingInline'
)} / 2 + var(--button-spacing));
/* hide code block button when there is no JavaScript */
@media (scripting: none) {
display: none;
}
/* RTL support: Code is always LTR, so the inline code block button
must match this to avoid overlapping the start of lines */
direction: ltr;
unicode-bidi: isolate;
button {
position: relative;
align-self: flex-end;
margin: 0;
padding: 0;
border: none;
border-radius: 0.2rem;
z-index: 1;
cursor: pointer;
transition-property: opacity, background, border-color;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
/* Mobile-first styles: Make the button visible and tappable */
width: 2.5rem;
height: 2.5rem;
background: var(--code-background);
opacity: 0.75;
div {
position: absolute;
inset: 0;
border-radius: inherit;
background: ${cssVar('frames.inlineButtonBackground')};
opacity: ${cssVar('frames.inlineButtonBackgroundIdleOpacity')};
transition-property: inherit;
transition-duration: inherit;
transition-timing-function: inherit;
}
&::before {
content: '';
position: absolute;
pointer-events: none;
inset: 0;
border-radius: inherit;
border: ${cssVar('borderWidth')} solid ${cssVar(
'frames.inlineButtonBorder'
)};
opacity: ${cssVar('frames.inlineButtonBorderOpacity')};
}
&::after {
content: '';
position: absolute;
pointer-events: none;
inset: 0;
background-color: ${cssVar('frames.inlineButtonForeground')};
-webkit-mask-image: ${svgUrl};
-webkit-mask-repeat: no-repeat;
mask-image: ${svgUrl};
mask-repeat: no-repeat;
margin: 0.475rem;
line-height: 0;
}
/*
On hover or focus, make the button fully opaque
and set hover/focus background opacity
*/
&:hover, &:focus:focus-visible {
opacity: 1;
div {
opacity: ${cssVar(
'frames.inlineButtonBackgroundHoverOrFocusOpacity'
)};
}
}
/* On press, set active background opacity */
&:active {
opacity: 1;
div {
opacity: ${cssVar(
'frames.inlineButtonBackgroundActiveOpacity'
)};
}
}
}
.feedback {
--tooltip-arrow-size: 0.35rem;
--tooltip-bg: ${cssVar('frames.tooltipSuccessBackground')};
color: ${cssVar('frames.tooltipSuccessForeground')};
pointer-events: none;
user-select: none;
-webkit-user-select: none;
position: relative;
align-self: center;
background-color: var(--tooltip-bg);
z-index: 99;
padding: 0.125rem 0.75rem;
border-radius: 0.2rem;
margin-inline-end: var(--tooltip-arrow-size);
opacity: 0;
transition-property: opacity, transform;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
transform: translate3d(0, 0.25rem, 0);
&::after {
content: '';
position: absolute;
pointer-events: none;
top: calc(50% - var(--tooltip-arrow-size));
inset-inline-end: calc(-2 * (var(--tooltip-arrow-size) - 0.5px));
border: var(--tooltip-arrow-size) solid transparent;
border-inline-start-color: var(--tooltip-bg);
}
&.show {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
}
@media (hover: hover) {
/* If a mouse is available, hide the button by default and make it smaller */
.${name} button {
opacity: 0;
width: 2rem;
height: 2rem;
}
/* Reveal the non-hovered button in the following cases:
- when the frame is hovered
- when a sibling inside the frame is focused
- when the code block button shows a visible feedback message
*/
.frame:hover .${name} button:not(:hover),
.frame:focus-within :focus-visible ~ .${name} button:not(:hover),
.frame .${name} .feedback.show ~ button:not(:hover) {
opacity: 0.75;
}
}
/* Increase end padding of the first line for the code block button */
:nth-child(1 of .${codeLineClass}) .code {
padding-inline-end: calc(2rem + ${cssVar('codePaddingInline')});
}`;
return buttonStyles;
}
export interface PluginFramesProps {
/**
* The code block's title. For terminal frames, this is displayed as the terminal window title,
* and for code frames, it's displayed as the file name in an open file tab.
*
* If no title is given, the plugin will try to automatically extract a title from a
* [file name comment](https://expressive-code.com/key-features/frames/#file-name-comments)
* inside your code, unless disabled by the `extractFileNameFromCode` option.
*/
title: string;
/**
* Allows you to override the automatic frame type detection for a code block.
*
* The supported values are `code`, `terminal`, `none` and `auto`.
*
* @default `auto`
*/
frame: FrameType;
}
export interface TextMap {
buttonTooltip: string;
buttonExecuted: string;
}
export const defaultPluginCodeBlockButtonTexts = new PluginTexts({
buttonTooltip: 'Run in terminal',
buttonExecuted: 'Command executing...',
});
const svg = [
`<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' viewBox='0 0 16 16'>`,
`<path style='stroke-width:1.67364' d='M 12.97535,8 2.5068619,2.5406619 l 0,10.8728961 z m 1.403517,-1.1001222 c 0.953442,0.4868922 0.953442,1.7133522 0,2.2002444 L 3.102884,14.935828 C 2.1813829,15.413179 0.91786322,14.86786 0.91786322,13.835705 V 2.1642945 c 0,-1.0321548 1.26351968,-1.57747398 2.18502078,-1.1001221 z'/>`,
`</svg>`,
].join('');
export function pluginCodeBlockButton(
name: string = 'runInTerminal',
iconSvg: string = svg,
pluginCodeBlockButtonTexts: PluginTexts<TextMap> = defaultPluginCodeBlockButtonTexts,
shouldShowButton: (
codeBlock: ExpressiveCodeBlock,
isTerminal: boolean
) => boolean = () => true,
addAttributes: (
codeBlock: ExpressiveCodeBlock,
isTerminal: boolean
) => Record<string, string> = () => ({})
): ExpressiveCodePlugin {
return {
name,
baseStyles: (context: any) => getFramesBaseStyles(name, iconSvg, context),
jsModules: [
codeBlockButtonJsModule
.replace('[SELECTOR]', `.expressive-code .${name} button`)
.replace('[BUTTON_NAME]', name),
],
hooks: {
postprocessRenderedBlock: ({ codeBlock, renderData, locale }) => {
// get text strings for the current locale
const texts = pluginCodeBlockButtonTexts.get(locale);
// retrieve information about the current block
const { frame = 'auto' } = codeBlock.props;
const isTerminal =
frame === 'terminal' ||
(frame === 'auto' && isTerminalLanguage(codeBlock.language));
const extraElements: any[] = [];
if (shouldShowButton(codeBlock, isTerminal)) {
extraElements.push(
h('div', { className: name }, [
h(
'button',
{
title: texts.buttonTooltip,
'data-copied': texts.buttonExecuted,
...addAttributes(codeBlock, isTerminal),
},
[h('div')]
),
])
);
renderData.blockAst.children.push(...extraElements);
}
},
},
};
}

View File

@ -0,0 +1,7 @@
/**
* GENERATED FILE - DO NOT EDIT
* This JS module code was built from the source file "js-module.ts".
* To change it, modify the source file and then re-run the build script.
*/
export default '(function(){async function i(n){const t=n.currentTarget,o=t.dataset,c=o.code?.replace(/\u007f/g,`\n`),u=o.filepath;if(t.dispatchEvent(new CustomEvent("tutorialkit:[BUTTON_NAME]",{detail:{code:c,filepath:u},bubbles:!0})),t.parentNode?.querySelector(".feedback"))return;let e=document.createElement("div");e.classList.add("feedback"),e.append(o.copied),t.before(e),e.offsetWidth,requestAnimationFrame(()=>e?.classList.add("show"));const a=()=>!e||e.classList.remove("show"),d=()=>{!e||parseFloat(getComputedStyle(e).opacity)>0||(e.remove(),e=void 0)};setTimeout(a,1500),setTimeout(d,2500),t.addEventListener("blur",a),e.addEventListener("transitioncancel",d),e.addEventListener("transitionend",d)}const r="[SELECTOR]";function s(n){n.querySelectorAll?.(r).forEach(t=>t.addEventListener("click",i))}s(document),new MutationObserver(n=>n.forEach(t=>t.addedNodes.forEach(o=>{s(o)}))).observe(document.body,{childList:!0,subtree:!0}),document.addEventListener("astro:page-load",()=>{s(document)})})();\n';

View File

@ -0,0 +1,91 @@
// Compile this with `npx esbuild nx-dev/tutorial/src/code-block-button/js-module.ts --minify`
(function () {
/**
* Handles clicks on a single copy button.
*/
async function clickHandler(event: Event) {
/*
* Attempt to perform the copy operation, first using the Clipboard API,
* and then falling back to a DOM-based approach
*/
const button = event.currentTarget as HTMLButtonElement;
const dataset = button.dataset as {
code: string;
copied: string;
filepath: string;
};
const code = dataset.code?.replace(/\u007f/g, '\n');
const filepath = dataset.filepath;
button.dispatchEvent(
new CustomEvent('tutorialkit:[BUTTON_NAME]', {
detail: { code, filepath },
bubbles: true,
})
);
// Exit if the copy operation failed or there is already a tooltip present
if (button.parentNode?.querySelector('.feedback')) {
return;
}
// Show feedback tooltip
let tooltip: HTMLDivElement | undefined = document.createElement('div');
tooltip.classList.add('feedback');
tooltip.append(dataset.copied);
button.before(tooltip);
/*
* Use offsetWidth and requestAnimationFrame to opt out of DOM batching,
* which helps to ensure that the transition on 'show' works
*/
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
tooltip.offsetWidth;
requestAnimationFrame(() => tooltip?.classList.add('show'));
// Hide & remove the tooltip again when we no longer need it
const hideTooltip = () => !tooltip || tooltip.classList.remove('show');
const removeTooltip = () => {
if (!(!tooltip || parseFloat(getComputedStyle(tooltip).opacity) > 0)) {
tooltip.remove();
tooltip = undefined;
}
};
setTimeout(hideTooltip, 1500);
setTimeout(removeTooltip, 2500);
button.addEventListener('blur', hideTooltip);
tooltip.addEventListener('transitioncancel', removeTooltip);
tooltip.addEventListener('transitionend', removeTooltip);
}
const SELECTOR = '[SELECTOR]';
/**
* Searches a node for matching buttons and initializes them
* unless the node does not support querySelectorAll (e.g. a text node).
*/
function initButtons(container: ParentNode | Document) {
container
.querySelectorAll?.(SELECTOR)
.forEach((btn) => btn.addEventListener('click', clickHandler));
}
// Use the function to initialize all buttons that exist right now
initButtons(document);
// Register a MutationObserver to initialize any new buttons added later
const newButtonsObserver = new MutationObserver((mutations) =>
mutations.forEach((mutation) =>
mutation.addedNodes.forEach((node) => {
initButtons(node as ParentNode);
})
)
);
newButtonsObserver.observe(document.body, { childList: true, subtree: true });
// Also re-initialize all buttons after view transitions initiated by popular frameworks
document.addEventListener('astro:page-load', () => {
initButtons(document);
});
})();

View File

@ -0,0 +1,36 @@
import { PluginTexts, type ExpressiveCodePlugin } from '@expressive-code/core';
import { pluginCodeBlockButton } from './base-plugin';
const svg = [
`<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='2 2 20 20' stroke-width='1.5' stroke='currentColor' class='size-6'>`,
`<path stroke-linecap='round' stroke-linejoin='round' d='M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z' />`,
`</svg>`,
].join('');
export const runInTerminalTexts = new PluginTexts({
buttonTooltip: 'Run in terminal',
buttonExecuted: 'Command executing...',
});
export function runInTerminalPlugin(): ExpressiveCodePlugin {
return pluginCodeBlockButton(
'runInTerminal',
svg,
runInTerminalTexts,
(_, isTerminal) => isTerminal,
(codeBlock, _) => {
// remove comment lines starting with `#` from terminal frames
let code = codeBlock.code.replace(/(?<=^|\n)\s*#.*($|\n+)/g, '').trim();
/**
* Replace all line breaks with a special character
* because HAST does not encode them in attribute values
* (which seems to work, but looks ugly in the HTML source)
*/
code = code.replace(/\n/g, '\u007f');
return {
'data-code': code,
};
}
);
}

View File

@ -0,0 +1,103 @@
'use client';
import { useEffect } from 'react';
import { webcontainer } from 'tutorialkit:core';
import tutorialStore from 'tutorialkit:store';
export function GlobalCustomizations() {
useEffect(() => {
// These actions run on every page load
// Disable previous and next buttons if this is the first or last lesson of a tutorial
function waitForTopBar() {
if (!document.querySelector('#top-bar')) {
setTimeout(waitForTopBar, 100);
} else {
if (document.querySelector('#top-bar.first-lesson')) {
const [topPrevButton, bottomPrevButton] = document
.querySelectorAll('[class*=i-ph-arrow-left]')
.values()
.map((el) => el.parentElement);
topPrevButton.classList.add('opacity-32', 'pointer-events-none');
topPrevButton.setAttribute('aria-disabled', 'true');
topPrevButton.removeAttribute('href');
bottomPrevButton?.remove();
}
if (document.querySelector('#top-bar.last-lesson')) {
const [topNextButton, bottomNextButton] = document
.querySelectorAll('[class*=i-ph-arrow-right]')
.values()
.map((el) => el.parentElement);
topNextButton.classList.add('opacity-32', 'pointer-events-none');
topNextButton.setAttribute('aria-disabled', 'true');
topNextButton.removeAttribute('href');
bottomNextButton?.remove();
}
}
}
waitForTopBar();
webcontainer.then(async (wc) => {
// Stub out git command
await wc.fs.writeFile(
'git',
'echo "Git is not available in a WebContainer"'
);
const terminal = tutorialStore.terminalConfig.get().panels[0]?.terminal;
if (!terminal) {
return;
}
terminal?.input('echo "hi"\n');
function callOnce(fn: Function) {
let called = false;
return function () {
if (!called) {
called = true;
fn();
}
};
}
(terminal as any).onLineFeed(
callOnce(() => {
setTimeout(() => {
terminal.input('export PATH="$PATH:/home/tutorial"\n');
setTimeout(() => {
terminal.input('clear\n');
}, 10);
}, 10);
})
);
});
// Run these actions only once
const tempData: any = window;
if (tempData.globalCustomizationsInitialized) {
return;
}
tempData.globalCustomizationsInitialized = true;
// Apply file changes
async function applyFileChanges(e: any) {
const { filepath } = e.detail;
if (!filepath) {
return;
}
tutorialStore.updateFile(
filepath,
(tutorialStore as any)._lessonSolution[filepath]
);
tutorialStore.setSelectedFile(filepath);
}
document.addEventListener('tutorialkit:applyFileChanges', applyFileChanges);
// Run code in terminal when tutorialkit:runInTerminal event is triggered
function runInTerminal(e: any) {
if (tutorialStore.hasTerminalPanel()) {
tutorialStore.terminalConfig
.get()
.panels[0].terminal?.input(e.detail.code + '\n');
}
}
document.addEventListener('tutorialkit:runInTerminal', runInTerminal);
}, []);
return null;
}

View File

@ -1,9 +1,16 @@
---
import { GlobalCustomizations } from "./GlobalCustomizations";
import { FrontendObservability } from "./Observability";
import { getCollection } from 'astro:content';
const entries = await getCollection('tutorial', ({ slug }) => {
return slug.startsWith(Astro.url.pathname.replace('/tutorials/', ''));
});
const customMeta = (entries[0]?.data as any)?.custom;
const addedClass = [customMeta?.first && 'first-lesson', customMeta?.last && 'last-lesson', ' '].filter(Boolean).join(' ');
---
<nav
class="bg-tk-elements-panel-header-backgroundColor transition-theme border-b border-tk-elements-app-borderColor flex max-w-full min-h-[68px]"
<nav id="top-bar"
class={addedClass + "bg-tk-elements-panel-header-backgroundColor transition-theme border-b border-tk-elements-app-borderColor flex max-w-full min-h-[68px]"}
>
<div class="flex flex-1 p-1 gap-4 lg:px-8 lg:py-3 text-tk-elements-app-textColor items-center">
<a
@ -28,4 +35,5 @@ import { FrontendObservability } from "./Observability";
</div>
</div>
<FrontendObservability client:load />
<GlobalCustomizations client:load />
</nav>

View File

@ -2,6 +2,8 @@
type: lesson
title: Starting Repository
focus: /package.json
custom:
first: true
---
# TypeScript Monorepo Tutorial

View File

@ -10,24 +10,8 @@ focus: /nx.json
You may have noticed in the `packages/zoo/package.json` file, there is a `serve` script that expects the `build` task to already have created the `dist` folder. Let's set up a task pipeline that will guarantee that the project's `build` task has been run.
```json title="nx.json" {4-6}
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"serve": {
"dependsOn": ["build"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"],
"cache": true
},
"typecheck": {
"cache": true
}
},
"defaultBase": "main"
}
```solution:/nx.json title="nx.json" {4-6} collapse={2,7-14}
```
The `serve` target's `dependsOn` line makes Nx run the `build` task for the current project before running the current project's `build` task.

View File

@ -26,19 +26,19 @@ Set the bundler to `tsc`, the linter to `none` and the unit test runner to `none
Now we can move the `getRandomItem` function from `packages/names/names.ts` and `packages/animals/animals.ts` into the `packages/util/src/lib/util.ts` file.
```ts title="packages/util/src/lib/util.ts"
```solution:/packages/util/src/lib/util.ts title="packages/util/src/lib/util.ts"
export function getRandomItem<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
```
```ts title="packages/animals/animals.ts"
```solution:/packages/animals/animals.ts title="packages/animals/animals.ts" collapse={3-150}
import { getRandomItem } from '@tuskdesign/util';
// ...
```
```ts title="packages/names/names.ts"
```solution:/packages/names/names.ts title="packages/names/names.ts" collapse={3-150}
import { getRandomItem } from '@tuskdesign/util';
// ...

View File

@ -1,6 +1,8 @@
---
type: lesson
title: Open a Pull Request
custom:
last: true
---
## Open a Pull Request

View File

@ -1,6 +1,8 @@
---
type: lesson
title: Why Use a Monorepo?
custom:
first: true
---
# React Monorepo Tutorial

View File

@ -23,14 +23,8 @@ npx nx show project @react-monorepo/react-store
If you expand the `build` task, you can see that it was created by the `@nx/vite` plugin by analyzing your `vite.config.ts` file. Notice the outputs are defined as `{projectRoot}/dist`. This value is being read from the `build.outDir` defined in your `vite.config.ts` file. Let's change that value in your `vite.config.ts` file:
```ts title="apps/react-store/vite.config.ts"
export default defineConfig({
// ...
build: {
outDir: './build',
// ...
},
});
```solution:/apps/react-store/vite.config.ts title="apps/react-store/vite.config.ts" collapse={1-6,8-23,26-30,32-42}
```
Now if you close and reopen the project details view, the outputs for the build target will say `{projectRoot}/build`. This feature ensures that Nx will always cache the correct files.

View File

@ -1,6 +1,8 @@
---
type: lesson
title: Open a Pull Request
custom:
last: true
---
## Open a Pull Request

View File

@ -1,6 +1,8 @@
---
type: lesson
title: Why Use a Monorepo?
custom:
first: true
---
# Angular Monorepo Tutorial

View File

@ -1,6 +1,8 @@
---
type: lesson
title: Open a Pull Request
custom:
last: true
---
## Open a Pull Request

View File

@ -9,5 +9,6 @@ i18n:
partTemplate: 'Tutorial: ${title}'
downloadAsZip: true
prepareCommands:
- ['chmod +x git', 'Stubbing git']
- ['npm install', 'Installing dependencies']
---

View File

@ -1,4 +1,3 @@
import { join } from 'path';
import { uriTransformer } from './uri-transformer';
export function transformImagePath(
@ -13,7 +12,14 @@ export function transformImagePath(
if (isRelative) {
return uriTransformer(
join('/', documentFilePath.split('/').splice(3).join('/'), '..', src)
new URL(
[
'http://example.com',
documentFilePath.split('/').splice(3).join('/'),
'..',
src,
].join('/')
).pathname
);
}

View File

@ -51,6 +51,7 @@
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^2.1.1",
"@eslint/js": "^8.48.0",
"@expressive-code/core": "0.35.6",
"@floating-ui/react": "0.26.6",
"@iconify-json/ph": "^1.1.12",
"@iconify-json/svg-spinners": "^1.1.2",
@ -130,10 +131,10 @@
"@swc/helpers": "0.5.11",
"@swc/jest": "0.2.36",
"@testing-library/react": "15.0.6",
"@tutorialkit/astro": "1.3.1",
"@tutorialkit/react": "1.3.1",
"@tutorialkit/theme": "1.3.1",
"@tutorialkit/types": "1.3.1",
"@tutorialkit/astro": "1.5.0",
"@tutorialkit/react": "1.5.0",
"@tutorialkit/theme": "1.5.0",
"@tutorialkit/types": "1.5.0",
"@types/cytoscape": "^3.18.2",
"@types/detect-port": "^1.3.2",
"@types/ejs": "3.1.2",

73
pnpm-lock.yaml generated
View File

@ -244,6 +244,9 @@ importers:
'@eslint/js':
specifier: ^8.48.0
version: 8.57.1
'@expressive-code/core':
specifier: 0.35.6
version: 0.35.6
'@floating-ui/react':
specifier: 0.26.6
version: 0.26.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -482,17 +485,17 @@ importers:
specifier: 15.0.6
version: 15.0.6(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tutorialkit/astro':
specifier: 1.3.1
version: 1.3.1(@types/node@20.16.10)(@types/react-dom@18.3.0)(astro@4.15.0(@types/node@20.16.10)(less@4.1.3)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(typescript@5.7.3))(less@4.1.3)(postcss@8.4.38)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
specifier: 1.5.0
version: 1.5.0(@types/node@20.16.10)(@types/react-dom@18.3.0)(astro@4.15.0(@types/node@20.16.10)(less@4.1.3)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(typescript@5.7.3))(less@4.1.3)(postcss@8.4.38)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/react':
specifier: 1.3.1
version: 1.3.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
specifier: 1.5.0
version: 1.5.0(@types/react-dom@18.3.0)(@types/react@18.3.1)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/theme':
specifier: 1.3.1
version: 1.3.1(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
specifier: 1.5.0
version: 1.5.0(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/types':
specifier: 1.3.1
version: 1.3.1
specifier: 1.5.0
version: 1.5.0
'@types/cytoscape':
specifier: ^3.18.2
version: 3.21.8
@ -8029,22 +8032,22 @@ packages:
resolution: {integrity: sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==}
engines: {node: ^18.17.0 || >=20.5.0}
'@tutorialkit/astro@1.3.1':
resolution: {integrity: sha512-1K+G32ZMLk+KgvIKFNIKYN3IzR7kWEn3X6ZOIO9yH6hr7BCIcxyVXDOH20w6F+RR0lFH2ylY3CCr5PxYHjsWKw==}
'@tutorialkit/astro@1.5.0':
resolution: {integrity: sha512-XFCCZIJkzsj2dOiLqQfrGSaUGGRMxSjqSMl7GO4de3k7Dkq55oFddQwzBdSFh6Zg1SSQ13QN6G8+cwVDdBNTiw==}
peerDependencies:
astro: ^4.15.0
'@tutorialkit/react@1.3.1':
resolution: {integrity: sha512-9DoSbZbHwsqAoPNf/v6o1jEJT6/CL/UptIUw4fxqBMEg33R1Uq704uM9ySOQL0HtA/fGfFdUnJGh4KniSe+fBw==}
'@tutorialkit/react@1.5.0':
resolution: {integrity: sha512-+fcDXxbK6RO0Gf9DqodWPcVTJPkPwnKbaS+ZQdaFLdVcxWQ237r0MbK/srL7EJrldITvGu6no+KbMS3E/GhGIg==}
'@tutorialkit/runtime@1.3.1':
resolution: {integrity: sha512-ou/GPeQpuSRI4i/M9dmsj07dOJGeZZB6ETbAQMwVWsTOhYCZ4qUk3blmrnkyKl2gHC9hYdProIKa5OkHBE37cg==}
'@tutorialkit/runtime@1.5.0':
resolution: {integrity: sha512-pVl2B4QAozAtIO7PCSUDPbSSSBUk2XnVRcafYnRWSRjbnWAxgnVQ+ZHSxl3G9acWhxujjLZjsYgVRQci3eIWVg==}
'@tutorialkit/theme@1.3.1':
resolution: {integrity: sha512-VTzDAPt3Hj9E3NfxnhOUshCWoAB4uMRP2AhvrqXLMQkdEluJ2GkXOreEzS8WWcsPzr4jqyFFj39wYwBBRuqd1w==}
'@tutorialkit/theme@1.5.0':
resolution: {integrity: sha512-EE4vrbuXaHZ0B4qcwKe9rdUiDja8wgxJScKvJHdNVDlWEusCmcmbhH2yj782H1Y8dGJ7kS32Qki6w4ycu115Ww==}
'@tutorialkit/types@1.3.1':
resolution: {integrity: sha512-4V7Jxsmeyq2a6UZBYTkNSl3NG75SZvFG/pUuxjtZVO1/EVY93Pz1f1RqNhuzlQf1d/rEOYlREMU25GWOij4z2Q==}
'@tutorialkit/types@1.5.0':
resolution: {integrity: sha512-+XnpaMwCLgqWyK29jopK/XGTvyEP6K3CoZuNa9eDk6HFndWawsm5ENrH7/f04kPfkhkbxaccSCFzykxQAneT2g==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@ -29126,7 +29129,7 @@ snapshots:
'@tufjs/canonical-json': 2.0.0
minimatch: 9.0.5
'@tutorialkit/astro@1.3.1(@types/node@20.16.10)(@types/react-dom@18.3.0)(astro@4.15.0(@types/node@20.16.10)(less@4.1.3)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(typescript@5.7.3))(less@4.1.3)(postcss@8.4.38)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
'@tutorialkit/astro@1.5.0(@types/node@20.16.10)(@types/react-dom@18.3.0)(astro@4.15.0(@types/node@20.16.10)(less@4.1.3)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(typescript@5.7.3))(less@4.1.3)(postcss@8.4.38)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
dependencies:
'@astrojs/mdx': 3.1.9(astro@4.15.0(@types/node@20.16.10)(less@4.1.3)(rollup@4.22.0)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(typescript@5.7.3))
'@astrojs/react': 3.6.3(@types/node@20.16.10)(@types/react-dom@18.3.0)(@types/react@18.3.20)(less@4.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)
@ -29134,10 +29137,10 @@ snapshots:
'@expressive-code/plugin-line-numbers': 0.35.6
'@nanostores/react': 0.7.2(nanostores@0.10.3)(react@18.3.1)
'@stackblitz/sdk': 1.11.0
'@tutorialkit/react': 1.3.1(@types/react-dom@18.3.0)(@types/react@18.3.20)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/runtime': 1.3.1
'@tutorialkit/theme': 1.3.1(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/types': 1.3.1
'@tutorialkit/react': 1.5.0(@types/react-dom@18.3.0)(@types/react@18.3.20)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/runtime': 1.5.0
'@tutorialkit/theme': 1.5.0(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/types': 1.5.0
'@types/react': 18.3.20
'@unocss/reset': 0.62.4
'@webcontainer/api': 1.5.1
@ -29175,7 +29178,7 @@ snapshots:
- terser
- vite
'@tutorialkit/react@1.3.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
'@tutorialkit/react@1.5.0(@types/react-dom@18.3.0)(@types/react@18.3.1)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/commands': 6.8.1
@ -29199,8 +29202,8 @@ snapshots:
'@radix-ui/react-context-menu': 2.2.7(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.18.6)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.3)(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2)
'@tutorialkit/runtime': 1.3.1
'@tutorialkit/theme': 1.3.1(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/runtime': 1.5.0
'@tutorialkit/theme': 1.5.0(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@webcontainer/api': 1.5.1
'@xterm/addon-fit': 0.10.0(@xterm/xterm@5.5.0)
'@xterm/addon-web-links': 0.11.0(@xterm/xterm@5.5.0)
@ -29222,7 +29225,7 @@ snapshots:
- supports-color
- vite
'@tutorialkit/react@1.3.1(@types/react-dom@18.3.0)(@types/react@18.3.20)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
'@tutorialkit/react@1.5.0(@types/react-dom@18.3.0)(@types/react@18.3.20)(postcss@8.4.38)(react-dom@18.3.1(react@18.3.1))(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/commands': 6.8.1
@ -29246,8 +29249,8 @@ snapshots:
'@radix-ui/react-context-menu': 2.2.7(@types/react-dom@18.3.0)(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.18.6)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.3)(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2)
'@tutorialkit/runtime': 1.3.1
'@tutorialkit/theme': 1.3.1(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@tutorialkit/runtime': 1.5.0
'@tutorialkit/theme': 1.5.0(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))
'@webcontainer/api': 1.5.1
'@xterm/addon-fit': 0.10.0(@xterm/xterm@5.5.0)
'@xterm/addon-web-links': 0.11.0(@xterm/xterm@5.5.0)
@ -29269,14 +29272,14 @@ snapshots:
- supports-color
- vite
'@tutorialkit/runtime@1.3.1':
'@tutorialkit/runtime@1.5.0':
dependencies:
'@tutorialkit/types': 1.3.1
'@tutorialkit/types': 1.5.0
'@webcontainer/api': 1.5.1
nanostores: 0.10.3
picomatch: 4.0.2
'@tutorialkit/theme@1.3.1(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
'@tutorialkit/theme@1.5.0(postcss@8.4.38)(rollup@4.22.0)(vite@6.2.0(@types/node@20.16.10)(jiti@1.21.6)(less@4.1.3)(sass-embedded@1.85.1)(sass@1.55.0)(stylus@0.64.0)(terser@5.39.0)(yaml@2.6.1))':
dependencies:
'@iconify-json/ph': 1.2.2
'@iconify-json/svg-spinners': 1.2.2
@ -29289,7 +29292,7 @@ snapshots:
- supports-color
- vite
'@tutorialkit/types@1.3.1':
'@tutorialkit/types@1.5.0':
dependencies:
zod: 3.23.8
@ -38619,7 +38622,7 @@ snapshots:
mlly@1.7.4:
dependencies:
acorn: 8.14.0
pathe: 2.0.2
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.5.4
@ -39857,7 +39860,7 @@ snapshots:
dependencies:
confbox: 0.1.8
mlly: 1.7.4
pathe: 2.0.2
pathe: 2.0.3
pkg-types@2.1.0:
dependencies: