Skip to content

Commit 5042481

Browse files
author
LessJS Bot
committed
fix(adapter-vite): add workspace alias generation to build-ssg too
- Extract findWorkspaceRoot/generateWorkspaceAliases to shared module (workspace-alias.ts), imported by both build-client and build-ssg - build-ssg now auto-generates aliases for Phase 3 SSR bundle build - Fixes @lessjs/core/render-dsd resolution failure in virtual:less-ssg-entry
1 parent 4a5b2c5 commit 5042481

3 files changed

Lines changed: 91 additions & 71 deletions

File tree

packages/adapter-vite/src/cli/build-client.ts

Lines changed: 6 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
* closeBundle() in less:build plugin. No longer a standalone CLI entry.
99
* ctx parameter is required (no globalThis fallback).
1010
*
11-
* Resolution: Aliases are auto-generated from workspace packages'
12-
* deno.json exports — not hardcoded, not in user config. When Vite/rolldown
13-
* supports Deno workspace resolution natively, this can be removed.
14-
*
1511
* Usage:
1612
* deno task build (unified entry — runs all 3 phases)
1713
*/
@@ -22,77 +18,16 @@ import process from 'node:process';
2218
import { type ClientIslandEntry, generateClientEntry } from '../entry-generators.js';
2319
import type { LessBuildContext } from '../build-context.js';
2420
import { createLogger } from '@lessjs/core/logger';
21+
import {
22+
findWorkspaceRoot,
23+
generateWorkspaceAliases,
24+
} from '../workspace-alias.js';
2525

2626
const log = createLogger('ssg');
2727

2828
const VIRTUAL_CLIENT_ENTRY_ID = 'virtual:less-client-entry';
2929
const RESOLVED_CLIENT_ENTRY_ID = '\0' + VIRTUAL_CLIENT_ENTRY_ID;
3030

