@canofold/vite 0.3.1 → 0.3.2
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 +268 -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,7 +431,11 @@ 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;
|
|
@@ -354,17 +448,23 @@ async function isolatedBuildConfig(context, options, plugin, build) {
|
|
|
354
448
|
esbuild: {
|
|
355
449
|
jsxDev: false
|
|
356
450
|
},
|
|
357
|
-
build
|
|
451
|
+
build: {
|
|
452
|
+
// Markdown browser modules are published as ES2022. Keep that baseline
|
|
453
|
+
// when a component project does not declare its own Vite target.
|
|
454
|
+
target: loaded?.config.build?.target ?? "es2022",
|
|
455
|
+
...build
|
|
456
|
+
}
|
|
358
457
|
};
|
|
359
458
|
return mergeConfig(sharedConfig, engineConfig);
|
|
360
459
|
}
|
|
361
460
|
async function prepareDemos(context, options) {
|
|
461
|
+
const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
|
|
362
462
|
let demos = [];
|
|
363
463
|
let setup;
|
|
364
464
|
const plugin = virtualClientPlugin({
|
|
465
|
+
projectRoot,
|
|
365
466
|
getDemos: () => demos,
|
|
366
|
-
getSetup: () => setup
|
|
367
|
-
styleUrls: context.mode === "build" ? [baseUrl(context.basePath, "/assets/canofold-demos/styles.css")] : []
|
|
467
|
+
getSetup: () => setup
|
|
368
468
|
});
|
|
369
469
|
const server = await createViteServer({
|
|
370
470
|
...inlineConfig(context.cwd, context.basePath, options, plugin),
|
|
@@ -384,6 +484,7 @@ async function prepareDemos(context, options) {
|
|
|
384
484
|
dependencyPaths: [
|
|
385
485
|
.../* @__PURE__ */ new Set([
|
|
386
486
|
...server.config.configFileDependencies.map((path) => canonicalPath(path)),
|
|
487
|
+
...await projectMetadataPaths(projectRoot),
|
|
387
488
|
...resolved?.dependencyPaths ?? []
|
|
388
489
|
])
|
|
389
490
|
],
|
|
@@ -393,6 +494,119 @@ async function prepareDemos(context, options) {
|
|
|
393
494
|
await server.close();
|
|
394
495
|
}
|
|
395
496
|
}
|
|
497
|
+
function devSessionKey(context, options) {
|
|
498
|
+
return JSON.stringify({
|
|
499
|
+
cwd: canonicalPath(context.cwd),
|
|
500
|
+
outputRoot: canonicalPath(context.outputRoot),
|
|
501
|
+
basePath: context.basePath,
|
|
502
|
+
root: options.root ?? null,
|
|
503
|
+
configFile: options.configFile ?? null
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async function configDependencySignature(paths) {
|
|
507
|
+
const hash = createHash("sha256");
|
|
508
|
+
for (const path of [...paths].map(canonicalPath).sort()) {
|
|
509
|
+
hash.update(path);
|
|
510
|
+
try {
|
|
511
|
+
hash.update(await readFile(path));
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error.code !== "ENOENT") throw error;
|
|
514
|
+
hash.update("\0missing");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return hash.digest("hex");
|
|
518
|
+
}
|
|
519
|
+
async function devEnvironmentSignature(server, projectRoot) {
|
|
520
|
+
return configDependencySignature([
|
|
521
|
+
...server.config.configFileDependencies,
|
|
522
|
+
...await projectMetadataPaths(projectRoot)
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
function isGeneratedPath(context, path) {
|
|
526
|
+
const projectRelative = relative(canonicalPath(context.cwd), canonicalPath(path));
|
|
527
|
+
const outputRelative = relative(canonicalPath(context.outputRoot), canonicalPath(path));
|
|
528
|
+
return projectRelative.split(sep).some((segment) => segment === "node_modules" || segment === ".git" || segment === ".canofold") || outputRelative !== ".." && !outputRelative.startsWith(`..${sep}`) && !isAbsolute(outputRelative);
|
|
529
|
+
}
|
|
530
|
+
async function prepareDevDemos(context, options) {
|
|
531
|
+
if (!context.server) {
|
|
532
|
+
throw new Error("@canofold/vite requires the shared Canofold HTTP server in dev mode");
|
|
533
|
+
}
|
|
534
|
+
const key = devSessionKey(context, options);
|
|
535
|
+
const projectRoot = canonicalPath(options.root ? resolve(context.cwd, options.root) : context.cwd);
|
|
536
|
+
let session = devSessions.get(context.server);
|
|
537
|
+
const environmentChanged = session?.key === key && await devEnvironmentSignature(session.server, projectRoot) !== session.environmentSignature;
|
|
538
|
+
if (environmentChanged && session) {
|
|
539
|
+
await session.server.restart(true);
|
|
540
|
+
session.environmentSignature = await devEnvironmentSignature(session.server, projectRoot);
|
|
541
|
+
}
|
|
542
|
+
if (session?.key !== key) {
|
|
543
|
+
if (session) await session.server.close();
|
|
544
|
+
const state = { demos: [] };
|
|
545
|
+
const plugin = virtualClientPlugin({
|
|
546
|
+
projectRoot,
|
|
547
|
+
getDemos: () => state.demos,
|
|
548
|
+
getSetup: () => state.setup
|
|
549
|
+
});
|
|
550
|
+
const server = await createViteServer({
|
|
551
|
+
...inlineConfig(context.cwd, context.basePath, options, plugin),
|
|
552
|
+
server: {
|
|
553
|
+
middlewareMode: true,
|
|
554
|
+
hmr: { server: context.server },
|
|
555
|
+
watch: { ignored: (path) => isGeneratedPath(context, path) }
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
session = {
|
|
559
|
+
key,
|
|
560
|
+
environmentSignature: await devEnvironmentSignature(server, projectRoot),
|
|
561
|
+
server,
|
|
562
|
+
plugin,
|
|
563
|
+
state
|
|
564
|
+
};
|
|
565
|
+
devSessions.set(context.server, session);
|
|
566
|
+
}
|
|
567
|
+
const wasInitialized = session.state.demos.length > 0 || session.state.setup !== void 0;
|
|
568
|
+
const previousSignature = JSON.stringify([
|
|
569
|
+
session.state.demos.map((demo) => [demo.id, demo.modulePath]),
|
|
570
|
+
session.state.setup
|
|
571
|
+
]);
|
|
572
|
+
const demos = await Promise.all(
|
|
573
|
+
context.demos.map((reference) => resolveDemo(session.server, reference, context.cwd))
|
|
574
|
+
);
|
|
575
|
+
const setup = await resolvedSetup(session.server, context.setup, context.cwd);
|
|
576
|
+
session.state.demos = demos;
|
|
577
|
+
session.state.setup = setup?.id;
|
|
578
|
+
const nextSignature = JSON.stringify([
|
|
579
|
+
session.state.demos.map((demo) => [demo.id, demo.modulePath]),
|
|
580
|
+
session.state.setup
|
|
581
|
+
]);
|
|
582
|
+
if (wasInitialized && previousSignature !== nextSignature) {
|
|
583
|
+
const module = session.server.moduleGraph.getModuleById(RESOLVED_CLIENT_ID);
|
|
584
|
+
if (module) session.server.moduleGraph.invalidateModule(module);
|
|
585
|
+
session.server.ws.send({ type: "full-reload" });
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
demos,
|
|
589
|
+
setup: session.state.setup,
|
|
590
|
+
dependencyPaths: [
|
|
591
|
+
.../* @__PURE__ */ new Set([
|
|
592
|
+
...session.server.config.configFileDependencies.map((path) => canonicalPath(path)),
|
|
593
|
+
...await projectMetadataPaths(projectRoot),
|
|
594
|
+
...setup?.dependencyPaths ?? []
|
|
595
|
+
])
|
|
596
|
+
],
|
|
597
|
+
plugin: session.plugin
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async function outputPathsUnder(root, prefix) {
|
|
601
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
602
|
+
const paths = await Promise.all(
|
|
603
|
+
entries.map(async (entry) => {
|
|
604
|
+
const relativePath = `${prefix}/${entry.name}`;
|
|
605
|
+
return entry.isDirectory() ? outputPathsUnder(join(root, entry.name), relativePath) : [relativePath];
|
|
606
|
+
})
|
|
607
|
+
);
|
|
608
|
+
return paths.flat().sort();
|
|
609
|
+
}
|
|
396
610
|
function vite(options = {}) {
|
|
397
611
|
return {
|
|
398
612
|
id: "@canofold/vite",
|
|
@@ -402,63 +616,46 @@ function vite(options = {}) {
|
|
|
402
616
|
configFile: options.configFile ?? null
|
|
403
617
|
},
|
|
404
618
|
async prepare(context) {
|
|
405
|
-
const prepared = await prepareDemos(context, options);
|
|
619
|
+
const prepared = context.mode === "dev" ? await prepareDevDemos(context, options) : await prepareDemos(context, options);
|
|
406
620
|
if (context.mode === "build") {
|
|
407
621
|
const build = demoBuildOptions(context);
|
|
408
622
|
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
623
|
}
|
|
624
|
+
const clientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/index.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_CLIENT_ID}`);
|
|
625
|
+
const markdownClientUrl = context.mode === "build" ? baseUrl(context.basePath, "/assets/canofold-demos/markdown.js") : baseUrl(context.basePath, `/@id/__x00__${PUBLIC_MARKDOWN_CLIENT_ID}`);
|
|
413
626
|
return {
|
|
414
|
-
clientUrl
|
|
415
|
-
|
|
627
|
+
clientUrl,
|
|
628
|
+
markdownClientUrl,
|
|
416
629
|
demos: Object.fromEntries(prepared.demos.map((demo) => [demo.id, demo])),
|
|
417
630
|
dependencyPaths: prepared.dependencyPaths,
|
|
418
631
|
...context.mode === "build" ? {
|
|
419
|
-
outputPaths:
|
|
632
|
+
outputPaths: await outputPathsUnder(
|
|
633
|
+
join(context.outputRoot, "assets/canofold-demos"),
|
|
634
|
+
"assets/canofold-demos"
|
|
635
|
+
)
|
|
420
636
|
} : {}
|
|
421
637
|
};
|
|
422
638
|
},
|
|
423
639
|
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
|
-
);
|
|
640
|
+
const session = devSessions.get(context.server);
|
|
641
|
+
if (!session) {
|
|
642
|
+
throw new Error("@canofold/vite dev session was not prepared on the shared HTTP server");
|
|
643
|
+
}
|
|
644
|
+
const { server } = session;
|
|
439
645
|
return {
|
|
440
646
|
middleware(request, response, next) {
|
|
441
647
|
server.middlewares(request, response, next);
|
|
442
648
|
},
|
|
443
649
|
handlesFile(path) {
|
|
444
650
|
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
651
|
return Boolean(server.moduleGraph.getModulesByFile(absolutePath)?.size);
|
|
450
652
|
},
|
|
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()
|
|
653
|
+
async close() {
|
|
654
|
+
if (devSessions.get(context.server) === session) {
|
|
655
|
+
devSessions.delete(context.server);
|
|
656
|
+
}
|
|
657
|
+
await server.close();
|
|
658
|
+
}
|
|
462
659
|
};
|
|
463
660
|
}
|
|
464
661
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canofold/vite",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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.2"
|
|
46
|
+
},
|
|
44
47
|
"peerDependencies": {
|
|
45
|
-
"canofold": "^0.3.
|
|
48
|
+
"canofold": "^0.3.2",
|
|
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.2"
|
|
57
62
|
},
|
|
58
63
|
"scripts": {
|
|
59
64
|
"build": "tsup",
|