sdocs 0.0.25 → 0.0.28
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/CHANGELOG.md +37 -0
- package/dist/client/views/ComponentView.svelte +13 -2
- package/dist/commands/build.js +6 -2
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.js +33 -5
- package/dist/server/app-gen.d.ts +4 -4
- package/dist/server/app-gen.js +51 -17
- package/dist/server/cli.js +4 -2
- package/dist/server/snippet-compiler.d.ts +17 -1
- package/dist/server/snippet-compiler.js +52 -22
- package/dist/vite.js +101 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.28] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`sdocs dev`/`run`/`build` no longer write a `.sdocs/` directory into the
|
|
15
|
+
project.** The generated app is staged in a unique directory under
|
|
16
|
+
`node_modules/.cache/` (imports resolve exactly as before) and removed on
|
|
17
|
+
exit. This also fixes `sdocs build` breaking a concurrently running dev
|
|
18
|
+
server — they previously shared the same staging directory.
|
|
19
|
+
|
|
20
|
+
## [0.0.27] - 2026-07-03
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`sdocs run`** — same as `dev`, built for `npx sdocs run`: works with no
|
|
25
|
+
local install. The dev server allows sdocs' own files from the npx cache,
|
|
26
|
+
and when the project has its own `svelte`, previews dedupe onto it, so
|
|
27
|
+
components and their dependencies always resolve from the project.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `sdocs build` applies the same svelte dedupe as the dev server.
|
|
32
|
+
|
|
33
|
+
## [0.0.26] - 2026-07-03
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Embedded production builds now include working previews.** The Vite
|
|
38
|
+
plugin emits each preview as a static page (plus your `css` stylesheets)
|
|
39
|
+
into the host app's client build under `previews/`, and `virtual:sdocs`
|
|
40
|
+
references those pages — preview iframes no longer 404 in deployed apps.
|
|
41
|
+
- The standalone CLI (`sdocs dev`/`sdocs build`) failed from an installed
|
|
42
|
+
package: the client app was staged from a wrong path, and its `ui/` styles
|
|
43
|
+
and fonts were not staged at all.
|
|
44
|
+
- The docs UI stops calling the dev-only highlight endpoint after the first
|
|
45
|
+
failure in production and falls back to plain code.
|
|
46
|
+
|
|
10
47
|
## [0.0.25] - 2026-07-02
|
|
11
48
|
|
|
12
49
|
### Added
|
|
@@ -169,13 +169,20 @@
|
|
|
169
169
|
return generateFallbackCode(componentName, propValues, cssValues);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
// Highlighted usage code via server-side Shiki
|
|
172
|
+
// Highlighted usage code via server-side Shiki. The endpoint is dev-server
|
|
173
|
+
// middleware; in a production build it doesn't exist, so after the first
|
|
174
|
+
// failure stop asking and fall back to plain code.
|
|
173
175
|
let highlightedUsageHtml = $state('');
|
|
174
176
|
let highlightTimer: ReturnType<typeof setTimeout> | undefined;
|
|
177
|
+
let highlightAvailable = true;
|
|
175
178
|
|
|
176
179
|
$effect(() => {
|
|
177
180
|
const code = usageCode;
|
|
178
181
|
clearTimeout(highlightTimer);
|
|
182
|
+
if (!highlightAvailable) {
|
|
183
|
+
highlightedUsageHtml = '';
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
179
186
|
highlightTimer = setTimeout(async () => {
|
|
180
187
|
try {
|
|
181
188
|
const res = await fetch('/__sdocs/highlight', {
|
|
@@ -186,9 +193,13 @@
|
|
|
186
193
|
if (res.ok) {
|
|
187
194
|
const { html } = await res.json();
|
|
188
195
|
highlightedUsageHtml = html;
|
|
196
|
+
} else {
|
|
197
|
+
highlightAvailable = false;
|
|
198
|
+
highlightedUsageHtml = '';
|
|
189
199
|
}
|
|
190
200
|
} catch {
|
|
191
|
-
|
|
201
|
+
highlightAvailable = false;
|
|
202
|
+
highlightedUsageHtml = '';
|
|
192
203
|
}
|
|
193
204
|
}, 150);
|
|
194
205
|
});
|
package/dist/commands/build.js
CHANGED
|
@@ -4,11 +4,12 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
|
4
4
|
import { loadConfig } from '../server/config.js';
|
|
5
5
|
import { sdocsPlugin } from '../vite.js';
|
|
6
6
|
import { generateBuildFiles, cleanBuildFiles } from '../server/app-gen.js';
|
|
7
|
+
import { svelteDedupe } from './dev.js';
|
|
7
8
|
export async function buildCommand() {
|
|
8
9
|
const cwd = process.cwd();
|
|
9
10
|
const config = await loadConfig(cwd);
|
|
10
11
|
console.log('[sdocs] Building static site...');
|
|
11
|
-
// Generate
|
|
12
|
+
// Generate the staging directory (in the OS temp dir) with entry + preview HTML files
|
|
12
13
|
const { sdocsDir, inputs } = await generateBuildFiles(config, cwd);
|
|
13
14
|
const inputCount = Object.keys(inputs).length;
|
|
14
15
|
console.log(`[sdocs] Generated ${inputCount} page(s) (1 main + ${inputCount - 1} previews)`);
|
|
@@ -18,6 +19,9 @@ export async function buildCommand() {
|
|
|
18
19
|
await build({
|
|
19
20
|
configFile: false,
|
|
20
21
|
root: sdocsDir,
|
|
22
|
+
resolve: {
|
|
23
|
+
dedupe: svelteDedupe(cwd),
|
|
24
|
+
},
|
|
21
25
|
plugins: [
|
|
22
26
|
svelte(),
|
|
23
27
|
sdocsPlugin({ ...config, include: absoluteIncludes, _buildMode: true }),
|
|
@@ -33,6 +37,6 @@ export async function buildCommand() {
|
|
|
33
37
|
console.log(`[sdocs] Build complete → dist/`);
|
|
34
38
|
}
|
|
35
39
|
finally {
|
|
36
|
-
await cleanBuildFiles(
|
|
40
|
+
await cleanBuildFiles(sdocsDir);
|
|
37
41
|
}
|
|
38
42
|
}
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
+
/** sdocs' own install location — under npx this is the npx cache, not the project */
|
|
2
|
+
export declare function sdocsPackageRoot(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Prefer the project's own svelte over the copy next to sdocs when both exist
|
|
5
|
+
* (e.g. running via npx): previews import the project's components, and two
|
|
6
|
+
* svelte runtimes in one page don't mix.
|
|
7
|
+
*/
|
|
8
|
+
export declare function svelteDedupe(cwd: string): string[];
|
|
1
9
|
export declare function devCommand(): Promise<void>;
|
package/dist/commands/dev.js
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
2
4
|
import { createServer } from 'vite';
|
|
3
5
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
4
6
|
import { loadConfig } from '../server/config.js';
|
|
5
7
|
import { sdocsPlugin } from '../vite.js';
|
|
6
8
|
import { generateDevFiles, cleanBuildFiles } from '../server/app-gen.js';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
/** sdocs' own install location — under npx this is the npx cache, not the project */
|
|
12
|
+
export function sdocsPackageRoot() {
|
|
13
|
+
return resolve(__dirname, '../..');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Prefer the project's own svelte over the copy next to sdocs when both exist
|
|
17
|
+
* (e.g. running via npx): previews import the project's components, and two
|
|
18
|
+
* svelte runtimes in one page don't mix.
|
|
19
|
+
*/
|
|
20
|
+
export function svelteDedupe(cwd) {
|
|
21
|
+
try {
|
|
22
|
+
require.resolve('svelte/package.json', { paths: [cwd] });
|
|
23
|
+
return ['svelte'];
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
export async function devCommand() {
|
|
8
30
|
const cwd = process.cwd();
|
|
9
31
|
const config = await loadConfig(cwd);
|
|
10
32
|
console.log('[sdocs] Starting dev server...');
|
|
11
|
-
// Generate
|
|
33
|
+
// Generate the staging directory (in the OS temp dir) with entry files
|
|
12
34
|
const sdocsDir = await generateDevFiles(config, cwd);
|
|
13
|
-
// Resolve include patterns to absolute paths (relative to cwd, not
|
|
35
|
+
// Resolve include patterns to absolute paths (relative to cwd, not the staging dir)
|
|
14
36
|
const absoluteIncludes = config.include.map((p) => resolve(cwd, p));
|
|
15
37
|
const server = await createServer({
|
|
16
38
|
configFile: false,
|
|
17
39
|
root: sdocsDir,
|
|
40
|
+
resolve: {
|
|
41
|
+
dedupe: svelteDedupe(cwd),
|
|
42
|
+
},
|
|
18
43
|
plugins: [
|
|
19
44
|
svelte(),
|
|
20
45
|
sdocsPlugin({ ...config, include: absoluteIncludes }),
|
|
@@ -23,7 +48,10 @@ export async function devCommand() {
|
|
|
23
48
|
port: config.port,
|
|
24
49
|
open: config.open,
|
|
25
50
|
fs: {
|
|
26
|
-
allow
|
|
51
|
+
// The staging dir (an explicit allow list replaces Vite's implicit
|
|
52
|
+
// root allowance), the project, and sdocs' own dependency tree
|
|
53
|
+
// (the npx cache when running without a local install).
|
|
54
|
+
allow: [sdocsDir, cwd, resolve(sdocsPackageRoot(), '..')],
|
|
27
55
|
},
|
|
28
56
|
},
|
|
29
57
|
});
|
|
@@ -32,7 +60,7 @@ export async function devCommand() {
|
|
|
32
60
|
// Cleanup on exit
|
|
33
61
|
const cleanup = async () => {
|
|
34
62
|
await server.close();
|
|
35
|
-
await cleanBuildFiles(
|
|
63
|
+
await cleanBuildFiles(sdocsDir);
|
|
36
64
|
process.exit(0);
|
|
37
65
|
};
|
|
38
66
|
process.on('SIGINT', cleanup);
|
package/dist/server/app-gen.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ResolvedSdocsConfig } from '../types.js';
|
|
2
|
-
/** Generate
|
|
2
|
+
/** Generate the staging directory with entry files for dev mode */
|
|
3
3
|
export declare function generateDevFiles(config: ResolvedSdocsConfig, cwd: string): Promise<string>;
|
|
4
|
-
/** Generate
|
|
4
|
+
/** Generate the staging directory with entry + preview HTML files for build mode */
|
|
5
5
|
export declare function generateBuildFiles(config: ResolvedSdocsConfig, cwd: string): Promise<{
|
|
6
6
|
sdocsDir: string;
|
|
7
7
|
inputs: Record<string, string>;
|
|
8
8
|
}>;
|
|
9
|
-
/** Remove
|
|
10
|
-
export declare function cleanBuildFiles(
|
|
9
|
+
/** Remove a staging directory created by generateDevFiles/generateBuildFiles */
|
|
10
|
+
export declare function cleanBuildFiles(stagingDir: string): Promise<void>;
|
package/dist/server/app-gen.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
-
import { mkdir, writeFile, rm, readFile, copyFile, readdir } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, mkdtemp, writeFile, rm, readFile, copyFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
2
4
|
import { dirname, resolve, join } from 'node:path';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { discoverDocFiles, getSdocKind } from './discovery.js';
|
|
5
7
|
import { extractSnippets, hasDefaultSnippet } from './snippet-extractor.js';
|
|
6
8
|
import { base64urlEncode } from './snippet-compiler.js';
|
|
7
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
/** Source client directory in the installed package */
|
|
10
|
+
/** Source client directory in the installed package (dist/client, next to dist/server) */
|
|
9
11
|
function getClientSourceDir() {
|
|
10
|
-
return resolve(__dirname, 'client');
|
|
12
|
+
return resolve(__dirname, '../client');
|
|
13
|
+
}
|
|
14
|
+
/** Copy the client app plus the ui/ tree it imports (styles, fonts, components) */
|
|
15
|
+
async function copyClientApp(sdocsDir) {
|
|
16
|
+
await copyDir(getClientSourceDir(), resolve(sdocsDir, 'client'));
|
|
17
|
+
await copyDir(resolve(__dirname, '../ui'), resolve(sdocsDir, 'ui'));
|
|
18
|
+
}
|
|
19
|
+
/** Nearest node_modules walking up from dir (mirrors Node's resolution) */
|
|
20
|
+
function findNearestNodeModules(dir) {
|
|
21
|
+
let current = resolve(dir);
|
|
22
|
+
for (;;) {
|
|
23
|
+
const candidate = join(current, 'node_modules');
|
|
24
|
+
if (existsSync(candidate))
|
|
25
|
+
return candidate;
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return null;
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a unique staging directory for the generated app. It lives inside
|
|
34
|
+
* node_modules/.cache — never a tracked file in the user's project — and being
|
|
35
|
+
* physically inside node_modules keeps the full walk-up resolution chain for
|
|
36
|
+
* bare imports (svelte, ...) intact, including hoisted monorepo setups. When
|
|
37
|
+
* no node_modules exists anywhere (running via npx in a bare folder), sdocs'
|
|
38
|
+
* own tree provides one; the OS temp dir is the last resort.
|
|
39
|
+
*/
|
|
40
|
+
async function createStagingDir(cwd) {
|
|
41
|
+
const nodeModules = findNearestNodeModules(cwd) ?? findNearestNodeModules(resolve(__dirname, '..'));
|
|
42
|
+
if (nodeModules) {
|
|
43
|
+
const cacheDir = join(nodeModules, '.cache');
|
|
44
|
+
await mkdir(cacheDir, { recursive: true });
|
|
45
|
+
return mkdtemp(join(cacheDir, 'sdocs-'));
|
|
46
|
+
}
|
|
47
|
+
return mkdtemp(join(tmpdir(), 'sdocs-'));
|
|
11
48
|
}
|
|
12
49
|
/** Copy a directory recursively */
|
|
13
50
|
async function copyDir(src, dest) {
|
|
@@ -124,22 +161,20 @@ async function discoverSnippets(config, cwd) {
|
|
|
124
161
|
}
|
|
125
162
|
return results;
|
|
126
163
|
}
|
|
127
|
-
/** Generate
|
|
164
|
+
/** Generate the staging directory with entry files for dev mode */
|
|
128
165
|
export async function generateDevFiles(config, cwd) {
|
|
129
|
-
const sdocsDir =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await copyDir(getClientSourceDir(), resolve(sdocsDir, 'client'));
|
|
166
|
+
const sdocsDir = await createStagingDir(cwd);
|
|
167
|
+
// Copy client components into the staging dir so they're compiled outside node_modules
|
|
168
|
+
await copyClientApp(sdocsDir);
|
|
133
169
|
await writeFile(resolve(sdocsDir, 'index.html'), generateIndexHtml(config.logo));
|
|
134
170
|
await writeFile(resolve(sdocsDir, 'entry.js'), generateEntryJs(config));
|
|
135
171
|
return sdocsDir;
|
|
136
172
|
}
|
|
137
|
-
/** Generate
|
|
173
|
+
/** Generate the staging directory with entry + preview HTML files for build mode */
|
|
138
174
|
export async function generateBuildFiles(config, cwd) {
|
|
139
|
-
const sdocsDir =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await copyDir(getClientSourceDir(), resolve(sdocsDir, 'client'));
|
|
175
|
+
const sdocsDir = await createStagingDir(cwd);
|
|
176
|
+
// Copy client components into the staging dir
|
|
177
|
+
await copyClientApp(sdocsDir);
|
|
143
178
|
await writeFile(resolve(sdocsDir, 'index.html'), generateIndexHtml(config.logo));
|
|
144
179
|
await writeFile(resolve(sdocsDir, 'entry.js'), generateEntryJs(config));
|
|
145
180
|
const inputs = {
|
|
@@ -161,8 +196,7 @@ export async function generateBuildFiles(config, cwd) {
|
|
|
161
196
|
}
|
|
162
197
|
return { sdocsDir, inputs };
|
|
163
198
|
}
|
|
164
|
-
/** Remove
|
|
165
|
-
export async function cleanBuildFiles(
|
|
166
|
-
|
|
167
|
-
await rm(sdocsDir, { recursive: true, force: true });
|
|
199
|
+
/** Remove a staging directory created by generateDevFiles/generateBuildFiles */
|
|
200
|
+
export async function cleanBuildFiles(stagingDir) {
|
|
201
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
168
202
|
}
|
package/dist/server/cli.js
CHANGED
|
@@ -18,7 +18,8 @@ Usage:
|
|
|
18
18
|
sdocs <command>
|
|
19
19
|
|
|
20
20
|
Commands:
|
|
21
|
-
dev Start development server with
|
|
21
|
+
dev Start development server with live reload
|
|
22
|
+
run Same as dev — works with npx and no local install
|
|
22
23
|
build Build static documentation site
|
|
23
24
|
preview Serve built site locally
|
|
24
25
|
init Scaffold sdocs.config.js
|
|
@@ -39,7 +40,8 @@ async function main() {
|
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
switch (command) {
|
|
42
|
-
case 'dev':
|
|
43
|
+
case 'dev':
|
|
44
|
+
case 'run': {
|
|
43
45
|
const { devCommand } = await import('../commands/dev.js');
|
|
44
46
|
await devCommand();
|
|
45
47
|
break;
|
|
@@ -7,14 +7,30 @@ export declare function resolveImportsToAbsolute(imports: string[], docFilePath:
|
|
|
7
7
|
/** Generate a virtual Svelte iframe wrapper component for a snippet.
|
|
8
8
|
* Includes $state for reactive prop updates via postMessage. */
|
|
9
9
|
export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string): string;
|
|
10
|
-
/**
|
|
10
|
+
/** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
|
|
11
|
+
export declare function generateMountScript(iframeComponentId: string): string;
|
|
12
|
+
/** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
|
|
11
13
|
export declare function generatePreviewHtml(iframeComponentId: string, css: string | Record<string, string> | null): string;
|
|
14
|
+
export interface StaticCssLink {
|
|
15
|
+
href: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Generate the HTML page for a preview emitted into a host app's build. */
|
|
20
|
+
export declare function generateStaticPreviewHtml(scriptSrc: string, cssLinks: StaticCssLink[]): string;
|
|
12
21
|
/** Build the virtual module ID for an iframe wrapper component */
|
|
13
22
|
export declare function iframeVirtualId(docFilePath: string, snippetName: string): string;
|
|
14
23
|
/** Build the preview URL for an iframe HTML page (dev mode) */
|
|
15
24
|
export declare function previewUrl(docFilePath: string, snippetName: string): string;
|
|
16
25
|
/** Build the preview URL for static build output */
|
|
17
26
|
export declare function buildPreviewUrl(docFilePath: string, snippetName: string): string;
|
|
27
|
+
/** Virtual module ID for a preview's mount script (embedded production builds) */
|
|
28
|
+
export declare function mountVirtualId(docFilePath: string, snippetName: string): string;
|
|
29
|
+
/** Parse a mount virtual ID back into its parts */
|
|
30
|
+
export declare function parseMountId(id: string): {
|
|
31
|
+
docFilePath: string;
|
|
32
|
+
snippetName: string;
|
|
33
|
+
} | null;
|
|
18
34
|
/** Parse an iframe virtual ID back into its parts */
|
|
19
35
|
export declare function parseIframeId(id: string): {
|
|
20
36
|
docFilePath: string;
|
|
@@ -97,9 +97,27 @@ function generateCssLinks(css) {
|
|
|
97
97
|
.map((name, i) => `<link rel="stylesheet" href="${normalizeCssHref(css[name])}" data-sdocs-stylesheet="${name}"${i > 0 ? ' disabled' : ''}>`)
|
|
98
98
|
.join('\n\t');
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
101
|
-
export function
|
|
102
|
-
|
|
100
|
+
/** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
|
|
101
|
+
export function generateMountScript(iframeComponentId) {
|
|
102
|
+
return `import { mount } from 'svelte';
|
|
103
|
+
import App from '${iframeComponentId}';
|
|
104
|
+
mount(App, { target: document.getElementById('app') });
|
|
105
|
+
|
|
106
|
+
// Listen for sdocs messages from the parent frame
|
|
107
|
+
window.addEventListener('message', (e) => {
|
|
108
|
+
if (e.data?.type === 'sdocs:update-stylesheet') {
|
|
109
|
+
const name = e.data.name;
|
|
110
|
+
document.querySelectorAll('link[data-sdocs-stylesheet]').forEach((link) => {
|
|
111
|
+
link.disabled = link.dataset.sdocsStylesheet !== name;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (e.data?.type === 'sdocs:scroll-to') {
|
|
115
|
+
const el = document.getElementById(e.data.id);
|
|
116
|
+
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
117
|
+
}
|
|
118
|
+
});`;
|
|
119
|
+
}
|
|
120
|
+
function previewHtmlShell(cssLinks, script) {
|
|
103
121
|
return `<!DOCTYPE html>
|
|
104
122
|
<html>
|
|
105
123
|
<head>
|
|
@@ -110,28 +128,26 @@ export function generatePreviewHtml(iframeComponentId, css) {
|
|
|
110
128
|
</head>
|
|
111
129
|
<body>
|
|
112
130
|
<div id="app"></div>
|
|
113
|
-
|
|
114
|
-
import { mount } from 'svelte';
|
|
115
|
-
import App from '${iframeComponentId}';
|
|
116
|
-
mount(App, { target: document.getElementById('app') });
|
|
117
|
-
|
|
118
|
-
// Listen for sdocs messages from the parent frame
|
|
119
|
-
window.addEventListener('message', (e) => {
|
|
120
|
-
if (e.data?.type === 'sdocs:update-stylesheet') {
|
|
121
|
-
const name = e.data.name;
|
|
122
|
-
document.querySelectorAll('link[data-sdocs-stylesheet]').forEach((link) => {
|
|
123
|
-
link.disabled = link.dataset.sdocsStylesheet !== name;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (e.data?.type === 'sdocs:scroll-to') {
|
|
127
|
-
const el = document.getElementById(e.data.id);
|
|
128
|
-
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
</script>
|
|
131
|
+
${script}
|
|
132
132
|
</body>
|
|
133
133
|
</html>`;
|
|
134
134
|
}
|
|
135
|
+
/** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
|
|
136
|
+
export function generatePreviewHtml(iframeComponentId, css) {
|
|
137
|
+
return previewHtmlShell(generateCssLinks(css), `<script type="module">
|
|
138
|
+
${generateMountScript(iframeComponentId)}
|
|
139
|
+
</script>`);
|
|
140
|
+
}
|
|
141
|
+
/** Generate the HTML page for a preview emitted into a host app's build. */
|
|
142
|
+
export function generateStaticPreviewHtml(scriptSrc, cssLinks) {
|
|
143
|
+
const links = cssLinks
|
|
144
|
+
.map(({ href, name, disabled }) => {
|
|
145
|
+
const named = name ? ` data-sdocs-stylesheet="${name}"` : '';
|
|
146
|
+
return `<link rel="stylesheet" href="${href}"${named}${disabled ? ' disabled' : ''}>`;
|
|
147
|
+
})
|
|
148
|
+
.join('\n\t');
|
|
149
|
+
return previewHtmlShell(links, `<script type="module" src="${scriptSrc}"></script>`);
|
|
150
|
+
}
|
|
135
151
|
/** Build the virtual module ID for an iframe wrapper component */
|
|
136
152
|
export function iframeVirtualId(docFilePath, snippetName) {
|
|
137
153
|
return `/@sdocs/iframe/${base64urlEncode(docFilePath)}/${snippetName}.svelte`;
|
|
@@ -144,6 +160,20 @@ export function previewUrl(docFilePath, snippetName) {
|
|
|
144
160
|
export function buildPreviewUrl(docFilePath, snippetName) {
|
|
145
161
|
return `/previews/${base64urlEncode(docFilePath)}/${snippetName}.html`;
|
|
146
162
|
}
|
|
163
|
+
/** Virtual module ID for a preview's mount script (embedded production builds) */
|
|
164
|
+
export function mountVirtualId(docFilePath, snippetName) {
|
|
165
|
+
return `/@sdocs/mount/${base64urlEncode(docFilePath)}/${snippetName}.js`;
|
|
166
|
+
}
|
|
167
|
+
/** Parse a mount virtual ID back into its parts */
|
|
168
|
+
export function parseMountId(id) {
|
|
169
|
+
const match = id.match(/^\/@sdocs\/mount\/([^/]+)\/(\w+)\.js$/);
|
|
170
|
+
if (!match)
|
|
171
|
+
return null;
|
|
172
|
+
return {
|
|
173
|
+
docFilePath: base64urlDecode(match[1]),
|
|
174
|
+
snippetName: match[2],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
147
177
|
/** Parse an iframe virtual ID back into its parts */
|
|
148
178
|
export function parseIframeId(id) {
|
|
149
179
|
const match = id.match(/^\/@sdocs\/iframe\/([^/]+)\/(\w+)\.svelte$/);
|
package/dist/vite.js
CHANGED
|
@@ -6,11 +6,12 @@ import { parseComponent } from './server/prop-parser.js';
|
|
|
6
6
|
import { extractSnippets, extractMarkupBody, hasDefaultSnippet, generateAutoDefault, } from './server/snippet-extractor.js';
|
|
7
7
|
import { highlight, disposeHighlighter } from './server/highlighter.js';
|
|
8
8
|
import { extractTocFromHtml } from './server/toc-extractor.js';
|
|
9
|
-
import { parseIframeId, parsePreviewUrl, resolveImportsToAbsolute, generateIframeComponent, generatePreviewHtml, iframeVirtualId, previewUrl, buildPreviewUrl, } from './server/snippet-compiler.js';
|
|
9
|
+
import { parseIframeId, parseMountId, parsePreviewUrl, resolveImportsToAbsolute, generateIframeComponent, generateMountScript, generatePreviewHtml, generateStaticPreviewHtml, iframeVirtualId, mountVirtualId, previewUrl, buildPreviewUrl, base64urlEncode, } from './server/snippet-compiler.js';
|
|
10
10
|
const VIRTUAL_MODULE_ID = 'virtual:sdocs';
|
|
11
11
|
const RESOLVED_VIRTUAL_ID = '\0virtual:sdocs';
|
|
12
12
|
const IFRAME_PREFIX = '/@sdocs/iframe/';
|
|
13
13
|
const PREVIEW_PREFIX = '/@sdocs/preview/';
|
|
14
|
+
const MOUNT_PREFIX = '/@sdocs/mount/';
|
|
14
15
|
export function sdocsPlugin(userConfig) {
|
|
15
16
|
let config;
|
|
16
17
|
let root;
|
|
@@ -18,10 +19,17 @@ export function sdocsPlugin(userConfig) {
|
|
|
18
19
|
let docEntries = new Map();
|
|
19
20
|
let docImportsCache = new Map();
|
|
20
21
|
const buildMode = userConfig?._buildMode ?? false;
|
|
22
|
+
let isBuild = false;
|
|
23
|
+
let isSsrBuild = false;
|
|
24
|
+
// Previews planned for emission into a host app's build (embedded mode)
|
|
25
|
+
let plannedPreviews = [];
|
|
26
|
+
let emittedCssLinks = [];
|
|
21
27
|
return {
|
|
22
28
|
name: 'sdocs',
|
|
23
29
|
async configResolved(resolvedConfig) {
|
|
24
30
|
root = resolvedConfig.root;
|
|
31
|
+
isBuild = resolvedConfig.command === 'build';
|
|
32
|
+
isSsrBuild = !!resolvedConfig.build?.ssr;
|
|
25
33
|
const fileConfig = await loadRawConfig(root);
|
|
26
34
|
const merged = { ...fileConfig, ...userConfig };
|
|
27
35
|
config = resolveAndFinalize(merged, root);
|
|
@@ -125,12 +133,85 @@ export function sdocsPlugin(userConfig) {
|
|
|
125
133
|
server.watcher.add(dir);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
136
|
+
// Embedded production build: emit each preview as its own chunk so the
|
|
137
|
+
// host app's build output contains working static preview pages. (The
|
|
138
|
+
// standalone CLI build passes _buildMode and provides HTML inputs itself;
|
|
139
|
+
// the SSR half of an app build has no use for browser preview pages.)
|
|
140
|
+
if (isBuild && !buildMode && !isSsrBuild) {
|
|
141
|
+
plannedPreviews = [];
|
|
142
|
+
emittedCssLinks = [];
|
|
143
|
+
const css = config.css;
|
|
144
|
+
if (typeof css === 'string') {
|
|
145
|
+
emittedCssLinks.push({ href: await emitCssAsset(this, css, 'preview') });
|
|
146
|
+
}
|
|
147
|
+
else if (css) {
|
|
148
|
+
for (const [i, name] of Object.keys(css).entries()) {
|
|
149
|
+
emittedCssLinks.push({
|
|
150
|
+
href: await emitCssAsset(this, css[name], name),
|
|
151
|
+
name,
|
|
152
|
+
disabled: i > 0,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const entry of docEntries.values()) {
|
|
157
|
+
const encoded = base64urlEncode(entry.filePath);
|
|
158
|
+
for (const snippet of entry.snippets) {
|
|
159
|
+
const jsFileName = `previews/${encoded}/${snippet.name}.js`;
|
|
160
|
+
this.emitFile({
|
|
161
|
+
type: 'chunk',
|
|
162
|
+
id: mountVirtualId(entry.filePath, snippet.name),
|
|
163
|
+
fileName: jsFileName,
|
|
164
|
+
});
|
|
165
|
+
plannedPreviews.push({
|
|
166
|
+
jsFileName,
|
|
167
|
+
htmlFileName: `previews/${encoded}/${snippet.name}.html`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
console.log(`[sdocs] Emitting ${plannedPreviews.length} static preview page(s)`);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
generateBundle(_options, bundle) {
|
|
175
|
+
for (const preview of plannedPreviews) {
|
|
176
|
+
const chunk = bundle[preview.jsFileName];
|
|
177
|
+
if (!chunk || chunk.type !== 'chunk')
|
|
178
|
+
continue;
|
|
179
|
+
// Collect CSS emitted for this chunk and everything it imports.
|
|
180
|
+
const cssFiles = new Set();
|
|
181
|
+
const queue = [preview.jsFileName];
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
while (queue.length) {
|
|
184
|
+
const fileName = queue.pop();
|
|
185
|
+
if (seen.has(fileName))
|
|
186
|
+
continue;
|
|
187
|
+
seen.add(fileName);
|
|
188
|
+
const mod = bundle[fileName];
|
|
189
|
+
if (!mod || mod.type !== 'chunk')
|
|
190
|
+
continue;
|
|
191
|
+
for (const css of mod.viteMetadata?.importedCss ?? [])
|
|
192
|
+
cssFiles.add(css);
|
|
193
|
+
queue.push(...mod.imports);
|
|
194
|
+
}
|
|
195
|
+
// The HTML sits at previews/<doc>/<name>.html — two levels deep.
|
|
196
|
+
const cssLinks = [
|
|
197
|
+
...emittedCssLinks,
|
|
198
|
+
...[...cssFiles].map((file) => ({ href: `../../${file}` })),
|
|
199
|
+
];
|
|
200
|
+
this.emitFile({
|
|
201
|
+
type: 'asset',
|
|
202
|
+
fileName: preview.htmlFileName,
|
|
203
|
+
source: generateStaticPreviewHtml(`./${preview.jsFileName.split('/').pop()}`, cssLinks),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
plannedPreviews = [];
|
|
128
207
|
},
|
|
129
208
|
resolveId(id) {
|
|
130
209
|
if (id === VIRTUAL_MODULE_ID)
|
|
131
210
|
return RESOLVED_VIRTUAL_ID;
|
|
132
211
|
if (id.startsWith(IFRAME_PREFIX))
|
|
133
212
|
return '\0' + id;
|
|
213
|
+
if (id.startsWith(MOUNT_PREFIX))
|
|
214
|
+
return '\0' + id;
|
|
134
215
|
},
|
|
135
216
|
load(id) {
|
|
136
217
|
if (id === RESOLVED_VIRTUAL_ID) {
|
|
@@ -151,6 +232,13 @@ export function sdocsPlugin(userConfig) {
|
|
|
151
232
|
const absoluteImports = docImportsCache.get(parsed.docFilePath) ?? [];
|
|
152
233
|
return generateIframeComponent(absoluteImports, snippet.body);
|
|
153
234
|
}
|
|
235
|
+
// Virtual mount script for an emitted preview page
|
|
236
|
+
if (id.startsWith('\0' + MOUNT_PREFIX)) {
|
|
237
|
+
const parsed = parseMountId(id.slice(1));
|
|
238
|
+
if (!parsed)
|
|
239
|
+
return null;
|
|
240
|
+
return generateMountScript(iframeVirtualId(parsed.docFilePath, parsed.snippetName));
|
|
241
|
+
}
|
|
154
242
|
},
|
|
155
243
|
async buildEnd() {
|
|
156
244
|
await disposeHighlighter();
|
|
@@ -232,7 +320,9 @@ export function sdocsPlugin(userConfig) {
|
|
|
232
320
|
name: s.name,
|
|
233
321
|
body: s.body,
|
|
234
322
|
highlightedHtml: s.highlightedHtml,
|
|
235
|
-
previewUrl: buildMode
|
|
323
|
+
previewUrl: buildMode || isBuild
|
|
324
|
+
? buildPreviewUrl(e.filePath, s.name)
|
|
325
|
+
: previewUrl(e.filePath, s.name),
|
|
236
326
|
})),
|
|
237
327
|
highlightedSource: e.highlightedSource,
|
|
238
328
|
toc: e.toc,
|
|
@@ -269,6 +359,15 @@ export function sdocsPlugin(userConfig) {
|
|
|
269
359
|
function isDocFile(filePath) {
|
|
270
360
|
return filePath.endsWith('.sdoc');
|
|
271
361
|
}
|
|
362
|
+
/** Emit a user stylesheet as a build asset; returns its href relative to a preview page. */
|
|
363
|
+
async function emitCssAsset(ctx, href, name) {
|
|
364
|
+
if (href.startsWith('http'))
|
|
365
|
+
return href;
|
|
366
|
+
const source = await readFile(href, 'utf-8');
|
|
367
|
+
const fileName = `previews/_css/${name}.css`;
|
|
368
|
+
ctx.emitFile({ type: 'asset', fileName, source });
|
|
369
|
+
return `../../${fileName}`;
|
|
370
|
+
}
|
|
272
371
|
function isComponentReferencedByDoc(filePath) {
|
|
273
372
|
for (const entry of docEntries.values()) {
|
|
274
373
|
if (entry.componentPath === filePath)
|