Skip to content

Commit 2a61696

Browse files
authored
Merge pull request #30 from EFNext/feat/docs-facelift
Enhance documentation workflow and add Blazor WASM playground support
2 parents 67909b0 + af542e3 commit 2a61696

90 files changed

Lines changed: 6409 additions & 2277 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmarks.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ jobs:
4545
restore-keys: |
4646
nuget-${{ runner.os }}-
4747
48+
# Required by Playground.Wasm's Release AOT compilation.
49+
- name: Install wasm-tools workload
50+
run: dotnet workload install wasm-tools
51+
4852
- name: Restore
4953
run: dotnet restore
5054

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ jobs:
3939
restore-keys: |
4040
nuget-${{ runner.os }}-
4141
42+
# Required by Playground.Wasm's Release AOT compilation.
43+
- name: Install wasm-tools workload
44+
run: dotnet workload install wasm-tools
45+
4246
- name: Restore
4347
run: dotnet restore
4448

.github/workflows/docs.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- 'docs/**'
8+
- 'src/Docs/**'
89
- '.github/workflows/docs.yml'
910
workflow_dispatch:
1011

@@ -20,6 +21,40 @@ jobs:
2021
- name: Checkout main
2122
uses: actions/checkout@v4
2223

24+
- name: Setup .NET 10
25+
uses: actions/setup-dotnet@v4
26+
with:
27+
dotnet-version: '10.0.x'
28+
29+
# Required by Playground.Wasm's Release AOT compilation.
30+
- name: Install wasm-tools workload
31+
run: dotnet workload install wasm-tools
32+
33+
- name: Publish ExpressiveSharp Playground (Blazor WASM)
34+
run: |
35+
dotnet publish src/Docs/Playground.Wasm/ExpressiveSharp.Docs.Playground.Wasm.csproj \
36+
-c Release \
37+
-o .artifacts/playground
38+
# Drop the Blazor publish output into VitePress' static asset folder
39+
# so it ships as part of the site under /playground/.
40+
rm -rf docs/public/_playground
41+
mkdir -p docs/public/_playground
42+
cp -r .artifacts/playground/wwwroot/. docs/public/_playground/
43+
# Rename to app.html so it doesn't collide with VitePress's route resolution
44+
mv docs/public/_playground/index.html docs/public/_playground/app.htm
45+
sed -i 's|<base href="/" />|<base href="/ExpressiveSharp/_playground/" />|' docs/public/_playground/app.htm
46+
# Copy _content/ to docs root so Blazor's dynamic imports resolve
47+
# when the web component is hosted directly on the VitePress page
48+
cp -r docs/public/_playground/_content docs/public/_content
49+
# Remove BlazorMonaco static assets (no longer used)
50+
rm -rf docs/public/_content/BlazorMonaco docs/public/_playground/_content/BlazorMonaco
51+
52+
- name: Pre-render doc samples
53+
run: |
54+
dotnet run --project src/Docs/Prerenderer \
55+
-c Release \
56+
-- --docs-root docs
57+
2358
- name: Setup Node.js
2459
uses: actions/setup-node@v4
2560
with:

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ jobs:
5353
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
5454
echo "Publishing version: $VERSION"
5555
56+
# Required by Playground.Wasm's Release AOT compilation.
57+
- name: Install wasm-tools workload
58+
run: dotnet workload install wasm-tools
59+
5660
- name: Restore
5761
run: dotnet restore
5862

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,5 +373,13 @@ ReadmeSample.db
373373
docs/node_modules/
374374
docs/.vitepress/cache/
375375
docs/.vitepress/dist/
376+
docs/.vitepress/data/
377+
378+
# Blazor WASM playground build artifact (published into docs/public/_playground/
379+
# at docs build time — never committed; gh-pages serves the regenerated copy).
380+
docs/public/_playground/
381+
docs/public/_content/
382+
.artifacts/
383+
376384
# Worktrees
377385
.worktrees/

