@arcote.tech/arc-cli 0.4.6 → 0.4.7
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/dist/index.js +393 -2124
- package/package.json +1 -1
- package/src/builder/module-builder.ts +17 -4
- package/src/commands/platform-dev.ts +2 -1
- package/src/commands/platform-start.ts +2 -1
- package/src/i18n/catalog.ts +43 -5
- package/src/i18n/compile.ts +6 -1
- package/src/platform/server.ts +94 -10
- package/src/platform/shared.ts +53 -31
package/package.json
CHANGED
|
@@ -97,8 +97,13 @@ export interface WorkspacePackage {
|
|
|
97
97
|
packageJson: Record<string, any>;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export interface ModuleEntry {
|
|
101
|
+
file: string;
|
|
102
|
+
name: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
100
105
|
export interface BuildManifest {
|
|
101
|
-
modules:
|
|
106
|
+
modules: ModuleEntry[];
|
|
102
107
|
buildTime: string;
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -203,9 +208,13 @@ export async function buildPackages(
|
|
|
203
208
|
mkdirSync(tmpDir, { recursive: true });
|
|
204
209
|
|
|
205
210
|
const entrypoints: string[] = [];
|
|
211
|
+
const fileToName = new Map<string, string>(); // safeName → module name
|
|
206
212
|
|
|
207
213
|
for (const pkg of packages) {
|
|
208
214
|
const safeName = pkg.path.split("/").pop()!;
|
|
215
|
+
// Module name: strip scope (e.g. @ndt/admin → admin)
|
|
216
|
+
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
217
|
+
fileToName.set(safeName, moduleName);
|
|
209
218
|
|
|
210
219
|
// All packages get a simple re-export wrapper.
|
|
211
220
|
// Context packages that use module().build() self-register via side effects.
|
|
@@ -262,12 +271,16 @@ export async function buildPackages(
|
|
|
262
271
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
263
272
|
|
|
264
273
|
// Build manifest
|
|
265
|
-
const
|
|
274
|
+
const moduleEntries: ModuleEntry[] = result.outputs
|
|
266
275
|
.filter((o) => o.kind === "entry-point")
|
|
267
|
-
.map((o) =>
|
|
276
|
+
.map((o) => {
|
|
277
|
+
const file = o.path.split("/").pop()!;
|
|
278
|
+
const safeName = file.replace(/\.js$/, "");
|
|
279
|
+
return { file, name: fileToName.get(safeName) ?? safeName };
|
|
280
|
+
});
|
|
268
281
|
|
|
269
282
|
const manifest: BuildManifest = {
|
|
270
|
-
modules:
|
|
283
|
+
modules: moduleEntries,
|
|
271
284
|
buildTime: new Date().toISOString(),
|
|
272
285
|
};
|
|
273
286
|
|
|
@@ -21,7 +21,7 @@ export async function platformDev(): Promise<void> {
|
|
|
21
21
|
|
|
22
22
|
// Load server context
|
|
23
23
|
log("Loading server context...");
|
|
24
|
-
const context = await loadServerContext(ws.packages);
|
|
24
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
25
25
|
if (context) {
|
|
26
26
|
ok("Context loaded");
|
|
27
27
|
} else {
|
|
@@ -34,6 +34,7 @@ export async function platformDev(): Promise<void> {
|
|
|
34
34
|
port,
|
|
35
35
|
manifest,
|
|
36
36
|
context,
|
|
37
|
+
moduleAccess,
|
|
37
38
|
dbPath: join(ws.rootDir, ".arc", "data", "dev.db"),
|
|
38
39
|
devMode: true,
|
|
39
40
|
});
|
|
@@ -26,7 +26,7 @@ export async function platformStart(): Promise<void> {
|
|
|
26
26
|
|
|
27
27
|
// Load server context
|
|
28
28
|
log("Loading server context...");
|
|
29
|
-
const context = await loadServerContext(ws.packages);
|
|
29
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
30
30
|
if (context) {
|
|
31
31
|
ok("Context loaded");
|
|
32
32
|
} else {
|
|
@@ -39,6 +39,7 @@ export async function platformStart(): Promise<void> {
|
|
|
39
39
|
port,
|
|
40
40
|
manifest,
|
|
41
41
|
context,
|
|
42
|
+
moduleAccess,
|
|
42
43
|
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
43
44
|
devMode: false,
|
|
44
45
|
});
|
package/src/i18n/catalog.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CatalogEntry {
|
|
|
18
18
|
locations: string[];
|
|
19
19
|
hash: string;
|
|
20
20
|
obsolete: boolean;
|
|
21
|
+
manual?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/** Short hash from msgid for change tracking */
|
|
@@ -37,6 +38,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
37
38
|
let msgid = "";
|
|
38
39
|
let msgstr = "";
|
|
39
40
|
let obsolete = false;
|
|
41
|
+
let inManual = false;
|
|
40
42
|
|
|
41
43
|
const flush = () => {
|
|
42
44
|
if (msgid) {
|
|
@@ -46,6 +48,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
46
48
|
locations,
|
|
47
49
|
hash: hash || hashMsgid(msgid),
|
|
48
50
|
obsolete,
|
|
51
|
+
...(inManual ? { manual: true } : {}),
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
locations = [];
|
|
@@ -58,6 +61,17 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
58
61
|
for (const line of lines) {
|
|
59
62
|
const trimmed = line.trim();
|
|
60
63
|
|
|
64
|
+
// Manual section markers
|
|
65
|
+
if (trimmed === "# manual") {
|
|
66
|
+
inManual = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (trimmed === "# end manual") {
|
|
70
|
+
if (msgid) flush();
|
|
71
|
+
inManual = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
if (trimmed === "" || trimmed.startsWith("#,")) {
|
|
62
76
|
// Empty line or flags — boundary between entries
|
|
63
77
|
if (msgid) flush();
|
|
@@ -110,10 +124,23 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
110
124
|
export function writePo(entries: CatalogEntry[]): string {
|
|
111
125
|
const lines: string[] = [];
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
const active = entries.filter((e) => !e.obsolete);
|
|
127
|
+
const manual = entries.filter((e) => e.manual);
|
|
128
|
+
const active = entries.filter((e) => !e.obsolete && !e.manual);
|
|
115
129
|
const obsolete = entries.filter((e) => e.obsolete);
|
|
116
130
|
|
|
131
|
+
// Manual section
|
|
132
|
+
if (manual.length > 0) {
|
|
133
|
+
lines.push("# manual");
|
|
134
|
+
for (const entry of manual) {
|
|
135
|
+
lines.push(`msgid ${quoteString(entry.msgid)}`);
|
|
136
|
+
lines.push(`msgstr ${quoteString(entry.msgstr)}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
lines.push("# end manual");
|
|
140
|
+
lines.push("");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extracted entries
|
|
117
144
|
for (const entry of active) {
|
|
118
145
|
for (const loc of entry.locations) {
|
|
119
146
|
lines.push(`#: ${loc}`);
|
|
@@ -124,6 +151,7 @@ export function writePo(entries: CatalogEntry[]): string {
|
|
|
124
151
|
lines.push("");
|
|
125
152
|
}
|
|
126
153
|
|
|
154
|
+
// Obsolete entries
|
|
127
155
|
if (obsolete.length > 0) {
|
|
128
156
|
lines.push("# Obsolete entries");
|
|
129
157
|
lines.push("");
|
|
@@ -152,12 +180,18 @@ export function mergeCatalog(
|
|
|
152
180
|
existingMap.set(entry.msgid, entry);
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
// Preserve manual entries as-is
|
|
184
|
+
const manualEntries = existing.filter((e) => e.manual);
|
|
185
|
+
const manualIds = new Set(manualEntries.map((e) => e.msgid));
|
|
186
|
+
|
|
155
187
|
const result: CatalogEntry[] = [];
|
|
156
188
|
const seen = new Set<string>();
|
|
157
189
|
|
|
158
|
-
// Process all extracted messages
|
|
190
|
+
// Process all extracted messages (skip those in manual section)
|
|
159
191
|
for (const [msgid, locations] of extracted) {
|
|
160
192
|
seen.add(msgid);
|
|
193
|
+
if (manualIds.has(msgid)) continue;
|
|
194
|
+
|
|
161
195
|
const prev = existingMap.get(msgid);
|
|
162
196
|
|
|
163
197
|
result.push({
|
|
@@ -171,7 +205,7 @@ export function mergeCatalog(
|
|
|
171
205
|
|
|
172
206
|
// Mark removed entries as obsolete (keep their translations)
|
|
173
207
|
for (const entry of existing) {
|
|
174
|
-
if (!seen.has(entry.msgid) && !entry.obsolete && entry.msgstr) {
|
|
208
|
+
if (!seen.has(entry.msgid) && !entry.obsolete && !entry.manual && entry.msgstr) {
|
|
175
209
|
result.push({
|
|
176
210
|
...entry,
|
|
177
211
|
obsolete: true,
|
|
@@ -180,7 +214,11 @@ export function mergeCatalog(
|
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
|
|
183
|
-
|
|
217
|
+
// Sort extracted entries alphabetically for deterministic output
|
|
218
|
+
result.sort((a, b) => a.msgid.localeCompare(b.msgid));
|
|
219
|
+
|
|
220
|
+
// Manual entries first, then sorted extracted, then obsolete
|
|
221
|
+
return [...manualEntries, ...result];
|
|
184
222
|
}
|
|
185
223
|
|
|
186
224
|
function extractQuoted(s: string): string {
|
package/src/i18n/compile.ts
CHANGED
|
@@ -18,7 +18,12 @@ export function compileCatalog(poPath: string): Record<string, string> {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Sort keys for deterministic JSON output
|
|
22
|
+
const sorted: Record<string, string> = {};
|
|
23
|
+
for (const key of Object.keys(result).sort()) {
|
|
24
|
+
sorted[key] = result[key];
|
|
25
|
+
}
|
|
26
|
+
return sorted;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
package/src/platform/server.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { existsSync, mkdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { readTranslationsConfig } from "../i18n";
|
|
14
|
-
import type { BuildManifest, WorkspaceInfo } from "./shared";
|
|
14
|
+
import type { BuildManifest, ModuleEntry, WorkspaceInfo } from "./shared";
|
|
15
|
+
import type { ModuleAccess } from "@arcote.tech/platform";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Types
|
|
@@ -23,6 +24,8 @@ export interface PlatformServerOptions {
|
|
|
23
24
|
manifest: BuildManifest;
|
|
24
25
|
/** Server context (from loadServerContext). If null, static-only mode. */
|
|
25
26
|
context?: any;
|
|
27
|
+
/** Module access rules (from registry after loadServerContext). */
|
|
28
|
+
moduleAccess?: Map<string, ModuleAccess>;
|
|
26
29
|
/** Path to SQLite database file */
|
|
27
30
|
dbPath?: string;
|
|
28
31
|
/** If true, enables SSE reload stream + mutable manifest (dev mode) */
|
|
@@ -144,20 +147,96 @@ function serveFile(
|
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Module access — signed URLs
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
const MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? crypto.randomUUID();
|
|
155
|
+
const MODULE_SIG_TTL = 3600; // 1 hour
|
|
156
|
+
|
|
157
|
+
function signModuleUrl(filename: string): string {
|
|
158
|
+
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
159
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
160
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
161
|
+
const sig = hasher.digest("hex").slice(0, 16);
|
|
162
|
+
return `/modules/${filename}?sig=${sig}&exp=${exp}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function verifyModuleSignature(filename: string, sig: string | null, exp: string | null): boolean {
|
|
166
|
+
if (!sig || !exp) return false;
|
|
167
|
+
if (Number(exp) < Date.now() / 1000) return false;
|
|
168
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
169
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
170
|
+
return hasher.digest("hex").slice(0, 16) === sig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function filterManifestForToken(
|
|
174
|
+
manifest: BuildManifest,
|
|
175
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
176
|
+
tokenPayload: any,
|
|
177
|
+
): Promise<BuildManifest> {
|
|
178
|
+
const filtered: ModuleEntry[] = [];
|
|
179
|
+
|
|
180
|
+
for (const mod of manifest.modules) {
|
|
181
|
+
const access = moduleAccessMap.get(mod.name);
|
|
182
|
+
|
|
183
|
+
if (!access) {
|
|
184
|
+
// Public module — always include
|
|
185
|
+
filtered.push(mod);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Protected module — check if token grants access
|
|
190
|
+
if (!tokenPayload) continue;
|
|
191
|
+
|
|
192
|
+
let granted = false;
|
|
193
|
+
for (const rule of access.rules) {
|
|
194
|
+
if (tokenPayload.tokenType === rule.token.name) {
|
|
195
|
+
granted = rule.check ? await rule.check(tokenPayload) : true;
|
|
196
|
+
if (granted) break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (granted) {
|
|
201
|
+
filtered.push({ ...mod, url: signModuleUrl(mod.file) } as any);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { modules: filtered, buildTime: manifest.buildTime };
|
|
206
|
+
}
|
|
207
|
+
|
|
147
208
|
// ---------------------------------------------------------------------------
|
|
148
209
|
// Platform-specific HTTP handlers
|
|
149
210
|
// ---------------------------------------------------------------------------
|
|
150
211
|
|
|
151
|
-
function staticFilesHandler(
|
|
212
|
+
function staticFilesHandler(
|
|
213
|
+
ws: WorkspaceInfo,
|
|
214
|
+
devMode: boolean,
|
|
215
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
216
|
+
): ArcHttpHandler {
|
|
152
217
|
return (_req, url, ctx) => {
|
|
153
218
|
const path = url.pathname;
|
|
154
219
|
if (path.startsWith("/shell/"))
|
|
155
220
|
return serveFile(join(ws.shellDir, path.slice(7)), ctx.corsHeaders);
|
|
156
|
-
if (path.startsWith("/modules/"))
|
|
157
|
-
|
|
221
|
+
if (path.startsWith("/modules/")) {
|
|
222
|
+
const fileWithParams = path.slice(9);
|
|
223
|
+
const filename = fileWithParams.split("?")[0];
|
|
224
|
+
const moduleName = filename.replace(/\.js$/, "");
|
|
225
|
+
|
|
226
|
+
// Check access for protected modules
|
|
227
|
+
if (moduleAccessMap.has(moduleName)) {
|
|
228
|
+
const sig = url.searchParams.get("sig");
|
|
229
|
+
const exp = url.searchParams.get("exp");
|
|
230
|
+
if (!verifyModuleSignature(filename, sig, exp)) {
|
|
231
|
+
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return serveFile(join(ws.modulesDir, filename), {
|
|
158
236
|
...ctx.corsHeaders,
|
|
159
237
|
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
160
238
|
});
|
|
239
|
+
}
|
|
161
240
|
if (path.startsWith("/locales/"))
|
|
162
241
|
return serveFile(join(ws.arcDir, path.slice(1)), ctx.corsHeaders);
|
|
163
242
|
if (path === "/styles.css")
|
|
@@ -185,10 +264,14 @@ function apiEndpointsHandler(
|
|
|
185
264
|
ws: WorkspaceInfo,
|
|
186
265
|
getManifest: () => BuildManifest,
|
|
187
266
|
cm: ConnectionManager | null,
|
|
267
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
188
268
|
): ArcHttpHandler {
|
|
189
269
|
return (_req, url, ctx) => {
|
|
190
|
-
if (url.pathname === "/api/modules")
|
|
191
|
-
|
|
270
|
+
if (url.pathname === "/api/modules") {
|
|
271
|
+
// Filter manifest based on token — protected modules only for authorized users
|
|
272
|
+
return filterManifestForToken(getManifest(), moduleAccessMap, ctx.tokenPayload)
|
|
273
|
+
.then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
|
|
274
|
+
}
|
|
192
275
|
|
|
193
276
|
if (url.pathname === "/api/translations") {
|
|
194
277
|
const config = readTranslationsConfig(ws.rootDir);
|
|
@@ -255,6 +338,7 @@ export async function startPlatformServer(
|
|
|
255
338
|
opts: PlatformServerOptions,
|
|
256
339
|
): Promise<PlatformServer> {
|
|
257
340
|
const { ws, port, devMode, context } = opts;
|
|
341
|
+
const moduleAccessMap = opts.moduleAccess ?? new Map();
|
|
258
342
|
let manifest = opts.manifest;
|
|
259
343
|
const getManifest = () => manifest;
|
|
260
344
|
|
|
@@ -285,9 +369,9 @@ export async function startPlatformServer(
|
|
|
285
369
|
|
|
286
370
|
// Platform handlers only
|
|
287
371
|
const handlers: ArcHttpHandler[] = [
|
|
288
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
372
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
289
373
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
290
|
-
staticFilesHandler(ws, !!devMode),
|
|
374
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
291
375
|
spaFallbackHandler(shellHtml),
|
|
292
376
|
];
|
|
293
377
|
|
|
@@ -331,9 +415,9 @@ export async function startPlatformServer(
|
|
|
331
415
|
port,
|
|
332
416
|
httpHandlers: [
|
|
333
417
|
// Platform-specific handlers (checked AFTER arc handlers)
|
|
334
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
418
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
335
419
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
336
|
-
staticFilesHandler(ws, !!devMode),
|
|
420
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
337
421
|
spaFallbackHandler(shellHtml),
|
|
338
422
|
],
|
|
339
423
|
onWsClose: (clientId) => cleanupClientSubs(clientId),
|
package/src/platform/shared.ts
CHANGED
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
discoverPackages,
|
|
8
8
|
isContextPackage,
|
|
9
9
|
type BuildManifest,
|
|
10
|
+
type ModuleEntry,
|
|
10
11
|
type WorkspacePackage,
|
|
11
12
|
} from "../builder/module-builder";
|
|
12
13
|
|
|
13
14
|
// Re-export for convenience
|
|
14
15
|
export { buildPackages, buildStyles, isContextPackage };
|
|
15
|
-
export type { BuildManifest, WorkspacePackage };
|
|
16
|
+
export type { BuildManifest, ModuleEntry, WorkspacePackage };
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Logging
|
|
@@ -212,36 +213,42 @@ export const { createPortal, flushSync } = ReactDOM;`,
|
|
|
212
213
|
["platform", "@arcote.tech/platform"],
|
|
213
214
|
];
|
|
214
215
|
|
|
215
|
-
const
|
|
216
|
+
const baseExternal = [
|
|
217
|
+
"react",
|
|
218
|
+
"react-dom",
|
|
219
|
+
"react/jsx-runtime",
|
|
220
|
+
"react/jsx-dev-runtime",
|
|
221
|
+
"react-dom/client",
|
|
222
|
+
];
|
|
223
|
+
const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
|
|
224
|
+
|
|
225
|
+
// Build each arc entry separately so it can import sibling arc packages
|
|
226
|
+
// as externals (resolved via import map) without circular self-reference.
|
|
216
227
|
for (const [name, pkg] of arcEntries) {
|
|
217
228
|
const f = join(tmpDir, `${name}.ts`);
|
|
218
229
|
Bun.write(f, `export * from "${pkg}";\n`);
|
|
219
|
-
arcEps.push(f);
|
|
220
|
-
}
|
|
221
230
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
for (const l of r2.logs) console.error(l);
|
|
244
|
-
throw new Error("Shell Arc build failed");
|
|
231
|
+
const r2 = await Bun.build({
|
|
232
|
+
entrypoints: [f],
|
|
233
|
+
outdir: outDir,
|
|
234
|
+
format: "esm",
|
|
235
|
+
target: "browser",
|
|
236
|
+
naming: "[name].[ext]",
|
|
237
|
+
external: [
|
|
238
|
+
...baseExternal,
|
|
239
|
+
// Other arc packages are external (not self)
|
|
240
|
+
...allArcPkgs.filter((p) => p !== pkg),
|
|
241
|
+
],
|
|
242
|
+
define: {
|
|
243
|
+
ONLY_SERVER: "false",
|
|
244
|
+
ONLY_BROWSER: "true",
|
|
245
|
+
ONLY_CLIENT: "true",
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
if (!r2.success) {
|
|
249
|
+
for (const l of r2.logs) console.error(l);
|
|
250
|
+
throw new Error(`Shell build failed for ${pkg}`);
|
|
251
|
+
}
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
// Clean tmp
|
|
@@ -255,9 +262,9 @@ export const { createPortal, flushSync } = ReactDOM;`,
|
|
|
255
262
|
|
|
256
263
|
export async function loadServerContext(
|
|
257
264
|
packages: WorkspacePackage[],
|
|
258
|
-
): Promise<any | null> {
|
|
265
|
+
): Promise<{ context: any | null; moduleAccess: Map<string, any> }> {
|
|
259
266
|
const ctxPackages = packages.filter((p) => isContextPackage(p.packageJson));
|
|
260
|
-
if (ctxPackages.length === 0) return null;
|
|
267
|
+
if (ctxPackages.length === 0) return { context: null, moduleAccess: new Map() };
|
|
261
268
|
|
|
262
269
|
// Set globals for server context — framework packages (arc-auth etc.)
|
|
263
270
|
// use these at runtime to tree-shake browser/server code paths.
|
|
@@ -278,6 +285,7 @@ export async function loadServerContext(
|
|
|
278
285
|
// Pre-import platform so it's cached with this absolute path
|
|
279
286
|
await import(platformEntry);
|
|
280
287
|
|
|
288
|
+
// Import context packages from server dist (has server-only code paths)
|
|
281
289
|
for (const ctx of ctxPackages) {
|
|
282
290
|
const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
|
|
283
291
|
if (!existsSync(serverDist)) {
|
|
@@ -292,6 +300,20 @@ export async function loadServerContext(
|
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
// Import non-context packages from source to capture module().protectedBy() metadata
|
|
304
|
+
const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
|
|
305
|
+
for (const pkg of nonCtxPackages) {
|
|
306
|
+
try {
|
|
307
|
+
await import(pkg.entrypoint);
|
|
308
|
+
} catch {
|
|
309
|
+
// Non-context packages may fail on server (React components etc.) — that's OK,
|
|
310
|
+
// module().protectedBy().build() runs synchronously before any rendering
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { getContext, getAllModuleAccess } = await import(platformEntry);
|
|
315
|
+
return {
|
|
316
|
+
context: getContext() ?? null,
|
|
317
|
+
moduleAccess: getAllModuleAccess(),
|
|
318
|
+
};
|
|
297
319
|
}
|