Skip to content

Commit

Permalink
Typed snippets with Shiki Twoslash (#1239)
Browse files Browse the repository at this point in the history
Adds the [Shiki Twoslash](https://shiki.style/packages/twoslash) plugin
to enable type-augmented JS/TS snippets.

First scope is
- cds-typer docs
- cds-server docs

Typed models (in `@cds-models/*`) are generated through a markdown
renderer plugin that runs cds-typer.
  • Loading branch information
chgeo committed Sep 11, 2024
1 parent a0ccfa9 commit 481d1e7
Show file tree
Hide file tree
Showing 13 changed files with 3,365 additions and 618 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
@cds-models/
.temp/
cache/
dist/
Expand Down
6 changes: 6 additions & 0 deletions .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { sidebar, nav4 } from './menu'
import * as redirects from './lib/redirects'
import * as cdsMavenSite from './lib/cds-maven-site'
import * as MdAttrsPropagate from './lib/md-attrs-propagate'
import * as MdTypedModels from './lib/md-typed-models'
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'

export type CapireThemeConfig = DefaultTheme.Config & {
capire: {
Expand Down Expand Up @@ -139,8 +141,12 @@ const config:UserConfig<CapireThemeConfig> = {
toc: {
level: [2,3]
},
codeTransformers: [
transformerTwoslash()
],
config: md => {
MdAttrsPropagate.install(md)
MdTypedModels.install(md)
},
},
sitemap: {
Expand Down
51 changes: 51 additions & 0 deletions .vitepress/lib/md-typed-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { MarkdownRenderer } from 'vitepress'
import { execSync } from 'node:child_process'
import { dirname, join, relative, resolve } from 'node:path'
import { existsSync } from 'node:fs'

type mdItEnv = { frontmatter: Record<string, any>, path: string, realPath: string }
const modelOut = '@cds-models'

/**
* A markdown renderer that runs cds-typer for all code fences in .md pages configured
* with `typedModel` frontmatter entries.
*
* 1. Runs cds-typer for each `typedModel` path
* 2. Replaces `%typedModels:...:resolved%` strings with the resolved typer model,
* e.g. `%typedModels:${modelKey}:resolved%` -> `tools/assets/bookshop/@cds-models/*`
*
* It's implemented as a Markdown renderer because the whole Shiki/Twoslash renderer runs there.
*/
export function install(md: MarkdownRenderer) {
const fence = md.renderer.rules.fence
md.renderer.rules.fence = (tokens, idx, options, env: mdItEnv, ...args) => {
const typedModels = env.frontmatter.typedModels as Record<string,string>|undefined
if (typedModels) {
const mdDir = dirname(env.realPath ?? env.path) // realPath is only set if Vitepress path rewrites are in place
for (const modelKey in typedModels) {
const modelPath = typedModels[modelKey]

const srcDir = join(mdDir, modelPath)
if (!existsSync(srcDir)) throw new Error(`${srcDir} does not exist. Check the '${modelPath}' path in frontmatter.`)

runTyper(srcDir, modelOut)

const resolvedPath = resolve(mdDir, modelPath, modelOut, '*')
tokens[idx].content = tokens[idx].content.replaceAll(`%typedModels:${modelKey}:resolved%`, resolvedPath)
}
}

return fence!(tokens, idx, options, env, ...args)
}
}

function runTyper(srcDir:string, out:string) {
const outPath = resolve(srcDir, out)
// If target dir exists, stop here. Delta compilation is done through cds-typer ion VS Code.
if (existsSync(outPath)) return

const label = '✓ running cds-typer in ' + relative(process.cwd(), srcDir)
console.time(label)
execSync(`npm exec --prefix ${srcDir} -- cds-typer '*' --outputDirectory ${out}`, {cwd: srcDir})
console.timeEnd(label)
}
4 changes: 4 additions & 0 deletions .vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import Beta from './components/Beta.vue';
import Concept from './components/Concept.vue'
import Since from './components/Since.vue';
import ScrollToTop from './components/ScrollToTop.vue'
import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'

import '@shikijs/vitepress-twoslash/style.css'
import './custom.scss'

/**
Expand All @@ -25,5 +27,7 @@ export default {
ctx.app.component('Concept', Concept)
ctx.app.component('Since', Since)
ctx.app.component('ScrollToTop', ScrollToTop)

ctx.app.use(TwoslashFloatingVue)
}
}
26 changes: 18 additions & 8 deletions node.js/cds-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ with `cds run` and `cds watch` as convenience variants.
The built-in `server.js` constructs an [express.js app](cds-facade#cds-app), and bootstraps all CAP services using [`cds.connect`](cds-connect) and [`cds.serve`](cds-serve).
Its implementation essentially is as follows:

```js
```js twoslash
const cds = require('@sap/cds')
module.exports = async function cds_server(options) {

Expand Down Expand Up @@ -103,7 +103,8 @@ The CLI command `cds serve` optionally bootstraps from project-local `./server.j

In custom `server.js`, you can plugin to all parts of `@sap/cds`. Most commonly you'd register own handlers to lifecycle events emitted to [the `cds` facade object](cds-facade) as below:

```js
```js twoslash
// @noErrors
const cds = require('@sap/cds')
// react on bootstrapping events...
cds.on('bootstrap', ...)
Expand All @@ -116,7 +117,8 @@ Provide an own bootstrapping function if you want to access and process the comm
This also allows you to override certain options before delegating to the built-in `server.js`.
In the example below, we construct the express.js app ourselves and fix the models to be loaded.

```js
```js twoslash
// @noErrors
const cds = require('@sap/cds')
// react on bootstrapping events...
cds.on('bootstrap', ...)
Expand All @@ -142,10 +144,11 @@ The `req` object in your express middleware is not the same as `req` in your CDS
A one-time event, emitted immediately after the [express.js app](cds-facade#cds-app)
has been created and before any middleware or CDS services are added to it.

```js
```js twoslash
// @checkJs
const cds = require('@sap/cds')
const express = require('express')
cds.on('bootstrap', (app)=>{
cds.on('bootstrap', app => {
// add your own middleware before any by cds are added

// for example, serve static resources incl. index.html
Expand Down Expand Up @@ -176,7 +179,8 @@ Emitted for each service constructed by [`cds.serve`](cds-serve).

A one-time event, emitted when all services have been bootstrapped and added to the [express.js app](cds-facade#cds-app).

```js
```js twoslash
// @checkJs
const cds = require('@sap/cds')
cds.on('served', (services)=>{
// We can savely access service instances through the provided argument:
Expand Down Expand Up @@ -210,15 +214,21 @@ This is due to `cds.on()` and `cds.emit()` using Node's [EventEmitter](https://n

In other words this asynchronous handler code does **not work** as expected:

```js
```js twoslash
// @checkJs
const cds = require('@sap/cds')
const asyncCode = async () => Promise.resolve()
// ---cut---
cds.on ('bootstrap', async ()=> {
await asyncCode() // [!code error] // will NOT be awaited
})
```

You can use the [served](#served) event's asynchronous nature though to wait for such bootstrap code:

```js
```js twoslash
const cds = require('@sap/cds')
// ---cut---
let done
cds.on('bootstrap', ()=> {
done = asyncCode()
Expand Down
Loading

0 comments on commit 481d1e7

Please sign in to comment.