Skip to content

Commit eb6cf09

Browse files
test(cli): add test for creating and building a project (#60)
1 parent 79cb18d commit eb6cf09

File tree

19 files changed

+1094
-236
lines changed

19 files changed

+1094
-236
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/workflows/ci.yaml

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,32 @@ on:
77
jobs:
88
test:
99
name: Test
10-
runs-on: ubuntu-latest
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest]
15+
node-version: [18, 20, 22]
16+
include:
17+
- os: macos-latest
18+
node_version: 20
19+
- os: windows-latest
20+
node_version: 20.13.1 # 20.14.0 keeps causing a native `node::SetCppgcReference+18123` error in Vitest
1121
steps:
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: ${{ matrix.node-version }}
1225
- name: Setup
1326
uses: pnpm/action-setup@v4
1427
with:
1528
version: 8
1629
- name: Checkout
17-
uses: actions/checkout@v3
30+
uses: actions/checkout@v4
1831
- name: Install dependencies
1932
run: pnpm install
2033
- name: Lint
2134
run: pnpm prettier --check .
2235
- name: Build
2336
run: pnpm build
37+
- name: Test
38+
run: pnpm test

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ dist-ssr
2020
.pnpm-store
2121
/tutorialkit/template
2222
tsconfig.tsbuildinfo
23+
.tmp

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"build": "pnpm run --filter=@tutorialkit/* --filter=tutorialkit --filter=create-tutorial build",
55
"template:dev": "TUTORIALKIT_DEV=true pnpm run build && pnpm run --filter=tutorialkit-starter dev",
66
"template:build": "pnpm run build && pnpm run --filter=tutorialkit-starter build",
7+
"test": "pnpm run --filter=tutorialkit test",
78
"prepare": "is-ci || husky install",
89
"clean": "./scripts/clean.sh"
910
},

packages/astro/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,16 @@
4747
"unocss": "^0.59.4",
4848
"zod": "3.23.8"
4949
},
50-
"peerDependencies": {
51-
"astro": "^4.8.6"
52-
},
5350
"devDependencies": {
5451
"@tutorialkit/types": "workspace:*",
5552
"@types/mdast": "^4.0.3",
5653
"esbuild": "^0.20.2",
5754
"esbuild-node-externals": "^1.13.0",
55+
"execa": "^9.2.0",
5856
"typescript": "^5.4.5",
5957
"vite-plugin-inspect": "0.8.4"
58+
},
59+
"peerDependencies": {
60+
"astro": "^4.10.2"
6061
}
6162
}

packages/astro/scripts/build.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import assert from 'node:assert';
22
import { existsSync } from 'node:fs';
3-
import { spawnSync } from 'node:child_process';
43
import { cp, rm } from 'node:fs/promises';
4+
import { execa } from 'execa';
55
import esbuild from 'esbuild';
66
import { nodeExternalsPlugin } from 'esbuild-node-externals';
77

88
// clean dist
99
await rm('dist', { recursive: true, force: true });
1010

1111
// only do typechecking and emit the type declarations with tsc
12-
spawnSync('tsc', ['--emitDeclarationOnly', '--project', './tsconfig.build.json'], { stdio: 'inherit' });
12+
execa('tsc', ['--emitDeclarationOnly', '--project', './tsconfig.build.json'], {
13+
stdio: 'inherit',
14+
preferLocal: true,
15+
});
1316

