Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
restore-keys: |
nuget-${{ runner.os }}-

# Required by Playground.Wasm's Release AOT compilation.
- name: Install wasm-tools workload
run: dotnet workload install wasm-tools

- name: Restore
run: dotnet restore

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
restore-keys: |
nuget-${{ runner.os }}-

# Required by Playground.Wasm's Release AOT compilation.
- name: Install wasm-tools workload
run: dotnet workload install wasm-tools

- name: Restore
run: dotnet restore

Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
paths:
- 'docs/**'
- 'src/Docs/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

Expand All @@ -20,6 +21,40 @@ jobs:
- name: Checkout main
uses: actions/checkout@v4

- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

# Required by Playground.Wasm's Release AOT compilation.
- name: Install wasm-tools workload
run: dotnet workload install wasm-tools

- name: Publish ExpressiveSharp Playground (Blazor WASM)
run: |
dotnet publish src/Docs/Playground.Wasm/ExpressiveSharp.Docs.Playground.Wasm.csproj \
-c Release \
-o .artifacts/playground
# Drop the Blazor publish output into VitePress' static asset folder
# so it ships as part of the site under /playground/.
rm -rf docs/public/_playground
mkdir -p docs/public/_playground
cp -r .artifacts/playground/wwwroot/. docs/public/_playground/
# Rename to app.html so it doesn't collide with VitePress's route resolution
mv docs/public/_playground/index.html docs/public/_playground/app.htm
sed -i 's|<base href="/" />|<base href="/ExpressiveSharp/_playground/" />|' docs/public/_playground/app.htm
# Copy _content/ to docs root so Blazor's dynamic imports resolve
# when the web component is hosted directly on the VitePress page
cp -r docs/public/_playground/_content docs/public/_content
# Remove BlazorMonaco static assets (no longer used)
rm -rf docs/public/_content/BlazorMonaco docs/public/_playground/_content/BlazorMonaco

- name: Pre-render doc samples
run: |
dotnet run --project src/Docs/Prerenderer \
-c Release \
-- --docs-root docs

- name: Setup Node.js
uses: actions/setup-node@v4
with:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing version: $VERSION"

# Required by Playground.Wasm's Release AOT compilation.
- name: Install wasm-tools workload
run: dotnet workload install wasm-tools

- name: Restore
run: dotnet restore

Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -373,5 +373,13 @@ ReadmeSample.db
docs/node_modules/
docs/.vitepress/cache/
docs/.vitepress/dist/
docs/.vitepress/data/

# Blazor WASM playground build artifact (published into docs/public/_playground/
# at docs build time — never committed; gh-pages serves the regenerated copy).
docs/public/_playground/
docs/public/_content/
.artifacts/

# Worktrees
.worktrees/
11 changes: 11 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<!-- Roslyn IntelliSense (CompletionService, QuickInfoService). Pinned to the
same 5.0.0 stable wave as the rest of our Roslyn surface — Features 5.0.0
requires Microsoft.CodeAnalysis.Workspaces.Common = 5.0.0 exactly. -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="5.0.0" />
<PackageVersion Include="Verify.MSTest" Version="31.13.5" />
<PackageVersion Include="Basic.Reference.Assemblies.Net80" Version="1.8.4" />
<PackageVersion Include="Basic.Reference.Assemblies.Net100" Version="1.8.4" />
Expand All @@ -14,6 +18,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.25" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
Expand All @@ -27,5 +32,11 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="8.0.25" />
<PackageVersion Include="MongoDB.Driver" Version="3.0.0" />
<PackageVersion Include="Testcontainers.MongoDb" Version="4.3.0" />
<!-- Blazor WebAssembly host for the docs playground. -->
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" />
<!-- Lets the playground register itself as an <expressive-playground>
custom element so VitePress markdown can drop it inline. -->
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="10.0.5" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions ExpressiveSharp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
<Project Path="src/ExpressiveSharp/ExpressiveSharp.csproj" />
<Project Path="src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj" />
</Folder>
<Folder Name="/src/Docs/">
<Project Path="src/Docs/PlaygroundModel/ExpressiveSharp.Docs.PlaygroundModel.csproj" />
<Project Path="src/Docs/Playground.Core/ExpressiveSharp.Docs.Playground.Core.csproj" />
<Project Path="src/Docs/Playground.Wasm/ExpressiveSharp.Docs.Playground.Wasm.csproj" />
<Project Path="src/Docs/Playground.WasmWorkspaceShim/ExpressiveSharp.Docs.Playground.WasmWorkspaceShim.csproj" />
<Project Path="src/Docs/Prerenderer/ExpressiveSharp.Docs.Prerenderer.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ExpressiveSharp.Generator.Tests/ExpressiveSharp.Generator.Tests.csproj" />
<Project Path="tests/ExpressiveSharp.IntegrationTests/ExpressiveSharp.IntegrationTests.csproj" />
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ ignore:
- "benchmarks/**"
- "samples/**"
- "docs/**"
- "src/Docs/**"
164 changes: 162 additions & 2 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {defineConfig, type DefaultTheme, type HeadConfig} from 'vitepress'
import llmstxt from 'vitepress-plugin-llms'
import {expressiveSamplePlugin} from './plugins/expressive-sample'
import {readFileSync, existsSync} from 'fs'
import {resolve, dirname} from 'path'
import {fileURLToPath} from 'url'
import {createHash} from 'crypto'

