@vellumai/assistant 0.4.37 → 0.4.41
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/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
1
5
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
6
|
|
|
7
|
+
import JSZip from "jszip";
|
|
8
|
+
|
|
3
9
|
// Mock the logger before importing the module under test
|
|
4
10
|
mock.module("../util/logger.js", () => ({
|
|
5
11
|
getLogger: () =>
|
|
@@ -8,10 +14,33 @@ mock.module("../util/logger.js", () => ({
|
|
|
8
14
|
}),
|
|
9
15
|
}));
|
|
10
16
|
|
|
17
|
+
// Temp directory for fake app data used in packageApp tests
|
|
18
|
+
const testAppsDir = join(tmpdir(), `app-bundler-test-${Date.now()}`);
|
|
19
|
+
|
|
20
|
+
// Mock app-store so packageApp can find our test apps
|
|
21
|
+
const mockApps = new Map<string, Record<string, unknown>>();
|
|
22
|
+
mock.module("../memory/app-store.js", () => ({
|
|
23
|
+
getApp: (id: string) => mockApps.get(id) ?? null,
|
|
24
|
+
getAppsDir: () => testAppsDir,
|
|
25
|
+
isMultifileApp: (app: Record<string, unknown>) => app.formatVersion === 2,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock content-id to avoid pulling in crypto internals
|
|
29
|
+
mock.module("../util/content-id.js", () => ({
|
|
30
|
+
computeContentId: () => "abcd1234abcd1234",
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Mock bundle-signer (not exercised in these tests)
|
|
34
|
+
mock.module("./bundle-signer.js", () => ({
|
|
35
|
+
signBundle: async () => ({}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
11
38
|
import {
|
|
12
39
|
extractRemoteUrls,
|
|
13
40
|
materializeAssets,
|
|
41
|
+
packageApp,
|
|
14
42
|
} from "../bundler/app-bundler.js";
|
|
43
|
+
import type { AppManifest } from "../bundler/manifest.js";
|
|
15
44
|
|
|
16
45
|
// ---------------------------------------------------------------------------
|
|
17
46
|
// extractRemoteUrls
|
|
@@ -323,3 +352,183 @@ describe("materializeAssets", () => {
|
|
|
323
352
|
expect(result.rewrittenHtml).not.toContain(cssUrl);
|
|
324
353
|
});
|
|
325
354
|
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// packageApp — multifile apps
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
describe("packageApp", () => {
|
|
361
|
+
afterEach(() => {
|
|
362
|
+
mockApps.clear();
|
|
363
|
+
try {
|
|
364
|
+
rmSync(testAppsDir, { recursive: true, force: true });
|
|
365
|
+
} catch {
|
|
366
|
+
// ignore cleanup errors
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Helper: set up a fake multifile app on disk with src/ and dist/ dirs
|
|
372
|
+
* so that compileApp (which we mock below) can pretend to succeed.
|
|
373
|
+
*/
|
|
374
|
+
function setupMultifileApp(appId: string, opts?: { withCss?: boolean }) {
|
|
375
|
+
const appDir = join(testAppsDir, appId);
|
|
376
|
+
const srcDir = join(appDir, "src");
|
|
377
|
+
mkdirSync(srcDir, { recursive: true });
|
|
378
|
+
|
|
379
|
+
// Write minimal src/ so the app directory looks valid
|
|
380
|
+
if (opts?.withCss) {
|
|
381
|
+
writeFileSync(join(srcDir, "styles.css"), "body { margin: 0; }");
|
|
382
|
+
writeFileSync(
|
|
383
|
+
join(srcDir, "main.tsx"),
|
|
384
|
+
'import "./styles.css";\nexport default () => "hi";',
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
writeFileSync(join(srcDir, "main.tsx"), 'export default () => "hi";');
|
|
388
|
+
}
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(srcDir, "index.html"),
|
|
391
|
+
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Write the app JSON (getApp reads from {appsDir}/{id}.json)
|
|
395
|
+
const appDef = {
|
|
396
|
+
id: appId,
|
|
397
|
+
name: "Test Multifile App",
|
|
398
|
+
description: "A test app",
|
|
399
|
+
schemaJson: "{}",
|
|
400
|
+
htmlDefinition: "<unused>",
|
|
401
|
+
createdAt: Date.now(),
|
|
402
|
+
updatedAt: Date.now(),
|
|
403
|
+
formatVersion: 2,
|
|
404
|
+
};
|
|
405
|
+
writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
|
|
406
|
+
mockApps.set(appId, appDef);
|
|
407
|
+
|
|
408
|
+
return appDef;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function setupLegacyApp(appId: string) {
|
|
412
|
+
const appDir = join(testAppsDir, appId);
|
|
413
|
+
mkdirSync(appDir, { recursive: true });
|
|
414
|
+
|
|
415
|
+
const appDef = {
|
|
416
|
+
id: appId,
|
|
417
|
+
name: "Test Legacy App",
|
|
418
|
+
schemaJson: "{}",
|
|
419
|
+
htmlDefinition: "<html><body>Hello legacy</body></html>",
|
|
420
|
+
createdAt: Date.now(),
|
|
421
|
+
updatedAt: Date.now(),
|
|
422
|
+
};
|
|
423
|
+
writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
|
|
424
|
+
mockApps.set(appId, appDef);
|
|
425
|
+
return appDef;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
test("packages a multifile app with compiled dist/ files in the zip", async () => {
|
|
429
|
+
const appId = "multi-test-1";
|
|
430
|
+
setupMultifileApp(appId, { withCss: true });
|
|
431
|
+
|
|
432
|
+
const result = await packageApp(appId);
|
|
433
|
+
const zipData = await readFile(result.bundlePath);
|
|
434
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
435
|
+
|
|
436
|
+
// Verify compiled files are present
|
|
437
|
+
expect(zip.file("index.html")).not.toBeNull();
|
|
438
|
+
expect(zip.file("main.js")).not.toBeNull();
|
|
439
|
+
expect(zip.file("main.css")).not.toBeNull();
|
|
440
|
+
expect(zip.file("manifest.json")).not.toBeNull();
|
|
441
|
+
|
|
442
|
+
// Verify content matches what we wrote to dist/
|
|
443
|
+
const indexContent = await zip.file("index.html")!.async("string");
|
|
444
|
+
expect(indexContent).toContain('src="main.js"');
|
|
445
|
+
|
|
446
|
+
// main.js should contain compiled output (esbuild minifies the source)
|
|
447
|
+
const jsContent = await zip.file("main.js")!.async("string");
|
|
448
|
+
expect(jsContent.length).toBeGreaterThan(0);
|
|
449
|
+
|
|
450
|
+
// CSS was imported, so main.css should be present
|
|
451
|
+
const cssContent = await zip.file("main.css")!.async("string");
|
|
452
|
+
expect(cssContent).toContain("margin");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("sets format_version to 2 in manifest for multifile apps", async () => {
|
|
456
|
+
const appId = "multi-test-2";
|
|
457
|
+
setupMultifileApp(appId);
|
|
458
|
+
|
|
459
|
+
const result = await packageApp(appId);
|
|
460
|
+
const zipData = await readFile(result.bundlePath);
|
|
461
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
462
|
+
|
|
463
|
+
const manifestJson = await zip.file("manifest.json")!.async("string");
|
|
464
|
+
const manifest: AppManifest = JSON.parse(manifestJson);
|
|
465
|
+
|
|
466
|
+
expect(manifest.format_version).toBe(2);
|
|
467
|
+
expect(manifest.entry).toBe("index.html");
|
|
468
|
+
expect(manifest.name).toBe("Test Multifile App");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("compile failure produces a clear error", async () => {
|
|
472
|
+
const appId = "multi-fail-1";
|
|
473
|
+
const appDir = join(testAppsDir, appId);
|
|
474
|
+
const srcDir = join(appDir, "src");
|
|
475
|
+
mkdirSync(srcDir, { recursive: true });
|
|
476
|
+
|
|
477
|
+
// Write intentionally broken source so esbuild fails
|
|
478
|
+
writeFileSync(join(srcDir, "main.tsx"), "const x: number = {{{BROKEN");
|
|
479
|
+
writeFileSync(
|
|
480
|
+
join(srcDir, "index.html"),
|
|
481
|
+
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const appDef = {
|
|
485
|
+
id: appId,
|
|
486
|
+
name: "Broken App",
|
|
487
|
+
schemaJson: "{}",
|
|
488
|
+
htmlDefinition: "<unused>",
|
|
489
|
+
createdAt: Date.now(),
|
|
490
|
+
updatedAt: Date.now(),
|
|
491
|
+
formatVersion: 2,
|
|
492
|
+
};
|
|
493
|
+
writeFileSync(join(testAppsDir, `${appId}.json`), JSON.stringify(appDef));
|
|
494
|
+
mockApps.set(appId, appDef);
|
|
495
|
+
|
|
496
|
+
await expect(packageApp(appId)).rejects.toThrow(
|
|
497
|
+
/Compilation failed for app "Broken App"/,
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("legacy app packaging remains unchanged (format_version 1)", async () => {
|
|
502
|
+
const appId = "legacy-test-1";
|
|
503
|
+
setupLegacyApp(appId);
|
|
504
|
+
|
|
505
|
+
const result = await packageApp(appId);
|
|
506
|
+
const zipData = await readFile(result.bundlePath);
|
|
507
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
508
|
+
|
|
509
|
+
const manifestJson = await zip.file("manifest.json")!.async("string");
|
|
510
|
+
const manifest: AppManifest = JSON.parse(manifestJson);
|
|
511
|
+
|
|
512
|
+
expect(manifest.format_version).toBe(1);
|
|
513
|
+
|
|
514
|
+
// Legacy app should have index.html with the original content
|
|
515
|
+
const indexContent = await zip.file("index.html")!.async("string");
|
|
516
|
+
expect(indexContent).toContain("Hello legacy");
|
|
517
|
+
|
|
518
|
+
// Should NOT have main.js (not a compiled app)
|
|
519
|
+
expect(zip.file("main.js")).toBeNull();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("multifile app without CSS omits main.css from zip", async () => {
|
|
523
|
+
const appId = "multi-no-css";
|
|
524
|
+
setupMultifileApp(appId, { withCss: false });
|
|
525
|
+
|
|
526
|
+
const result = await packageApp(appId);
|
|
527
|
+
const zipData = await readFile(result.bundlePath);
|
|
528
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
529
|
+
|
|
530
|
+
expect(zip.file("index.html")).not.toBeNull();
|
|
531
|
+
expect(zip.file("main.js")).not.toBeNull();
|
|
532
|
+
expect(zip.file("main.css")).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import { compileApp } from "../bundler/app-compiler.js";
|
|
8
|
+
import {
|
|
9
|
+
ALLOWED_PACKAGES,
|
|
10
|
+
getCacheDir,
|
|
11
|
+
isBareImport,
|
|
12
|
+
packageName,
|
|
13
|
+
resolvePackage,
|
|
14
|
+
} from "../bundler/package-resolver.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Shared temp directory
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
tempDir = await mkdtemp(join(tmpdir(), "app-compiler-test-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Scaffold a minimal app directory with src/main.tsx and src/index.html. */
|
|
31
|
+
async function scaffold(
|
|
32
|
+
name: string,
|
|
33
|
+
files: Record<string, string>,
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const appDir = join(tempDir, name);
|
|
36
|
+
const srcDir = join(appDir, "src");
|
|
37
|
+
await mkdir(srcDir, { recursive: true });
|
|
38
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
39
|
+
const filePath = join(srcDir, filename);
|
|
40
|
+
await writeFile(filePath, content);
|
|
41
|
+
}
|
|
42
|
+
return appDir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MINIMAL_HTML = `<!DOCTYPE html>
|
|
46
|
+
<html>
|
|
47
|
+
<head><title>Test</title></head>
|
|
48
|
+
<body>
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Tests
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe("compileApp", () => {
|
|
57
|
+
test("compiles minimal TSX app and produces dist/main.js + dist/index.html", async () => {
|
|
58
|
+
const appDir = await scaffold("basic", {
|
|
59
|
+
"main.tsx": `const App = () => { const el = document.createElement("div"); el.textContent = "hello"; return el; }; App();`,
|
|
60
|
+
"index.html": MINIMAL_HTML,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await compileApp(appDir);
|
|
64
|
+
|
|
65
|
+
expect(result.ok).toBe(true);
|
|
66
|
+
expect(result.errors).toHaveLength(0);
|
|
67
|
+
expect(result.durationMs).toBeLessThan(5000);
|
|
68
|
+
|
|
69
|
+
// dist/main.js should exist and contain bundled code
|
|
70
|
+
const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
|
|
71
|
+
expect(js.length).toBeGreaterThan(0);
|
|
72
|
+
|
|
73
|
+
// dist/index.html should exist and have the script tag injected
|
|
74
|
+
const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
|
|
75
|
+
expect(html).toContain('src="main.js"');
|
|
76
|
+
expect(html).toContain('type="module"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("compiles preact JSX correctly", async () => {
|
|
80
|
+
const appDir = await scaffold("preact-jsx", {
|
|
81
|
+
"main.tsx": `import { render } from "preact";
|
|
82
|
+
const App = () => <div>Hello</div>;
|
|
83
|
+
render(<App />, document.body);`,
|
|
84
|
+
"index.html": MINIMAL_HTML,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await compileApp(appDir);
|
|
88
|
+
|
|
89
|
+
expect(result.ok).toBe(true);
|
|
90
|
+
expect(result.errors).toHaveLength(0);
|
|
91
|
+
|
|
92
|
+
const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
|
|
93
|
+
// The output should contain preact's runtime code (bundled)
|
|
94
|
+
expect(js.length).toBeGreaterThan(100);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("strips TypeScript types", async () => {
|
|
98
|
+
const appDir = await scaffold("ts-types", {
|
|
99
|
+
"main.tsx": `interface Greeting { name: string; }
|
|
100
|
+
const greet = (g: Greeting): string => g.name;
|
|
101
|
+
console.log(greet({ name: "world" }));`,
|
|
102
|
+
"index.html": MINIMAL_HTML,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await compileApp(appDir);
|
|
106
|
+
|
|
107
|
+
expect(result.ok).toBe(true);
|
|
108
|
+
|
|
109
|
+
const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
|
|
110
|
+
// Interface declarations should be stripped
|
|
111
|
+
expect(js).not.toContain("interface");
|
|
112
|
+
expect(js).toContain("world");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("CSS imports produce dist/main.css and inject link tag", async () => {
|
|
116
|
+
const appDir = await scaffold("css-import", {
|
|
117
|
+
"main.tsx": `import "./style.css";
|
|
118
|
+
console.log("styled");`,
|
|
119
|
+
"style.css": `body { background: red; }`,
|
|
120
|
+
"index.html": MINIMAL_HTML,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await compileApp(appDir);
|
|
124
|
+
|
|
125
|
+
expect(result.ok).toBe(true);
|
|
126
|
+
|
|
127
|
+
const css = await readFile(join(appDir, "dist", "main.css"), "utf-8");
|
|
128
|
+
expect(css).toContain("background");
|
|
129
|
+
|
|
130
|
+
const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
|
|
131
|
+
expect(html).toContain('href="main.css"');
|
|
132
|
+
expect(html).toContain("stylesheet");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns ok: false with diagnostics on syntax error", async () => {
|
|
136
|
+
const appDir = await scaffold("syntax-error", {
|
|
137
|
+
"main.tsx": `const x: number = <<<INVALID>>>;`,
|
|
138
|
+
"index.html": MINIMAL_HTML,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await compileApp(appDir);
|
|
142
|
+
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
145
|
+
expect(result.errors[0].text).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("dist/index.html has script tag injected", async () => {
|
|
149
|
+
const appDir = await scaffold("script-injection", {
|
|
150
|
+
"main.tsx": `console.log("hi");`,
|
|
151
|
+
"index.html": `<!DOCTYPE html>
|
|
152
|
+
<html>
|
|
153
|
+
<head><title>Inject Test</title></head>
|
|
154
|
+
<body>
|
|
155
|
+
<div id="app"></div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>`,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = await compileApp(appDir);
|
|
161
|
+
expect(result.ok).toBe(true);
|
|
162
|
+
|
|
163
|
+
const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
|
|
164
|
+
expect(html).toContain('<script type="module" src="main.js"></script>');
|
|
165
|
+
// Original content should be preserved
|
|
166
|
+
expect(html).toContain('<div id="app"></div>');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("does not duplicate script tag if already present", async () => {
|
|
170
|
+
const appDir = await scaffold("no-dup-script", {
|
|
171
|
+
"main.tsx": `console.log("hi");`,
|
|
172
|
+
"index.html": `<!DOCTYPE html>
|
|
173
|
+
<html>
|
|
174
|
+
<head><title>Test</title></head>
|
|
175
|
+
<body>
|
|
176
|
+
<script type="module" src="main.js"></script>
|
|
177
|
+
</body>
|
|
178
|
+
</html>`,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await compileApp(appDir);
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
|
|
184
|
+
const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
|
|
185
|
+
const matches = html.match(/src="main\.js"/g);
|
|
186
|
+
expect(matches).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("disallowed package import produces a clear error", async () => {
|
|
190
|
+
const appDir = await scaffold("disallowed-pkg", {
|
|
191
|
+
"main.tsx": `import leftpad from "left-pad";\nconsole.log(leftpad("hi", 5));`,
|
|
192
|
+
"index.html": MINIMAL_HTML,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await compileApp(appDir);
|
|
196
|
+
|
|
197
|
+
expect(result.ok).toBe(false);
|
|
198
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
199
|
+
expect(result.errors[0].text).toContain("not in the allowed list");
|
|
200
|
+
expect(result.errors[0].text).toContain("left-pad");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("allowed package (zod) compiles successfully", async () => {
|
|
204
|
+
const appDir = await scaffold("allowed-pkg-zod", {
|
|
205
|
+
"main.tsx": `import { z } from "zod";\nconst schema = z.string();\nconsole.log(schema.parse("hello"));`,
|
|
206
|
+
"index.html": MINIMAL_HTML,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const result = await compileApp(appDir);
|
|
210
|
+
|
|
211
|
+
expect(result.ok).toBe(true);
|
|
212
|
+
expect(result.errors).toHaveLength(0);
|
|
213
|
+
|
|
214
|
+
const js = await readFile(join(appDir, "dist", "main.js"), "utf-8");
|
|
215
|
+
expect(js.length).toBeGreaterThan(100);
|
|
216
|
+
}, 30_000);
|
|
217
|
+
|
|
218
|
+
test("allowed package uses shared cache on second build", async () => {
|
|
219
|
+
// First build installs the package
|
|
220
|
+
const appDir1 = await scaffold("cache-test-1", {
|
|
221
|
+
"main.tsx": `import { z } from "zod";\nconsole.log(z.string());`,
|
|
222
|
+
"index.html": MINIMAL_HTML,
|
|
223
|
+
});
|
|
224
|
+
const r1 = await compileApp(appDir1);
|
|
225
|
+
expect(r1.ok).toBe(true);
|
|
226
|
+
|
|
227
|
+
// The cache dir should now have zod
|
|
228
|
+
const cacheDir = getCacheDir();
|
|
229
|
+
expect(existsSync(join(cacheDir, "node_modules", "zod"))).toBe(true);
|
|
230
|
+
|
|
231
|
+
// Second build should reuse the cache (no install needed)
|
|
232
|
+
const appDir2 = await scaffold("cache-test-2", {
|
|
233
|
+
"main.tsx": `import { z } from "zod";\nconst s = z.number();\nconsole.log(s.parse(42));`,
|
|
234
|
+
"index.html": MINIMAL_HTML,
|
|
235
|
+
});
|
|
236
|
+
const r2 = await compileApp(appDir2);
|
|
237
|
+
expect(r2.ok).toBe(true);
|
|
238
|
+
}, 30_000);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Package resolver unit tests
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe("package-resolver", () => {
|
|
246
|
+
test("isBareImport identifies bare specifiers", () => {
|
|
247
|
+
expect(isBareImport("date-fns")).toBe(true);
|
|
248
|
+
expect(isBareImport("zod")).toBe(true);
|
|
249
|
+
expect(isBareImport("@scope/pkg")).toBe(true);
|
|
250
|
+
expect(isBareImport("./local")).toBe(false);
|
|
251
|
+
expect(isBareImport("../parent")).toBe(false);
|
|
252
|
+
expect(isBareImport("/absolute")).toBe(false);
|
|
253
|
+
expect(isBareImport("preact")).toBe(false);
|
|
254
|
+
expect(isBareImport("preact/hooks")).toBe(false);
|
|
255
|
+
expect(isBareImport("react")).toBe(false);
|
|
256
|
+
expect(isBareImport("react-dom")).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("packageName extracts top-level name", () => {
|
|
260
|
+
expect(packageName("date-fns")).toBe("date-fns");
|
|
261
|
+
expect(packageName("date-fns/format")).toBe("date-fns");
|
|
262
|
+
expect(packageName("@scope/pkg")).toBe("@scope/pkg");
|
|
263
|
+
expect(packageName("@scope/pkg/sub")).toBe("@scope/pkg");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("resolvePackage returns null for disallowed packages", async () => {
|
|
267
|
+
const result = await resolvePackage("left-pad");
|
|
268
|
+
expect(result).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("ALLOWED_PACKAGES contains expected entries", () => {
|
|
272
|
+
expect(ALLOWED_PACKAGES).toContain("date-fns");
|
|
273
|
+
expect(ALLOWED_PACKAGES).toContain("chart.js");
|
|
274
|
+
expect(ALLOWED_PACKAGES).toContain("lodash-es");
|
|
275
|
+
expect(ALLOWED_PACKAGES).toContain("zod");
|
|
276
|
+
expect(ALLOWED_PACKAGES).toContain("clsx");
|
|
277
|
+
expect(ALLOWED_PACKAGES).toContain("lucide");
|
|
278
|
+
});
|
|
279
|
+
});
|