1417
// build with esbuild
1518
esbuild.build({

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ async function getFilesRef(pathToFolder: string): Promise<FilesRef> {
222222
const root = path.join(CONTENT_DIR, pathToFolder);
223223

224224
const filePaths = (
225-
await glob(`${root}/**/*`, {
225+
await glob(`${glob.convertPathToPattern(root)}/**/*`, {
226226
onlyFiles: true,
227227
})
228228
).map((filePath) => `/${path.relative(root, filePath)}`);

packages/astro/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export default function createPlugin({ defaultRoutes = true, isolation, enterpri
117117
_config = config;
118118
},
119119
'astro:server:setup'(options) {
120+
if (!_config) {
121+
return;
122+
}
123+
120124
const { server, logger } = options;
121125
const projectRoot = fileURLToPath(_config.root);
122126

packages/astro/src/webcontainer-files.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export class WebContainerFiles {
2020
const cache = new FileMapCache(logger, server, { contentDir, templatesDir });
2121

2222
this._watcher = watch([
23-
`${contentDir}/**/${FILES_FOLDER_NAME}/**/*`,
24-
`${contentDir}/**/${SOLUTION_FOLDER_NAME}/**/*`,
23+
path.join(contentDir, `**/${FILES_FOLDER_NAME}/**/*`),
24+
path.join(contentDir, `**/${SOLUTION_FOLDER_NAME}/**/*`),
2525
templatesDir,
2626
]);
2727

@@ -57,12 +57,18 @@ export class WebContainerFiles {
5757
const { contentDir, templatesDir } = this._folders(projectRoot);
5858

5959
const folders = await glob(
60-
[`${contentDir}/**/${FILES_FOLDER_NAME}`, `${contentDir}/**/${SOLUTION_FOLDER_NAME}`, `${templatesDir}/*`],
60+
[
61+
`${glob.convertPathToPattern(contentDir)}/**/${FILES_FOLDER_NAME}`,
62+
`${glob.convertPathToPattern(contentDir)}/**/${SOLUTION_FOLDER_NAME}`,
63+
`${glob.convertPathToPattern(templatesDir)}/*`,
64+
],
6165
{ onlyDirectories: true },
6266
);
6367

6468
await Promise.all(
6569
folders.map(async (folder) => {
70+
folder = path.normalize(folder);
71+
6672
const fileRef = getFilesRef(folder, { contentDir, templatesDir });
6773
const dest = fileURLToPath(new URL(fileRef, dir));
6874

@@ -198,7 +204,7 @@ class FileMapCache {
198204
}
199205

200206
async function createFileMap(dir: string) {
201-
const filePaths = await glob(`${dir}/**/*`, {
207+
const filePaths = await glob(`${glob.convertPathToPattern(dir)}/**/*`, {
202208
onlyFiles: true,
203209
});
204210

@@ -261,5 +267,5 @@ function getFilesRef(pathToFolder: string, { contentDir, templatesDir }: Content
261267
pathToFolder = 'template' + pathToFolder.slice(templatesDir.length);
262268
}
263269

264-
return encodeURIComponent(pathToFolder.replaceAll('/', '-').replaceAll('_', '')) + '.json';
270+
return encodeURIComponent(pathToFolder.replaceAll(/[\/\\]+/g, '-').replaceAll('_', '')) + '.json';
265271
}

packages/cli/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
"scripts": {
1717
"build": "node scripts/build.js",
1818
"prepack": "node scripts/pre-pack.js",
19-
"postpack": "node scripts/cleanup.js"
19+
"postpack": "node scripts/cleanup.js",
20+
"test": "vitest --testTimeout=180000"
2021
},
2122
"files": [
2223
"dist",
2324
"template",
2425
"template/.gitignore"
2526
],
2627
"dependencies": {
27-
"@babel/parser": "7.24.5",
2828
"@babel/generator": "7.24.5",
29+
"@babel/parser": "7.24.5",
2930
"@babel/traverse": "7.24.5",
3031
"@babel/types": "7.24.5",
3132
"@clack/prompts": "^0.7.0",
@@ -42,7 +43,9 @@
4243
"@types/yargs-parser": "^21.0.3",
4344
"esbuild": "^0.20.2",
4445
"esbuild-node-externals": "^1.13.0",
45-
"fs-extra": "^11.2.0"
46+
"execa": "^9.2.0",
47+
"fs-extra": "^11.2.0",
48+
"vitest": "^1.6.0"
4649
},
4750
"engines": {
4851
"node": ">=18.18.0"

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,13 @@ function updateWorkspaceVersions(dependencies: Record<string, string>, version:
220220
const depVersion = dependencies[dependency];
221221

222222
if (depVersion === 'workspace:*') {
223-
dependencies[dependency] = version;
223+
if (process.env.TK_DIRECTORY) {
224+
const name = dependency.split('/')[1];
225+
226+
dependencies[dependency] = `file:${process.env.TK_DIRECTORY}/packages/${name.replace('-', '/')}`;
227+
} else {
228+
dependencies[dependency] = version;
229+
}
224230
}
225231
}
226232
}

packages/cli/src/commands/create/options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1818
export const templatePath = path.resolve(__dirname, process.env.TUTORIALKIT_TEMPLATE_PATH ?? '../_template');
1919

2020
export const DEFAULT_VALUES = {
21-
git: true,
21+
git: process.env.CI ? false : true,
2222
install: true,
2323
dryRun: false,
2424
force: false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`create a project 1`] = `
4+
[
5+
".gitignore",
6+
".vscode",
7+
".vscode/extensions.json",
8+
".vscode/launch.json",
9+
"README.md",
10+
"astro.config.ts",
11+
"icons",
12+
"icons/languages",
13+
"icons/languages/css.svg",
14+
"icons/languages/html.svg",
15+
"icons/languages/js.svg",
16+
"icons/languages/json.svg",
17+
"icons/languages/markdown.svg",
18+
"icons/languages/sass.svg",
19+
"icons/languages/ts.svg",
20+
"package.json",
21+
"public",
22+
"public/favicon.svg",
23+
"public/logo.svg",
24+
"src",
25+
"src/content",
26+
"src/content/config.ts",
27+
"src/content/tutorial",
28+
"src/content/tutorial/1-basics",
29+
"src/content/tutorial/1-basics/1-introduction",
30+
"src/content/tutorial/1-basics/1-introduction/1-welcome",
31+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_files",
32+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src",
33+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/index.js",
34+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/test",
35+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/test/bar.js",
36+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_solution",
37+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_solution/src",
38+
"src/content/tutorial/1-basics/1-introduction/1-welcome/_solution/src/index.js",
39+
"src/content/tutorial/1-basics/1-introduction/1-welcome/content.md",
40+
"src/content/tutorial/1-basics/1-introduction/2-foo",
41+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files",
42+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/bar",
43+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/bar/styles.css",
44+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/src",
45+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/index.html",
46+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/unicorn.js",
47+
"src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/windows_xp.png",
48+
"src/content/tutorial/1-basics/1-introduction/2-foo/content.mdx",
49+
"src/content/tutorial/1-basics/1-introduction/3-bar",
50+
"src/content/tutorial/1-basics/1-introduction/3-bar/_files",
51+
"src/content/tutorial/1-basics/1-introduction/3-bar/_files/src",
52+
"src/content/tutorial/1-basics/1-introduction/3-bar/_files/src/index.html",
53+
"src/content/tutorial/1-basics/1-introduction/3-bar/content.md",
54+
"src/content/tutorial/1-basics/1-introduction/meta.md",
55+
"src/content/tutorial/1-basics/2-foo",
56+
"src/content/tutorial/1-basics/2-foo/1-welcome",
57+
"src/content/tutorial/1-basics/2-foo/1-welcome/content.md",
58+
"src/content/tutorial/1-basics/2-foo/meta.md",
59+
"src/content/tutorial/1-basics/meta.md",
60+
"src/content/tutorial/2-advanced",
61+
"src/content/tutorial/2-advanced/1-unicorn",
62+
"src/content/tutorial/2-advanced/1-unicorn/1-welcome",
63+
"src/content/tutorial/2-advanced/1-unicorn/1-welcome/content.md",
64+
"src/content/tutorial/2-advanced/1-unicorn/meta.md",
65+
"src/content/tutorial/2-advanced/meta.md",
66+
"src/content/tutorial/meta.md",
67+
"src/env.d.ts",
68+
"src/templates",
69+
"src/templates/default",
70+
"src/templates/default/package-lock.json",
71+
"src/templates/default/package.json",
72+
"src/templates/default/src",
73+
"src/templates/default/src/index.js",
74+
"src/templates/vite-app",
75+
"src/templates/vite-app/.gitignore",
76+
"src/templates/vite-app/counter.js",
77+
"src/templates/vite-app/index.html",
78+
"src/templates/vite-app/javascript.svg",
79+
"src/templates/vite-app/main.js",
80+
"src/templates/vite-app/package-lock.json",
81+
"src/templates/vite-app/package.json",
82+
"src/templates/vite-app/public",
83+
"src/templates/vite-app/public/vite.svg",
84+
"src/templates/vite-app/style.css",
85+
"tsconfig.json",
86+
"uno.config.ts",
87+
]
88+
`;
89+
90+
exports[`create and build a project 1`] = `
91+
[
92+
"1-basics",
93+
"1-basics-1-introduction-1-welcome-files.json",
94+
"1-basics-1-introduction-1-welcome-solution.json",
95+
"1-basics-1-introduction-2-foo-files.json",
96+
"1-basics-1-introduction-3-bar-files.json",
97+
"1-basics/2-foo",
98+
"1-basics/2-foo/1-welcome",
99+
"1-basics/2-foo/1-welcome/index.html",
100+
"1-basics/custom-chapter-slug",
101+
"1-basics/custom-chapter-slug/1-welcome",
102+
"1-basics/custom-chapter-slug/1-welcome/index.html",
103+
"1-basics/custom-chapter-slug/bar",
104+
"1-basics/custom-chapter-slug/bar/index.html",
105+
"1-basics/custom-chapter-slug/foo",
106+
"1-basics/custom-chapter-slug/foo/index.html",
107+
"2-advanced",
108+
"2-advanced/1-unicorn",
109+
"2-advanced/1-unicorn/1-welcome",
110+
"2-advanced/1-unicorn/1-welcome/index.html",
111+
"favicon.svg",
112+
"index.html",
113+
"logo.svg",
114+
"template-default.json",
115+
"template-vite-app.json",
116+
]
117+
`;
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs/promises';
3+
import { afterAll, beforeAll, expect, test } from 'vitest';
4+
import { execa } from 'execa';
5+
6+
const tmpDir = path.join(__dirname, '.tmp');
7+
const baseDir = path.resolve(__dirname, '../../..');
8+
9+
const cli = path.join(baseDir, 'packages/cli/dist/index.js');
10+
11+
beforeAll(async () => {
12+
await fs.rm(tmpDir, { force: true, recursive: true });
13+
await fs.mkdir(tmpDir);
14+
});
15+
16+
afterAll(async () => {
17+
await fs.rm(tmpDir, { force: true, recursive: true });
18+
});
19+
20+
test('create a project', async (context) => {
21+
const name = context.task.id;
22+
const dest = path.join(tmpDir, name);
23+
24+
await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--defaults'], {
25+
cwd: tmpDir,
26+
env: {
27+
TK_DIRECTORY: baseDir,
28+
},
29+
});
30+
31+
const projectFiles = await fs.readdir(dest, { recursive: true });
32+
33+
expect(projectFiles.map(normaliseSlash).sort()).toMatchSnapshot();
34+
});
35+
36+
test('create and build a project', async (context) => {
37+
const name = context.task.id;
38+
const dest = path.join(tmpDir, name);
39+
40+
await execa('node', [cli, 'create', name, '--no-git', '--defaults'], {
41+
cwd: tmpDir,
42+
env: {
43+
TK_DIRECTORY: baseDir,
44+
},
45+
});
46+
47+
await execa('npm', ['run', 'build'], {
48+
cwd: dest,
49+
});
50+
51+
// remove `_astro` before taking the snapshot
52+
await fs.rm(path.join(dest, 'dist/_astro'), { force: true, recursive: true });
53+
54+
const distFiles = await fs.readdir(path.join(dest, 'dist'), { recursive: true });
55+
56+
expect(distFiles.map(normaliseSlash).sort()).toMatchSnapshot();
57+
});
58+
59+
function normaliseSlash(filePath: string) {
60+
return filePath.replace(/\\/g, '/');
61+
}

0 commit comments

Comments
 (0)