Skip to content

Commit

Permalink
feat: add template.visibleFiles option
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Jul 29, 2024
1 parent eb2521f commit 0e4e9e4
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 10 deletions.
21 changes: 21 additions & 0 deletions docs/tutorialkit.dev/src/content/docs/guides/creating-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ To get a better understanding of how the code you create in each lesson results

The `_solution` folder contains the code for the lesson in its solved state. This is the code that the user will see when they click the "Solve" button in the tutorial app. The folder usually contains the same files as the `_files` folder, but with the code filled in. It may also include additional files.

### File Visibility

Editor's files are resolved by three steps. Each step overrides previous one:

1. Display files matching `template.visibleFiles` (lowest priority)
2. Display files from `_files` directory
3. When solution is revealed, display files from `_solution` directory. (highest priority)

## Code templates

For the code to run in the preview, it must be an actual, working application, including a dev server and any necessary configuration files. Usually, it's not practical to include this kind of boilerplate files in a lesson content (and copy it for every further lesson!). Instead, you can use the `template` feature of TutorialKit to provide a base project that will be used for all lessons.
Expand All @@ -110,6 +118,19 @@ template: my-advanced-template

This declaration will make TutorialKit use the `src/templates/my-advanced-template` directory as the base for the lesson.

By default files in template are not shown in the code editor.
To make them visible, you can use `visibleFiles` option.
This can reduce repetition when you want to show same files visible in multiple lessons.

```markdown {5}
---
title: Advanced Topics
template:
name: my-advanced-template
visibleFiles: ['src/index.js', '**/utils/**']
---
```

If you start having a lot of templates and they all share some files, you can create a shared template that they all extend. This way, you can keep the shared files in one place and avoid duplication. To do that, you need to specify the `extends` property in the template's `.tk-config.json` file:

```json
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"kleur": "4.1.5",
"mdast-util-directive": "^3.0.0",
"mdast-util-to-markdown": "^2.1.0",
"micromatch": "^4.0.7",
"nanostores": "^0.10.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -62,6 +63,7 @@
"devDependencies": {
"@tutorialkit/types": "workspace:*",
"@types/mdast": "^4.0.4",
"@types/micromatch": "^4.0.9",
"esbuild": "^0.20.2",
"esbuild-node-externals": "^1.13.1",
"execa": "^9.2.0",
Expand Down
31 changes: 29 additions & 2 deletions packages/astro/src/default/utils/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { folderPathToFilesRef, interpolateString } from '@tutorialkit/types';
import { getCollection } from 'astro:content';
import glob from 'fast-glob';
import mm from 'micromatch';
import path from 'node:path';
import { IGNORED_FILES } from './constants';
import { DEFAULT_LOCALIZATION } from './content/default-localization';
Expand All @@ -18,6 +19,7 @@ import { logger } from './logger';
import { joinPaths } from './url';

const CONTENT_DIR = path.join(process.cwd(), 'src/content/tutorial');
const TEMPLATES_DIR = path.join(process.cwd(), 'src/templates');

export async function getTutorial(): Promise<Tutorial> {
const collection = sortCollection(await getCollection('tutorial'));
Expand Down Expand Up @@ -262,6 +264,22 @@ export async function getTutorial(): Promise<Tutorial> {
),
};

if (lesson.data.template && typeof lesson.data.template !== 'string' && lesson.data.template.visibleFiles?.length) {
const templateFilesRef = await getFilesRefList(lesson.data.template.name, TEMPLATES_DIR);

for (const filename of templateFilesRef[1]) {
if (lesson.files[1].includes(filename)) {
continue;
}

if (mm.isMatch(filename, lesson.data.template.visibleFiles, { format: formatTemplateFile })) {
lesson.files[1].push(filename);
}
}

lesson.files[1].sort();
}

if (prevLesson) {
const partSlug = _tutorial.parts[prevLesson.part.id].slug;
const chapterSlug = _tutorial.parts[prevLesson.part.id].chapters[prevLesson.chapter.id].slug;
Expand Down Expand Up @@ -330,8 +348,8 @@ function getSlug(entry: CollectionEntryTutorial) {
return slug;
}

async function getFilesRefList(pathToFolder: string): Promise<FilesRefList> {
const root = path.join(CONTENT_DIR, pathToFolder);
async function getFilesRefList(pathToFolder: string, base = CONTENT_DIR): Promise<FilesRefList> {
const root = path.join(base, pathToFolder);

const filePaths = (
await glob(`${glob.convertPathToPattern(root)}/**/*`, {
Expand All @@ -348,6 +366,15 @@ async function getFilesRefList(pathToFolder: string): Promise<FilesRefList> {
return [filesRef, filePaths];
}

function formatTemplateFile(filename: string) {
// compare files without leading "/" so that patterns like ["src/index.js"] match "/src/index.js"
if (filename.startsWith('/')) {
return filename.substring(1);
}

return filename;
}

interface CollectionEntryTutorial {
id: string;
slug: string;
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/commands/eject/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ interface PackageJson {
}

const TUTORIALKIT_VERSION = pkg.version;
const REQUIRED_DEPENDENCIES = ['@tutorialkit/runtime', '@webcontainer/api', 'nanostores', '@nanostores/react', 'kleur'];
const REQUIRED_DEPENDENCIES = [
'@tutorialkit/runtime',
'@webcontainer/api',
'nanostores',
'@nanostores/react',
'kleur',
'micromatch',
'@types/micromatch',
];

export function ejectRoutes(flags: Arguments) {
if (flags._[1] === 'help' || flags.help || flags.h) {
Expand Down Expand Up @@ -104,6 +112,7 @@ async function _eject(flags: EjectOptions) {
for (const dep of REQUIRED_DEPENDENCIES) {
if (!(dep in pkgJson.dependencies) && !(dep in pkgJson.devDependencies)) {
pkgJson.dependencies[dep] = astroIntegrationPkgJson.dependencies[dep];
pkgJson.devDependencies[dep] = astroIntegrationPkgJson.devDependencies[dep];

newDependencies.push(dep);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ exports[`create a project 1`] = `
"src/templates/default/package.json",
"src/templates/default/src",
"src/templates/default/src/index.js",
"src/templates/default/src/template-only-file.js",
"src/templates/vite-app",
"src/templates/vite-app-2",
"src/templates/vite-app-2/.tk-config.json",
Expand Down Expand Up @@ -233,6 +234,7 @@ exports[`create and eject a project 1`] = `
"src/templates/default/package.json",
"src/templates/default/src",
"src/templates/default/src/index.js",
"src/templates/default/src/template-only-file.js",
"src/templates/vite-app",
"src/templates/vite-app-2",
"src/templates/vite-app-2/.tk-config.json",
Expand Down
28 changes: 22 additions & 6 deletions packages/runtime/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,18 @@ export class TutorialStore {
private _ref: number = 1;
private _themeRef = atom(1);

/** Files from lesson's `_files` directory */
private _lessonFiles: Files | undefined;

/** Files from lesson's `_solution` directory */
private _lessonSolution: Files | undefined;

/** All files from `template` directory */
private _lessonTemplate: Files | undefined;

/** Files from `template` directory that match `template.visibleFiles` patterns */
private _visibleTemplateFiles: Files | undefined;

/**
* Whether or not the current lesson is fully loaded in WebContainer
* and in every stores.
Expand Down Expand Up @@ -165,15 +173,17 @@ export class TutorialStore {

signal.throwIfAborted();

this._lessonTemplate = template;
this._lessonFiles = files;
this._lessonSolution = solution;
this._lessonTemplate = template;
this._visibleTemplateFiles = filterEntries(template, lesson.files[1]);

this._editorStore.setDocuments(files);
const editorFiles = { ...this._visibleTemplateFiles, ...this._lessonFiles };
this._editorStore.setDocuments(editorFiles);

if (lesson.data.focus === undefined) {
this._editorStore.setSelectedFile(undefined);
} else if (files[lesson.data.focus] !== undefined) {
} else if (editorFiles[lesson.data.focus] !== undefined) {
this._editorStore.setSelectedFile(lesson.data.focus);
}

Expand Down Expand Up @@ -279,8 +289,10 @@ export class TutorialStore {
return;
}

this._editorStore.setDocuments(this._lessonFiles);
this._runner.updateFiles(this._lessonFiles);
const files = { ...this._visibleTemplateFiles, ...this._lessonFiles };

this._editorStore.setDocuments(files);
this._runner.updateFiles(files);
}

solve() {
Expand All @@ -290,7 +302,7 @@ export class TutorialStore {
return;
}

const files = { ...this._lessonFiles, ...this._lessonSolution };
const files = { ...this._visibleTemplateFiles, ...this._lessonFiles, ...this._lessonSolution };

this._editorStore.setDocuments(files);
this._runner.updateFiles(files);
Expand Down Expand Up @@ -353,3 +365,7 @@ export class TutorialStore {
this._themeRef.set(this._themeRef.get() + 1);
}
}

function filterEntries<T extends object>(obj: T, filter: string[]) {
return Object.fromEntries(Object.entries(obj).filter(([entry]) => filter.includes(entry)));
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
type: lesson
title: Welcome to TutorialKit
focus: /src/index.js
focus: /src/template-only-file.js
previews: [8080]
mainCommand: ['node -e setTimeout(()=>{},10_000)', 'Running dev server']
prepareCommands:
Expand All @@ -13,6 +13,7 @@ terminal:
panels: ['terminal', 'output']
template:
name: default
visibleFiles: ['src/template-only-file.js']
---

# Kitchen Sink [Heading 1]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'This file is only present in template';
10 changes: 10 additions & 0 deletions packages/types/src/schemas/common.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ describe('webcontainerSchema', () => {
}).not.toThrow();
});
it('should allow specifying the template by object type', () => {
expect(() => {
webcontainerSchema.parse({
template: {
name: 'default',
visibleFiles: ['**/fixture.json', '*/tests/*'],
},
});
}).not.toThrow();
});
it('should allow specifying the template to omit visibleFiles', () => {
expect(() => {
webcontainerSchema.parse({
template: {
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/schemas/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export const webcontainerSchema = commandsSchema.extend({
z.strictObject({
// name of the template
name: z.string(),

// list of globs of files that should be visible
visibleFiles: z.array(z.string()).optional(),
}),
])
.describe(
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0e4e9e4

Please sign in to comment.