Directory.Packages.props

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
77
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
88
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
9+
<!-- Roslyn IntelliSense (CompletionService, QuickInfoService). Pinned to the
10+
same 5.0.0 stable wave as the rest of our Roslyn surface — Features 5.0.0
11+
requires Microsoft.CodeAnalysis.Workspaces.Common = 5.0.0 exactly. -->
12+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="5.0.0" />
913
<PackageVersion Include="Verify.MSTest" Version="31.13.5" />
1014
<PackageVersion Include="Basic.Reference.Assemblies.Net80" Version="1.8.4" />
1115
<PackageVersion Include="Basic.Reference.Assemblies.Net100" Version="1.8.4" />
@@ -14,6 +18,7 @@
1418
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.25" />
1519
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.25" />
1620
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.25" />
21+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.25" />
1722
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.25" />
1823
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
1924
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
@@ -27,5 +32,11 @@
2732
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="8.0.25" />
2833
<PackageVersion Include="MongoDB.Driver" Version="3.0.0" />
2934
<PackageVersion Include="Testcontainers.MongoDb" Version="4.3.0" />
35+
<!-- Blazor WebAssembly host for the docs playground. -->
36+
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
37+
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" />
38+
<!-- Lets the playground register itself as an <expressive-playground>
39+
custom element so VitePress markdown can drop it inline. -->
40+
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="10.0.5" />
3041
</ItemGroup>
3142
</Project>

ExpressiveSharp.slnx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
<Project Path="src/ExpressiveSharp/ExpressiveSharp.csproj" />
1818
<Project Path="src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj" />
1919
</Folder>
20+
<Folder Name="/src/Docs/">
21+
<Project Path="src/Docs/PlaygroundModel/ExpressiveSharp.Docs.PlaygroundModel.csproj" />
22+
<Project Path="src/Docs/Playground.Core/ExpressiveSharp.Docs.Playground.Core.csproj" />
23+
<Project Path="src/Docs/Playground.Wasm/ExpressiveSharp.Docs.Playground.Wasm.csproj" />
24+
<Project Path="src/Docs/Playground.WasmWorkspaceShim/ExpressiveSharp.Docs.Playground.WasmWorkspaceShim.csproj" />
25+
<Project Path="src/Docs/Prerenderer/ExpressiveSharp.Docs.Prerenderer.csproj" />
26+
</Folder>
2027
<Folder Name="/tests/">
2128
<Project Path="tests/ExpressiveSharp.Generator.Tests/ExpressiveSharp.Generator.Tests.csproj" />
2229
<Project Path="tests/ExpressiveSharp.IntegrationTests/ExpressiveSharp.IntegrationTests.csproj" />

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ ignore:
1414
- "benchmarks/**"
1515
- "samples/**"
1616
- "docs/**"
17+
- "src/Docs/**"

docs/.vitepress/config.mts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import {defineConfig, type DefaultTheme, type HeadConfig} from 'vitepress'
22
import llmstxt from 'vitepress-plugin-llms'
3+
import {expressiveSamplePlugin} from './plugins/expressive-sample'
4+
import {readFileSync, existsSync} from 'fs'
5+
import {resolve, dirname} from 'path'
6+
import {fileURLToPath} from 'url'
7+
import {createHash} from 'crypto'
8+
9+
const __dirname = dirname(fileURLToPath(import.meta.url))
310

411
const base = '/ExpressiveSharp/'
512