31-
// ─── Alias auto-generation from workspace ────────────────────────────
32-
33-
interface AliasEntry {
34-
find: string;
35-
replacement: string;
36-
}
37-
38-
/**
39-
* Walk up from startDir to find a deno.json with a "workspace" field.
40-
*/
41-
async function findWorkspaceRoot(startDir: string): Promise<string | null> {
42-
let dir = resolve(startDir);
43-
const fsRoot = resolve('/');
44-
while (dir !== fsRoot && dir !== resolve(dir, '..')) {
45-
try {
46-
const cfg = JSON.parse(await Deno.readTextFile(resolve(dir, 'deno.json')));
47-
if (cfg.workspace && Array.isArray(cfg.workspace)) return dir;
48-
} catch { /* not found or no workspace */ }
49-
dir = resolve(dir, '..');
50-
}
51-
return null;
52-
}
53-
54-
/**
55-
* Generate Vite resolve.alias from workspace packages' deno.json exports.
56-
* Subpath aliases come before parent (Vite prefix matching rule).
57-
*/
58-
async function generateWorkspaceAliases(workspaceRoot: string): Promise<AliasEntry[]> {
59-
const rootCfg = JSON.parse(await Deno.readTextFile(resolve(workspaceRoot, 'deno.json')));
60-
const members: string[] = rootCfg.workspace || [];
61-
const aliases: AliasEntry[] = [];
62-
63-
for (const member of members) {
64-
const memberDir = resolve(workspaceRoot, member);
65-
let memberCfg: Record<string, unknown>;
66-
try {
67-
memberCfg = JSON.parse(await Deno.readTextFile(resolve(memberDir, 'deno.json')));
68-
} catch {
69-
continue;
70-
}
71-
const name = memberCfg.name as string | undefined;
72-
const exports = memberCfg.exports as Record<string, string> | string | undefined;
73-
if (!name || !exports) continue;
74-
75-
if (typeof exports === 'string') {
76-
aliases.push({ find: name, replacement: resolve(memberDir, exports) });
77-
continue;
78-
}
79-
80-
// Subpath aliases first (Vite prefix matching)
81-
for (const [exportPath, sourcePath] of Object.entries(exports)) {
82-
if (exportPath === '.') continue;
83-
const subpath = exportPath.replace(/^\.\//, '/');
84-
aliases.push({ find: `${name}${subpath}`, replacement: resolve(memberDir, sourcePath as string) });
85-
}
86-
// Parent alias last
87-
if (exports['.']) {
88-
aliases.push({ find: name, replacement: resolve(memberDir, exports['.'] as string) });
89-
}
90-
}
91-
return aliases;
92-
}
93-
94-
// ─── Build function ──────────────────────────────────────────────────
95-
9631
async function buildClient(ctx: LessBuildContext): Promise<void> {
9732
const root = ctx.root || process.cwd();
9833
const outDir = ctx.outDir || 'dist';
@@ -101,8 +36,8 @@ async function buildClient(ctx: LessBuildContext): Promise<void> {
10136
const localIslandFiles = ctx.islandFiles || [];
10237
const packageIslands = ctx.packageIslands || [];
10338

104-
// Auto-generate aliases from workspace
105-
let serializedAlias: AliasEntry[] | null = null;
39+
// Auto-generate aliases from workspace packages
40+
let serializedAlias = null;
10641
const workspaceRoot = await findWorkspaceRoot(root);
10742
if (workspaceRoot) {
10843
serializedAlias = await generateWorkspaceAliases(workspaceRoot);

packages/adapter-vite/src/cli/build-ssg.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import type { LessBuildContext } from '../build-context.js';
2222
import { SsrRenderError } from '@lessjs/core/errors';
2323
import { createLogger } from '@lessjs/core/logger';
2424
import { createBlogDataPlugin, createI18nDataPlugin } from '../virtual-data.js';
25+
import {
26+
findWorkspaceRoot,
27+
generateWorkspaceAliases,
28+
} from '../workspace-alias.js';
2529

2630
const log = createLogger('ssg');
2731

@@ -216,9 +220,16 @@ async function buildSSG(options: BuildSSGOptions = {}, ctx: LessBuildContext): P
216220
const ssrOutDir = join(root, outDir, 'server');
217221
log.info(`Building SSR bundle → ${ssrOutDir}`);
218222

223+
// Auto-generate aliases from workspace packages
224+
const ssgAliases = await (async () => {
225+
const wsRoot = await findWorkspaceRoot(root);
226+
return wsRoot ? await generateWorkspaceAliases(wsRoot) : null;
227+
})();
228+
219229
await viteBuild({
220230
configFile: false,
221231
root,
232+
resolve: ssgAliases ? { alias: ssgAliases } : undefined,
222233
build: {
223234
ssr: true,
224235
outDir: ssrOutDir,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @lessjs/adapter-vite - Workspace alias auto-generation
3+
*
4+
* Reads Deno workspace deno.json exports and generates
5+
* Vite resolve.alias entries. Used by build-client and build-ssg
6+
* to resolve @lessjs/* packages when rolldown doesn't support
7+
* Deno workspace resolution natively.
8+
*/
9+
10+
import { resolve } from 'node:path';
11+
12+
export interface AliasEntry {
13+
find: string;
14+
replacement: string;
15+
}
16+
17+
/**
18+
* Walk up from startDir to find a deno.json with a "workspace" field.
19+
*/
20+
export async function findWorkspaceRoot(startDir: string): Promise<string | null> {
21+
let dir = resolve(startDir);
22+
const fsRoot = resolve('/');
23+
while (dir !== fsRoot && dir !== resolve(dir, '..')) {
24+
try {
25+
const cfg = JSON.parse(await Deno.readTextFile(resolve(dir, 'deno.json')));
26+
if (cfg.workspace && Array.isArray(cfg.workspace)) return dir;
27+
} catch { /* not found or no workspace */ }
28+
dir = resolve(dir, '..');
29+
}
30+
return null;
31+
}
32+
33+
/**
34+
* Generate Vite resolve.alias from workspace packages' deno.json exports.
35+
* Subpath aliases come before parent (Vite prefix matching rule).
36+
*/
37+
export async function generateWorkspaceAliases(workspaceRoot: string): Promise<AliasEntry[]> {
38+
const rootCfg = JSON.parse(await Deno.readTextFile(resolve(workspaceRoot, 'deno.json')));
39+
const members: string[] = rootCfg.workspace || [];
40+
const aliases: AliasEntry[] = [];
41+
42+
for (const member of members) {
43+
const memberDir = resolve(workspaceRoot, member);
44+
let memberCfg: Record<string, unknown>;
45+
try {
46+
memberCfg = JSON.parse(await Deno.readTextFile(resolve(memberDir, 'deno.json')));
47+
} catch {
48+
continue;
49+
}
50+
const name = memberCfg.name as string | undefined;
51+
const exports = memberCfg.exports as Record<string, string> | string | undefined;
52+
if (!name || !exports) continue;
53+
54+
if (typeof exports === 'string') {
55+
aliases.push({ find: name, replacement: resolve(memberDir, exports) });
56+
continue;
57+
}
58+
59+
// Subpath aliases first (Vite prefix matching)
60+
for (const [exportPath, sourcePath] of Object.entries(exports)) {
61+
if (exportPath === '.') continue;
62+
const subpath = exportPath.replace(/^\.\//, '/');
63+
aliases.push({
64+
find: `${name}${subpath}`,
65+
replacement: resolve(memberDir, sourcePath as string),
66+
});
67+
}
68+
// Parent alias last
69+
if (exports['.']) {
70+
aliases.push({ find: name, replacement: resolve(memberDir, exports['.'] as string) });
71+
}
72+
}
73+
return aliases;
74+
}

0 commit comments

Comments
 (0)