const __dirname = dirname(fileURLToPath(import.meta.url))

const base = '/ExpressiveSharp/'

Expand All @@ -16,13 +23,20 @@ const sidebar: DefaultTheme.Sidebar = {
{
text: 'Core APIs',
items: [
{ text: 'IExpressiveQueryable<T>', link: '/guide/expressive-queryable' },
{ text: '[Expressive] Properties', link: '/guide/expressive-properties' },
{ text: '[Expressive] Methods', link: '/guide/expressive-methods' },
{ text: 'Extension Members', link: '/guide/extension-members' },
{ text: 'Constructor Projections', link: '/guide/expressive-constructors' },
{ text: 'ExpressionPolyfill.Create', link: '/guide/expression-polyfill' },
{ text: 'IExpressiveQueryable<T>', link: '/guide/expressive-queryable' },
{ text: 'EF Core Integration', link: '/guide/ef-core-integration' },
]
},
{
text: 'Integrations',
items: [
{ text: 'EF Core', link: '/guide/integrations/ef-core' },
{ text: 'MongoDB', link: '/guide/integrations/mongodb' },
{ text: 'Custom Providers', link: '/guide/integrations/custom-providers' },
]
},
{
Expand Down Expand Up @@ -99,11 +113,154 @@ const headers = process.env.GITHUB_ACTIONS === "true" ?
[...baseHeaders, umamiScript] :
baseHeaders;

// Vite plugin: serve _playground/app.htm as raw HTML in dev mode.
// VitePress's dev server applies its SPA transform to all HTML files in
// public/, which breaks the Blazor WASM app. This middleware intercepts
// requests to _playground/app.htm and serves the raw file directly.
const mimeTypes: Record<string, string> = {
'.htm': 'text/html', '.html': 'text/html', '.js': 'application/javascript',
'.mjs': 'application/javascript', '.css': 'text/css', '.json': 'application/json',
'.wasm': 'application/wasm', '.dll': 'application/octet-stream',
'.dat': 'application/octet-stream', '.br': 'application/octet-stream',
'.gz': 'application/octet-stream', '.woff': 'font/woff', '.woff2': 'font/woff2',
}

// Expands `::: expressive-sample` containers into fenced code blocks for each
// render target BEFORE VitePress or llmstxt sees the markdown. This way:
// - llms.txt sees the actual SQL / MongoDB / generator output
// - VitePress renders the fenced blocks as regular code blocks (with Shiki
// highlighting) which our markdown-it plugin picks up and wraps as tabs
// The fenced blocks are the single source of truth the Vue component reads
// from via the `data-expressive-sample` marker injected on the first block.
function expandExpressiveSamplesPlugin() {
return {
name: 'expand-expressive-samples',
enforce: 'pre' as const,
transform(code: string, id: string) {
if (!id.endsWith('.md')) return null
if (!code.includes('::: expressive-sample')) return null

const relPath = id.includes('/docs/')
? id.substring(id.indexOf('/docs/') + 6).replace(/\?.*$/, '')
: id
const jsonPath = resolve(__dirname, 'data/samples', relPath.replace(/\.md$/, '.json'))
if (!existsSync(jsonPath)) return null

type Target = { label: string; language: string; output: string }
type Sample = { key: string; snippet: string; setup?: string | null; targets: Record<string, Target> }
let samples: Sample[]
try { samples = JSON.parse(readFileSync(jsonPath, 'utf-8')) } catch { return null }

const lines = code.split('\n')
const result: string[] = []
let i = 0
while (i < lines.length) {
if (!lines[i].trimStart().startsWith('::: expressive-sample')) {
result.push(lines[i]); i++; continue
}
i++
const bodyLines: string[] = []
while (i < lines.length && lines[i].trimStart() !== ':::') {
bodyLines.push(lines[i]); i++
}
i++ // closing :::

const body = bodyLines.join('\n').trim()
const sepIdx = body.indexOf('---setup---')
const snippet = sepIdx >= 0 ? body.slice(0, sepIdx).trim() : body
const setup = sepIdx >= 0 ? body.slice(sepIdx + '---setup---'.length).trim() : undefined

const key = createHash('sha256')
.update(snippet + '\0' + (setup ?? ''))
.digest('hex').slice(0, 12).toLowerCase()
const sample = samples.find(s => s.key === key)
if (!sample) {
// Fallback: leave the container for our markdown-it plugin's warning
result.push('::: expressive-sample')
result.push(...bodyLines)
result.push(':::')
continue
}

// Preserve original container — our markdown-it plugin (VitePress
// render stage) reads this and emits the interactive Vue tabs.
result.push('::: expressive-sample')
result.push(...bodyLines)
result.push(':::')

// Also emit fenced code blocks inside a hidden div. These are invisible
// on the rendered page (Vue component handles the UI) but are included
// in the raw .md that llms.txt sees, so crawlers/LLMs get the full SQL
// and pipeline output for each render target.
result.push('')
result.push('<div class="expressive-sample-llms" style="display:none">')
result.push('')
// For LLMs: include C# input and ONE representative SQL output (SQLite).
// The other providers are mostly SQL-dialect noise that doesn't teach
// anything about ExpressiveSharp; the generator output is boilerplate
// that shouldn't influence LLM suggestions toward [InterceptsLocation].
let csharpContent = sample.snippet
if (sample.setup) csharpContent += '\n\n// Setup\n' + sample.setup
result.push('```csharp')
result.push(csharpContent)
result.push('```')
const sqlite = sample.targets['sqlite']
if (sqlite) {
result.push('')
result.push(`**Generated SQL:**`)
result.push('')
result.push('```' + sqlite.language)
result.push(sqlite.output)
result.push('```')
}
result.push('')
result.push('</div>')
result.push('')
}
return { code: result.join('\n'), map: null }
}
}
}

function servePlaygroundPlugin() {
return {
name: 'serve-playground',
configureServer(server: any) {
// Serve everything under /_playground/ as raw static files so VitePress's
// SPA transform and module system don't intercept Blazor WASM resources.
server.middlewares.use((req: any, res: any, next: any) => {
const prefix = '/ExpressiveSharp/_playground/'
if (!req.url?.startsWith(prefix)) return next()

const relPath = req.url.slice(prefix.length).split('?')[0]
const filePath = resolve(__dirname, '../public/_playground', relPath)
if (!existsSync(filePath)) return next()

const ext = '.' + relPath.split('.').pop()
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
res.end(readFileSync(filePath))
})
}
}
}

export default defineConfig({
title: "ExpressiveSharp",
description: "Modern C# syntax in LINQ expression trees — source-generated at compile time",
base,
head: headers,
markdown: {
config: (md) => {
md.use(expressiveSamplePlugin)
}
},
vue: {
template: {
compilerOptions: {
isCustomElement: (tag) => tag === 'expressive-playground',
}
}
},
themeConfig: {
logo: '/logo.png',
nav: [
Expand All @@ -112,6 +269,7 @@ export default defineConfig({
{ text: 'Reference', link: '/reference/expressive-attribute' },
{ text: 'Advanced', link: '/advanced/how-it-works' },
{ text: 'Recipes', link: '/recipes/computed-properties' },
{ text: 'Playground', link: '/playground-editor' },
{ text: 'Benchmarks', link: 'https://efnext.github.io/ExpressiveSharp/dev/bench/' },
],

Expand All @@ -132,6 +290,8 @@ export default defineConfig({
},
vite: {
plugins: [
expandExpressiveSamplesPlugin(),
servePlaygroundPlugin(),
llmstxt({
domain: 'https://efnext.github.io',
description: 'Modern C# syntax in LINQ expression trees — source-generated at compile time',
Expand Down
Loading
Loading