@@ -16,13 +23,20 @@ const sidebar: DefaultTheme.Sidebar = {
1623
{
1724
text: 'Core APIs',
1825
items: [
26+
{ text: 'IExpressiveQueryable<T>', link: '/guide/expressive-queryable' },
1927
{ text: '[Expressive] Properties', link: '/guide/expressive-properties' },
2028
{ text: '[Expressive] Methods', link: '/guide/expressive-methods' },
2129
{ text: 'Extension Members', link: '/guide/extension-members' },
2230
{ text: 'Constructor Projections', link: '/guide/expressive-constructors' },
2331
{ text: 'ExpressionPolyfill.Create', link: '/guide/expression-polyfill' },
24-
{ text: 'IExpressiveQueryable<T>', link: '/guide/expressive-queryable' },
25-
{ text: 'EF Core Integration', link: '/guide/ef-core-integration' },
32+
]
33+
},
34+
{
35+
text: 'Integrations',
36+
items: [
37+
{ text: 'EF Core', link: '/guide/integrations/ef-core' },
38+
{ text: 'MongoDB', link: '/guide/integrations/mongodb' },
39+
{ text: 'Custom Providers', link: '/guide/integrations/custom-providers' },
2640
]
2741
},
2842
{
@@ -99,11 +113,154 @@ const headers = process.env.GITHUB_ACTIONS === "true" ?
99113
[...baseHeaders, umamiScript] :
100114
baseHeaders;
101115

116+
// Vite plugin: serve _playground/app.htm as raw HTML in dev mode.
117+
// VitePress's dev server applies its SPA transform to all HTML files in
118+
// public/, which breaks the Blazor WASM app. This middleware intercepts
119+
// requests to _playground/app.htm and serves the raw file directly.
120+
const mimeTypes: Record<string, string> = {
121+
'.htm': 'text/html', '.html': 'text/html', '.js': 'application/javascript',
122+
'.mjs': 'application/javascript', '.css': 'text/css', '.json': 'application/json',
123+
'.wasm': 'application/wasm', '.dll': 'application/octet-stream',
124+
'.dat': 'application/octet-stream', '.br': 'application/octet-stream',
125+
'.gz': 'application/octet-stream', '.woff': 'font/woff', '.woff2': 'font/woff2',
126+
}
127+
128+
// Expands `::: expressive-sample` containers into fenced code blocks for each
129+
// render target BEFORE VitePress or llmstxt sees the markdown. This way:
130+
// - llms.txt sees the actual SQL / MongoDB / generator output
131+
// - VitePress renders the fenced blocks as regular code blocks (with Shiki
132+
// highlighting) which our markdown-it plugin picks up and wraps as tabs
133+
// The fenced blocks are the single source of truth the Vue component reads
134+
// from via the `data-expressive-sample` marker injected on the first block.
135+
function expandExpressiveSamplesPlugin() {
136+
return {
137+
name: 'expand-expressive-samples',
138+
enforce: 'pre' as const,
139+
transform(code: string, id: string) {
140+
if (!id.endsWith('.md')) return null
141+
if (!code.includes('::: expressive-sample')) return null
142+
143+
const relPath = id.includes('/docs/')
144+
? id.substring(id.indexOf('/docs/') + 6).replace(/\?.*$/, '')
145+
: id
146+
const jsonPath = resolve(__dirname, 'data/samples', relPath.replace(/\.md$/, '.json'))
147+
if (!existsSync(jsonPath)) return null
148+
149+
type Target = { label: string; language: string; output: string }
150+
type Sample = { key: string; snippet: string; setup?: string | null; targets: Record<string, Target> }
151+
let samples: Sample[]
152+
try { samples = JSON.parse(readFileSync(jsonPath, 'utf-8')) } catch { return null }
153+
154+
const lines = code.split('\n')
155+
const result: string[] = []
156+
let i = 0
157+
while (i < lines.length) {
158+
if (!lines[i].trimStart().startsWith('::: expressive-sample')) {
159+
result.push(lines[i]); i++; continue
160+
}
161+
i++
162+
const bodyLines: string[] = []
163+
while (i < lines.length && lines[i].trimStart() !== ':::') {
164+
bodyLines.push(lines[i]); i++
165+
}
166+
i++ // closing :::
167+
168+
const body = bodyLines.join('\n').trim()
169+
const sepIdx = body.indexOf('---setup---')
170+
const snippet = sepIdx >= 0 ? body.slice(0, sepIdx).trim() : body
171+
const setup = sepIdx >= 0 ? body.slice(sepIdx + '---setup---'.length).trim() : undefined
172+
173+
const key = createHash('sha256')
174+
.update(snippet + '\0' + (setup ?? ''))
175+
.digest('hex').slice(0, 12).toLowerCase()
176+
const sample = samples.find(s => s.key === key)
177+
if (!sample) {
178+
// Fallback: leave the container for our markdown-it plugin's warning
179+
result.push('::: expressive-sample')
180+
result.push(...bodyLines)
181+
result.push(':::')
182+
continue
183+
}
184+
185+
// Preserve original container — our markdown-it plugin (VitePress
186+
// render stage) reads this and emits the interactive Vue tabs.
187+
result.push('::: expressive-sample')
188+
result.push(...bodyLines)
189+
result.push(':::')
190+
191+
// Also emit fenced code blocks inside a hidden div. These are invisible
192+
// on the rendered page (Vue component handles the UI) but are included
193+
// in the raw .md that llms.txt sees, so crawlers/LLMs get the full SQL
194+
// and pipeline output for each render target.
195+
result.push('')
196+
result.push('<div class="expressive-sample-llms" style="display:none">')
197+
result.push('')
198+
// For LLMs: include C# input and ONE representative SQL output (SQLite).
199+
// The other providers are mostly SQL-dialect noise that doesn't teach
200+
// anything about ExpressiveSharp; the generator output is boilerplate
201+
// that shouldn't influence LLM suggestions toward [InterceptsLocation].
202+
let csharpContent = sample.snippet
203+
if (sample.setup) csharpContent += '\n\n// Setup\n' + sample.setup
204+
result.push('```csharp')
205+
result.push(csharpContent)
206+
result.push('```')
207+
const sqlite = sample.targets['sqlite']
208+
if (sqlite) {
209+
result.push('')
210+
result.push(`**Generated SQL:**`)
211+
result.push('')
212+
result.push('```' + sqlite.language)
213+
result.push(sqlite.output)
214+
result.push('```')
215+
}
216+
result.push('')
217+
result.push('</div>')
218+
result.push('')
219+
}
220+
return { code: result.join('\n'), map: null }
221+
}
222+
}
223+
}
224+
225+
function servePlaygroundPlugin() {
226+
return {
227+
name: 'serve-playground',
228+
configureServer(server: any) {
229+
// Serve everything under /_playground/ as raw static files so VitePress's
230+
// SPA transform and module system don't intercept Blazor WASM resources.
231+
server.middlewares.use((req: any, res: any, next: any) => {
232+
const prefix = '/ExpressiveSharp/_playground/'
233+
if (!req.url?.startsWith(prefix)) return next()
234+
235+
const relPath = req.url.slice(prefix.length).split('?')[0]
236+
const filePath = resolve(__dirname, '../public/_playground', relPath)
237+
if (!existsSync(filePath)) return next()
238+
239+
const ext = '.' + relPath.split('.').pop()
240+
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
241+
res.end(readFileSync(filePath))
242+
})
243+
}
244+
}
245+
}
246+
102247
export default defineConfig({
103248
title: "ExpressiveSharp",
104249
description: "Modern C# syntax in LINQ expression trees — source-generated at compile time",
105250
base,
106251
head: headers,
252+
markdown: {
253+
config: (md) => {
254+
md.use(expressiveSamplePlugin)
255+
}
256+
},
257+
vue: {
258+
template: {
259+
compilerOptions: {
260+
isCustomElement: (tag) => tag === 'expressive-playground',
261+
}
262+
}
263+
},
107264
themeConfig: {
108265
logo: '/logo.png',
109266
nav: [
@@ -112,6 +269,7 @@ export default defineConfig({
112269
{ text: 'Reference', link: '/reference/expressive-attribute' },
113270
{ text: 'Advanced', link: '/advanced/how-it-works' },
114271
{ text: 'Recipes', link: '/recipes/computed-properties' },
272+
{ text: 'Playground', link: '/playground-editor' },
115273
{ text: 'Benchmarks', link: 'https://efnext.github.io/ExpressiveSharp/dev/bench/' },
116274
],
117275

@@ -132,6 +290,8 @@ export default defineConfig({
132290
},
133291
vite: {
134292
plugins: [
293+
expandExpressiveSamplesPlugin(),
294+
servePlaygroundPlugin(),
135295
llmstxt({
136296
domain: 'https://efnext.github.io',
137297
description: 'Modern C# syntax in LINQ expression trees — source-generated at compile time',

0 commit comments

Comments
 (0)