feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Webpack 4 Compatibility Setup
|
||||
|
||||
This project demonstrates how to configure `markstream-vue2` to work with Vue CLI (which uses Webpack 4 by default).
|
||||
|
||||
## The Problem
|
||||
|
||||
Many modern npm packages (like `@antv/infographic`, `measury`, `shiki`) use the `package.json` `exports` field to define subpath exports. However, **Webpack 4 does not support the `exports` field**, which was introduced in Webpack 5. Additionally, pnpm uses symlinks for dependency management, which can cause additional resolution issues in Webpack 4.
|
||||
|
||||
## Solution
|
||||
|
||||
This playground includes a working `vue.config.js` and app entry that address the most common Webpack 4 issues:
|
||||
|
||||
1) Subpath imports like `markstream-vue2/index.css` fail because Webpack 4 ignores `exports`.
|
||||
2) Worker imports like `?worker` are Vite-specific and won’t work in Vue CLI (Webpack 4).
|
||||
3) Some peers (e.g. `mermaid`) have `exports` and no `main` entry; Webpack 4 can’t resolve them.
|
||||
|
||||
### `vue.config.js` (resolver + aliases)
|
||||
|
||||
The `vue.config.js` file contains the necessary configuration to work around these Webpack 4 limitations. Key differences from the older “hardcoded .pnpm path” approach:
|
||||
|
||||
- Use `require.resolve()` to resolve the *real* file paths at install time (no version pinning in aliases).
|
||||
- Use the resolved package entry to locate `markstream-vue2/dist/*` files.
|
||||
- Force a single Vue runtime instance via `alias: { 'vue$': ... }` to avoid Composition API runtime warnings caused by duplicated Vue copies in workspace setups.
|
||||
|
||||
```js
|
||||
const path = require('node:path')
|
||||
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
'markstream-vue2',
|
||||
'stream-markdown-parser',
|
||||
'stream-monaco',
|
||||
'@antv/infographic',
|
||||
'measury',
|
||||
'mermaid'
|
||||
],
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
// Add pnpm store paths to module resolution
|
||||
modules: [
|
||||
'node_modules',
|
||||
path.resolve(__dirname, '../node_modules')
|
||||
],
|
||||
alias: {
|
||||
// Webpack 4 doesn't support package.json exports field.
|
||||
// Prefer `require.resolve()` based aliases (see the repo's vue.config.js).
|
||||
},
|
||||
symlinks: false
|
||||
}
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
// Disable eslint to avoid linting dist files
|
||||
config.module.rules.delete('eslint')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### App entry: CDN workers + explicit mermaid loader
|
||||
|
||||
In `src/main.js`, this playground avoids Webpack 4 worker import limitations by using the built-in CDN worker helpers:
|
||||
|
||||
- `createKaTeXWorkerFromCDN()` + `setKaTeXWorker()`
|
||||
- `createMermaidWorkerFromCDN()` + `setMermaidWorker()`
|
||||
|
||||
Also, this playground loads Mermaid via a global CDN build (see `public/index.html`) to avoid pulling Mermaid's modern ESM dependency graph into a Webpack 4 bundle.
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **`transpileDependencies`**: Add packages that use ES2020+ syntax (like `??`, optional chaining) so Babel can transpile them for older browsers.
|
||||
|
||||
2. **`resolve.modules`**: Add the parent `node_modules` directory to the module search path since pnpm stores packages differently.
|
||||
|
||||
3. **`resolve.alias`**: Manually map subpath exports to their actual file locations since Webpack 4 can't resolve them through `package.json` exports.
|
||||
|
||||
4. **`resolve.symlinks: false`**: This helps Webpack resolve pnpm's symlink structure correctly.
|
||||
|
||||
5. **Optional features**: On Webpack 4, some optional integrations can pull in `exports`-heavy or modern syntax graphs (e.g. Shiki languages, Monaco worker helpers). This playground keeps the core renderer usable by ignoring those modules in `vue.config.js` via `IgnorePlugin`.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
For production projects using Vue CLI, consider **upgrading to Webpack 5** or using **Vite** instead, as they have better support for modern npm package features. You can:
|
||||
|
||||
1. Upgrade to Vue CLI 5+ (which uses Webpack 5)
|
||||
2. Migrate to Vite (which has native ESM support and better package resolution)
|
||||
|
||||
## Alternative: Use Webpack 5 Plugin
|
||||
|
||||
If you need to stay on Webpack 4, you can use the `exports-field` plugin to add support for the `package.json` exports field:
|
||||
|
||||
```bash
|
||||
npm install --save-dev webpack-exports-field
|
||||
```
|
||||
|
||||
Then add to your `vue.config.js`:
|
||||
|
||||
```js
|
||||
const ExportsFieldPlugin = require('webpack-exports-field')
|
||||
|
||||
module.exports = {
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new ExportsFieldPlugin()
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: This plugin may not cover all cases, so manual aliases may still be needed.
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
plugins: [
|
||||
// Webpack 4 (acorn) can't parse some newer syntax used by Monaco/Shiki.
|
||||
// Transform them so the bundled code remains compatible.
|
||||
'@babel/plugin-transform-numeric-separator',
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
'@babel/plugin-transform-nullish-coalescing-operator',
|
||||
'@babel/plugin-transform-optional-chaining',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "playground-vue2-cli",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve --port 3335",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/composition-api": "^1.7.2",
|
||||
"katex": "^0.16.44",
|
||||
"markstream-vue2": "workspace:*",
|
||||
"stream-markdown": "latest",
|
||||
"vue": "2.6.14",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"core-js": "^3.49.0",
|
||||
"vue-template-compiler": "2.6.14"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<style>
|
||||
path { fill: #222; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
<path d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.svg" type="image/svg+xml" />
|
||||
<meta
|
||||
name="description"
|
||||
content="markstream-vue2 Vue CLI playground for Vue 2.6 and Webpack 4 compatibility testing."
|
||||
/>
|
||||
<title>markstream-vue2 Playground | Vue CLI 4 / Webpack 4</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>This app requires JavaScript to run.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- Load Mermaid as a global for Webpack 4 compatibility (avoid `exports`-only ESM entry). -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Vue Markdown Renderer">
|
||||
<title>Vue Markdown Renderer</title>
|
||||
<defs>
|
||||
<linearGradient id="g-vue" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#42b883"/>
|
||||
<stop offset="1" stop-color="#35495e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g-md" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#111827"/>
|
||||
<stop offset="1" stop-color="#374151"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="10" width="112" height="108" rx="14" fill="url(#g-md)"/>
|
||||
<path d="M64 18L38 18L18 18L64 100L110 18L90 18L64 64L38 18Z" fill="url(#g-vue)"/>
|
||||
<path d="M30 74h10l8 10 8-10h10v26h-9V89l-9 11-9-11v11h-9V74zm58 0h10l8 10 8-10h10v26h-9V89l-9 11-9-11v11h-9V74z" fill="#f9fafb"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 853 B |
@@ -0,0 +1,6 @@
|
||||
function OptionalD2Unavailable() {
|
||||
throw new Error('Optional dependency "@terrastruct/d2" is not available in playground-vue2-cli.')
|
||||
}
|
||||
|
||||
module.exports = OptionalD2Unavailable
|
||||
module.exports.D2 = OptionalD2Unavailable
|
||||
@@ -0,0 +1,346 @@
|
||||
<script>
|
||||
/* global __MARKSTREAM_VUE2_CLI_DEMO_MD__ */
|
||||
|
||||
import MarkdownRender, { setCustomComponents } from 'markstream-vue2'
|
||||
import ThinkingNode from './components/ThinkingNode.vue'
|
||||
|
||||
const markdownSource = typeof __MARKSTREAM_VUE2_CLI_DEMO_MD__ === 'string'
|
||||
? __MARKSTREAM_VUE2_CLI_DEMO_MD__
|
||||
: String(__MARKSTREAM_VUE2_CLI_DEMO_MD__ || '')
|
||||
const customHtmlTags = Object.freeze(['thinking'])
|
||||
const codeBlockProps = Object.freeze({ isDark: true })
|
||||
const probeMode = typeof window !== 'undefined'
|
||||
&& typeof window.location !== 'undefined'
|
||||
&& /(?:^|[?&])probe=1(?:&|$)/.test(window.location.search)
|
||||
|
||||
setCustomComponents('vue2-demo', { thinking: ThinkingNode })
|
||||
|
||||
export default {
|
||||
name: 'Vue2CliPlayground',
|
||||
components: {
|
||||
MarkdownRender,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
delay: 24,
|
||||
chunkSize: 2,
|
||||
timer: null,
|
||||
running: true,
|
||||
customHtmlTags,
|
||||
codeBlockProps,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalLength() {
|
||||
return markdownSource.length
|
||||
},
|
||||
progress() {
|
||||
if (!this.totalLength)
|
||||
return 0
|
||||
return Math.min(100, Math.round((this.content.length / this.totalLength) * 100))
|
||||
},
|
||||
normalizedChunkSize() {
|
||||
const n = Math.floor(Number(this.chunkSize) || 1)
|
||||
return Math.max(1, n)
|
||||
},
|
||||
isDone() {
|
||||
return this.content.length >= this.totalLength
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
delay() {
|
||||
if (this.running)
|
||||
this.restartStream()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!probeMode)
|
||||
this.startStream()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopStream()
|
||||
},
|
||||
methods: {
|
||||
scheduleNextTick(previousElapsed = 0) {
|
||||
if (!this.running || this.isDone)
|
||||
return
|
||||
|
||||
if (this.timer != null)
|
||||
window.clearTimeout(this.timer)
|
||||
|
||||
const wait = Math.max(
|
||||
0,
|
||||
Math.max(Number(this.delay) || 0, Math.ceil(Number(previousElapsed) || 0)),
|
||||
)
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.timer = null
|
||||
this.tick()
|
||||
}, wait)
|
||||
},
|
||||
tick() {
|
||||
if (!this.running)
|
||||
return
|
||||
|
||||
if (this.isDone) {
|
||||
this.stopStream()
|
||||
return
|
||||
}
|
||||
|
||||
const start = this.content.length
|
||||
const nextChunk = markdownSource.slice(start, start + this.normalizedChunkSize)
|
||||
const tickStartedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
||||
this.content += nextChunk
|
||||
|
||||
this.$nextTick(() => {
|
||||
const schedule = () => {
|
||||
if (!this.running)
|
||||
return
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
||||
this.scheduleNextTick(now - tickStartedAt)
|
||||
}
|
||||
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function')
|
||||
window.requestAnimationFrame(() => schedule())
|
||||
else
|
||||
schedule()
|
||||
})
|
||||
},
|
||||
startStream() {
|
||||
this.stopStream()
|
||||
this.running = true
|
||||
this.scheduleNextTick()
|
||||
},
|
||||
stopStream() {
|
||||
if (this.timer != null) {
|
||||
window.clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
this.running = false
|
||||
},
|
||||
toggleStream() {
|
||||
if (this.running)
|
||||
this.stopStream()
|
||||
else
|
||||
this.startStream()
|
||||
},
|
||||
restartStream() {
|
||||
if (!this.running)
|
||||
return
|
||||
this.startStream()
|
||||
},
|
||||
resetStream() {
|
||||
this.content = ''
|
||||
this.startStream()
|
||||
},
|
||||
fillAll() {
|
||||
this.content = markdownSource
|
||||
this.stopStream()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<header class="header">
|
||||
<div class="title">
|
||||
markstream-vue2 playground
|
||||
</div>
|
||||
<div class="sub">
|
||||
Vue CLI 4 demo: streaming markdown into the renderer with CDN workers
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<section class="panel controls">
|
||||
<h2>Stream controls</h2>
|
||||
<div class="field">
|
||||
<label for="delay">Delay (ms)</label>
|
||||
<input id="delay" v-model.number="delay" type="number" min="4" max="200">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="chunk">Chunk size</label>
|
||||
<input id="chunk" v-model.number="chunkSize" type="number" min="1" max="16">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn" @click="toggleStream">
|
||||
{{ running ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button type="button" class="btn" @click="resetStream">
|
||||
Reset
|
||||
</button>
|
||||
<button type="button" class="btn ghost" @click="fillAll">
|
||||
Render all
|
||||
</button>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="progress">
|
||||
<div class="bar" :style="{ width: `${progress}%` }" />
|
||||
</div>
|
||||
<div class="meta">
|
||||
{{ content.length }} / {{ totalLength }} ({{ progress }}%)
|
||||
</div>
|
||||
</div>
|
||||
<p class="note">
|
||||
This Webpack 4 playground uses CDN workers for KaTeX and Mermaid. D2 remains optional and falls back to source mode here.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel preview">
|
||||
<MarkdownRender
|
||||
:content="content"
|
||||
:final="isDone"
|
||||
:typewriter="false"
|
||||
:code-block-stream="true"
|
||||
code-block-dark-theme="vitesse-dark"
|
||||
code-block-light-theme="vitesse-dark"
|
||||
:code-block-props="codeBlockProps"
|
||||
:viewport-priority="false"
|
||||
:defer-nodes-until-visible="false"
|
||||
:batch-rendering="false"
|
||||
custom-id="vue2-demo"
|
||||
:custom-html-tags="customHtmlTags"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 1.5rem 2rem 2.5rem;
|
||||
background: #f7f7fb;
|
||||
color: #111827;
|
||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.field input {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.7rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #6366f1);
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.preview {
|
||||
min-height: 70vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.page {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script>
|
||||
import { NestedRenderer } from 'markstream-vue2'
|
||||
|
||||
export default {
|
||||
name: 'ThinkingNode',
|
||||
components: {
|
||||
NestedRenderer,
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="thinking-node">
|
||||
<div class="thinking-node__icon-wrap">
|
||||
<div class="thinking-node__icon">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M12 3C7.03 3 3 6.58 3 11c0 1.86.66 3.57 1.77 4.98L4 21l5.2-1.9C10.06 19.35 11 19.5 12 19.5c4.97 0 9-3.58 9-8.5S16.97 3 12 3z" stroke="currentColor" stroke-width="0.8" fill="currentColor" opacity="0.9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thinking-node__body">
|
||||
<div class="thinking-node__header">
|
||||
<strong class="thinking-node__title">Thinking</strong>
|
||||
<span class="thinking-node__meta">(assistant)</span>
|
||||
<span class="thinking-node__status" aria-hidden="true">
|
||||
<span class="thinking-dots" :class="[node.loading ? 'visible' : 'hidden']" aria-hidden="true">
|
||||
<span class="dot dot-1" />
|
||||
<span class="dot dot-2" />
|
||||
<span class="dot dot-3" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="thinking-node__content">
|
||||
<span v-if="node.loading" class="thinking-node__sr-only" aria-live="polite">Thinking…</span>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div :key="node.loading ? 'loading' : 'ready'" class="content-area">
|
||||
<NestedRenderer
|
||||
:node="node"
|
||||
custom-id="vue2-demo"
|
||||
:custom-html-tags="['thinking']"
|
||||
:typewriter="false"
|
||||
:batch-rendering="false"
|
||||
:viewport-priority="false"
|
||||
:defer-nodes-until-visible="false"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.thinking-node {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
border-left: 4px solid #60a5fa;
|
||||
border-radius: 0.375rem;
|
||||
background: #eff6ff;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.thinking-node__icon-wrap {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.thinking-node__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 9999px;
|
||||
background: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.thinking-node__icon svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.thinking-node__body {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.thinking-node__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.thinking-node__title {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.thinking-node__meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.thinking-node__status {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.thinking-node__content {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.thinking-node__sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.thinking-dots {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 36px;
|
||||
justify-content: flex-start;
|
||||
height: 12px;
|
||||
transition: opacity 160ms linear, transform 160ms linear;
|
||||
opacity: 0;
|
||||
}
|
||||
.thinking-dots .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background: #1e3a8a;
|
||||
opacity: 0.25;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.thinking-dots.visible { opacity: 1; }
|
||||
.thinking-dots.hidden { opacity: 0; transform: translateY(0); }
|
||||
.thinking-dots.visible .dot-1 { animation: think-bounce 1s infinite ease-in-out; animation-delay: 0s }
|
||||
.thinking-dots.visible .dot-2 { animation: think-bounce 1s infinite ease-in-out; animation-delay: 0.12s }
|
||||
.thinking-dots.visible .dot-3 { animation: think-bounce 1s infinite ease-in-out; animation-delay: 0.24s }
|
||||
.dark .thinking-dots .dot { background: #bfdbfe; opacity: 0.28 }
|
||||
|
||||
@keyframes think-bounce {
|
||||
0%, 80%, 100% { transform: translateY(0); opacity: 0.25 }
|
||||
40% { transform: translateY(-6px); opacity: 1 }
|
||||
}
|
||||
|
||||
.content-area { min-height: 1.25rem; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0 }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 160ms ease }
|
||||
.fade-enter-to, .fade-leave-from { opacity: 1 }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.thinking-node {
|
||||
background: rgba(30, 64, 175, 0.18);
|
||||
color: #e6f0ff;
|
||||
}
|
||||
|
||||
.thinking-node__icon {
|
||||
background: #1d4ed8;
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.thinking-node__meta,
|
||||
.thinking-node__content {
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.dark .thinking-dots .dot,
|
||||
.thinking-node .thinking-dots .dot {
|
||||
background: #bfdbfe;
|
||||
opacity: 0.28;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
createKaTeXWorkerFromCDN,
|
||||
createMermaidWorkerFromCDN,
|
||||
MarkdownCodeBlockNode,
|
||||
setCustomComponents,
|
||||
setKaTeXWorker,
|
||||
setMermaidWorker,
|
||||
VueRendererMarkdown,
|
||||
} from 'markstream-vue2'
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import './vue2-composition-api-shim'
|
||||
import 'markstream-vue2/dist/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// In Vue CLI (Webpack 4), Monaco+Shiki integration is fragile and can fail with
|
||||
// internal Monaco service errors or Shiki TextMate regex engine crashes.
|
||||
// For this playground, use stream-markdown's Shiki renderer instead.
|
||||
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||
|
||||
Vue.use(VueRendererMarkdown)
|
||||
|
||||
async function bootstrap() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Webpack 4 cannot handle Vite-style `?worker` imports. Use CDN workers instead.
|
||||
const { worker: katexWorker } = createKaTeXWorkerFromCDN({
|
||||
mode: 'classic',
|
||||
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.js',
|
||||
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/contrib/mhchem.min.js',
|
||||
})
|
||||
if (katexWorker)
|
||||
setKaTeXWorker(katexWorker)
|
||||
|
||||
const { worker: mermaidWorker } = createMermaidWorkerFromCDN({
|
||||
mode: 'classic',
|
||||
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
|
||||
})
|
||||
if (mermaidWorker)
|
||||
setMermaidWorker(mermaidWorker)
|
||||
}
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
@@ -0,0 +1,18 @@
|
||||
import CompositionApi, * as CompositionApiExports from '@vue/composition-api'
|
||||
import Vue from 'vue'
|
||||
|
||||
// Vue 2.6 needs the Composition API plugin to support `setup`.
|
||||
// Also expose Composition API helpers on the Vue export because
|
||||
// the built markstream-vue2 bundle imports them from "vue".
|
||||
const compositionApiInstalled = Vue.__composition_api_installed__ || Vue.__compositionApiInstalled
|
||||
if (!compositionApiInstalled) {
|
||||
Vue.use(CompositionApi)
|
||||
Vue.__compositionApiInstalled = true
|
||||
}
|
||||
|
||||
Object.keys(CompositionApiExports).forEach((key) => {
|
||||
if (key === 'default')
|
||||
return
|
||||
if (Vue[key] == null)
|
||||
Vue[key] = CompositionApiExports[key]
|
||||
})
|
||||
@@ -0,0 +1,292 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const webpack = require('webpack')
|
||||
|
||||
function tryResolve(specifier) {
|
||||
try {
|
||||
return require.resolve(specifier)
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNodeModulesPackageRoot(pkgName) {
|
||||
// Some ESM-only packages cannot be resolved via `require.resolve(pkgName)` in
|
||||
// this CJS config file (no `require` entry + `exports` restrictions). Fall
|
||||
// back to checking `node_modules/<pkgName>` directly.
|
||||
const candidates = [
|
||||
path.join(__dirname, 'node_modules', pkgName),
|
||||
path.resolve(__dirname, '../node_modules', pkgName),
|
||||
]
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(p))
|
||||
return p
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function resolvePackageRoot(pkgName) {
|
||||
const entry = tryResolve(pkgName)
|
||||
if (!entry)
|
||||
return null
|
||||
// `markstream-vue2` resolves to `<pkgRoot>/dist/index.cjs`
|
||||
return path.dirname(path.dirname(entry))
|
||||
}
|
||||
|
||||
function resolveSiblingPackagePath(fromPkgRoot, siblingPkgName) {
|
||||
if (!fromPkgRoot)
|
||||
return null
|
||||
const candidate = path.join(path.dirname(fromPkgRoot), siblingPkgName)
|
||||
try {
|
||||
if (fs.existsSync(candidate))
|
||||
return candidate
|
||||
}
|
||||
catch {}
|
||||
return null
|
||||
}
|
||||
|
||||
function setAliasIfResolved(alias, key, specifier) {
|
||||
const resolved = tryResolve(specifier)
|
||||
if (resolved)
|
||||
alias[key] = resolved
|
||||
}
|
||||
|
||||
function setAliasIfExists(alias, key, filePath) {
|
||||
if (!filePath)
|
||||
return
|
||||
alias[key] = filePath
|
||||
}
|
||||
|
||||
const markstreamVue2Root = resolvePackageRoot('markstream-vue2')
|
||||
const streamMonacoRoot = resolvePackageRoot('stream-monaco')
|
||||
const streamMarkdownRoot = resolvePackageRoot('stream-markdown') || resolveNodeModulesPackageRoot('stream-markdown')
|
||||
const floatingUiDomEntry = tryResolve('@floating-ui/dom')
|
||||
const floatingUiDomRoot = resolvePackageRoot('@floating-ui/dom')
|
||||
const floatingUiCoreRoot = resolveSiblingPackagePath(floatingUiDomRoot, 'core')
|
||||
const floatingUiUtilsRoot = resolveSiblingPackagePath(floatingUiDomRoot, 'utils')
|
||||
// `monaco-editor` does not provide a Node `main` entry, so `require.resolve('monaco-editor')`
|
||||
// fails even when it is installed. Resolve it via the `stream-monaco` peer location instead.
|
||||
const monacoEditorRoot = resolveSiblingPackagePath(streamMonacoRoot, 'monaco-editor')
|
||||
const shikiRoot = resolvePackageRoot('shiki')
|
||||
const shikijsLangsRoot = resolvePackageRoot('@shikijs/langs')
|
||||
const shikijsThemesRoot = resolvePackageRoot('@shikijs/themes')
|
||||
const shikijsOnigurumaRoot = resolvePackageRoot('@shikijs/engine-oniguruma')
|
||||
const shikijsMonacoRoot = resolvePackageRoot('@shikijs/monaco')
|
||||
|
||||
const alias = {}
|
||||
|
||||
function loadPlaygroundMarkdown() {
|
||||
const file = path.resolve(__dirname, '../playground/src/const/markdown.ts')
|
||||
try {
|
||||
const src = fs.readFileSync(file, 'utf8')
|
||||
const marker = 'export const streamContent = `'
|
||||
const start = src.indexOf(marker)
|
||||
if (start === -1)
|
||||
return ''
|
||||
const from = start + marker.length
|
||||
const end = src.lastIndexOf('`')
|
||||
if (end <= from)
|
||||
return ''
|
||||
|
||||
// The source file is a TS template literal. We want the *runtime string*
|
||||
// value (i.e. decode common template escapes) without executing any code.
|
||||
// This is enough for our demo needs and avoids leaking double-escaped
|
||||
// delimiters like `\\[` (which breaks math parsing).
|
||||
const raw = src.slice(from, end)
|
||||
return raw
|
||||
// `\\` -> `\`
|
||||
.replace(/\\\\/g, '\\')
|
||||
// `\`` -> '`'
|
||||
.replace(/\\`/g, '`')
|
||||
// `\${` -> `${` (avoid template interpolation escapes being shown)
|
||||
.replace(/\\\$\{/g, '${')
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Important: ensure the entire app (including workspace-linked packages) uses a
|
||||
// single Vue 2 runtime instance. Otherwise you will see Composition API warnings
|
||||
// like "provide() can only be used inside setup()" due to duplicate Vue copies.
|
||||
// Use the CJS runtime so CommonJS bundles (like markstream-vue2/dist/index.cjs)
|
||||
// can access Vue static helpers (e.g. defineComponent) without ESM interop issues.
|
||||
setAliasIfResolved(alias, 'vue$', 'vue/dist/vue.runtime.common.js')
|
||||
// Force vue-demi into Vue 2 mode so defineComponent comes from @vue/composition-api.
|
||||
setAliasIfResolved(alias, 'vue-demi$', 'vue-demi/lib/v2/index.cjs')
|
||||
|
||||
// Force Webpack 4 to use the CJS entry. The ESM entry (`dist/index.js`) is
|
||||
// currently not compatible with Webpack 4 consumption in this playground.
|
||||
setAliasIfExists(
|
||||
alias,
|
||||
'markstream-vue2$',
|
||||
markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.cjs') : null,
|
||||
)
|
||||
|
||||
// Webpack 4 doesn't support `package.json#exports`, so subpath imports like
|
||||
// `markstream-vue2/index.css` won't resolve. Map them to real files in `dist/`.
|
||||
setAliasIfExists(alias, 'markstream-vue2/index.css', markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.css') : null)
|
||||
setAliasIfExists(alias, 'markstream-vue2/index.tailwind.css', markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.tailwind.css') : null)
|
||||
setAliasIfExists(alias, 'markstream-vue2/workers/katexRenderer.worker', markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/workers/katexRenderer.worker.js') : null)
|
||||
setAliasIfExists(alias, 'markstream-vue2/workers/mermaidParser.worker', markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/workers/mermaidParser.worker.js') : null)
|
||||
|
||||
// Force Floating UI to CJS/UMD-compatible entries. Webpack 4 + pnpm otherwise
|
||||
// follows the ESM build into symlinked package internals and misses peer deps.
|
||||
setAliasIfExists(alias, '@floating-ui/dom$', floatingUiDomEntry)
|
||||
setAliasIfExists(alias, '@floating-ui/core$', floatingUiCoreRoot ? path.join(floatingUiCoreRoot, 'dist/floating-ui.core.umd.js') : null)
|
||||
setAliasIfExists(alias, '@floating-ui/utils$', floatingUiUtilsRoot ? path.join(floatingUiUtilsRoot, 'dist/floating-ui.utils.umd.js') : null)
|
||||
setAliasIfExists(alias, '@floating-ui/utils/dom$', floatingUiUtilsRoot ? path.join(floatingUiUtilsRoot, 'dom/floating-ui.utils.dom.umd.js') : null)
|
||||
|
||||
// The CLI demo does not need the full D2 runtime. Use a tiny shim so the
|
||||
// optional peer still degrades to source view instead of failing the build.
|
||||
setAliasIfExists(alias, '@terrastruct/d2$', path.resolve(__dirname, 'shims/optional-d2.cjs'))
|
||||
|
||||
// Webpack 4 doesn't support `package.json#exports`, so subpath imports like
|
||||
// `stream-monaco/legacy` won't resolve. Map them to real files in `dist/`.
|
||||
setAliasIfExists(
|
||||
alias,
|
||||
'stream-monaco/legacy',
|
||||
// Webpack 4 doesn't support `package.json#exports`, so subpath imports like
|
||||
// `stream-monaco/legacy` won't resolve. Map it to the built legacy entry.
|
||||
streamMonacoRoot ? path.join(streamMonacoRoot, 'dist/index.legacy.js') : null,
|
||||
)
|
||||
|
||||
// Force `stream-monaco` to resolve to the local package we installed for this
|
||||
// playground. This avoids accidentally picking up a hoisted copy from the
|
||||
// workspace root.
|
||||
setAliasIfExists(alias, 'stream-monaco$', streamMonacoRoot ? path.join(streamMonacoRoot, 'dist/index.js') : null)
|
||||
setAliasIfExists(alias, 'stream-monaco', streamMonacoRoot ? path.join(streamMonacoRoot, 'dist') : null)
|
||||
|
||||
// Make `monaco-editor` resolvable for Webpack 4 + pnpm.
|
||||
// We alias the package directory so both `monaco-editor` and subpath imports work.
|
||||
setAliasIfExists(alias, 'monaco-editor', monacoEditorRoot)
|
||||
|
||||
// Webpack 4 doesn't support `package.json#exports`, so deep imports like
|
||||
// `@shikijs/langs/javascript` won't resolve. Map package prefixes to `dist/`.
|
||||
setAliasIfExists(alias, '@shikijs/langs', shikijsLangsRoot ? path.join(shikijsLangsRoot, 'dist') : null)
|
||||
setAliasIfExists(alias, '@shikijs/themes', shikijsThemesRoot ? path.join(shikijsThemesRoot, 'dist') : null)
|
||||
setAliasIfExists(alias, '@shikijs/engine-oniguruma', shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist') : null)
|
||||
setAliasIfExists(alias, '@shikijs/engine-oniguruma$', shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/index.mjs') : null)
|
||||
setAliasIfExists(alias, '@shikijs/engine-oniguruma/wasm-inlined', shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/wasm-inlined.mjs') : null)
|
||||
setAliasIfExists(alias, '@shikijs/engine-oniguruma/wasm-inlined$', shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/wasm-inlined.mjs') : null)
|
||||
setAliasIfExists(alias, '@shikijs/monaco', shikijsMonacoRoot ? path.join(shikijsMonacoRoot, 'dist') : null)
|
||||
// Force a single Shiki instance so `loadWasm()` initializes the same
|
||||
// Oniguruma binding used by stream-monaco.
|
||||
setAliasIfExists(alias, 'shiki$', shikiRoot ? path.join(shikiRoot, 'dist/index.mjs') : null)
|
||||
setAliasIfExists(alias, 'shiki', shikiRoot ? path.join(shikiRoot, 'dist') : null)
|
||||
setAliasIfExists(alias, 'shiki/wasm', shikiRoot ? path.join(shikiRoot, 'dist/wasm.mjs') : null)
|
||||
|
||||
// Webpack 4 doesn't support `package.json#exports`, so ensure `stream-markdown`
|
||||
// resolves to a real file.
|
||||
setAliasIfExists(alias, 'stream-markdown$', streamMarkdownRoot ? path.join(streamMarkdownRoot, 'dist/index.js') : null)
|
||||
setAliasIfExists(alias, 'stream-markdown', streamMarkdownRoot ? path.join(streamMarkdownRoot, 'dist') : null)
|
||||
|
||||
// Webpack 4 doesn't support `package.json#exports`, so map `alien-signals`
|
||||
// (a dependency of stream-monaco) to an actual file.
|
||||
setAliasIfResolved(alias, 'alien-signals$', 'alien-signals/esm')
|
||||
setAliasIfResolved(alias, 'alien-signals', 'alien-signals/esm')
|
||||
|
||||
// Third-party packages that rely on `exports` subpaths (optional features).
|
||||
setAliasIfResolved(alias, '@antv/infographic/jsx-runtime', '@antv/infographic/jsx-runtime')
|
||||
setAliasIfResolved(alias, '@antv/infographic/jsx-dev-runtime', '@antv/infographic/jsx-dev-runtime')
|
||||
setAliasIfResolved(alias, 'measury/fonts/AlibabaPuHuiTi-Regular', 'measury/fonts/AlibabaPuHuiTi-Regular')
|
||||
|
||||
function createOptionalIgnoreRegex() {
|
||||
const alwaysIgnore = [
|
||||
'mermaid',
|
||||
'@mermaid-js/parser',
|
||||
'langium',
|
||||
'@antv/infographic',
|
||||
'@antv/hierarchy',
|
||||
]
|
||||
|
||||
// If Monaco is installed, we keep it enabled so `code_block` renders with
|
||||
// CodeBlockNode instead of falling back to <pre>.
|
||||
const maybeIgnore = []
|
||||
if (!streamMarkdownRoot)
|
||||
maybeIgnore.push('stream-markdown')
|
||||
if (!tryResolve('stream-monaco'))
|
||||
maybeIgnore.push('stream-monaco')
|
||||
if (!monacoEditorRoot)
|
||||
maybeIgnore.push('monaco-editor')
|
||||
if (!tryResolve('shiki'))
|
||||
maybeIgnore.push('shiki')
|
||||
if (!tryResolve('@shikijs/langs'))
|
||||
maybeIgnore.push('@shikijs/langs')
|
||||
if (!tryResolve('@shikijs/themes'))
|
||||
maybeIgnore.push('@shikijs/themes')
|
||||
|
||||
const combined = [...alwaysIgnore, ...maybeIgnore]
|
||||
return new RegExp(`^(${combined.join('|')})$`)
|
||||
}
|
||||
|
||||
function createMonacoAssetCopyPlugins() {
|
||||
if (!monacoEditorRoot)
|
||||
return []
|
||||
// Monaco ESM workers import other `esm/vs/*` modules by relative paths, so we
|
||||
// copy the whole `esm/vs` tree into the app's static output. This avoids CDN
|
||||
// workers and keeps everything same-origin for Worker security.
|
||||
const from = path.join(monacoEditorRoot, 'esm/vs')
|
||||
if (!fs.existsSync(from))
|
||||
return []
|
||||
return [
|
||||
// Vue CLI 4 ships CopyWebpackPlugin v5 which uses the array signature.
|
||||
new CopyWebpackPlugin([
|
||||
{ from, to: 'monaco/vs' },
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
'markstream-vue2',
|
||||
'stream-markdown-parser',
|
||||
'stream-markdown',
|
||||
'stream-monaco',
|
||||
'monaco-editor',
|
||||
'shiki',
|
||||
'@shikijs/core',
|
||||
'@shikijs/engine-javascript',
|
||||
'@shikijs/engine-oniguruma',
|
||||
'@shikijs/types',
|
||||
'@shikijs/vscode-textmate',
|
||||
'@shikijs/monaco',
|
||||
'@shikijs/langs',
|
||||
'@shikijs/themes',
|
||||
'oniguruma-to-es',
|
||||
'@antv/infographic',
|
||||
],
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__MARKSTREAM_VUE2_CLI_DEMO_MD__: JSON.stringify(loadPlaygroundMarkdown()),
|
||||
}),
|
||||
// These are optional features. On Webpack 4 they may pull in `exports`-heavy
|
||||
// dependency graphs (Shiki languages, AntV internals) that are hard to
|
||||
// polyfill. Ignoring them keeps the core renderer usable.
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: createOptionalIgnoreRegex(),
|
||||
}),
|
||||
...createMonacoAssetCopyPlugins(),
|
||||
],
|
||||
resolve: {
|
||||
// pnpm uses symlinks; this helps webpack 4 resolve dependencies correctly.
|
||||
symlinks: false,
|
||||
// Add workspace root `node_modules` for Webpack 4's resolver.
|
||||
modules: [
|
||||
'node_modules',
|
||||
path.resolve(__dirname, '../node_modules'),
|
||||
],
|
||||
alias,
|
||||
},
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
// Avoid linting generated bundles / linked workspace packages in this playground.
|
||||
config.module.rules.delete('eslint')
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user