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 30, 2024
1 parent ca4d723 commit 4b0695d
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 10 deletions.
86 changes: 86 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 @@ -3,6 +3,7 @@ title: Content creation
description: 'Creating content in TutorialKit.'
---
import { FileTree } from '@astrojs/starlight/components';
import { Tabs, TabItem } from '@astrojs/starlight/components';

From an information architecture perspective, tutorial content is divided into **parts**, which are further divided into **chapters**, each consisting of **lessons**.

Expand Down Expand Up @@ -110,6 +111,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 Expand Up @@ -144,3 +158,75 @@ src/templates
│ # Overrides "index.js" from "shared-template"
└── index.js
```

## Editor File Visibility

Editor's files are resolved in 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)

<Tabs syncKey="file-visibilty">
<TabItem label="Initially">

```markdown ins=/.{24}├── (first.js)/ ins=/└── (second.js)/ ins=/third.js/
---
template:
name: default
visibleFiles: ['src/**']
---

src
├── content
│ └── tutorial
│ └── 1-basics
│ └── 1-introduction
│ └── 1-welcome
│ ├── _files
│ │ ├── first.js
│ │ └── second.js
│ └── _solution
│ └── first.js
└── templates
└── default
├── src
│ ├── first.js
│ ├── second.js
│ └── third.js
└── package.json
```

</TabItem>

<TabItem label="After solution is revealed">

```markdown ins=/└── (first.js)/ ins=/└── (second.js)/ ins=/third.js/
---
template:
name: default
visibleFiles: ['src/**']
---

src
├── content
│ └── tutorial
│ └── 1-basics
│ └── 1-introduction
│ └── 1-welcome
│ ├── _files
│ │ ├── first.js
│ │ └── second.js
│ └── _solution
│ └── first.js
└── templates
└── default
├── src
│ ├── first.js
│ ├── second.js
│ └── third.js
└── package.json
```

</TabItem>
</Tabs>
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 4b0695d

Please sign in to comment.