From 0cea9af9e9ead2cb059b8d9de57099842bb595f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B2an?= Date: Mon, 15 Jul 2024 21:55:03 +0000 Subject: [PATCH 1/6] feat: metadata autocompletion --- extensions/vscode/build.mjs | 87 +++++-- extensions/vscode/package.json | 32 ++- extensions/vscode/src/extension.ts | 37 ++- .../vscode/src/language-server/index.ts | 56 +++++ .../src/language-server/languagePlugin.ts | 100 ++++++++ .../vscode/src/language-server/schema.ts | 11 + extensions/vscode/tsconfig.json | 7 +- pnpm-lock.yaml | 221 ++++++++++++++++-- 8 files changed, 497 insertions(+), 54 deletions(-) create mode 100644 extensions/vscode/src/language-server/index.ts create mode 100644 extensions/vscode/src/language-server/languagePlugin.ts create mode 100644 extensions/vscode/src/language-server/schema.ts diff --git a/extensions/vscode/build.mjs b/extensions/vscode/build.mjs index c573a9a72..ee4ff94d3 100644 --- a/extensions/vscode/build.mjs +++ b/extensions/vscode/build.mjs @@ -1,37 +1,60 @@ +import { chapterSchema, lessonSchema, partSchema, tutorialSchema } from '@tutorialkit/types'; +import { watch } from 'chokidar'; import * as esbuild from 'esbuild'; -import fs from 'node:fs'; import { execa } from 'execa'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +const require = createRequire(import.meta.url); const production = process.argv.includes('--production'); -const watch = process.argv.includes('--watch'); +const isWatch = process.argv.includes('--watch'); async function main() { const ctx = await esbuild.context({ - entryPoints: ['src/extension.ts'], + entryPoints: { + extension: 'src/extension.ts', + server: './src/language-server/index.ts', + }, bundle: true, format: 'cjs', minify: production, sourcemap: !production, sourcesContent: false, + tsconfig: './tsconfig.json', platform: 'node', - outfile: 'dist/extension.js', + outdir: 'dist', + define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"' }, external: ['vscode'], - logLevel: 'silent', - plugins: [ - /* add to the end of plugins array */ - esbuildProblemMatcherPlugin, - ], + plugins: [esbuildUMD2ESMPlugin], }); - if (watch) { + if (isWatch) { + const buildMetadataSchemaDebounced = debounce(buildMetadataSchema); + + watch(join(require.resolve('@tutorialkit/types'), 'dist'), { + followSymlinks: false, + }).on('all', (eventName, path) => { + if (eventName !== 'change' && eventName !== 'add' && eventName !== 'unlink') { + return; + } + + buildMetadataSchemaDebounced(); + }); + await Promise.all([ ctx.watch(), - execa('tsc', ['--noEmit', '--watch', '--project', 'tsconfig.json'], { stdio: 'inherit', preferLocal: true }), + execa('tsc', ['--noEmit', '--watch', '--preserveWatchOutput', '--project', 'tsconfig.json'], { + stdio: 'inherit', + preferLocal: true, + }), ]); } else { await ctx.rebuild(); await ctx.dispose(); + buildMetadataSchema(); + if (production) { // rename name in package json to match extension name on store: const pkgJSON = JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf8' })); @@ -43,21 +66,24 @@ async function main() { } } +function buildMetadataSchema() { + const schema = tutorialSchema.strict().or(partSchema.strict()).or(chapterSchema.strict()).or(lessonSchema.strict()); + + fs.mkdirSync('./dist', { recursive: true }); + fs.writeFileSync('./dist/schema.json', JSON.stringify(zodToJsonSchema(schema), undefined, 2), 'utf-8'); +} + /** * @type {import('esbuild').Plugin} */ -const esbuildProblemMatcherPlugin = { - name: 'esbuild-problem-matcher', +const esbuildUMD2ESMPlugin = { + name: 'umd2esm', setup(build) { - build.onStart(() => { - console.log('[watch] build started'); - }); - build.onEnd((result) => { - result.errors.forEach(({ text, location }) => { - console.error(`✘ [ERROR] ${text}`); - console.error(` ${location.file}:${location.line}:${location.column}:`); - }); - console.log('[watch] build finished'); + build.onResolve({ filter: /^(vscode-.*-languageservice|jsonc-parser)/ }, (args) => { + const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }); + const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\'); + + return { path: pathEsm }; }); }, }; @@ -66,3 +92,20 @@ main().catch((error) => { console.error(error); process.exit(1); }); + +/** + * Debounce the provided function. + * + * @param {Function} fn Function to debounce + * @param {number} duration Duration of the debounce + * @returns {Function} Debounced function + */ +function debounce(fn, duration) { + let timeoutId = 0; + + return function () { + clearTimeout(timeoutId); + + timeoutId = setTimeout(fn.bind(this), duration, ...arguments); + }; +} diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 5d1a544ca..46586f59f 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -102,7 +102,21 @@ "when": "view == tutorialkit-lessons-tree && viewItem == part" } ] - } + }, + "languages": [ + { + "id": "markdown", + "extensions": [ + ".md" + ] + }, + { + "id": "mdx", + "extensions": [ + ".mdx" + ] + } + ] }, "scripts": { "__esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", @@ -116,15 +130,25 @@ "package": "pnpm run check-types && node build.mjs --production" }, "dependencies": { + "@volar/language-core": "2.3.4", + "@volar/language-server": "2.3.4", + "@volar/language-service": "2.3.4", + "@volar/vscode": "2.3.4", "case-anything": "^3.1.0", - "gray-matter": "^4.0.3" + "gray-matter": "^4.0.3", + "volar-service-yaml": "volar-2.3", + "vscode-languageclient": "^9.0.1", + "vscode-uri": "^3.0.8", + "yaml-language-server": "1.15.0" }, "devDependencies": { - "@types/mocha": "^10.0.6", + "@tutorialkit/types": "workspace:*", "@types/node": "20.14.11", "@types/vscode": "^1.80.0", + "chokidar": "3.6.0", "esbuild": "^0.21.5", "execa": "^9.2.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "zod-to-json-schema": "3.23.1" } } diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 37958594b..fafa5d979 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -1,16 +1,49 @@ +import * as serverProtocol from '@volar/language-server/protocol'; +import { createLabsInfo } from '@volar/vscode'; import * as vscode from 'vscode'; import { useCommands } from './commands'; import { useLessonTree } from './views/lessonsTree'; +import * as lsp from 'vscode-languageclient/node'; export let extContext: vscode.ExtensionContext; -export function activate(context: vscode.ExtensionContext) { +let client: lsp.BaseLanguageClient; + +export async function activate(context: vscode.ExtensionContext) { extContext = context; useCommands(); useLessonTree(); + + const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js'); + const runOptions = { execArgv: [] }; + const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; + const serverOptions: lsp.ServerOptions = { + run: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: runOptions, + }, + debug: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: debugOptions, + }, + }; + const clientOptions: lsp.LanguageClientOptions = { + documentSelector: [{ language: 'markdown' }, { language: 'mdx' }], + initializationOptions: {}, + }; + client = new lsp.LanguageClient('tutorialkit-language-server', 'TutorialKit', serverOptions, clientOptions); + + await client.start(); + + const labsInfo = createLabsInfo(serverProtocol); + labsInfo.addLanguageClient(client); + + return labsInfo.extensionExports; } export function deactivate() { - // do nothing + return client?.stop(); } diff --git a/extensions/vscode/src/language-server/index.ts b/extensions/vscode/src/language-server/index.ts new file mode 100644 index 000000000..120265e3d --- /dev/null +++ b/extensions/vscode/src/language-server/index.ts @@ -0,0 +1,56 @@ +import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node'; +import { create as createYamlService } from 'volar-service-yaml'; +import { SchemaPriority } from 'yaml-language-server'; +import { frontmatterPlugin } from './languagePlugin'; +import { readSchema } from './schema'; + +const connection = createConnection(); +const server = createServer(connection); + +connection.listen(); + +connection.onInitialize((params) => { + connection.console.debug('CONNECTED' + params.capabilities); + + const yamlService = createYamlService({ + getLanguageSettings(_context) { + const schema = readSchema(); + + return { + completion: true, + validate: true, + hover: true, + format: true, + yamlVersion: '1.2', + isKubernetes: false, + schemas: [ + { + uri: 'https://tutorialkit.dev/schema.json', + schema, + fileMatch: [ + '**/*', + + // TODO: those don't work + 'src/content/*.md', + 'src/content/**/*.md', + 'src/content/**/*.mdx', + ], + priority: SchemaPriority.Settings, + }, + ], + }; + }, + }); + + delete yamlService.capabilities.codeLensProvider; + + return server.initialize( + params, + createSimpleProject([frontmatterPlugin(connection.console.debug.bind(connection.console.debug))]), + [yamlService], + ); +}); + +connection.onInitialized(server.initialized); + +connection.onShutdown(server.shutdown); diff --git a/extensions/vscode/src/language-server/languagePlugin.ts b/extensions/vscode/src/language-server/languagePlugin.ts new file mode 100644 index 000000000..c802877e4 --- /dev/null +++ b/extensions/vscode/src/language-server/languagePlugin.ts @@ -0,0 +1,100 @@ +import { CodeMapping, type LanguagePlugin, type VirtualCode } from '@volar/language-core'; +import type * as ts from 'typescript'; +import type { URI } from 'vscode-uri'; + +export const frontmatterPlugin = (debug: (message: string) => void) => + ({ + getLanguageId(uri) { + debug('URI: ' + uri.path); + + if (uri.path.endsWith('.md')) { + return 'markdown'; + } + + if (uri.path.endsWith('.mdx')) { + return 'mdx'; + } + + return undefined; + }, + createVirtualCode(_uri, languageId, snapshot) { + if (languageId === 'markdown' || languageId === 'mdx') { + return new FrontMatterVirtualCode(snapshot); + } + + return undefined; + }, + }) satisfies LanguagePlugin; + +export class FrontMatterVirtualCode implements VirtualCode { + id = 'root'; + languageId = 'markdown'; + mappings: CodeMapping[]; + embeddedCodes: VirtualCode[] = []; + + constructor(public snapshot: ts.IScriptSnapshot) { + this.mappings = [ + { + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [snapshot.getLength()], + data: { + completion: true, + format: true, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }, + ]; + + this.embeddedCodes = [...frontMatterCode(snapshot)]; + } +} + +function* frontMatterCode(snapshot: ts.IScriptSnapshot): Generator { + const content = snapshot.getText(0, snapshot.getLength()); + + let frontMatterStartIndex = content.indexOf('---'); + + if (frontMatterStartIndex === -1) { + return; + } + + frontMatterStartIndex += 3; + + let frontMatterEndIndex = content.indexOf('---', frontMatterStartIndex); + + if (frontMatterEndIndex === -1) { + frontMatterEndIndex = snapshot.getLength(); + } + + const frontMatterText = content.substring(frontMatterStartIndex, frontMatterEndIndex); + + yield { + id: 'frontmatter_1', + languageId: 'yaml', + snapshot: { + getText: (start, end) => frontMatterText.slice(start, end), + getLength: () => frontMatterText.length, + getChangeRange: () => undefined, + }, + mappings: [ + { + sourceOffsets: [frontMatterStartIndex], + generatedOffsets: [0], + lengths: [frontMatterText.length], + data: { + completion: true, + format: true, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }, + ], + embeddedCodes: [], + }; +} diff --git a/extensions/vscode/src/language-server/schema.ts b/extensions/vscode/src/language-server/schema.ts new file mode 100644 index 000000000..4e3f5db20 --- /dev/null +++ b/extensions/vscode/src/language-server/schema.ts @@ -0,0 +1,11 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export function readSchema() { + try { + const fileContent = fs.readFileSync(path.join(__dirname, './schema.json'), 'utf-8'); + return JSON.parse(fileContent); + } catch { + return {}; + } +} diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index 69df86d3d..547ecfffe 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -1,12 +1,15 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "allowJs": true, "module": "Node16", "target": "ES2022", "outDir": "dist", "lib": ["ES2022"], "verbatimModuleSyntax": false, "sourceMap": true, - "rootDir": "src" - } + "rootDir": "." + }, + "include": ["src", "./build.mjs"], + "references": [{ "path": "../../packages/types" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d67eefaef..d3e95c344 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,22 +163,49 @@ importers: extensions/vscode: dependencies: + '@volar/language-core': + specifier: 2.3.4 + version: 2.3.4 + '@volar/language-server': + specifier: 2.3.4 + version: 2.3.4 + '@volar/language-service': + specifier: 2.3.4 + version: 2.3.4 + '@volar/vscode': + specifier: 2.3.4 + version: 2.3.4 case-anything: specifier: ^3.1.0 version: 3.1.0 gray-matter: specifier: ^4.0.3 version: 4.0.3 + volar-service-yaml: + specifier: volar-2.3 + version: 0.0.54(@volar/language-service@2.3.4) + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + vscode-uri: + specifier: ^3.0.8 + version: 3.0.8 + yaml-language-server: + specifier: 1.15.0 + version: 1.15.0 devDependencies: - '@types/mocha': - specifier: ^10.0.6 - version: 10.0.7 + '@tutorialkit/types': + specifier: workspace:* + version: link:../../packages/types '@types/node': specifier: 20.14.11 version: 20.14.11 '@types/vscode': specifier: ^1.80.0 version: 1.91.0 + chokidar: + specifier: 3.6.0 + version: 3.6.0 esbuild: specifier: ^0.21.5 version: 0.21.5 @@ -188,6 +215,9 @@ importers: typescript: specifier: ^5.4.5 version: 5.5.3 + zod-to-json-schema: + specifier: 3.23.1 + version: 3.23.1(zod@3.23.8) integration: dependencies: @@ -2982,10 +3012,6 @@ packages: /@types/mdx@2.0.13: resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - /@types/mocha@10.0.7: - resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==} - dev: true - /@types/ms@0.7.34: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -3431,12 +3457,33 @@ packages: vscode-uri: 3.0.8 dev: true + /@volar/language-core@2.3.4: + resolution: {integrity: sha512-wXBhY11qG6pCDAqDnbBRFIDSIwbqkWI7no+lj5+L7IlA7HRIjRP7YQLGzT0LF4lS6eHkMSsclXqy9DwYJasZTQ==} + dependencies: + '@volar/source-map': 2.3.4 + dev: false + /@volar/language-core@2.4.0-alpha.16: resolution: {integrity: sha512-oOTnIZlx0P/idFwVw+W0NbzKDtZAQMzXSdIFfTePCKcXlb4Ys12GaGkx8NF9dsvPYV3nbv3ZsSxnkZWBmNKd7A==} dependencies: '@volar/source-map': 2.4.0-alpha.16 dev: true + /@volar/language-server@2.3.4: + resolution: {integrity: sha512-I0usa8dI0nTVTqbNyVTQNMDMOHeOaBs84Onmr6oJH3K0EiqLb+Py/Zd5hP/mX22P6MQLhPI2cMVmk0DrSyZFaw==} + dependencies: + '@volar/language-core': 2.3.4 + '@volar/language-service': 2.3.4 + '@volar/snapshot-document': 2.3.4 + '@volar/typescript': 2.3.4 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + dev: false + /@volar/language-server@2.4.0-alpha.16: resolution: {integrity: sha512-DswMBlmmXPo9fb1Dmb2qrCtxRDgQPej5jUjAoUm+1wO5k02Tk+jIvbbd/R3EzyHFTARmiRH5/bSOfRefHyuMsg==} dependencies: @@ -3452,6 +3499,15 @@ packages: vscode-uri: 3.0.8 dev: true + /@volar/language-service@2.3.4: + resolution: {integrity: sha512-mtzvYb33l17VVRwmX7C39EjVHBU9LbBJeo1rLXFKoXOzbZCanm0XtPZEENsm05VUvse929y4ParujW7k4G5H0w==} + dependencies: + '@volar/language-core': 2.3.4 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + dev: false + /@volar/language-service@2.4.0-alpha.16: resolution: {integrity: sha512-iIRUY0EL9jp8Od7Py/GlYpCu469GFDYl7ai716pQgwipjpjEjRQiuGAD2+cSFjOVXDsMPFpJ+Dpei7aSvE/8pQ==} dependencies: @@ -3461,6 +3517,13 @@ packages: vscode-uri: 3.0.8 dev: true + /@volar/snapshot-document@2.3.4: + resolution: {integrity: sha512-mSyxrKWa181r5Hv7CRjwrYXEnzqBLJ3M2Nse9+0KPRff2pAGIx0yl03O7e3Z7IyvMAkXxv3MIVYvP14zY3gVEQ==} + dependencies: + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + dev: false + /@volar/snapshot-document@2.4.0-alpha.16: resolution: {integrity: sha512-X9xZeLvkmhjkrz27J6nq9JhYWV8AUT1KS9fi4s+Mo1FOh5HHUIx/QzhrwsUN/pY1z3kO+vtrl2DE6NVJRYwwbw==} dependencies: @@ -3468,10 +3531,22 @@ packages: vscode-languageserver-textdocument: 1.0.11 dev: true + /@volar/source-map@2.3.4: + resolution: {integrity: sha512-C+t63nwcblqLIVTYXaVi/+gC8NukDaDIQI72J3R7aXGvtgaVB16c+J8Iz7/VfOy7kjYv7lf5GhBny6ACw9fTGQ==} + dev: false + /@volar/source-map@2.4.0-alpha.16: resolution: {integrity: sha512-sL9vNG7iR2hiKZor7UkD5Sufu3QCia4cbp2gX/nGRNSdaPbhOpdAoavwlBm0PrVkpiA19NZuavZoobD8krviFg==} dev: true + /@volar/typescript@2.3.4: + resolution: {integrity: sha512-acCvt7dZECyKcvO5geNybmrqOsu9u8n5XP1rfiYsOLYGPxvHRav9BVmEdRyZ3vvY6mNyQ1wLL5Hday4IShe17w==} + dependencies: + '@volar/language-core': 2.3.4 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + dev: false + /@volar/typescript@2.4.0-alpha.16: resolution: {integrity: sha512-WCx7z5O81McCQp2cC0c8081y+MgTiAR2WAiJjVL4tr4Qh4GgqK0lgn3CqAjcKizaK1R5y3wfrUqgIYr+QeFYcw==} dependencies: @@ -3480,6 +3555,15 @@ packages: vscode-uri: 3.0.8 dev: true + /@volar/vscode@2.3.4: + resolution: {integrity: sha512-rdsFjEvRiQbh16a/vhp64aQ/7nxpbWGIC8NmTkaXRCH1YBCMs/06akhXy7lriw3XDDtv9/ntu2eMZgIGnyohIw==} + dependencies: + '@volar/language-server': 2.3.4 + path-browserify: 1.0.1 + vscode-languageclient: 9.0.1 + vscode-nls: 5.2.0 + dev: false + /@vscode/emmet-helper@2.9.3: resolution: {integrity: sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==} dependencies: @@ -3563,7 +3647,6 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true /ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3745,7 +3828,6 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /bare-events@2.4.2: resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} @@ -3842,7 +3924,6 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true /braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -4857,7 +4938,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -5656,7 +5736,6 @@ packages: /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5681,6 +5760,10 @@ packages: resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} dev: true + /jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + dev: false + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -5798,6 +5881,10 @@ packages: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -6403,6 +6490,13 @@ packages: brace-expansion: 1.1.11 dev: true + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimatch@9.0.4: resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} @@ -6718,7 +6812,6 @@ packages: /path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: true /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -6866,6 +6959,14 @@ packages: sass-formatter: 0.7.9 dev: true + /prettier@2.8.7: + resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} + engines: {node: '>=10.13.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + /prettier@3.3.2: resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} engines: {node: '>=14'} @@ -6910,7 +7011,6 @@ packages: /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7126,9 +7226,12 @@ packages: mdast-util-to-markdown: 2.1.0 unified: 11.0.5 + /request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + dev: false + /request-light@0.7.0: resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} - dev: true /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -7138,7 +7241,6 @@ packages: /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: true /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -8052,7 +8154,6 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.1 - dev: true /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8348,6 +8449,19 @@ packages: vscode-uri: 3.0.8 dev: true + /volar-service-yaml@0.0.54(@volar/language-service@2.3.4): + resolution: {integrity: sha512-gT+RbDviF3XfavtzlLCI3ai7c12do5dricXIEJ5Gg1t2Cy+6n6x4bLaqQgyOwcPVRh35Hti6hvUJ5LQUuuzpkw==} + peerDependencies: + '@volar/language-service': ~2.3.1 + peerDependenciesMeta: + '@volar/language-service': + optional: true + dependencies: + '@volar/language-service': 2.3.4 + vscode-uri: 3.0.8 + yaml-language-server: 1.15.0 + dev: false + /vscode-css-languageservice@6.3.0: resolution: {integrity: sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw==} dependencies: @@ -8366,36 +8480,73 @@ packages: vscode-uri: 3.0.8 dev: true + /vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + dev: false + + /vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + dev: false + /vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} - dev: true + + /vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} + dependencies: + minimatch: 5.1.6 + semver: 7.6.2 + vscode-languageserver-protocol: 3.17.5 + dev: false + + /vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + dev: false /vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} dependencies: vscode-jsonrpc: 8.2.0 vscode-languageserver-types: 3.17.5 - dev: true /vscode-languageserver-textdocument@1.0.11: resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} - dev: true + + /vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + dev: false /vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - dev: true + + /vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + dependencies: + vscode-languageserver-protocol: 3.16.0 + dev: false /vscode-languageserver@9.0.1: resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} hasBin: true dependencies: vscode-languageserver-protocol: 3.17.5 - dev: true /vscode-nls@5.2.0: resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - dev: true /vscode-uri@2.1.2: resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} @@ -8403,7 +8554,6 @@ packages: /vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - dev: true /w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -8488,6 +8638,29 @@ packages: /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + /yaml-language-server@1.15.0: + resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} + hasBin: true + dependencies: + ajv: 8.16.0 + lodash: 4.17.21 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + yaml: 2.2.2 + optionalDependencies: + prettier: 2.8.7 + dev: false + + /yaml@2.2.2: + resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} + engines: {node: '>= 14'} + dev: false + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} From 1d4cfa19ed6054bd6af21435b5e100653ca5976b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B2an?= Date: Tue, 16 Jul 2024 09:34:19 +0000 Subject: [PATCH 2/6] feat: provide a description for every property. --- .../vscode/src/language-server/index.ts | 4 +- packages/types/src/schemas/chapter.ts | 7 +- packages/types/src/schemas/common.ts | 129 +++++++++++++----- packages/types/src/schemas/i18n.ts | 27 ++-- packages/types/src/schemas/lesson.ts | 7 +- packages/types/src/schemas/part.ts | 7 +- packages/types/src/schemas/tutorial.ts | 7 +- 7 files changed, 133 insertions(+), 55 deletions(-) diff --git a/extensions/vscode/src/language-server/index.ts b/extensions/vscode/src/language-server/index.ts index 120265e3d..4bfff3648 100644 --- a/extensions/vscode/src/language-server/index.ts +++ b/extensions/vscode/src/language-server/index.ts @@ -10,8 +10,6 @@ const server = createServer(connection); connection.listen(); connection.onInitialize((params) => { - connection.console.debug('CONNECTED' + params.capabilities); - const yamlService = createYamlService({ getLanguageSettings(_context) { const schema = readSchema(); @@ -25,7 +23,7 @@ connection.onInitialize((params) => { isKubernetes: false, schemas: [ { - uri: 'https://tutorialkit.dev/schema.json', + uri: 'https://tutorialkit.dev/reference/configuration/', schema, fileMatch: [ '**/*', diff --git a/packages/types/src/schemas/chapter.ts b/packages/types/src/schemas/chapter.ts index 7b0cff182..bd4fa10c9 100644 --- a/packages/types/src/schemas/chapter.ts +++ b/packages/types/src/schemas/chapter.ts @@ -3,7 +3,12 @@ import { baseSchema } from './common.js'; export const chapterSchema = baseSchema.extend({ type: z.literal('chapter'), - lessons: z.array(z.string()).optional(), + lessons: z + .array(z.string()) + .optional() + .describe( + 'The list of lessons in this chapter. The order in this array defines the order of the lessons. If not specified a folder-based numbering system is used instead.', + ), }); export type ChapterSchema = z.infer; diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index 3c4bb9e01..c93b10b21 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -9,16 +9,21 @@ export const commandSchema = z.union([ z.tuple([z.string(), z.string()]), z.strictObject({ - command: z.string(), - title: z.string(), + command: z.string().describe('Command to execute in WebContainer.'), + title: z.string().describe('Title to show for this step in the Prepare Environment section.'), }), ]); export type CommandSchema = z.infer; export const commandsSchema = z.object({ - mainCommand: commandSchema.optional(), - prepareCommands: commandSchema.array().optional(), + mainCommand: commandSchema.optional().describe('The last command to be executed. Typically a dev server.'), + prepareCommands: commandSchema + .array() + .optional() + .describe( + 'List of commands to be executed to prepare the environment in WebContainer. Each command executed and its status will be shown in the Prepare Environment section.', + ), }); export type CommandsSchema = z.infer; @@ -36,8 +41,8 @@ export const previewSchema = z.union([ z.tuple([z.number(), z.string()]), z.strictObject({ - port: z.number(), - title: z.string(), + port: z.number().describe('Port number of the preview.'), + title: z.string().describe('Title of the preview.'), }), ]) .array(), @@ -45,7 +50,19 @@ export const previewSchema = z.union([ export type PreviewSchema = z.infer; -const panelType = z.union([z.literal('output'), z.literal('terminal')]); +const panelTypeSchema = z + .union([z.literal('output'), z.literal('terminal')]) + .describe(`The type of the terminal which can either be 'output' or 'terminal'.`); + +const allowRedirectsSchema = z + .boolean() + .optional() + .describe('`true` if you want to enable output redirects in the terminal, disabled by default.'); + +const allowCommandsSchema = z + .array(z.string()) + .optional() + .describe('List of command that are allowed in the terminal, if not provided, all commands are allowed.'); export const terminalSchema = z.union([ // `false` if you want to disable the terminal entirely @@ -67,12 +84,12 @@ export const terminalSchema = z.union([ .array( z.union([ // the type of the panel - panelType, + panelTypeSchema, // or a tuple with the type and the title of the panel z.tuple([ // the type of the panel - panelType, + panelTypeSchema, // the title of the panel which is shown in the tab z.string(), @@ -81,19 +98,24 @@ export const terminalSchema = z.union([ // or an object defining the panel z.strictObject({ // the type of the panel - type: panelType, + type: panelTypeSchema, // an id linking the terminal of multiple lessons together - id: z.string().optional(), + id: z + .string() + .optional() + .describe( + 'An id linking the terminal of multiple lessons together so that its state is preserved between lessons.', + ), // the title of the panel which is shown in the tab - title: z.string().optional(), + title: z.string().optional().describe('The title of the panel which is shown in the tab.'), // `true` if you want to enable output redirects in the terminal, disabled by default - allowRedirects: z.boolean().optional(), + allowRedirects: allowRedirectsSchema, // list of command that are allowed in the terminal, if not provided, all commands are allowed - allowCommands: z.array(z.string()).optional(), + allowCommands: allowCommandsSchema, }), ]), ) @@ -122,35 +144,62 @@ export const terminalSchema = z.union([ }, ), ]), - activePanel: z.number().gte(0).optional(), + activePanel: z.number().gte(0).optional().describe('Defines which panel should be visible by default.'), // `true` if you want to enable output redirects in the terminal, disabled by default - allowRedirects: z.boolean().optional(), + allowRedirects: allowRedirectsSchema, // list of command that are allowed in the terminal, if not provided, all commands are allowed - allowCommands: z.array(z.string()).optional(), + allowCommands: allowCommandsSchema, }), ]); -export type TerminalPanelType = z.infer; +export type TerminalPanelType = z.infer; export type TerminalSchema = z.infer; export const webcontainerSchema = commandsSchema.extend({ - previews: previewSchema.optional(), - autoReload: z.boolean().optional(), - template: z.string().optional(), - terminal: terminalSchema.optional(), - focus: z.string().optional(), - editor: z.union([ - // can either be completely removed by setting it to `false` - z.boolean().optional(), - - // or you can only remove the file tree - z.strictObject({ - fileTree: z.boolean().optional(), - }), - ]), - i18n: i18nSchema.optional(), + previews: previewSchema + .optional() + .describe( + 'Configure which ports should be used for the previews allowing you to align the behavior with your demo application’s dev server setup. If not specified, the lowest port will be used.', + ), + autoReload: z + .boolean() + .optional() + .describe( + 'Navigating to a lesson that specifies autoReload will always reload the preview. This is typically only needed if your server does not support HMR.', + ), + template: z + .string() + .optional() + .describe( + 'Specified which folder from the src/templates/ directory should be used as the basis for the code. See the “Code templates” guide for a detailed explainer.', + ), + terminal: terminalSchema + .optional() + .describe( + 'Configures one or more terminals. TutorialKit provides two types of terminals: read-only, called output, and interactive, called terminal.', + ), + focus: z + .string() + .optional() + .describe('Defines which file should be opened in the code editor by default when lesson loads.'), + editor: z + .union([ + // can either be completely removed by setting it to `false` + z.boolean().optional(), + + // or you can only remove the file tree + z.strictObject({ + fileTree: z.boolean().optional(), + }), + ]) + .describe( + 'Configure whether or not the editor should be rendered. If an object is provided with fileTree: false, only the file tree is hidden.', + ), + i18n: i18nSchema + .optional() + .describe('Lets you define alternative texts used in the UI. This is useful for localization.'), editPageLink: z .union([ // pattern for creating the URL @@ -159,12 +208,20 @@ export const webcontainerSchema = commandsSchema.extend({ // `false` for disabling the edit link z.boolean(), ]) - .optional(), + .optional() + .describe( + 'Display a link in lesson for editing the page content. The value is a URL pattern where ${path} is replaced with the lesson’s location relative to src/content/tutorial.', + ), }); export const baseSchema = webcontainerSchema.extend({ - title: z.string(), - slug: z.string().optional(), + title: z.string().describe('The title of the part, chapter, or lesson.'), + slug: z + .string() + .optional() + .describe( + 'Customize the URL segment of this part / chapter or lesson. The full URL path is /:partSlug/:chapterSlug/:lessonSlug.', + ), }); export type BaseSchema = z.infer; diff --git a/packages/types/src/schemas/i18n.ts b/packages/types/src/schemas/i18n.ts index d9881bfdc..017a3a398 100644 --- a/packages/types/src/schemas/i18n.ts +++ b/packages/types/src/schemas/i18n.ts @@ -6,64 +6,69 @@ export const i18nSchema = z.object({ * * @default 'Part ${index}: ${title}' */ - partTemplate: z.string().optional(), + partTemplate: z.string().optional().describe('Template on how to format a part. Variables: ${index} and ${title}.'), /** * Text of the edit page link. * * @default 'Edit this page' */ - editPageText: z.string().optional(), + editPageText: z.string().optional().describe('Text of the edit page link.'), /** * Text shown when there are no previews or steps to show in the prepare environment section. * * @default 'Start WebContainer' */ - startWebContainerText: z.string().optional(), + startWebContainerText: z + .string() + .optional() + .describe('Text shown when there are no previews or steps to show in the prepare environment section.'), /** - * Text shown on the call to action button to start webcontainer when boot was blocked - * due to memory restrictions. + * Text shown in the preview section when there are no steps to run and no preview to show. * * @default 'No preview to run nor steps to show' */ - noPreviewNorStepsText: z.string().optional(), + noPreviewNorStepsText: z + .string() + .optional() + .describe('Text shown in the preview section when there are no steps to run and no preview to show.'), /** * Text shown on top of the file tree. * * @default 'Files' */ - filesTitleText: z.string().optional(), + filesTitleText: z.string().optional().describe('Text shown on top of the file tree.'), /** * Text shown on top of the steps section. * * @default 'Preparing Environment' */ - prepareEnvironmentTitleText: z.string().optional(), + prepareEnvironmentTitleText: z.string().optional().describe('Text shown on top of the steps section.'), /** * Text shown for the toggle terminal button. * * @default 'Toggle Terminal' */ - toggleTerminalButtonText: z.string().optional(), + toggleTerminalButtonText: z.string().optional().describe('Text shown for the toggle terminal button.'), /** * Text shown for the solve button. * * @default 'Solve' */ - solveButtonText: z.string().optional(), + solveButtonText: z.string().optional().describe('Text shown for the solve button.'), /** * Text shown for the reset button. * * @default 'Reset' */ - resetButtonText: z.string().optional(), + resetButtonText: z.string().optional().describe('Text shown for the reset button.'), }); export type I18nSchema = z.infer; diff --git a/packages/types/src/schemas/lesson.ts b/packages/types/src/schemas/lesson.ts index 7b501d7c5..73e3d8bb2 100644 --- a/packages/types/src/schemas/lesson.ts +++ b/packages/types/src/schemas/lesson.ts @@ -3,8 +3,11 @@ import { baseSchema } from './common.js'; export const lessonSchema = baseSchema.extend({ type: z.literal('lesson'), - scope: z.string().optional(), - hideRoot: z.boolean().optional(), + scope: z.string().optional().describe('A prefix that all file paths must match to be visible in the file tree.'), + hideRoot: z + .boolean() + .optional() + .describe('If set to false, `/` is shown at the top of the file tree. Defaults to true.'), }); export type LessonSchema = z.infer; diff --git a/packages/types/src/schemas/part.ts b/packages/types/src/schemas/part.ts index cce512857..4591e90bb 100644 --- a/packages/types/src/schemas/part.ts +++ b/packages/types/src/schemas/part.ts @@ -3,7 +3,12 @@ import { baseSchema } from './common.js'; export const partSchema = baseSchema.extend({ type: z.literal('part'), - chapters: z.array(z.string()).optional(), + chapters: z + .array(z.string()) + .optional() + .describe( + 'The list of chapters in this part. The order in this array defines the order of the chapters. If not specified a folder-based numbering system is used instead.', + ), }); export type PartSchema = z.infer; diff --git a/packages/types/src/schemas/tutorial.ts b/packages/types/src/schemas/tutorial.ts index 0f86c1339..74fb3671f 100644 --- a/packages/types/src/schemas/tutorial.ts +++ b/packages/types/src/schemas/tutorial.ts @@ -4,7 +4,12 @@ import { webcontainerSchema } from './common.js'; export const tutorialSchema = webcontainerSchema.extend({ type: z.literal('tutorial'), logoLink: z.string().optional(), - parts: z.array(z.string()).optional(), + parts: z + .array(z.string()) + .optional() + .describe( + 'The list of parts in this tutorial. The order in this array defines the order of the parts. If not specified a folder-based numbering system is used instead.', + ), }); export type TutorialSchema = z.infer; From 2b481ddacae78c614d115c5754e615a95630ec1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B2an?= Date: Wed, 17 Jul 2024 15:11:16 +0000 Subject: [PATCH 3/6] fix: issues with schema re-generation --- extensions/vscode/package.json | 6 ++--- extensions/vscode/{ => scripts}/build.mjs | 26 ++++++++++++------- .../vscode/scripts/load-schema-worker.mjs | 7 +++++ package.json | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) rename extensions/vscode/{ => scripts}/build.mjs (78%) create mode 100644 extensions/vscode/scripts/load-schema-worker.mjs diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 46586f59f..f66030df8 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -123,11 +123,11 @@ "__dev": "pnpm run esbuild-base -- --sourcemap --watch", "__vscode:prepublish": "pnpm run esbuild-base -- --minify", "__build": "vsce package", - "dev": "node build.mjs --watch", - "build": "pnpm run check-types && node build.mjs", + "dev": "node scripts/build.mjs --watch", + "build": "pnpm run check-types && node scripts/build.mjs", "check-types": "tsc --noEmit", "vscode:prepublish": "pnpm run package", - "package": "pnpm run check-types && node build.mjs --production" + "package": "pnpm run check-types && node scripts/build.mjs --production" }, "dependencies": { "@volar/language-core": "2.3.4", diff --git a/extensions/vscode/build.mjs b/extensions/vscode/scripts/build.mjs similarity index 78% rename from extensions/vscode/build.mjs rename to extensions/vscode/scripts/build.mjs index ee4ff94d3..2fb898deb 100644 --- a/extensions/vscode/build.mjs +++ b/extensions/vscode/scripts/build.mjs @@ -1,11 +1,13 @@ -import { chapterSchema, lessonSchema, partSchema, tutorialSchema } from '@tutorialkit/types'; import { watch } from 'chokidar'; import * as esbuild from 'esbuild'; import { execa } from 'execa'; import fs from 'node:fs'; import { createRequire } from 'node:module'; -import { zodToJsonSchema } from 'zod-to-json-schema'; +import { join, dirname } from 'path'; +import { Worker } from 'node:worker_threads'; +import { fileURLToPath } from 'node:url'; +const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const production = process.argv.includes('--production'); const isWatch = process.argv.includes('--watch'); @@ -30,11 +32,10 @@ async function main() { }); if (isWatch) { - const buildMetadataSchemaDebounced = debounce(buildMetadataSchema); + const buildMetadataSchemaDebounced = debounce(buildMetadataSchema, 100); + const dependencyPath = dirname(require.resolve('@tutorialkit/types')); - watch(join(require.resolve('@tutorialkit/types'), 'dist'), { - followSymlinks: false, - }).on('all', (eventName, path) => { + watch(dependencyPath).on('all', (eventName, path) => { if (eventName !== 'change' && eventName !== 'add' && eventName !== 'unlink') { return; } @@ -53,7 +54,7 @@ async function main() { await ctx.rebuild(); await ctx.dispose(); - buildMetadataSchema(); + await buildMetadataSchema(); if (production) { // rename name in package json to match extension name on store: @@ -66,11 +67,16 @@ async function main() { } } -function buildMetadataSchema() { - const schema = tutorialSchema.strict().or(partSchema.strict()).or(chapterSchema.strict()).or(lessonSchema.strict()); +async function buildMetadataSchema() { + const schema = await new Promise((resolve) => { + const worker = new Worker(join(__dirname, './load-schema-worker.mjs')); + worker.on('message', (value) => resolve(value)); + }); fs.mkdirSync('./dist', { recursive: true }); - fs.writeFileSync('./dist/schema.json', JSON.stringify(zodToJsonSchema(schema), undefined, 2), 'utf-8'); + fs.writeFileSync('./dist/schema.json', JSON.stringify(schema, undefined, 2), 'utf-8'); + + console.log('Updated schema.json'); } /** diff --git a/extensions/vscode/scripts/load-schema-worker.mjs b/extensions/vscode/scripts/load-schema-worker.mjs new file mode 100644 index 000000000..2f836147f --- /dev/null +++ b/extensions/vscode/scripts/load-schema-worker.mjs @@ -0,0 +1,7 @@ +import { parentPort } from 'node:worker_threads'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { chapterSchema, lessonSchema, partSchema, tutorialSchema } from '@tutorialkit/types'; + +const schema = tutorialSchema.strict().or(partSchema.strict()).or(chapterSchema.strict()).or(lessonSchema.strict()); + +parentPort.postMessage(zodToJsonSchema(schema)); diff --git a/package.json b/package.json index b582b649a..2964aeacb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "changelog": "./scripts/changelog.mjs", "clean": "./scripts/clean.sh", "prepare": "is-ci || husky install", - "extension:dev": "pnpm run --filter=tutorialkit-vscode dev", + "extension:dev": "pnpm --parallel --stream --filter=@tutorialkit/types --filter=tutorialkit-vscode run dev", "extension:build": "pnpm run --filter=tutorialkit-vscode build", "template:dev": "TUTORIALKIT_DEV=true pnpm run build && pnpm run --filter=tutorialkit-starter dev", "template:build": "pnpm run build && pnpm run --filter=tutorialkit-starter build", From 143bde71cc7f09fc99aba20cab05774676307cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B2an?= Date: Wed, 17 Jul 2024 15:25:58 +0000 Subject: [PATCH 4/6] fix: update tsconfig --- extensions/vscode/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index 547ecfffe..e029bdc09 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -10,6 +10,6 @@ "sourceMap": true, "rootDir": "." }, - "include": ["src", "./build.mjs"], + "include": ["src", "scripts"], "references": [{ "path": "../../packages/types" }] } From e4a5f0e0713536626174a09989b71897a9b2b5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 22 Jul 2024 19:10:11 +0300 Subject: [PATCH 5/6] fix: code review --- extensions/vscode/scripts/build.mjs | 2 +- extensions/vscode/src/extension.ts | 3 +++ extensions/vscode/src/language-server/index.ts | 2 +- .../vscode/src/language-server/languagePlugin.ts | 7 ++++--- packages/types/src/schemas/chapter.ts | 2 +- packages/types/src/schemas/common.ts | 8 ++++---- packages/types/src/schemas/i18n.ts | 12 ++++++------ 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/extensions/vscode/scripts/build.mjs b/extensions/vscode/scripts/build.mjs index 2fb898deb..48d1c8b45 100644 --- a/extensions/vscode/scripts/build.mjs +++ b/extensions/vscode/scripts/build.mjs @@ -57,7 +57,7 @@ async function main() { await buildMetadataSchema(); if (production) { - // rename name in package json to match extension name on store: + // rename name in `package.json` to match extension name on store const pkgJSON = JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf8' })); pkgJSON.name = 'tutorialkit'; diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index fafa5d979..2a12a2065 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -18,6 +18,7 @@ export async function activate(context: vscode.ExtensionContext) { const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js'); const runOptions = { execArgv: [] }; const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; + const serverOptions: lsp.ServerOptions = { run: { module: serverModule.fsPath, @@ -30,10 +31,12 @@ export async function activate(context: vscode.ExtensionContext) { options: debugOptions, }, }; + const clientOptions: lsp.LanguageClientOptions = { documentSelector: [{ language: 'markdown' }, { language: 'mdx' }], initializationOptions: {}, }; + client = new lsp.LanguageClient('tutorialkit-language-server', 'TutorialKit', serverOptions, clientOptions); await client.start(); diff --git a/extensions/vscode/src/language-server/index.ts b/extensions/vscode/src/language-server/index.ts index 4bfff3648..25a84bd0b 100644 --- a/extensions/vscode/src/language-server/index.ts +++ b/extensions/vscode/src/language-server/index.ts @@ -23,7 +23,7 @@ connection.onInitialize((params) => { isKubernetes: false, schemas: [ { - uri: 'https://tutorialkit.dev/reference/configuration/', + uri: 'https://tutorialkit.dev/reference/configuration', schema, fileMatch: [ '**/*', diff --git a/extensions/vscode/src/language-server/languagePlugin.ts b/extensions/vscode/src/language-server/languagePlugin.ts index c802877e4..9b1996509 100644 --- a/extensions/vscode/src/language-server/languagePlugin.ts +++ b/extensions/vscode/src/language-server/languagePlugin.ts @@ -2,8 +2,8 @@ import { CodeMapping, type LanguagePlugin, type VirtualCode } from '@volar/langu import type * as ts from 'typescript'; import type { URI } from 'vscode-uri'; -export const frontmatterPlugin = (debug: (message: string) => void) => - ({ +export function frontmatterPlugin(debug: (message: string) => void): LanguagePlugin { + return { getLanguageId(uri) { debug('URI: ' + uri.path); @@ -24,7 +24,8 @@ export const frontmatterPlugin = (debug: (message: string) => void) => return undefined; }, - }) satisfies LanguagePlugin; + }; +} export class FrontMatterVirtualCode implements VirtualCode { id = 'root'; diff --git a/packages/types/src/schemas/chapter.ts b/packages/types/src/schemas/chapter.ts index bd4fa10c9..24d29c371 100644 --- a/packages/types/src/schemas/chapter.ts +++ b/packages/types/src/schemas/chapter.ts @@ -7,7 +7,7 @@ export const chapterSchema = baseSchema.extend({ .array(z.string()) .optional() .describe( - 'The list of lessons in this chapter. The order in this array defines the order of the lessons. If not specified a folder-based numbering system is used instead.', + 'The list of lessons in this chapter. The order of the array defines the order of the lessons. If not specified a folder-based numbering system is used instead.', ), }); diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index c93b10b21..a2ad0df55 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -57,7 +57,7 @@ const panelTypeSchema = z const allowRedirectsSchema = z .boolean() .optional() - .describe('`true` if you want to enable output redirects in the terminal, disabled by default.'); + .describe("Set to `true` if you want to enable output redirects in the terminal. It's disabled by default."); const allowCommandsSchema = z .array(z.string()) @@ -173,7 +173,7 @@ export const webcontainerSchema = commandsSchema.extend({ .string() .optional() .describe( - 'Specified which folder from the src/templates/ directory should be used as the basis for the code. See the “Code templates” guide for a detailed explainer.', + 'Specified which folder from the `src/templates/` directory should be used as the basis for the code. See the "Code templates" guide for a detailed explainer.', ), terminal: terminalSchema .optional() @@ -210,7 +210,7 @@ export const webcontainerSchema = commandsSchema.extend({ ]) .optional() .describe( - 'Display a link in lesson for editing the page content. The value is a URL pattern where ${path} is replaced with the lesson’s location relative to src/content/tutorial.', + 'Display a link in lesson for editing the page content. The value is a URL pattern where `${path}` is replaced with the lesson’s location relative to `src/content/tutorial`.', ), }); @@ -220,7 +220,7 @@ export const baseSchema = webcontainerSchema.extend({ .string() .optional() .describe( - 'Customize the URL segment of this part / chapter or lesson. The full URL path is /:partSlug/:chapterSlug/:lessonSlug.', + 'Customize the URL segment of this part, chapter or lesson. The full URL path is `/:partSlug/:chapterSlug/:lessonSlug`.', ), }); diff --git a/packages/types/src/schemas/i18n.ts b/packages/types/src/schemas/i18n.ts index 017a3a398..cdd49e50c 100644 --- a/packages/types/src/schemas/i18n.ts +++ b/packages/types/src/schemas/i18n.ts @@ -50,25 +50,25 @@ export const i18nSchema = z.object({ prepareEnvironmentTitleText: z.string().optional().describe('Text shown on top of the steps section.'), /** - * Text shown for the toggle terminal button. + * Text for the toggle terminal button. * * @default 'Toggle Terminal' */ - toggleTerminalButtonText: z.string().optional().describe('Text shown for the toggle terminal button.'), + toggleTerminalButtonText: z.string().optional().describe('Text for the toggle terminal button.'), /** - * Text shown for the solve button. + * Text for the solve button. * * @default 'Solve' */ - solveButtonText: z.string().optional().describe('Text shown for the solve button.'), + solveButtonText: z.string().optional().describe('Text for the solve button.'), /** - * Text shown for the reset button. + * Text for the reset button. * * @default 'Reset' */ - resetButtonText: z.string().optional().describe('Text shown for the reset button.'), + resetButtonText: z.string().optional().describe('Text for the reset button.'), }); export type I18nSchema = z.infer; From 1e6b67f3de536fa0e1cf21a1376725a922d7dc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 25 Jul 2024 10:36:07 +0300 Subject: [PATCH 6/6] fix: code review --- .../src/content/docs/reference/configuration.mdx | 2 +- extensions/vscode/scripts/load-schema-worker.mjs | 6 ++---- extensions/vscode/src/language-server/index.ts | 2 +- packages/types/src/schemas/common.ts | 2 +- packages/types/src/schemas/part.ts | 2 +- packages/types/src/schemas/tutorial.ts | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx index b97a6e928..893a8ac50 100644 --- a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx +++ b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx @@ -174,7 +174,7 @@ Navigating to a lesson that specifies `autoReload` will always reload the previe ##### `template` -Specified which folder from the `src/templates/` directory should be used as the basis for the code. See the "[Code templates](/guides/creating-content/#code-templates)" guide for a detailed explainer. +Specifies which folder from the `src/templates/` directory should be used as the basis for the code. See the "[Code templates](/guides/creating-content/#code-templates)" guide for a detailed explainer. #### `editPageLink` diff --git a/extensions/vscode/scripts/load-schema-worker.mjs b/extensions/vscode/scripts/load-schema-worker.mjs index 2f836147f..35036568d 100644 --- a/extensions/vscode/scripts/load-schema-worker.mjs +++ b/extensions/vscode/scripts/load-schema-worker.mjs @@ -1,7 +1,5 @@ import { parentPort } from 'node:worker_threads'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { chapterSchema, lessonSchema, partSchema, tutorialSchema } from '@tutorialkit/types'; +import { contentSchema } from '@tutorialkit/types'; -const schema = tutorialSchema.strict().or(partSchema.strict()).or(chapterSchema.strict()).or(lessonSchema.strict()); - -parentPort.postMessage(zodToJsonSchema(schema)); +parentPort.postMessage(zodToJsonSchema(contentSchema)); diff --git a/extensions/vscode/src/language-server/index.ts b/extensions/vscode/src/language-server/index.ts index 25a84bd0b..5d43b7615 100644 --- a/extensions/vscode/src/language-server/index.ts +++ b/extensions/vscode/src/language-server/index.ts @@ -28,7 +28,7 @@ connection.onInitialize((params) => { fileMatch: [ '**/*', - // TODO: those don't work + // TODO: these don't work 'src/content/*.md', 'src/content/**/*.md', 'src/content/**/*.mdx', diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index a2ad0df55..e071489be 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -173,7 +173,7 @@ export const webcontainerSchema = commandsSchema.extend({ .string() .optional() .describe( - 'Specified which folder from the `src/templates/` directory should be used as the basis for the code. See the "Code templates" guide for a detailed explainer.', + 'Specifies which folder from the `src/templates/` directory should be used as the basis for the code. See the "Code templates" guide for a detailed explainer.', ), terminal: terminalSchema .optional() diff --git a/packages/types/src/schemas/part.ts b/packages/types/src/schemas/part.ts index 4591e90bb..ce3682e1b 100644 --- a/packages/types/src/schemas/part.ts +++ b/packages/types/src/schemas/part.ts @@ -7,7 +7,7 @@ export const partSchema = baseSchema.extend({ .array(z.string()) .optional() .describe( - 'The list of chapters in this part. The order in this array defines the order of the chapters. If not specified a folder-based numbering system is used instead.', + 'The list of chapters in this part. The order of this array defines the order of the chapters. If not specified a folder-based numbering system is used instead.', ), }); diff --git a/packages/types/src/schemas/tutorial.ts b/packages/types/src/schemas/tutorial.ts index 74fb3671f..6ae8bf4fc 100644 --- a/packages/types/src/schemas/tutorial.ts +++ b/packages/types/src/schemas/tutorial.ts @@ -8,7 +8,7 @@ export const tutorialSchema = webcontainerSchema.extend({ .array(z.string()) .optional() .describe( - 'The list of parts in this tutorial. The order in this array defines the order of the parts. If not specified a folder-based numbering system is used instead.', + 'The list of parts in this tutorial. The order of this array defines the order of the parts. If not specified a folder-based numbering system is used instead.', ), });