Skip to content

Commit f7adbc0

Browse files
WIP
Co-Authored-By: Aviv Keller <[email protected]>
1 parent 7f0264a commit f7adbc0

File tree

17 files changed

+611
-97
lines changed

17 files changed

+611
-97
lines changed

.c8rc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"all": true,
33
"exclude": [
44
"eslint.config.mjs",
5+
"**/legacy-html/**",
56
"**/*.test.mjs",
67
"**/fixtures",
78
"src/generators/legacy-html/assets",

src/generators/metadata/__tests__/parse.test.mjs renamed to src/generators/metadata/utils/__tests__/parse.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { strictEqual, deepStrictEqual } from 'node:assert';
1+
import { deepStrictEqual, strictEqual } from 'node:assert/strict';
22
import { describe, it } from 'node:test';
33

44
import { u } from 'unist-builder';
55
import { VFile } from 'vfile';
66

7-
import { parseApiDoc } from '../utils/parse.mjs';
7+
import { parseApiDoc } from '../parse.mjs';
88

9-
describe('generators/metadata/utils/parse', () => {
9+
describe('parseApiDoc', () => {
1010
it('parses heading, stability, YAML and converts markdown links', () => {
1111
const tree = u('root', [
1212
u('heading', { depth: 1 }, [u('text', 'My API')]),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtemp, readFile } from 'node:fs/promises';
3+
import os from 'node:os';
4+
import { join } from 'node:path';
5+
import { describe, it } from 'node:test';
6+
7+
import webGen from '../index.mjs';
8+
9+
describe('generators/web - index generate', () => {
10+
it('writes files when output is provided', async () => {
11+
const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-test-'));
12+
13+
const results = [{ html: Buffer.from('<div>ok</div>'), api: 'api' }];
14+
const css = 'body{}';
15+
const chunks = [{ fileName: 'chunk.js', code: 'console.log(1)' }];
16+
17+
const stubProcess = async () => ({ results, css, chunks });
18+
19+
const out = await webGen.generate([], {
20+
output: tmp,
21+
version: { version: '1.2.3' },
22+
overrides: { processJSXEntries: stubProcess },
23+
});
24+
25+
// returns results as strings + css
26+
assert.equal(out.length, 1);
27+
const htmlPath = join(tmp, 'api.html');
28+
const written = await readFile(htmlPath, 'utf-8');
29+
assert.ok(written.includes('<div>ok</div>'));
30+
31+
const chunkPath = join(tmp, 'chunk.js');
32+
const chunkContent = await readFile(chunkPath, 'utf-8');
33+
assert.equal(chunkContent, 'console.log(1)');
34+
35+
const cssPath = join(tmp, 'styles.css');
36+
const cssContent = await readFile(cssPath, 'utf-8');
37+
assert.equal(cssContent, css);
38+
});
39+
});

src/generators/web/index.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default {
3737
* @param {Partial<GeneratorOptions>} options - Generator options.
3838
* @returns {Promise<Output>} Processed HTML/CSS/JS content.
3939
*/
40-
async generate(input, { output, version }) {
40+
async generate(input, { output, version, overrides } = {}) {
4141
const template = await readFile(
4242
new URL('template.html', import.meta.url),
4343
'utf-8'
@@ -50,12 +50,14 @@ export default {
5050
const requireFn = createRequire(import.meta.url);
5151

5252
// Process all entries: convert JSX to HTML/CSS/JS
53-
const { results, css, chunks } = await processJSXEntries(
53+
const processFn = overrides?.processJSXEntries || processJSXEntries;
54+
55+
const { results, css, chunks } = await processFn(
5456
input,
5557
template,
5658
astBuilders,
5759
requireFn,
58-
{ version }
60+
{ version, overrides }
5961
);
6062

6163
// Process all entries together (required for code-split bundles)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
describe('generators/web/utils - bundle', () => {
5+
it('bundleCode separates assets and chunks and returns expected shape', async () => {
6+
const bundleCode = (await import('../bundle.mjs')).default;
7+
8+
const codeMap = new Map([['a.js', 'export default 1;']]);
9+
10+
const result = await bundleCode(codeMap, { server: false });
11+
12+
// Basic shape assertions to keep this test hermetic without module mocking
13+
assert.equal(typeof result.css, 'string');
14+
assert.ok(Array.isArray(result.chunks));
15+
assert.equal(
16+
typeof result.importMap === 'string' || result.importMap === undefined,
17+
true
18+
);
19+
assert.ok(
20+
result.chunks.every(
21+
c => typeof c.fileName === 'string' && 'code' in c && 'isEntry' in c
22+
)
23+
);
24+
});
25+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { createChunkedRequire } from '../chunks.mjs';
5+
6+
describe('generators/web/utils - chunks', () => {
7+
it('createChunkedRequire resolves virtual chunks and falls back to require', () => {
8+
const chunks = [
9+
{ fileName: 'a.js', code: 'module.exports = { val: 1 };' },
10+
{
11+
fileName: 'b.js',
12+
code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };',
13+
},
14+
];
15+
16+
const fakeRequire = path => {
17+
if (path === 'fs') {
18+
return { read: true };
19+
}
20+
return null;
21+
};
22+
23+
const req = createChunkedRequire(chunks, fakeRequire);
24+
25+
// resolve virtual module
26+
const a = req('./a.js');
27+
assert.deepEqual(a, { val: 1 });
28+
29+
// module that requires another virtual module
30+
const b = req('./b.js');
31+
assert.deepEqual(b, { val: 2 });
32+
33+
// fallback to external
34+
const ext = req('fs');
35+
assert.deepEqual(ext, { read: true });
36+
});
37+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import cssPlugin from '../css.mjs';
5+
6+
describe('generators/web/utils - css', () => {
7+
it('css plugin buildEnd is a no-op when no chunks processed', () => {
8+
const plugin = cssPlugin();
9+
10+
let emitted = null;
11+
const thisArg = { emitFile: info => (emitted = info) };
12+
13+
// Should not throw and should not emit anything
14+
plugin.buildEnd.call(thisArg);
15+
assert.equal(emitted, null);
16+
});
17+
18+
it('css plugin processes .module.css files and emits styles.css asset', async () => {
19+
const plugin = cssPlugin();
20+
21+
// create temp .module.css file
22+
const { writeFile, unlink } = await import('node:fs/promises');
23+
const { tmpdir } = await import('node:os');
24+
const { join } = await import('node:path');
25+
26+
const id = join(
27+
tmpdir(),
28+
`doc-kit-test-${Date.now()}-${Math.random()}.module.css`
29+
);
30+
await writeFile(id, '.btn { color: red; }', 'utf8');
31+
32+
// Call the handler to process the css file
33+
const { handler } = plugin.load;
34+
const result = await handler(id);
35+
36+
// Should return a JS module exporting the mapped class names
37+
assert.match(result.code, /export default/);
38+
assert.match(result.code, /"btn"/);
39+
40+
// buildEnd should emit a styles.css asset containing the compiled CSS
41+
let emitted = null;
42+
const thisArg = { emitFile: info => (emitted = info) };
43+
plugin.buildEnd.call(thisArg);
44+
45+
assert.ok(emitted, 'expected styles.css to be emitted');
46+
assert.equal(emitted.name, 'styles.css');
47+
assert.match(String(emitted.source), /color:\s*red/);
48+
49+
// cleanup
50+
await unlink(id);
51+
});
52+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import createBuilders, { createImportDeclaration } from '../generate.mjs';
5+
6+
describe('generators/web/utils - generate', () => {
7+
it('createImportDeclaration produces correct import strings', () => {
8+
// side-effect import
9+
assert.equal(
10+
createImportDeclaration(null, './style.css'),
11+
'import "./style.css";'
12+
);
13+
14+
// default import
15+
assert.equal(
16+
createImportDeclaration('X', './mod'),
17+
'import X from "./mod";'
18+
);
19+
20+
// named import
21+
assert.equal(
22+
createImportDeclaration('Y', './mod', false),
23+
'import { Y } from "./mod";'
24+
);
25+
});
26+
27+
it('builders produce client and server programs containing expected markers', () => {
28+
const { buildClientProgram, buildServerProgram } = createBuilders();
29+
30+
const client = buildClientProgram('MyComp()');
31+
assert.match(client, /hydrate\(MyComp\(\)/);
32+
assert.match(client, /index\.css/);
33+
34+
const server = buildServerProgram('MyComp()');
35+
assert.match(server, /return render\(MyComp\(\)\);/);
36+
});
37+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { executeServerCode, processWithCodeMaps } from '../processing.mjs';
5+
6+
describe('generators/web/utils - processing (injectable)', () => {
7+
it('executeServerCode accepts injected bundler and chunked require', async () => {
8+
const serverCodeMap = new Map([[`api.jsx`, 'ignored']]);
9+
10+
const fakeRequire = () => ({ fs: true });
11+
12+
const mockBundle = async () => ({
13+
chunks: [
14+
{ fileName: 'api.js', isEntry: true, code: 'return "<p>ok</p>";' },
15+
],
16+
css: 'body{}',
17+
});
18+
19+
const { pages, css } = await executeServerCode(serverCodeMap, fakeRequire, {
20+
bundleCode: mockBundle,
21+
createChunkedRequire: (chunks, req) => req,
22+
});
23+
24+
assert.equal(pages.get('api.js'), '<p>ok</p>');
25+
assert.equal(css, 'body{}');
26+
});
27+
28+
it('processWithCodeMaps builds final HTML and css using injected bundlers', async () => {
29+
const serverCodeMap = new Map([[`api.jsx`, 'ignored']]);
30+
const clientCodeMap = new Map([[`api.jsx`, 'ignored']]);
31+
32+
const entries = [
33+
{ data: { api: 'api', heading: { data: { name: 'My API' } } } },
34+
];
35+
36+
const template =
37+
'<title>{{title}}</title>{{dehydrated}}{{importMap}}{{entrypoint}}{{speculationRules}}';
38+
39+
const fakeRequire = () => ({ fs: true });
40+
41+
const mockServerBundle = async () => ({
42+
chunks: [
43+
{
44+
fileName: 'api.js',
45+
isEntry: true,
46+
code: 'return "<div>server</div>";',
47+
},
48+
],
49+
css: 's{}',
50+
});
51+
52+
const mockClientBundle = async () => ({
53+
chunks: [{ fileName: 'api.js', isEntry: true, code: '/* client */' }],
54+
css: 'c{}',
55+
importMap: '{}',
56+
});
57+
58+
const { results, css } = await processWithCodeMaps(
59+
serverCodeMap,
60+
clientCodeMap,
61+
entries,
62+
template,
63+
fakeRequire,
64+
{ version: { version: '1.0.0' } },
65+
{
66+
bundleCode: async map =>
67+
map === serverCodeMap
68+
? await mockServerBundle()
69+
: await mockClientBundle(),
70+
createChunkedRequire: (chunks, req) => req,
71+
transform: ({ code }) => ({ code: Buffer.from(String(code)) }),
72+
}
73+
);
74+
75+
assert.equal(results.length, 1);
76+
const html = results[0].html.toString();
77+
assert.match(html, /<div>server<\/div>/);
78+
assert.match(html, /My API/);
79+
assert.ok(String(css).includes('s{}') || String(css).includes('c{}'));
80+
});
81+
});

0 commit comments

Comments
 (0)