Skip to content

Commit 3a11ee5

Browse files
committed
feat: add template.visibleFiles option
1 parent fee4b29 commit 3a11ee5

File tree

11 files changed

+86
-4
lines changed

11 files changed

+86
-4
lines changed

docs/tutorialkit.dev/src/content/docs/guides/creating-content.mdx

+13
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ template: my-advanced-template
110110

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

113+
By default files in template are not shown in the code editor.
114+
To make them visible, you can use `visibleFiles` option.
115+
This can reduce repetition when you want to show same files visible in multiple lessons.
116+
117+
```markdown {5}
118+
---
119+
title: Advanced Topics
120+
template:
121+
name: my-advanced-template
122+
visibleFiles: ['index.js', '**/utils/**']
123+
---
124+
```
125+
113126
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:
114127

115128
```json

packages/astro/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"kleur": "4.1.5",
5151
"mdast-util-directive": "^3.0.0",
5252
"mdast-util-to-markdown": "^2.1.0",
53+
"micromatch": "^4.0.7",
5354
"nanostores": "^0.10.3",
5455
"react": "^18.3.1",
5556
"react-dom": "^18.3.1",
@@ -62,6 +63,7 @@
6263
"devDependencies": {
6364
"@tutorialkit/types": "workspace:*",
6465
"@types/mdast": "^4.0.4",
66+
"@types/micromatch": "^4.0.9",
6567
"esbuild": "^0.20.2",
6668
"esbuild-node-externals": "^1.13.1",
6769
"execa": "^9.2.0",

packages/astro/src/default/utils/content.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
import { folderPathToFilesRef, interpolateString } from '@tutorialkit/types';
1111
import { getCollection } from 'astro:content';
1212
import glob from 'fast-glob';
13+
import mm from 'micromatch';
1314
import path from 'node:path';
1415
import { IGNORED_FILES } from './constants';
1516
import { DEFAULT_LOCALIZATION } from './content/default-localization';
@@ -18,6 +19,7 @@ import { logger } from './logger';
1819
import { joinPaths } from './url';
1920

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

2224
export async function getTutorial(): Promise<Tutorial> {
2325
const collection = sortCollection(await getCollection('tutorial'));
@@ -262,6 +264,18 @@ export async function getTutorial(): Promise<Tutorial> {
262264
),
263265
};
264266

267+
if (lesson.data.template) {
268+
const template = typeof lesson.data.template === 'string' ? { name: lesson.data.template } : lesson.data.template;
269+
270+
const allTemplateFiles = template.visibleFiles?.length
271+
? (await getFilesRefList(template.name, TEMPLATES_DIR))[1]
272+
: [];
273+
274+
const templateFiles = mm(allTemplateFiles, template.visibleFiles || []);
275+
276+
lesson.files[1] = [...lesson.files[1], ...templateFiles].filter(filterUnique).sort();
277+
}
278+
265279
if (prevLesson) {
266280
const partSlug = _tutorial.parts[prevLesson.part.id].slug;
267281
const chapterSlug = _tutorial.parts[prevLesson.part.id].chapters[prevLesson.chapter.id].slug;
@@ -330,8 +344,8 @@ function getSlug(entry: CollectionEntryTutorial) {
330344
return slug;
331345
}
332346

333-
async function getFilesRefList(pathToFolder: string): Promise<FilesRefList> {
334-
const root = path.join(CONTENT_DIR, pathToFolder);
347+
async function getFilesRefList(pathToFolder: string, base = CONTENT_DIR): Promise<FilesRefList> {
348+
const root = path.join(base, pathToFolder);
335349

336350
const filePaths = (
337351
await glob(`${glob.convertPathToPattern(root)}/**/*`, {
@@ -348,6 +362,10 @@ async function getFilesRefList(pathToFolder: string): Promise<FilesRefList> {
348362
return [filesRef, filePaths];
349363
}
350364

365+
function filterUnique<T>(item: T, index: number, array: T[]) {
366+
return array.indexOf(item) === index;
367+
}
368+
351369
interface CollectionEntryTutorial {
352370
id: string;
353371
slug: string;

packages/cli/src/commands/eject/index.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ interface PackageJson {
1818
}
1919

2020
const TUTORIALKIT_VERSION = pkg.version;
21-
const REQUIRED_DEPENDENCIES = ['@tutorialkit/runtime', '@webcontainer/api', 'nanostores', '@nanostores/react', 'kleur'];
21+
const REQUIRED_DEPENDENCIES = [
22+
'@tutorialkit/runtime',
23+
'@webcontainer/api',
24+
'nanostores',
25+
'@nanostores/react',
26+
'kleur',
27+
'micromatch',
28+
'@types/micromatch',
29+
];
2230

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

108117
newDependencies.push(dep);
109118
}

packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ exports[`create a project 1`] = `
7272
"src/templates/default/package.json",
7373
"src/templates/default/src",
7474
"src/templates/default/src/index.js",
75+
"src/templates/default/src/template-only-file.js",
7576
"src/templates/vite-app",
7677
"src/templates/vite-app-2",
7778
"src/templates/vite-app-2/.tk-config.json",
@@ -226,6 +227,7 @@ exports[`create and eject a project 1`] = `
226227
"src/templates/default/package.json",
227228
"src/templates/default/src",
228229
"src/templates/default/src/index.js",
230+
"src/templates/default/src/template-only-file.js",
229231
"src/templates/vite-app",
230232
"src/templates/vite-app-2",
231233
"src/templates/vite-app-2/.tk-config.json",

packages/runtime/src/store/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,14 @@ export class TutorialStore {
169169
this._lessonFiles = files;
170170
this._lessonSolution = solution;
171171

172-
this._editorStore.setDocuments(files);
172+
this._editorStore.setDocuments(
173+
Object.fromEntries(
174+
Object.entries({
175+
...template,
176+
...files,
177+
}).filter(([filename]) => lesson.files[1].includes(filename)),
178+
),
179+
);
173180

174181
if (lesson.data.focus === undefined) {
175182
this._editorStore.setSelectedFile(undefined);

packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/content.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ terminal:
1313
panels: ['terminal', 'output']
1414
template:
1515
name: default
16+
visibleFiles: ['/src/template-only-file.js']
1617
---
1718

1819
# Kitchen Sink [Heading 1]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'This file is only present in template';

packages/types/src/schemas/common.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,16 @@ describe('webcontainerSchema', () => {
358358
}).not.toThrow();
359359
});
360360
it('should allow specifying the template by object type', () => {
361+
expect(() => {
362+
webcontainerSchema.parse({
363+
template: {
364+
name: 'default',
365+
visibleFiles: ['**/fixture.json', '*/tests/*'],
366+
},
367+
});
368+
}).not.toThrow();
369+
});
370+
it('should allow specifying the template to omit visibleFiles', () => {
361371
expect(() => {
362372
webcontainerSchema.parse({
363373
template: {

packages/types/src/schemas/common.ts

+3
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ export const webcontainerSchema = commandsSchema.extend({
145145
z.strictObject({
146146
// name of the template
147147
name: z.string(),
148+
149+
// list of globs of files that should be visible
150+
visibleFiles: z.array(z.string()).optional(),
148151
}),
149152
]),
150153
terminal: terminalSchema.optional(),

pnpm-lock.yaml

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)