@canofold/vite 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/dist/index.js +272 -71
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -15,4 +15,14 @@ export default defineConfig({
|
|
|
15
15
|
|
|
16
16
|
Run `canofold dev`; Vite is mounted on the same development server and port.
|
|
17
17
|
|
|
18
|
+
Canofold and Vite share one HTTP server, and a development session creates only one long-lived Vite server. Vite's module graph watches components, styles, and Demo dependencies and delivers their HMR updates; Canofold continues to rebuild Markdown and Canofold configuration inputs.
|
|
19
|
+
|
|
20
|
+
No documentation-specific Vite configuration is required. `@canofold/vite` reuses the project's existing `vite.config`, plugins, aliases, React resolution, TypeScript/JSX settings, and CSS pipeline. Demos should import components through the package name or aliases the project already uses. Component CSS belongs in the component or Demo dependency graph, not in Canofold's site-level `styles` list.
|
|
21
|
+
|
|
22
|
+
For a standard component library with a package name and one `build.lib.entry`, Canofold resolves the package name to that source entry when no same-name alias exists, so the entry does not need to be repeated as an alias. Multi-entry libraries and workspaces are not inferred and keep using their existing exports or aliases.
|
|
23
|
+
|
|
24
|
+
Production uses one Vite graph for the Markdown browser enhancer, Demo runtime, and project components. The component project owns React and React DOM resolution, while Vite deduplicates them and splits Demo JavaScript and CSS on demand instead of placing every example in the entry chunk. That graph emits a lightweight Markdown entry and a Demo entry: pages without Demos do not statically load React for native Markdown behavior, while Demo pages reuse the enhancer from the Demo entry.
|
|
25
|
+
|
|
26
|
+
Use `demos.setup` only for shared providers, themes, internationalization, or routing context. Ordinary components do not need it.
|
|
27
|
+
|
|
18
28
|
Demos render inline by default. `sandbox="iframe"` places trusted local demo code in a restricted iframe for DOM and global-style isolation; it is not a security boundary for untrusted code.
|
package/README.zh-CN.md
CHANGED
|
@@ -13,4 +13,14 @@ export default defineConfig({
|
|
|
13
13
|
})
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
运行 `canofold dev` 即可。Canofold 与 Vite 共用一个 HTTP Server 和端口;同一开发会话只创建一个 Vite Server,组件、样式和 Demo 依赖由 Vite module graph 监听并通过 HMR 更新,Markdown 与 Canofold 配置仍由 Canofold 重建。
|
|
17
|
+
|
|
18
|
+
无需为文档单独维护 Vite 配置。`@canofold/vite` 会复用项目现有的 `vite.config`、插件、alias、React、TypeScript/JSX 与 CSS 配置。Demo 应像应用代码一样从组件包名或项目已有 alias 导入组件,组件 CSS 由组件或 Demo 自己 import;不要把每个组件样式登记到 Canofold 的 `styles`。
|
|
19
|
+
|
|
20
|
+
对于 `package.json` 有包名且 `build.lib.entry` 只有一个入口的标准组件库,Canofold 会在没有同名自定义 alias 时自动把包名解析到该源码入口,因此不需要把同一入口再写一遍 alias。多入口库和 workspace 不做推断,继续使用项目已有的 exports 或 alias。
|
|
21
|
+
|
|
22
|
+
生产构建中,Markdown 浏览器增强、Demo 运行时和项目组件进入同一 Vite 构建图。React 与 React DOM 由组件项目解析并去重,Demo 与 CSS 按需分块,不会把全部示例和样式无条件放进入口。同一构建图会输出轻量 Markdown 入口和 Demo 入口:没有 Demo 的页面不会因为原生 Markdown 交互而静态加载 React,有 Demo 的页面则复用 Demo 入口中的 Markdown 增强能力。
|
|
23
|
+
|
|
24
|
+
`demos.setup` 只用于所有 Demo 共享的 Provider、主题、国际化或路由环境;普通组件不需要设置。
|
|
25
|
+
|
|
16
26
|
Demo 默认渲染在文档页面中。`sandbox="iframe"` 会把可信的本地 Demo 放进受限 iframe,隔离 DOM 与全局样式;它不是运行不可信代码的安全边界。
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { realpathSync } from "fs";
|
|
3
|
-
import { readFile,
|
|
4
|
+
import { access, readFile, readdir } from "fs/promises";
|
|
4
5
|
import { extname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
5
7
|
import {
|
|
6
8
|
build as viteBuild,
|
|
7
9
|
createServer as createViteServer,
|
|
@@ -11,16 +13,16 @@ import {
|
|
|
11
13
|
} from "vite";
|
|
12
14
|
|
|
13
15
|
// src/runtime.ts
|
|
14
|
-
function demoRuntimeSource(registryEntries, setupImport,
|
|
16
|
+
function demoRuntimeSource(registryEntries, setupImport, markdownClientSpecifier) {
|
|
15
17
|
const imports = registryEntries.join("\n");
|
|
16
18
|
const setup = setupImport ?? "const CanofoldDemoSetup = null;";
|
|
17
19
|
return `${imports}
|
|
18
20
|
${setup}
|
|
21
|
+
export { enhanceMarkdown } from ${JSON.stringify(markdownClientSpecifier)};
|
|
19
22
|
import React from 'react';
|
|
20
23
|
import { createRoot } from 'react-dom/client';
|
|
21
24
|
|
|
22
25
|
const registry = new Map(CANOFOLD_DEMO_REGISTRY);
|
|
23
|
-
const iframeStyleUrls = ${JSON.stringify(styleUrls)};
|
|
24
26
|
const roots = new Set();
|
|
25
27
|
const frameObservers = new Set();
|
|
26
28
|
let eventController;
|
|
@@ -34,15 +36,16 @@ class DemoBoundary extends React.Component {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
function componentFor(id) {
|
|
38
|
-
const
|
|
39
|
+
async function componentFor(id) {
|
|
40
|
+
const load = registry.get(id);
|
|
41
|
+
const value = load && await load();
|
|
39
42
|
const component = value && (value.default || value.Demo);
|
|
40
43
|
if (!component) throw new Error('Demo module ' + id + ' must export a default React component.');
|
|
41
44
|
return component;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
export function mountDemo(element, id, failedLabel) {
|
|
45
|
-
const Demo = componentFor(id);
|
|
47
|
+
export async function mountDemo(element, id, failedLabel) {
|
|
48
|
+
const Demo = await componentFor(id);
|
|
46
49
|
const content = React.createElement(Demo);
|
|
47
50
|
const wrapped = CanofoldDemoSetup ? React.createElement(CanofoldDemoSetup, null, content) : content;
|
|
48
51
|
const root = createRoot(element);
|
|
@@ -53,8 +56,7 @@ export function mountDemo(element, id, failedLabel) {
|
|
|
53
56
|
|
|
54
57
|
function iframeDocument(id, failedLabel) {
|
|
55
58
|
const script = \`import(\${JSON.stringify(import.meta.url)}).then(({ mountDemo }) => mountDemo(document.getElementById('root'),\${JSON.stringify(id)},\${JSON.stringify(failedLabel)})).catch((error) => { console.error('[Canofold demo iframe]', error); const root = document.getElementById('root'); root.setAttribute('role', 'alert'); root.textContent = \${JSON.stringify(failedLabel)}; });\`;
|
|
56
|
-
|
|
57
|
-
return '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' + styles + '<style>html,body,#root{min-height:100%;margin:0}body{display:grid;place-items:center;padding:24px;box-sizing:border-box;font-family:system-ui,sans-serif}</style></head><body><div id="root"></div><script type="module">' + script.replace(/<\\/script/gi, '<\\\\/script') + '<\\/script></body></html>';
|
|
59
|
+
return '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html,body,#root{min-height:100%;margin:0}body{display:grid;place-items:center;padding:24px;box-sizing:border-box;font-family:system-ui,sans-serif}</style></head><body><div id="root"></div><script type="module">' + script.replace(/<\\/script/gi, '<\\\\/script') + '<\\/script></body></html>';
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function fitFrame(frame) {
|
|
@@ -86,7 +88,7 @@ function bindSourceToggle(button, signal) {
|
|
|
86
88
|
button.addEventListener('click', () => setExpanded(button.getAttribute('aria-expanded') !== 'true'), { signal });
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
function standaloneDemo() {
|
|
91
|
+
async function standaloneDemo() {
|
|
90
92
|
const id = new URL(window.location.href).searchParams.get('canofold-demo');
|
|
91
93
|
if (!id || !registry.has(id)) return false;
|
|
92
94
|
const preview = Array.from(document.querySelectorAll('[data-cf-demo-preview]')).find(
|
|
@@ -98,13 +100,13 @@ function standaloneDemo() {
|
|
|
98
100
|
root.setAttribute('data-cf-demo-id', id);
|
|
99
101
|
document.documentElement.setAttribute('data-cf-demo-standalone', '');
|
|
100
102
|
document.body.replaceChildren(root);
|
|
101
|
-
mountDemo(root, id, failed);
|
|
103
|
+
await mountDemo(root, id, failed);
|
|
102
104
|
return true;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export async function bootstrapDemos() {
|
|
106
108
|
window.__canofoldDemoDispose?.();
|
|
107
|
-
if (standaloneDemo()) {
|
|
109
|
+
if (await standaloneDemo()) {
|
|
108
110
|
window.__canofoldDemoDispose = () => {
|
|
109
111
|
roots.forEach((root) => root.unmount());
|
|
110
112
|
roots.clear();
|
|
@@ -115,7 +117,7 @@ export async function bootstrapDemos() {
|
|
|
115
117
|
document.documentElement.removeAttribute('data-cf-demo-standalone');
|
|
116
118
|
eventController = new AbortController();
|
|
117
119
|
document.querySelectorAll('[data-cf-demo-source-toggle]').forEach((button) => bindSourceToggle(button, eventController.signal));
|
|
118
|
-
document.querySelectorAll('[data-cf-demo-preview]').
|
|
120
|
+
await Promise.all(Array.from(document.querySelectorAll('[data-cf-demo-preview]')).map(async (preview) => {
|
|
119
121
|
const id = preview.getAttribute('data-cf-demo-id');
|
|
120
122
|
if (!id) return;
|
|
121
123
|
const loading = preview.getAttribute('data-cf-demo-loading-label') || 'Loading example\u2026';
|
|
@@ -135,14 +137,14 @@ export async function bootstrapDemos() {
|
|
|
135
137
|
preview.replaceChildren(frame);
|
|
136
138
|
} else {
|
|
137
139
|
preview.textContent = '';
|
|
138
|
-
mountDemo(preview, id, failed);
|
|
140
|
+
await mountDemo(preview, id, failed);
|
|
139
141
|
}
|
|
140
142
|
} catch (error) {
|
|
141
143
|
console.error('[Canofold demo]', error);
|
|
142
144
|
preview.textContent = failed;
|
|
143
145
|
preview.setAttribute('data-cf-demo-error', '');
|
|
144
146
|
}
|
|
145
|
-
});
|
|
147
|
+
}));
|
|
146
148
|
window.__canofoldDemoDispose = () => {
|
|
147
149
|
eventController?.abort();
|
|
148
150
|
eventController = undefined;
|
|
@@ -165,6 +167,21 @@ if (import.meta.hot) {
|
|
|
165
167
|
// src/index.ts
|
|
166
168
|
var PUBLIC_CLIENT_ID = "virtual:canofold-demo-client";
|
|
167
169
|
var RESOLVED_CLIENT_ID = `\0${PUBLIC_CLIENT_ID}`;
|
|
170
|
+
var PUBLIC_MARKDOWN_CLIENT_ID = "virtual:canofold-markdown-client";
|
|
171
|
+
var RESOLVED_MARKDOWN_CLIENT_ID = `\0${PUBLIC_MARKDOWN_CLIENT_ID}`;
|
|
172
|
+
var PACKAGE_MODULE_PATH = fileURLToPath(import.meta.url);
|
|
173
|
+
var PROJECT_METADATA_FILES = [
|
|
174
|
+
"package.json",
|
|
175
|
+
"pnpm-lock.yaml",
|
|
176
|
+
"package-lock.json",
|
|
177
|
+
"yarn.lock",
|
|
178
|
+
"bun.lock",
|
|
179
|
+
"bun.lockb"
|
|
180
|
+
];
|
|
181
|
+
var devSessions = /* @__PURE__ */ new WeakMap();
|
|
182
|
+
function jsonForJavaScript(value) {
|
|
183
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
184
|
+
}
|
|
168
185
|
function baseUrl(basePath, path) {
|
|
169
186
|
const prefix = basePath === "/" ? "" : basePath.replace(/\/$/, "");
|
|
170
187
|
return `${prefix}/${path.replace(/^\//, "")}`;
|
|
@@ -191,6 +208,21 @@ function canonicalPath(path) {
|
|
|
191
208
|
return resolve(path);
|
|
192
209
|
}
|
|
193
210
|
}
|
|
211
|
+
async function projectMetadataPaths(projectRoot) {
|
|
212
|
+
const candidates = PROJECT_METADATA_FILES.map((file) => join(projectRoot, file));
|
|
213
|
+
const existing = await Promise.all(
|
|
214
|
+
candidates.map(async (path) => {
|
|
215
|
+
try {
|
|
216
|
+
await access(path);
|
|
217
|
+
return canonicalPath(path);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code === "ENOENT") return void 0;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
return existing.filter((path) => Boolean(path));
|
|
225
|
+
}
|
|
194
226
|
function isProjectSource(cwd, path) {
|
|
195
227
|
const projectRelative = relative(canonicalPath(cwd), canonicalPath(path));
|
|
196
228
|
return projectRelative !== ".." && !projectRelative.startsWith(`..${sep}`) && !isAbsolute(projectRelative) && !projectRelative.split(sep).some((segment) => segment === "node_modules" || segment === ".canofold");
|
|
@@ -273,30 +305,78 @@ async function resolvedSetup(server, setup, cwd) {
|
|
|
273
305
|
dependencyPaths: await collectLocalDependencies(server, modulePath, cwd)
|
|
274
306
|
};
|
|
275
307
|
}
|
|
308
|
+
function hasPackageAlias(config, packageName) {
|
|
309
|
+
const alias = config.resolve?.alias;
|
|
310
|
+
if (!alias) return false;
|
|
311
|
+
if (!Array.isArray(alias)) return Object.hasOwn(alias, packageName);
|
|
312
|
+
return alias.some(
|
|
313
|
+
({ find }) => typeof find === "string" ? find === packageName : new RegExp(find.source, find.flags).test(packageName)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
function singleLibraryEntry(config) {
|
|
317
|
+
const library = config.build?.lib;
|
|
318
|
+
if (!library) return void 0;
|
|
319
|
+
const { entry } = library;
|
|
320
|
+
if (typeof entry === "string") return entry;
|
|
321
|
+
if (Array.isArray(entry)) return entry.length === 1 ? entry[0] : void 0;
|
|
322
|
+
const entries = Object.values(entry);
|
|
323
|
+
return entries.length === 1 ? entries[0] : void 0;
|
|
324
|
+
}
|
|
325
|
+
async function inferredPackageAlias(config, projectRoot) {
|
|
326
|
+
const entry = singleLibraryEntry(config);
|
|
327
|
+
if (!entry) return void 0;
|
|
328
|
+
try {
|
|
329
|
+
const manifest = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8"));
|
|
330
|
+
if (typeof manifest.name !== "string" || manifest.name.length === 0) return void 0;
|
|
331
|
+
if (hasPackageAlias(config, manifest.name)) return void 0;
|
|
332
|
+
return {
|
|
333
|
+
find: new RegExp(`^${manifest.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
|
|
334
|
+
replacement: canonicalPath(resolve(projectRoot, entry))
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error.code === "ENOENT") return void 0;
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
276
341
|
function virtualClientPlugin({
|
|
342
|
+
projectRoot,
|
|
277
343
|
getDemos,
|
|
278
|
-
getSetup
|
|
279
|
-
styleUrls = []
|
|
344
|
+
getSetup
|
|
280
345
|
}) {
|
|
281
346
|
return {
|
|
282
347
|
name: "canofold-demo-client",
|
|
283
348
|
enforce: "post",
|
|
349
|
+
async config(config) {
|
|
350
|
+
const alias = await inferredPackageAlias(config, projectRoot);
|
|
351
|
+
return alias ? { resolve: { alias: [alias] } } : void 0;
|
|
352
|
+
},
|
|
284
353
|
resolveId(id) {
|
|
285
|
-
|
|
354
|
+
if (id === PUBLIC_CLIENT_ID) return RESOLVED_CLIENT_ID;
|
|
355
|
+
if (id === PUBLIC_MARKDOWN_CLIENT_ID) return RESOLVED_MARKDOWN_CLIENT_ID;
|
|
356
|
+
return void 0;
|
|
286
357
|
},
|
|
287
|
-
load(id) {
|
|
358
|
+
async load(id) {
|
|
359
|
+
if (id === RESOLVED_MARKDOWN_CLIENT_ID) {
|
|
360
|
+
const markdownClient = await this.resolve("@canofold/markdown/client", PACKAGE_MODULE_PATH, {
|
|
361
|
+
skipSelf: true
|
|
362
|
+
});
|
|
363
|
+
if (!markdownClient || markdownClient.external) {
|
|
364
|
+
throw new Error("@canofold/vite could not resolve its @canofold/markdown client dependency");
|
|
365
|
+
}
|
|
366
|
+
return `export { enhanceMarkdown } from ${jsonForJavaScript(normalizePath(markdownClient.id))};`;
|
|
367
|
+
}
|
|
288
368
|
if (id !== RESOLVED_CLIENT_ID) return void 0;
|
|
289
369
|
const entries = [];
|
|
290
370
|
const registry = [];
|
|
291
|
-
getDemos().forEach((demo
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
371
|
+
getDemos().forEach((demo) => {
|
|
372
|
+
registry.push(
|
|
373
|
+
`[${jsonForJavaScript(demo.id)}, () => import(${jsonForJavaScript(normalizePath(demo.modulePath))})]`
|
|
374
|
+
);
|
|
295
375
|
});
|
|
296
376
|
entries.push(`const CANOFOLD_DEMO_REGISTRY = [${registry.join(",")}];`);
|
|
297
377
|
const setup = getSetup();
|
|
298
|
-
const setupImport = setup ? `import CanofoldDemoSetup from ${
|
|
299
|
-
return demoRuntimeSource(entries, setupImport,
|
|
378
|
+
const setupImport = setup ? `import CanofoldDemoSetup from ${jsonForJavaScript(setup)};` : void 0;
|
|
379
|
+
return demoRuntimeSource(entries, setupImport, PUBLIC_MARKDOWN_CLIENT_ID);
|
|
300
380
|
}
|
|
301
381
|
};
|
|
302
382
|
}
|
|
@@ -308,6 +388,9 @@ function inlineConfig(cwd, basePath, options, plugins) {
|
|
|
308
388
|
base: basePath,
|
|
309
389
|
appType: "custom",
|
|
310
390
|
plugins: Array.isArray(plugins) ? plugins : [plugins],
|
|
391
|
+
// The dev server ships native ESM to the current browser. Avoid asking
|
|
392
|
+
// esbuild to lower dependency syntax that its transformer cannot lower.
|
|
393
|
+
optimizeDeps: { esbuildOptions: { target: "esnext" } },
|
|
311
394
|
resolve: { dedupe: ["react", "react-dom"] }
|
|
312
395
|
};
|
|
313
396
|
}
|
|
@@ -320,15 +403,22 @@ function demoBuildOptions(context) {
|
|
|
320
403
|
outDir: join(context.outputRoot, "assets/canofold-demos"),
|
|
321
404
|
emptyOutDir: true,
|
|
322
405
|
copyPublicDir: false,
|
|
323
|
-
cssCodeSplit:
|
|
406
|
+
cssCodeSplit: true,
|
|
324
407
|
rollupOptions: {
|
|
325
408
|
external: () => false,
|
|
326
|
-
|
|
409
|
+
// Canofold loads both browser entries with dynamic import() and consumes
|
|
410
|
+
// their named exports. Vite's application default may otherwise remove
|
|
411
|
+
// entry exports that are not referenced inside the bundle.
|
|
412
|
+
preserveEntrySignatures: "strict",
|
|
413
|
+
input: {
|
|
414
|
+
index: PUBLIC_CLIENT_ID,
|
|
415
|
+
markdown: PUBLIC_MARKDOWN_CLIENT_ID
|
|
416
|
+
},
|
|
327
417
|
output: {
|
|
328
418
|
format: "es",
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
assetFileNames:
|
|
419
|
+
entryFileNames: "[name].js",
|
|
420
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
421
|
+
assetFileNames: "assets/[name]-[hash][extname]"
|
|
332
422
|
}
|
|
333
423
|
}
|
|
334
424
|
};
|
|
@@ -341,12 +431,20 @@ async function isolatedBuildConfig(context, options, plugin, build) {
|
|
|
341
431
|
configFile,
|
|
342
432
|
root
|
|
343
433
|
);
|
|
344
|
-
|
|
434
|
+
let sharedConfig = { ...loaded?.config ?? {} };
|
|
435
|
+
const packageAlias = await inferredPackageAlias(sharedConfig, root);
|
|
436
|
+
if (packageAlias) {
|
|
437
|
+
sharedConfig = mergeConfig(sharedConfig, { resolve: { alias: [packageAlias] } });
|
|
438
|
+
}
|
|
345
439
|
delete sharedConfig.build;
|
|
346
440
|
delete sharedConfig.server;
|
|
347
441
|
delete sharedConfig.preview;
|
|
348
442
|
const engineConfig = {
|
|
349
443
|
...inlineConfig(context.cwd, context.basePath, { ...options, configFile: false }, plugin),
|
|
444
|
+
// The demo bundle is emitted below the site's own asset directory. Vite 8
|
|
445
|
+
// resolves preload dependencies from `base`, so using the site root here
|
|
446
|
+
// would incorrectly request demo chunks from `/chunks` and `/assets`.
|
|
447
|
+
base: baseUrl(context.basePath, "/assets/canofold-demos/"),
|
|
350
448
|
mode: "production",
|
|
351
449
|
define: {
|
|
352
450
|
"process.env.NODE_ENV": JSON.stringify("production")
|
|
@@ -354,17 +452,23 @@ async function isolatedBuildConfig(context, options, plugin, build) {
|
|
|
354
452
|
esbuild: {
|
|
355
453
|
jsxDev: false
|
|
356
454
|
},
|
|
357
|
-
build
|
|
455
|
+
build: {
|
|
456
|
+
// Markdown browser modules are published as ES2022. Keep that baseline
|
|
457
|
+
// when a component project does not declare its own Vite target.
|
|
458
|
+
target: loaded?.config.build?.target ?? "es2022",
|
|
459
|
+
...build
|
|
460
|
+
}
|
|
358
461
|
};
|
|
359
462
|
return mergeConfig(sharedConfig, engineConfig);
|
|
360
463
|
}
|
|
361
464
|
async function prepareDemos(context, options) {
|
|
465
|
+
const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
|
|
362
466
|
let demos = [];
|
|
363
467
|
let setup;
|
|
364
468
|
const plugin = virtualClientPlugin({
|
|
469
|
+
projectRoot,
|
|
365
470
|
getDemos: () => demos,
|
|
366
|
-
getSetup: () => setup
|
|
367
|
-
styleUrls: context.mode === "build" ? [baseUrl(context.basePath, "/assets/canofold-demos/styles.css")] : []
|
|
471
|
+
getSetup: () => setup
|
|
368
472
|
});
|
|
369
473
|
const server = await createViteServer({
|
|
370
474
|
...inlineConfig(context.cwd, context.basePath, options, plugin),
|
|
@@ -384,6 +488,7 @@ async function prepareDemos(context, options) {
|
|
|
384
488
|
dependencyPaths: [
|
|
385
489
|
.../* @__PURE__ */ new Set([
|
|
386
490
|
...server.config.configFileDependencies.map((path) => canonicalPath(path)),
|
|
491
|
+
...await projectMetadataPaths(projectRoot),
|
|
387
492
|
...resolved?.dependencyPaths ?? []
|
|
388
493
|
])
|
|
389
494
|
],
|
|
@@ -393,6 +498,119 @@ async function prepareDemos(context, options) {
|
|
|
393
498
|
await server.close();
|
|
394
499
|
}
|
|
395
500
|
}
|
|
501
|
+
function devSessionKey(context, options) {
|
|
502
|
+
return JSON.stringify({
|
|
503
|
+
cwd: canonicalPath(context.cwd),
|
|
504
|
+
outputRoot: canonicalPath(context.outputRoot),
|
|
505
|
+
basePath: context.basePath,
|
|
506
|
+
root: options.root ?? null,
|
|
507
|
+
configFile: options.configFile ?? null
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function configDependencySignature(paths) {
|
|
511
|
+
const hash = createHash("sha256");
|
|
512
|
+
for (const path of [...paths].map(canonicalPath).sort()) {
|
|
513
|
+
hash.update(path);
|
|
514
|
+
try {
|
|
515
|
+
hash.update(await readFile(path));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error.code !== "ENOENT") throw error;
|
|
518
|
+
hash.update("\0missing");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return hash.digest("hex");
|
|
522
|
+
}
|
|
523
|
+
async function devEnvironmentSignature(server, projectRoot) {
|
|
524
|
+
return configDependencySignature([
|
|
525
|
+
...server.config.configFileDependencies,
|
|
526
|
+
...await projectMetadataPaths(projectRoot)
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
function isGeneratedPath(context, path) {
|
|
530
|
+
const projectRelative = relative(canonicalPath(context.cwd), canonicalPath(path));
|
|
531
|
+
const outputRelative = relative(canonicalPath(context.outputRoot), canonicalPath(path));
|
|
532
|
+
return projectRelative.split(sep).some((segment) => segment === "node_modules" || segment === ".git" || segment === ".canofold") || outputRelative !== ".." && !outputRelative.startsWith(`..${sep}`) && !isAbsolute(outputRelative);
|
|
533
|
+
}
|
|
534
|
+
async function prepareDevDemos(context, options) {
|
|
535
|
+
if (!context.server) {
|
|
536
|
+
throw new Error("@canofold/vite requires the shared Canofold HTTP server in dev mode");
|
|
537
|
+
}
|
|
538
|
+
const key = devSessionKey(context, options);
|
|
539
|
+
const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
|
|
540
|
+
let session = devSessions.get(context.server);
|
|
541
|
+
const environmentChanged = session?.key === key && await devEnvironmentSignature(session.server, projectRoot) !== session.environmentSignature;
|
|
542
|
+
if (environmentChanged && session) {
|
|
543
|
+
await session.server.restart(true);
|
|
544
|
+
session.environmentSignature = await devEnvironmentSignature(session.server, projectRoot);
|
|
545
|
+
}
|
|
546
|
+
if (session?.key !== key) {
|
|
547
|
+
if (session) await session.server.close();
|
|
548
|
+
const state = { demos: [] };
|
|
549
|
+
const plugin = virtualClientPlugin({
|
|
550
|
+
projectRoot,
|
|
551
|
+
getDemos: () => state.demos,
|
|
552
|
+
getSetup: () => state.setup
|
|
553
|
+
});
|
|
554
|
+
const server = await createViteServer({
|
|
555
|
+
...inlineConfig(context.cwd, context.basePath, options, plugin),
|
|
556
|
+
server: {
|
|
557
|
+
middlewareMode: true,
|
|
558
|
+
hmr: { server: context.server },
|
|
559
|
+
watch: { ignored: (path) => isGeneratedPath(context, path) }
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
session = {
|
|
563
|
+
key,
|
|
564
|
+
environmentSignature: await devEnvironmentSignature(server, projectRoot),
|
|
565
|
+
server,
|
|
566
|
+
plugin,
|
|
567
|
+
state
|
|
568
|
+
};
|
|
569
|
+
devSessions.set(context.server, session);
|
|
570
|
+
}
|
|
571
|
+
const wasInitialized = session.state.demos.length > 0 || session.state.setup !== void 0;
|
|
572
|
+
const previousSignature = JSON.stringify([
|
|
573
|
+
session.state.demos.map((demo) => [demo.id, demo.modulePath]),
|
|
574
|
+
session.state.setup
|
|
575
|
+
]);
|
|
576
|
+
const demos = await Promise.all(
|
|
577
|
+
context.demos.map((reference) => resolveDemo(session.server, reference, context.cwd))
|
|
578
|
+
);
|
|
579
|
+
const setup = await resolvedSetup(session.server, context.setup, context.cwd);
|
|
580
|
+
session.state.demos = demos;
|
|
581
|
+
session.state.setup = setup?.id;
|
|
582
|
+
const nextSignature = JSON.stringify([
|
|
583
|
+
session.state.demos.map((demo) => [demo.id, demo.modulePath]),
|
|
584
|
+
session.state.setup
|
|
585
|
+
]);
|
|
586
|
+
if (wasInitialized && previousSignature !== nextSignature) {
|
|
587
|
+
const module = session.server.moduleGraph.getModuleById(RESOLVED_CLIENT_ID);
|
|
588
|
+
if (module) session.server.moduleGraph.invalidateModule(module);
|
|
589
|
+
session.server.ws.send({ type: "full-reload" });
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
demos,
|
|
593
|
+
setup: session.state.setup,
|
|
594
|
+
dependencyPaths: [
|
|
595
|
+
.../* @__PURE__ */ new Set([
|
|
596
|
+
...session.server.config.configFileDependencies.map((path) => canonicalPath(path)),
|
|
597
|
+
...await projectMetadataPaths(projectRoot),
|
|
598
|
+
...setup?.dependencyPaths ?? []
|
|
599
|
+
])
|
|
600
|
+
],
|
|
601
|
+
plugin: session.plugin
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
async function outputPathsUnder(root, prefix) {
|
|
605
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
606
|
+
const paths = await Promise.all(
|
|
607
|
+
entries.map(async (entry) => {
|
|
608
|
+
const relativePath = `${prefix}/${entry.name}`;
|
|
609
|
+
return entry.isDirectory() ? outputPathsUnder(join(root, entry.name), relativePath) : [relativePath];
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
return paths.flat().sort();
|
|
613
|
+
}
|
|
396
614
|
function vite(options = {}) {
|
|
397
615
|
return {
|
|
398
616
|
id: "@canofold/vite",
|
|
@@ -402,63 +620,46 @@ function vite(options = {}) {
|
|
|
402
620
|
configFile: options.configFile ?? null
|
|
403
621
|
},
|
|
404
622
|
async prepare(context) {
|
|
405
|
-
const prepared = await prepareDemos(context, options);
|
|
623
|
+
const prepared = context.mode === "dev" ? await prepareDevDemos(context, options) : await prepareDemos(context, options);
|
|
406
624
|
if (context.mode === "build") {
|
|
407
625
|
const build = demoBuildOptions(context);
|
|
408
626
|
await viteBuild(await isolatedBuildConfig(context, options, prepared.plugin, build));
|
|
409
|
-
await writeFile(join(context.outputRoot, "assets/canofold-demos/styles.css"), "", {
|
|
410
|
-
flag: "a"
|
|
411
|
-
});
|
|
412
627
|
}
|
|
628
|
+
const clientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/index.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_CLIENT_ID}`);
|
|
629
|
+
const markdownClientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/markdown.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_MARKDOWN_CLIENT_ID}`);
|
|
413
630
|
return {
|
|
414
|
-
clientUrl
|
|
415
|
-
|
|
631
|
+
clientUrl,
|
|
632
|
+
markdownClientUrl,
|
|
416
633
|
demos: Object.fromEntries(prepared.demos.map((demo) => [demo.id, demo])),
|
|
417
634
|
dependencyPaths: prepared.dependencyPaths,
|
|
418
635
|
...context.mode === "build" ? {
|
|
419
|
-
outputPaths:
|
|
636
|
+
outputPaths: await outputPathsUnder(
|
|
637
|
+
join(context.outputRoot, "assets/canofold-demos"),
|
|
638
|
+
"assets/canofold-demos"
|
|
639
|
+
)
|
|
420
640
|
} : {}
|
|
421
641
|
};
|
|
422
642
|
},
|
|
423
643
|
async startDev(context) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
server: {
|
|
430
|
-
middlewareMode: true,
|
|
431
|
-
hmr: { server: context.server },
|
|
432
|
-
watch: { ignored: (path) => context.shouldIgnorePath(path) }
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
setup = (await resolvedSetup(server, context.setup, context.cwd))?.id;
|
|
436
|
-
let demoSignature = JSON.stringify(
|
|
437
|
-
currentDemos().map((demo) => [demo.id, normalizePath(demo.modulePath)])
|
|
438
|
-
);
|
|
644
|
+
const session = devSessions.get(context.server);
|
|
645
|
+
if (!session) {
|
|
646
|
+
throw new Error("@canofold/vite dev session was not prepared on the shared HTTP server");
|
|
647
|
+
}
|
|
648
|
+
const { server } = session;
|
|
439
649
|
return {
|
|
440
650
|
middleware(request, response, next) {
|
|
441
651
|
server.middlewares(request, response, next);
|
|
442
652
|
},
|
|
443
653
|
handlesFile(path) {
|
|
444
654
|
const absolutePath = canonicalPath(path);
|
|
445
|
-
const isEntry = currentDemos().some((demo) => canonicalPath(demo.modulePath) === absolutePath);
|
|
446
|
-
if (isEntry || setup && canonicalPath(filePathFromResolvedId(setup)) === absolutePath) {
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
449
655
|
return Boolean(server.moduleGraph.getModulesByFile(absolutePath)?.size);
|
|
450
656
|
},
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const module = server.moduleGraph.getModuleById(RESOLVED_CLIENT_ID);
|
|
458
|
-
if (module) server.moduleGraph.invalidateModule(module);
|
|
459
|
-
server.ws.send({ type: "full-reload" });
|
|
460
|
-
},
|
|
461
|
-
close: () => server.close()
|
|
657
|
+
async close() {
|
|
658
|
+
if (devSessions.get(context.server) === session) {
|
|
659
|
+
devSessions.delete(context.server);
|
|
660
|
+
}
|
|
661
|
+
await server.close();
|
|
662
|
+
}
|
|
462
663
|
};
|
|
463
664
|
}
|
|
464
665
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canofold/vite",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Official Vite demo engine for interactive Canofold component documentation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Canofold Contributors",
|
|
@@ -41,19 +41,24 @@
|
|
|
41
41
|
"access": "public",
|
|
42
42
|
"provenance": true
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@canofold/markdown": "0.3.3"
|
|
46
|
+
},
|
|
44
47
|
"peerDependencies": {
|
|
45
|
-
"canofold": "^0.3.
|
|
48
|
+
"canofold": "^0.3.3",
|
|
46
49
|
"react": "^18.2.0 || ^19.0.0",
|
|
47
50
|
"react-dom": "^18.2.0 || ^19.0.0",
|
|
48
51
|
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^22.
|
|
54
|
+
"@types/node": "^22.20.3",
|
|
55
|
+
"react": "^19.3.0",
|
|
56
|
+
"react-dom": "^19.3.0",
|
|
52
57
|
"tsup": "^8.3.5",
|
|
53
58
|
"typescript": "^6.0.3",
|
|
54
59
|
"vite": "^6.4.3",
|
|
55
60
|
"vitest": "^4.1.11",
|
|
56
|
-
"canofold": "0.3.
|
|
61
|
+
"canofold": "0.3.3"
|
|
57
62
|
},
|
|
58
63
|
"scripts": {
|
|
59
64
|
"build": "tsup",
|