@lunora/cli 1.0.0-alpha.6 → 1.0.0-alpha.61
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/__assets__/package-og.svg +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts +238 -115
- package/dist/index.d.ts +238 -115
- package/dist/index.mjs +8 -8
- package/dist/packem_chunks/handler.mjs +87 -6
- package/dist/packem_chunks/handler10.mjs +8 -14
- package/dist/packem_chunks/handler11.mjs +19 -189
- package/dist/packem_chunks/handler12.mjs +176 -115
- package/dist/packem_chunks/handler13.mjs +118 -52
- package/dist/packem_chunks/handler14.mjs +50 -43
- package/dist/packem_chunks/handler15.mjs +48 -67
- package/dist/packem_chunks/handler16.mjs +74 -37
- package/dist/packem_chunks/handler17.mjs +39 -100
- package/dist/packem_chunks/handler18.mjs +87 -152
- package/dist/packem_chunks/handler19.mjs +148 -67
- package/dist/packem_chunks/handler2.mjs +6 -4
- package/dist/packem_chunks/handler20.mjs +77 -75
- package/dist/packem_chunks/handler21.mjs +71 -288
- package/dist/packem_chunks/handler3.mjs +6 -3
- package/dist/packem_chunks/handler4.mjs +2 -2
- package/dist/packem_chunks/handler5.mjs +7 -4
- package/dist/packem_chunks/handler6.mjs +10 -7
- package/dist/packem_chunks/handler7.mjs +2 -2
- package/dist/packem_chunks/handler8.mjs +1 -1
- package/dist/packem_chunks/handler9.mjs +315 -12
- package/dist/packem_chunks/planDevCommand.mjs +561 -32
- package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
- package/dist/packem_chunks/runDeployCommand.mjs +115 -20
- package/dist/packem_chunks/runInitCommand.mjs +1601 -172
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
- package/dist/packem_chunks/runResetCommand.mjs +2 -2
- package/dist/packem_chunks/runRpcCommand.mjs +3 -2
- package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-0BD3K1pb.mjs} +79 -21
- package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
- package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
- package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
- package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
- package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-vnRRkhNE.mjs} +123 -15
- package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
- package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
- package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-Cxo6Tpyx.mjs} +10 -1
- package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
- package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
- package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
- package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
- package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
- package/dist/packem_shared/storage-bGfgwOgF.mjs +84 -0
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
- package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +47 -0
- package/package.json +9 -7
- package/skills/lunora-quickstart/SKILL.md +25 -5
- package/skills/lunora-setup-storage/SKILL.md +7 -3
- package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
- package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
- package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, mkdtempSync,
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, mkdtempSync, lstatSync, cpSync, renameSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import { detectFramework as detectFramework$1, isInteractive } from '@lunora/config';
|
|
3
|
+
import { detectFramework as detectFramework$1, isInteractive, LUNA_ART, LUNA_NAME, LUNA_SIGNOFF, paintAnswer, BADGES } from '@lunora/config';
|
|
4
4
|
import { walkSync } from '@visulima/fs';
|
|
5
5
|
import { join as join$1, dirname as dirname$1, basename, resolve, relative } from '@visulima/path';
|
|
6
6
|
import { downloadTemplate } from 'giget';
|
|
7
7
|
import { modify, applyEdits } from 'jsonc-parser';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
|
-
import { d as defineHandler } from '../packem_shared/command-
|
|
10
|
-
import { a as detectInstalledManagers, i as installArgsFor } from '../packem_shared/detect-package-manager-
|
|
9
|
+
import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
|
|
10
|
+
import { d as detectPackageManager, a as detectInstalledManagers, i as installArgsFor, r as runScriptCommand } from '../packem_shared/detect-package-manager-Cxo6Tpyx.mjs';
|
|
11
11
|
import MagicString from 'magic-string';
|
|
12
12
|
import { Project, SyntaxKind } from 'ts-morph';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { P as PromptCancelledError } from '../packem_shared/prompt-cancelled-APzX1Im-.mjs';
|
|
14
|
+
import { c as resolveTagVersions, d as resolveSourceRef, e as resolvePinnedSourceRef, f as resolveDistTag, r as runAddCommand } from '../packem_shared/commands-vnRRkhNE.mjs';
|
|
15
|
+
import { defaultSpawner } from '../packem_shared/createRecordingSpawner-Cw5Iu73G.mjs';
|
|
16
|
+
import { d as tuiMascot, e as tuiStep, f as tuiMoonrise, t as tuiText, g as tuiHeadline, h as tuiInfo, a as tuiSelect, b as tuiConfirm, w as withTuiSpinner, i as tuiNextSteps, j as tuiTasks, k as tuiMultiSelect, l as withTuiBadgeProgress } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
|
|
17
|
+
import { logStep } from '../packem_shared/createLogger-CIWSHrTL.mjs';
|
|
18
|
+
import { E as EMAIL_ITEM, p as promptBucketName, w as withStorageBucketName, M as MAIL_DESTINATION_PROMPT, r as resolveTypedDestination, f as withMailDestination, e as promptAuthProvider, c as promptDatabaseName, g as withAuthDatabaseName } from '../packem_shared/storage-bGfgwOgF.mjs';
|
|
19
|
+
import dns from 'node:dns/promises';
|
|
17
20
|
|
|
18
21
|
const GITHUB_CONTENT = `name: Deploy
|
|
19
22
|
|
|
@@ -285,6 +288,60 @@ const patchViteConfig = (source) => {
|
|
|
285
288
|
return { changed: true, code: ms.toString() };
|
|
286
289
|
};
|
|
287
290
|
|
|
291
|
+
const NETWORK_ERROR_PATTERN = /enotfound|eai_again|econnrefused|etimedout|network|fetch failed|getaddrinfo/u;
|
|
292
|
+
const NOT_FOUND_ERROR_PATTERN = /404|not found|could not find|no such/u;
|
|
293
|
+
const describeDownloadFailure = (error, context) => {
|
|
294
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
295
|
+
const lower = raw.toLowerCase();
|
|
296
|
+
const genericMessage = `failed to download template "${context.templateType}" from ${context.remote}: ${raw}`;
|
|
297
|
+
if (NOT_FOUND_ERROR_PATTERN.test(lower)) {
|
|
298
|
+
return {
|
|
299
|
+
hints: [
|
|
300
|
+
`Check the template name "${context.templateType}" and the ref "${context.ref}".`,
|
|
301
|
+
"List/inspect available templates, or target a branch/tag with `--ref <branch>`."
|
|
302
|
+
],
|
|
303
|
+
message: `template "${context.templateType}" not found at ${context.remote}: ${raw}`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (NETWORK_ERROR_PATTERN.test(lower)) {
|
|
307
|
+
return {
|
|
308
|
+
hints: [
|
|
309
|
+
"You appear to be offline or unable to reach GitHub.",
|
|
310
|
+
"To scaffold without a network, point at a local template root: `lunora init --from <dir>`."
|
|
311
|
+
],
|
|
312
|
+
message: genericMessage
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
hints: ["If this is a network/offline issue, scaffold from a local root with `lunora init --from <dir>`."],
|
|
317
|
+
message: genericMessage
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const emitStep = async (type, message, answer) => {
|
|
322
|
+
if (isInteractive()) {
|
|
323
|
+
await tuiStep(BADGES[type], message, answer);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
process.stdout.write("\n");
|
|
327
|
+
if (answer === void 0 || answer === "") {
|
|
328
|
+
logStep(type, message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const dimmed = answer.split("\n").map((line) => paintAnswer(line)).join("\n");
|
|
332
|
+
logStep(type, `${message}
|
|
333
|
+
${dimmed}`);
|
|
334
|
+
};
|
|
335
|
+
const emitMascot = async (logger) => {
|
|
336
|
+
if (isInteractive()) {
|
|
337
|
+
await tuiMascot();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
logger.info(`
|
|
341
|
+
${LUNA_ART}
|
|
342
|
+
${LUNA_NAME}: ${LUNA_SIGNOFF}`);
|
|
343
|
+
};
|
|
344
|
+
|
|
288
345
|
const STACK_FEATURE_OPTIONS = [
|
|
289
346
|
{ description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
|
|
290
347
|
{ description: "Cloudflare Email Workers + a dev mail catcher", label: "Transactional email", value: "email" },
|
|
@@ -294,22 +351,985 @@ const STACK_FEATURE_OPTIONS = [
|
|
|
294
351
|
{ description: "Live presence / who's-online over hibernated WebSockets", label: "Presence", value: "presence" },
|
|
295
352
|
{ description: "Snapshot + restore your Durable Object data", label: "Backups", value: "backup" }
|
|
296
353
|
];
|
|
354
|
+
const STACK_FEATURE_VALUES = STACK_FEATURE_OPTIONS.map((option) => option.value);
|
|
355
|
+
const featureItem = (feature) => feature === "email" ? EMAIL_ITEM : feature;
|
|
356
|
+
const parseFeatureList = (raw, warn) => {
|
|
357
|
+
const features = [];
|
|
358
|
+
for (const part of raw.split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
359
|
+
if (STACK_FEATURE_VALUES.includes(part)) {
|
|
360
|
+
if (!features.includes(part)) {
|
|
361
|
+
features.push(part);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
warn(`init: unknown --add feature "${part}" — expected ${STACK_FEATURE_VALUES.join(" | ")}; skipping.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return features;
|
|
368
|
+
};
|
|
369
|
+
const collectAuthFeature = async (deps) => {
|
|
370
|
+
const provider = await promptAuthProvider(deps.select);
|
|
371
|
+
const databaseName = await promptDatabaseName(deps.text, deps.projectName);
|
|
372
|
+
return { label: "auth", names: [provider], transformManifest: (manifest) => withAuthDatabaseName(manifest, databaseName) };
|
|
373
|
+
};
|
|
374
|
+
const collectEmailFeature = async (deps) => {
|
|
375
|
+
const answer = await deps.text(MAIL_DESTINATION_PROMPT, { placeholder: "you@yourdomain.com" });
|
|
376
|
+
const destination = resolveTypedDestination(answer, (message) => {
|
|
377
|
+
deps.logger.warn(message);
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
label: "email",
|
|
381
|
+
names: [EMAIL_ITEM],
|
|
382
|
+
transformManifest: destination === void 0 ? void 0 : (manifest) => withMailDestination(manifest, destination)
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
const collectStorageFeature = async (deps) => {
|
|
386
|
+
const bucketName = await promptBucketName(deps.text, deps.projectName);
|
|
387
|
+
return { label: "storage", names: ["storage"], transformManifest: (manifest) => withStorageBucketName(manifest, bucketName) };
|
|
388
|
+
};
|
|
389
|
+
const FEATURE_COLLECTORS = {
|
|
390
|
+
auth: collectAuthFeature,
|
|
391
|
+
email: collectEmailFeature,
|
|
392
|
+
storage: collectStorageFeature
|
|
393
|
+
};
|
|
297
394
|
const offerRegistryExtras = async (deps) => {
|
|
395
|
+
if (deps.preselected !== void 0 && deps.preselected.length > 0) {
|
|
396
|
+
await deps.applyAll(
|
|
397
|
+
deps.preselected.map((feature) => {
|
|
398
|
+
return { label: feature, names: [featureItem(feature)] };
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
298
403
|
if (!deps.interactive) {
|
|
299
404
|
deps.logger.info("tip: add features later with `lunora add <auth|email|storage|ratelimit|crons|presence|backup>`.");
|
|
300
405
|
return;
|
|
301
406
|
}
|
|
302
407
|
const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
|
|
408
|
+
if (picked.length === 0) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const plans = [];
|
|
303
412
|
for (const feature of picked) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await deps.apply([provider]);
|
|
307
|
-
} else {
|
|
308
|
-
await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
|
|
309
|
-
}
|
|
413
|
+
const collect = FEATURE_COLLECTORS[feature];
|
|
414
|
+
plans.push(collect ? await collect(deps) : { label: feature, names: [feature] });
|
|
310
415
|
}
|
|
416
|
+
await deps.applyAll(plans);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const LOGO_PATH = "M 259.500 10.552 C 220.080 15.859, 182.424 32.566, 152.500 58.025 C 110.179 94.031, 85.380 137.183, 77.518 188.500 C 75.410 202.255, 74.569 225.677, 75.796 236.466 C 76.757 244.917, 76.683 245.692, 74.518 249.966 C 63.118 272.466, 53.141 303.876, 51.382 322.799 L 50.718 329.943 71.960 320.471 C 83.643 315.262, 93.326 311, 93.478 311 C 93.630 311, 96.547 316.063, 99.959 322.250 C 103.371 328.438, 107.249 334.850, 108.577 336.500 L 110.990 339.500 110.981 336 C 110.977 334.075, 111.499 324.991, 112.143 315.813 L 113.312 299.127 121.406 293.336 C 132.495 285.403, 149.593 271.554, 161 261.268 C 171.556 251.748, 189.116 235, 188.540 235 C 188.337 235, 183.069 238.648, 176.835 243.106 C 142.318 267.789, 68.537 314, 63.646 314 C 61.843 314, 72.791 281.179, 80.905 262.259 C 92.233 235.845, 107.473 212.389, 132.106 183.453 L 138.451 176 148.268 176 C 176.192 176, 197.512 187.154, 212.868 209.797 C 216.470 215.108, 217.035 216.595, 216.477 219.297 C 211.386 243.968, 202.359 274.496, 193.797 296 C 183.898 320.861, 167.147 352.101, 152.395 373.215 L 147.004 380.930 152.891 385.830 C 161.400 392.911, 165.563 396, 166.594 395.998 C 167.092 395.998, 168.772 391.641, 170.327 386.317 C 176.279 365.934, 188.422 338.749, 200.942 317.778 C 223.060 280.731, 256.432 244.369, 294.500 215.836 C 309.956 204.252, 313.937 201.603, 314.719 202.385 C 315.116 202.783, 315.449 213.096, 315.460 225.304 C 315.474 241.855, 315.021 250.405, 313.680 258.924 C 307.009 301.272, 291.175 336.677, 263.112 372 C 255.259 381.883, 227.182 410.673, 218.516 417.727 L 213.532 421.783 223.439 424.880 C 281.705 443.093, 349.165 436.018, 398.616 406.508 C 446.728 377.797, 483.322 331.466, 497.366 281.481 C 503.381 260.075, 504.480 250.741, 504.491 221 C 504.501 191.997, 503.598 184.047, 497.987 163.732 C 484.768 115.871, 452.505 72.708, 407.718 42.964 C 381.051 25.254, 352.818 14.828, 319.695 10.460 C 305.932 8.645, 273.298 8.695, 259.500 10.552";
|
|
420
|
+
const WELCOME_CSS = `/* The welcome page is a full-bleed starter — reset the browser's default
|
|
421
|
+
body margin/padding so it sits flush; everything else is scoped under
|
|
422
|
+
.lunora-welcome (collision-safe). */
|
|
423
|
+
body {
|
|
424
|
+
margin: 0;
|
|
425
|
+
padding: 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.lunora-welcome {
|
|
429
|
+
--cyan: hsl(186 84% 56%); --violet: hsl(256 72% 68%); --rose: hsl(330 80% 64%);
|
|
430
|
+
--ribbon: linear-gradient(115deg, var(--cyan), var(--violet) 52%, var(--rose));
|
|
431
|
+
--sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
432
|
+
--mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
433
|
+
/* NIGHT (default) */
|
|
434
|
+
--bg: #0e0e11; --surface: hsl(240 12% 8% / 0.72); --surface-2: hsl(240 11% 11% / 0.82);
|
|
435
|
+
--line: hsl(0 0% 100% / 0.08); --line-2: hsl(0 0% 100% / 0.14);
|
|
436
|
+
--t-display: hsl(228 30% 97%); --t-primary: hsl(228 26% 90%); --t-secondary: hsl(228 12% 62%); --t-faint: hsl(228 10% 44%);
|
|
437
|
+
--logo: hsl(228 30% 97%); --accent: var(--violet); --shot-bg: hsl(240 12% 6%);
|
|
438
|
+
--glow-1: hsl(256 80% 52% / 0.30); --glow-2: hsl(196 84% 52% / 0.13); --arc: hsl(256 60% 70% / 0.11);
|
|
439
|
+
|
|
440
|
+
position: relative; min-height: 100vh; background: var(--bg); color: var(--t-primary);
|
|
441
|
+
font-family: var(--sans); line-height: 1.55; -webkit-font-smoothing: antialiased; overflow-x: hidden;
|
|
442
|
+
transition: background .3s, color .3s;
|
|
443
|
+
}
|
|
444
|
+
.lunora-welcome[data-theme="light"] {
|
|
445
|
+
--bg: hsl(228 32% 97%); --surface: hsl(0 0% 100% / 0.82); --surface-2: hsl(0 0% 100% / 0.95);
|
|
446
|
+
--line: hsl(228 16% 88%); --line-2: hsl(228 14% 80%);
|
|
447
|
+
--t-display: hsl(240 14% 10%); --t-primary: hsl(240 12% 18%); --t-secondary: hsl(235 9% 42%); --t-faint: hsl(235 8% 58%);
|
|
448
|
+
--logo: hsl(240 16% 9%); --accent: hsl(256 58% 56%); --shot-bg: hsl(228 26% 99%);
|
|
449
|
+
--glow-1: hsl(256 80% 60% / 0.14); --glow-2: hsl(196 84% 58% / 0.08); --arc: hsl(256 40% 55% / 0.12);
|
|
450
|
+
}
|
|
451
|
+
.lunora-welcome *, .lunora-welcome *::before, .lunora-welcome *::after { box-sizing: border-box; }
|
|
452
|
+
.lunora-welcome a { color: inherit; text-decoration: none; }
|
|
453
|
+
.lunora-welcome button { font-family: inherit; cursor: pointer; }
|
|
454
|
+
.lunora-welcome ::selection { background: hsl(256 72% 68% / 0.3); }
|
|
455
|
+
.lunora-welcome code { font-family: var(--mono); }
|
|
456
|
+
|
|
457
|
+
/* glow background */
|
|
458
|
+
.lunora-welcome .lw-bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
|
|
459
|
+
.lunora-welcome .lw-bg .glow { position: absolute; left: 50%; top: -4%; width: 860px; height: 720px; transform: translateX(-50%);
|
|
460
|
+
border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1), var(--glow-2) 40%, transparent 66%); }
|
|
461
|
+
.lunora-welcome .lw-bg .arc { position: absolute; left: 50%; border-radius: 50%; border: 1px solid var(--arc); transform: translateX(-50%); }
|
|
462
|
+
.lunora-welcome .lw-bg .arc.a1 { top: -340px; width: 980px; height: 980px; }
|
|
463
|
+
.lunora-welcome .lw-bg .arc.a2 { top: -240px; width: 720px; height: 720px; opacity: .7; }
|
|
464
|
+
|
|
465
|
+
/* theme toggle */
|
|
466
|
+
.lunora-welcome .lw-toggle { position: fixed; z-index: 5; top: 20px; right: clamp(16px,4vw,36px); display: inline-flex; align-items: center;
|
|
467
|
+
gap: 7px; border: 1px solid var(--line-2); background: var(--surface); backdrop-filter: blur(12px); color: var(--t-secondary);
|
|
468
|
+
font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; padding: 7px 11px; transition: .15s; }
|
|
469
|
+
.lunora-welcome .lw-toggle:hover { color: var(--t-display); border-color: var(--accent); }
|
|
470
|
+
.lunora-welcome .lw-toggle svg { width: 13px; height: 13px; }
|
|
471
|
+
|
|
472
|
+
.lunora-welcome .lw-wrap { position: relative; z-index: 2; width: 100%; max-width: 1080px; margin: 0 auto;
|
|
473
|
+
padding: clamp(44px,8vh,92px) clamp(20px,5vw,48px); display: flex; flex-direction: column; min-height: 100vh; }
|
|
474
|
+
.lunora-welcome .brand { display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: clamp(38px,6vh,68px); color: var(--logo); }
|
|
475
|
+
.lunora-welcome .brand svg { width: 54px; height: auto; display: block; }
|
|
476
|
+
.lunora-welcome .brand .word { font-size: 34px; font-weight: 600; letter-spacing: -0.03em; color: var(--t-display); }
|
|
477
|
+
|
|
478
|
+
.lunora-welcome .grid { display: grid; grid-template-columns: 1.06fr 0.94fr; gap: 16px; align-items: stretch; flex: 1; max-height: 580px; }
|
|
479
|
+
@media (max-width: 820px) { .lunora-welcome .grid { grid-template-columns: 1fr; max-height: none; } }
|
|
480
|
+
|
|
481
|
+
.lunora-welcome .card { border: 1px solid var(--line); background: var(--surface); backdrop-filter: blur(10px); transition: border-color .2s, background .2s; }
|
|
482
|
+
.lunora-welcome .card:hover { border-color: var(--line-2); background: var(--surface-2); }
|
|
483
|
+
.lunora-welcome .card:hover .arrow { color: var(--accent); transform: translateX(3px); }
|
|
484
|
+
.lunora-welcome .arrow { color: var(--t-faint); transition: color .2s, transform .2s; }
|
|
485
|
+
.lunora-welcome .arrow svg { width: 20px; height: 20px; display: block; }
|
|
486
|
+
.lunora-welcome .ic { display: grid; place-items: center; color: var(--accent);
|
|
487
|
+
border: 1px solid color-mix(in oklab, var(--accent) 36%, transparent); background: color-mix(in oklab, var(--accent) 12%, transparent); }
|
|
488
|
+
.lunora-welcome .ic svg { display: block; }
|
|
489
|
+
|
|
490
|
+
/* left feature card — stretches to fill the box height */
|
|
491
|
+
.lunora-welcome .feature { padding: clamp(18px,2vw,24px); display: flex; flex-direction: column; }
|
|
492
|
+
.lunora-welcome .shot { border: 1px solid var(--line); overflow: hidden; background: var(--shot-bg);
|
|
493
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); }
|
|
494
|
+
.lunora-welcome .shot .top { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
|
|
495
|
+
.lunora-welcome .shot .wm { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; color: var(--t-primary); }
|
|
496
|
+
.lunora-welcome .shot .wm i { width: 11px; height: 11px; background: var(--ribbon); -webkit-mask: radial-gradient(circle,#000 60%,transparent 62%); mask: radial-gradient(circle,#000 60%,transparent 62%); }
|
|
497
|
+
.lunora-welcome .shot .search { flex: 1; height: 22px; border: 1px solid var(--line); border-radius: 4px; }
|
|
498
|
+
.lunora-welcome .shot .ver { font-family: var(--mono); font-size: 8px; letter-spacing: .08em; color: var(--t-faint); }
|
|
499
|
+
.lunora-welcome .shot .body { display: grid; grid-template-columns: 92px 1fr; min-height: 224px; }
|
|
500
|
+
.lunora-welcome .shot .nav { border-right: 1px solid var(--line); padding: 14px 12px; display: flex; flex-direction: column; gap: 11px; }
|
|
501
|
+
.lunora-welcome .shot .nav i, .lunora-welcome .shot .doc i { height: 5px; border-radius: 2px; background: var(--line); }
|
|
502
|
+
.lunora-welcome .shot .doc { padding: 16px 18px; display: flex; flex-direction: column; gap: 9px; }
|
|
503
|
+
.lunora-welcome .shot .doc .h { height: 8px; width: 46%; border-radius: 2px; background: var(--line-2); margin-bottom: 5px; }
|
|
504
|
+
.lunora-welcome .shot .doc .accent { height: 5px; width: 26%; border-radius: 2px; background: var(--accent); }
|
|
505
|
+
.lunora-welcome .feature .info { margin-top: auto; padding-top: clamp(18px,2.4vh,28px); }
|
|
506
|
+
.lunora-welcome .feature .ic { width: 40px; height: 40px; margin-bottom: 15px; }
|
|
507
|
+
.lunora-welcome .feature .ic svg { width: 19px; height: 19px; }
|
|
508
|
+
.lunora-welcome .feature h2 { margin: 0 0 10px; font-size: 19px; font-weight: 600; letter-spacing: -0.015em; color: var(--t-display); }
|
|
509
|
+
.lunora-welcome .feature .row { display: flex; align-items: flex-end; gap: 16px; }
|
|
510
|
+
.lunora-welcome .feature p { margin: 0; color: var(--t-secondary); font-size: 14px; max-width: 50ch; }
|
|
511
|
+
.lunora-welcome .feature .row .arrow { margin-left: auto; }
|
|
512
|
+
|
|
513
|
+
/* right stack — smaller cards, spread to align bottoms with the feature */
|
|
514
|
+
.lunora-welcome .stack { display: flex; flex-direction: column; gap: 16px; height: 100%; }
|
|
515
|
+
.lunora-welcome .mini { flex: 1; padding: 15px 17px; display: flex; align-items: center; gap: 16px; min-height: 0; }
|
|
516
|
+
.lunora-welcome .mini .mc { flex: 1; }
|
|
517
|
+
.lunora-welcome .mini .ic { width: 32px; height: 32px; margin-bottom: 10px; }
|
|
518
|
+
.lunora-welcome .mini .ic svg { width: 16px; height: 16px; }
|
|
519
|
+
.lunora-welcome .mini h3 { margin: 0 0 5px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--t-display); }
|
|
520
|
+
.lunora-welcome .mini p { margin: 0; color: var(--t-secondary); font-size: 12.5px; line-height: 1.45; }
|
|
521
|
+
|
|
522
|
+
.lunora-welcome .lw-foot { text-align: center; padding-top: 26px; font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--t-faint); }
|
|
523
|
+
|
|
524
|
+
@media (prefers-reduced-motion: reduce) { .lunora-welcome * { transition: none !important; } }
|
|
525
|
+
`;
|
|
526
|
+
const REACT_APP = `import { useEffect, useState } from "react";
|
|
527
|
+
|
|
528
|
+
type Theme = "dark" | "light";
|
|
529
|
+
|
|
530
|
+
export default function App() {
|
|
531
|
+
// Default to "dark" for a stable first paint, then reconcile to the OS
|
|
532
|
+
// preference; the toggle takes over after that.
|
|
533
|
+
const [theme, setTheme] = useState<Theme>("dark");
|
|
534
|
+
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
537
|
+
setTheme("light");
|
|
538
|
+
}
|
|
539
|
+
}, []);
|
|
540
|
+
|
|
541
|
+
const isLight = theme === "light";
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<div className="lunora-welcome" data-theme={theme}>
|
|
545
|
+
<div className="lw-bg">
|
|
546
|
+
<div className="arc a1" />
|
|
547
|
+
<div className="arc a2" />
|
|
548
|
+
<div className="glow" />
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<button className="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight ? "dark" : "light")}>
|
|
552
|
+
{isLight ? (
|
|
553
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
|
554
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
|
555
|
+
</svg>
|
|
556
|
+
) : (
|
|
557
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
|
558
|
+
<circle cx="12" cy="12" r="4" />
|
|
559
|
+
<path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
|
|
560
|
+
</svg>
|
|
561
|
+
)}
|
|
562
|
+
<span>{isLight ? "Ivory" : "Night"}</span>
|
|
563
|
+
</button>
|
|
564
|
+
|
|
565
|
+
<div className="lw-wrap">
|
|
566
|
+
<div className="brand">
|
|
567
|
+
<svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
|
|
568
|
+
<path d="${LOGO_PATH}" fill="currentColor" fillRule="evenodd" />
|
|
569
|
+
</svg>
|
|
570
|
+
<span className="word">Lunora</span>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<div className="grid">
|
|
574
|
+
<a className="card feature" href="https://lunora.sh/docs">
|
|
575
|
+
<div className="shot" aria-hidden="true">
|
|
576
|
+
<div className="top">
|
|
577
|
+
<span className="wm">
|
|
578
|
+
<i /> Lunora
|
|
579
|
+
</span>
|
|
580
|
+
<span className="search" />
|
|
581
|
+
<span className="ver">v0.1</span>
|
|
582
|
+
</div>
|
|
583
|
+
<div className="body">
|
|
584
|
+
<div className="nav">
|
|
585
|
+
<i style={{ width: "80%" }} />
|
|
586
|
+
<i style={{ width: "60%" }} />
|
|
587
|
+
<i style={{ width: "72%" }} />
|
|
588
|
+
<i style={{ width: "50%" }} />
|
|
589
|
+
<i style={{ width: "66%" }} />
|
|
590
|
+
<i style={{ width: "44%" }} />
|
|
591
|
+
<i style={{ width: "58%" }} />
|
|
592
|
+
</div>
|
|
593
|
+
<div className="doc">
|
|
594
|
+
<span className="h" />
|
|
595
|
+
<i style={{ width: "92%" }} />
|
|
596
|
+
<i style={{ width: "88%" }} />
|
|
597
|
+
<span className="accent" />
|
|
598
|
+
<i style={{ width: "80%" }} />
|
|
599
|
+
<i style={{ width: "90%" }} />
|
|
600
|
+
<i style={{ width: "72%" }} />
|
|
601
|
+
<i style={{ width: "84%" }} />
|
|
602
|
+
<i style={{ width: "78%" }} />
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
<div className="info">
|
|
607
|
+
<span className="ic">
|
|
608
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
|
|
609
|
+
<path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
|
610
|
+
<path d="M14 3v5h5" />
|
|
611
|
+
</svg>
|
|
612
|
+
</span>
|
|
613
|
+
<h2>Documentation</h2>
|
|
614
|
+
<div className="row">
|
|
615
|
+
<p>
|
|
616
|
+
Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
|
|
617
|
+
you'll have a live app fast.
|
|
618
|
+
</p>
|
|
619
|
+
<span className="arrow">
|
|
620
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
621
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
622
|
+
</svg>
|
|
623
|
+
</span>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
</a>
|
|
627
|
+
|
|
628
|
+
<div className="stack">
|
|
629
|
+
<a className="card mini" href="https://lunora.sh/blog">
|
|
630
|
+
<div className="mc">
|
|
631
|
+
<span className="ic">
|
|
632
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
|
|
633
|
+
<path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
|
|
634
|
+
</svg>
|
|
635
|
+
</span>
|
|
636
|
+
<h3>Blog</h3>
|
|
637
|
+
<p>Product updates, deep dives, and what's new in Lunora.</p>
|
|
638
|
+
</div>
|
|
639
|
+
<span className="arrow">
|
|
640
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
641
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
642
|
+
</svg>
|
|
643
|
+
</span>
|
|
644
|
+
</a>
|
|
645
|
+
<a className="card mini" href="/_lunora">
|
|
646
|
+
<div className="mc">
|
|
647
|
+
<span className="ic">
|
|
648
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
|
|
649
|
+
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
650
|
+
<path d="M3 9h18M9 21V9" />
|
|
651
|
+
</svg>
|
|
652
|
+
</span>
|
|
653
|
+
<h3>Lunora Studio</h3>
|
|
654
|
+
<p>Local admin for schema, data, logs, and advisors.</p>
|
|
655
|
+
</div>
|
|
656
|
+
<span className="arrow">
|
|
657
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
658
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
659
|
+
</svg>
|
|
660
|
+
</span>
|
|
661
|
+
</a>
|
|
662
|
+
<a className="card mini" href="https://lunora.sh/packages">
|
|
663
|
+
<div className="mc">
|
|
664
|
+
<span className="ic">
|
|
665
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
|
|
666
|
+
<path d="M12 2 3 7v10l9 5 9-5V7z" />
|
|
667
|
+
<path d="M3 7l9 5 9-5M12 12v10" />
|
|
668
|
+
</svg>
|
|
669
|
+
</span>
|
|
670
|
+
<h3>Cloudflare ecosystem</h3>
|
|
671
|
+
<p>Auth, mail, storage, AI, payments — one deploy.</p>
|
|
672
|
+
</div>
|
|
673
|
+
<span className="arrow">
|
|
674
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
675
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
676
|
+
</svg>
|
|
677
|
+
</span>
|
|
678
|
+
</a>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
<div className="lw-foot">Running on Lunora · Vite + React</div>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
`;
|
|
688
|
+
const VUE_APP = `<script setup lang="ts">
|
|
689
|
+
import { onMounted, ref } from "vue";
|
|
690
|
+
|
|
691
|
+
// Default to "dark" for a stable first paint, then reconcile to the OS
|
|
692
|
+
// preference; the toggle takes over after that.
|
|
693
|
+
const theme = ref<"dark" | "light">("dark");
|
|
694
|
+
|
|
695
|
+
onMounted(() => {
|
|
696
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
697
|
+
theme.value = "light";
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const toggle = (): void => {
|
|
702
|
+
theme.value = theme.value === "light" ? "dark" : "light";
|
|
703
|
+
};
|
|
704
|
+
<\/script>
|
|
705
|
+
|
|
706
|
+
<template>
|
|
707
|
+
<div class="lunora-welcome" :data-theme="theme">
|
|
708
|
+
<div class="lw-bg">
|
|
709
|
+
<div class="arc a1" />
|
|
710
|
+
<div class="arc a2" />
|
|
711
|
+
<div class="glow" />
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<button class="lw-toggle" type="button" aria-label="Toggle color theme" @click="toggle">
|
|
715
|
+
<svg v-if="theme === 'light'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
716
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
|
717
|
+
</svg>
|
|
718
|
+
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
719
|
+
<circle cx="12" cy="12" r="4" />
|
|
720
|
+
<path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
|
|
721
|
+
</svg>
|
|
722
|
+
<span>{{ theme === "light" ? "Ivory" : "Night" }}</span>
|
|
723
|
+
</button>
|
|
724
|
+
|
|
725
|
+
<div class="lw-wrap">
|
|
726
|
+
<div class="brand">
|
|
727
|
+
<svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
|
|
728
|
+
<path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
|
|
729
|
+
</svg>
|
|
730
|
+
<span class="word">Lunora</span>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<div class="grid">
|
|
734
|
+
<a class="card feature" href="https://lunora.sh/docs">
|
|
735
|
+
<div class="shot" aria-hidden="true">
|
|
736
|
+
<div class="top">
|
|
737
|
+
<span class="wm"><i /> Lunora</span>
|
|
738
|
+
<span class="search" />
|
|
739
|
+
<span class="ver">v0.1</span>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="body">
|
|
742
|
+
<div class="nav">
|
|
743
|
+
<i style="width: 80%" />
|
|
744
|
+
<i style="width: 60%" />
|
|
745
|
+
<i style="width: 72%" />
|
|
746
|
+
<i style="width: 50%" />
|
|
747
|
+
<i style="width: 66%" />
|
|
748
|
+
<i style="width: 44%" />
|
|
749
|
+
<i style="width: 58%" />
|
|
750
|
+
</div>
|
|
751
|
+
<div class="doc">
|
|
752
|
+
<span class="h" />
|
|
753
|
+
<i style="width: 92%" />
|
|
754
|
+
<i style="width: 88%" />
|
|
755
|
+
<span class="accent" />
|
|
756
|
+
<i style="width: 80%" />
|
|
757
|
+
<i style="width: 90%" />
|
|
758
|
+
<i style="width: 72%" />
|
|
759
|
+
<i style="width: 84%" />
|
|
760
|
+
<i style="width: 78%" />
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="info">
|
|
765
|
+
<span class="ic">
|
|
766
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
767
|
+
<path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
|
768
|
+
<path d="M14 3v5h5" />
|
|
769
|
+
</svg>
|
|
770
|
+
</span>
|
|
771
|
+
<h2>Documentation</h2>
|
|
772
|
+
<div class="row">
|
|
773
|
+
<p>
|
|
774
|
+
Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
|
|
775
|
+
you'll have a live app fast.
|
|
776
|
+
</p>
|
|
777
|
+
<span class="arrow">
|
|
778
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
779
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
780
|
+
</svg>
|
|
781
|
+
</span>
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
</a>
|
|
785
|
+
|
|
786
|
+
<div class="stack">
|
|
787
|
+
<a class="card mini" href="https://lunora.sh/blog">
|
|
788
|
+
<div class="mc">
|
|
789
|
+
<span class="ic">
|
|
790
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
791
|
+
<path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
|
|
792
|
+
</svg>
|
|
793
|
+
</span>
|
|
794
|
+
<h3>Blog</h3>
|
|
795
|
+
<p>Product updates, deep dives, and what's new in Lunora.</p>
|
|
796
|
+
</div>
|
|
797
|
+
<span class="arrow">
|
|
798
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
799
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
800
|
+
</svg>
|
|
801
|
+
</span>
|
|
802
|
+
</a>
|
|
803
|
+
<a class="card mini" href="/_lunora">
|
|
804
|
+
<div class="mc">
|
|
805
|
+
<span class="ic">
|
|
806
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
807
|
+
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
808
|
+
<path d="M3 9h18M9 21V9" />
|
|
809
|
+
</svg>
|
|
810
|
+
</span>
|
|
811
|
+
<h3>Lunora Studio</h3>
|
|
812
|
+
<p>Local admin for schema, data, logs, and advisors.</p>
|
|
813
|
+
</div>
|
|
814
|
+
<span class="arrow">
|
|
815
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
816
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
817
|
+
</svg>
|
|
818
|
+
</span>
|
|
819
|
+
</a>
|
|
820
|
+
<a class="card mini" href="https://lunora.sh/packages">
|
|
821
|
+
<div class="mc">
|
|
822
|
+
<span class="ic">
|
|
823
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
824
|
+
<path d="M12 2 3 7v10l9 5 9-5V7z" />
|
|
825
|
+
<path d="M3 7l9 5 9-5M12 12v10" />
|
|
826
|
+
</svg>
|
|
827
|
+
</span>
|
|
828
|
+
<h3>Cloudflare ecosystem</h3>
|
|
829
|
+
<p>Auth, mail, storage, AI, payments — one deploy.</p>
|
|
830
|
+
</div>
|
|
831
|
+
<span class="arrow">
|
|
832
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
833
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
834
|
+
</svg>
|
|
835
|
+
</span>
|
|
836
|
+
</a>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
|
|
840
|
+
<div class="lw-foot">Running on Lunora · Vite + Vue</div>
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
</template>
|
|
844
|
+
`;
|
|
845
|
+
const SOLID_APP = `import { createSignal, onMount } from "solid-js";
|
|
846
|
+
|
|
847
|
+
export default function App() {
|
|
848
|
+
// Default to "dark" for a stable first paint, then reconcile to the OS
|
|
849
|
+
// preference; the toggle takes over after that.
|
|
850
|
+
const [theme, setTheme] = createSignal<"dark" | "light">("dark");
|
|
851
|
+
|
|
852
|
+
onMount(() => {
|
|
853
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
854
|
+
setTheme("light");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const isLight = () => theme() === "light";
|
|
859
|
+
|
|
860
|
+
return (
|
|
861
|
+
<div class="lunora-welcome" data-theme={theme()}>
|
|
862
|
+
<div class="lw-bg">
|
|
863
|
+
<div class="arc a1" />
|
|
864
|
+
<div class="arc a2" />
|
|
865
|
+
<div class="glow" />
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<button class="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight() ? "dark" : "light")}>
|
|
869
|
+
{isLight() ? (
|
|
870
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
871
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
|
872
|
+
</svg>
|
|
873
|
+
) : (
|
|
874
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
875
|
+
<circle cx="12" cy="12" r="4" />
|
|
876
|
+
<path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
|
|
877
|
+
</svg>
|
|
878
|
+
)}
|
|
879
|
+
<span>{isLight() ? "Ivory" : "Night"}</span>
|
|
880
|
+
</button>
|
|
881
|
+
|
|
882
|
+
<div class="lw-wrap">
|
|
883
|
+
<div class="brand">
|
|
884
|
+
<svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
|
|
885
|
+
<path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
|
|
886
|
+
</svg>
|
|
887
|
+
<span class="word">Lunora</span>
|
|
888
|
+
</div>
|
|
889
|
+
|
|
890
|
+
<div class="grid">
|
|
891
|
+
<a class="card feature" href="https://lunora.sh/docs">
|
|
892
|
+
<div class="shot" aria-hidden="true">
|
|
893
|
+
<div class="top">
|
|
894
|
+
<span class="wm">
|
|
895
|
+
<i /> Lunora
|
|
896
|
+
</span>
|
|
897
|
+
<span class="search" />
|
|
898
|
+
<span class="ver">v0.1</span>
|
|
899
|
+
</div>
|
|
900
|
+
<div class="body">
|
|
901
|
+
<div class="nav">
|
|
902
|
+
<i style={{ width: "80%" }} />
|
|
903
|
+
<i style={{ width: "60%" }} />
|
|
904
|
+
<i style={{ width: "72%" }} />
|
|
905
|
+
<i style={{ width: "50%" }} />
|
|
906
|
+
<i style={{ width: "66%" }} />
|
|
907
|
+
<i style={{ width: "44%" }} />
|
|
908
|
+
<i style={{ width: "58%" }} />
|
|
909
|
+
</div>
|
|
910
|
+
<div class="doc">
|
|
911
|
+
<span class="h" />
|
|
912
|
+
<i style={{ width: "92%" }} />
|
|
913
|
+
<i style={{ width: "88%" }} />
|
|
914
|
+
<span class="accent" />
|
|
915
|
+
<i style={{ width: "80%" }} />
|
|
916
|
+
<i style={{ width: "90%" }} />
|
|
917
|
+
<i style={{ width: "72%" }} />
|
|
918
|
+
<i style={{ width: "84%" }} />
|
|
919
|
+
<i style={{ width: "78%" }} />
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
<div class="info">
|
|
924
|
+
<span class="ic">
|
|
925
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
926
|
+
<path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
|
927
|
+
<path d="M14 3v5h5" />
|
|
928
|
+
</svg>
|
|
929
|
+
</span>
|
|
930
|
+
<h2>Documentation</h2>
|
|
931
|
+
<div class="row">
|
|
932
|
+
<p>
|
|
933
|
+
Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
|
|
934
|
+
you'll have a live app fast.
|
|
935
|
+
</p>
|
|
936
|
+
<span class="arrow">
|
|
937
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
938
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
939
|
+
</svg>
|
|
940
|
+
</span>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</a>
|
|
944
|
+
|
|
945
|
+
<div class="stack">
|
|
946
|
+
<a class="card mini" href="https://lunora.sh/blog">
|
|
947
|
+
<div class="mc">
|
|
948
|
+
<span class="ic">
|
|
949
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
950
|
+
<path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
|
|
951
|
+
</svg>
|
|
952
|
+
</span>
|
|
953
|
+
<h3>Blog</h3>
|
|
954
|
+
<p>Product updates, deep dives, and what's new in Lunora.</p>
|
|
955
|
+
</div>
|
|
956
|
+
<span class="arrow">
|
|
957
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
958
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
959
|
+
</svg>
|
|
960
|
+
</span>
|
|
961
|
+
</a>
|
|
962
|
+
<a class="card mini" href="/_lunora">
|
|
963
|
+
<div class="mc">
|
|
964
|
+
<span class="ic">
|
|
965
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
966
|
+
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
967
|
+
<path d="M3 9h18M9 21V9" />
|
|
968
|
+
</svg>
|
|
969
|
+
</span>
|
|
970
|
+
<h3>Lunora Studio</h3>
|
|
971
|
+
<p>Local admin for schema, data, logs, and advisors.</p>
|
|
972
|
+
</div>
|
|
973
|
+
<span class="arrow">
|
|
974
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
975
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
976
|
+
</svg>
|
|
977
|
+
</span>
|
|
978
|
+
</a>
|
|
979
|
+
<a class="card mini" href="https://lunora.sh/packages">
|
|
980
|
+
<div class="mc">
|
|
981
|
+
<span class="ic">
|
|
982
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
983
|
+
<path d="M12 2 3 7v10l9 5 9-5V7z" />
|
|
984
|
+
<path d="M3 7l9 5 9-5M12 12v10" />
|
|
985
|
+
</svg>
|
|
986
|
+
</span>
|
|
987
|
+
<h3>Cloudflare ecosystem</h3>
|
|
988
|
+
<p>Auth, mail, storage, AI, payments — one deploy.</p>
|
|
989
|
+
</div>
|
|
990
|
+
<span class="arrow">
|
|
991
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
992
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
993
|
+
</svg>
|
|
994
|
+
</span>
|
|
995
|
+
</a>
|
|
996
|
+
</div>
|
|
997
|
+
</div>
|
|
998
|
+
|
|
999
|
+
<div class="lw-foot">Running on Lunora · Vite + Solid</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
`;
|
|
1005
|
+
const SVELTE_APP = `<script lang="ts">
|
|
1006
|
+
import { onMount } from "svelte";
|
|
1007
|
+
|
|
1008
|
+
// Default to "dark" for a stable first paint, then reconcile to the OS
|
|
1009
|
+
// preference; the toggle takes over after that.
|
|
1010
|
+
let theme = $state<"dark" | "light">("dark");
|
|
1011
|
+
const isLight = $derived(theme === "light");
|
|
1012
|
+
|
|
1013
|
+
onMount(() => {
|
|
1014
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
1015
|
+
theme = "light";
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const toggle = (): void => {
|
|
1020
|
+
theme = theme === "light" ? "dark" : "light";
|
|
1021
|
+
};
|
|
1022
|
+
<\/script>
|
|
1023
|
+
|
|
1024
|
+
<div class="lunora-welcome" data-theme={theme}>
|
|
1025
|
+
<div class="lw-bg">
|
|
1026
|
+
<div class="arc a1"></div>
|
|
1027
|
+
<div class="arc a2"></div>
|
|
1028
|
+
<div class="glow"></div>
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<button class="lw-toggle" type="button" aria-label="Toggle color theme" onclick={toggle}>
|
|
1032
|
+
{#if isLight}
|
|
1033
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
1034
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
|
1035
|
+
</svg>
|
|
1036
|
+
{:else}
|
|
1037
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
1038
|
+
<circle cx="12" cy="12" r="4" />
|
|
1039
|
+
<path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
|
|
1040
|
+
</svg>
|
|
1041
|
+
{/if}
|
|
1042
|
+
<span>{isLight ? "Ivory" : "Night"}</span>
|
|
1043
|
+
</button>
|
|
1044
|
+
|
|
1045
|
+
<div class="lw-wrap">
|
|
1046
|
+
<div class="brand">
|
|
1047
|
+
<svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
|
|
1048
|
+
<path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
|
|
1049
|
+
</svg>
|
|
1050
|
+
<span class="word">Lunora</span>
|
|
1051
|
+
</div>
|
|
1052
|
+
|
|
1053
|
+
<div class="grid">
|
|
1054
|
+
<a class="card feature" href="https://lunora.sh/docs">
|
|
1055
|
+
<div class="shot" aria-hidden="true">
|
|
1056
|
+
<div class="top">
|
|
1057
|
+
<span class="wm"><i></i> Lunora</span>
|
|
1058
|
+
<span class="search"></span>
|
|
1059
|
+
<span class="ver">v0.1</span>
|
|
1060
|
+
</div>
|
|
1061
|
+
<div class="body">
|
|
1062
|
+
<div class="nav">
|
|
1063
|
+
<i style="width: 80%"></i>
|
|
1064
|
+
<i style="width: 60%"></i>
|
|
1065
|
+
<i style="width: 72%"></i>
|
|
1066
|
+
<i style="width: 50%"></i>
|
|
1067
|
+
<i style="width: 66%"></i>
|
|
1068
|
+
<i style="width: 44%"></i>
|
|
1069
|
+
<i style="width: 58%"></i>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="doc">
|
|
1072
|
+
<span class="h"></span>
|
|
1073
|
+
<i style="width: 92%"></i>
|
|
1074
|
+
<i style="width: 88%"></i>
|
|
1075
|
+
<span class="accent"></span>
|
|
1076
|
+
<i style="width: 80%"></i>
|
|
1077
|
+
<i style="width: 90%"></i>
|
|
1078
|
+
<i style="width: 72%"></i>
|
|
1079
|
+
<i style="width: 84%"></i>
|
|
1080
|
+
<i style="width: 78%"></i>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
<div class="info">
|
|
1085
|
+
<span class="ic">
|
|
1086
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1087
|
+
<path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
|
1088
|
+
<path d="M14 3v5h5" />
|
|
1089
|
+
</svg>
|
|
1090
|
+
</span>
|
|
1091
|
+
<h2>Documentation</h2>
|
|
1092
|
+
<div class="row">
|
|
1093
|
+
<p>
|
|
1094
|
+
Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
|
|
1095
|
+
you'll have a live app fast.
|
|
1096
|
+
</p>
|
|
1097
|
+
<span class="arrow">
|
|
1098
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1099
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1100
|
+
</svg>
|
|
1101
|
+
</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
1104
|
+
</a>
|
|
1105
|
+
|
|
1106
|
+
<div class="stack">
|
|
1107
|
+
<a class="card mini" href="https://lunora.sh/blog">
|
|
1108
|
+
<div class="mc">
|
|
1109
|
+
<span class="ic">
|
|
1110
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1111
|
+
<path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
|
|
1112
|
+
</svg>
|
|
1113
|
+
</span>
|
|
1114
|
+
<h3>Blog</h3>
|
|
1115
|
+
<p>Product updates, deep dives, and what's new in Lunora.</p>
|
|
1116
|
+
</div>
|
|
1117
|
+
<span class="arrow">
|
|
1118
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1119
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1120
|
+
</svg>
|
|
1121
|
+
</span>
|
|
1122
|
+
</a>
|
|
1123
|
+
<a class="card mini" href="/_lunora">
|
|
1124
|
+
<div class="mc">
|
|
1125
|
+
<span class="ic">
|
|
1126
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1127
|
+
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
1128
|
+
<path d="M3 9h18M9 21V9" />
|
|
1129
|
+
</svg>
|
|
1130
|
+
</span>
|
|
1131
|
+
<h3>Lunora Studio</h3>
|
|
1132
|
+
<p>Local admin for schema, data, logs, and advisors.</p>
|
|
1133
|
+
</div>
|
|
1134
|
+
<span class="arrow">
|
|
1135
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1136
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1137
|
+
</svg>
|
|
1138
|
+
</span>
|
|
1139
|
+
</a>
|
|
1140
|
+
<a class="card mini" href="https://lunora.sh/packages">
|
|
1141
|
+
<div class="mc">
|
|
1142
|
+
<span class="ic">
|
|
1143
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1144
|
+
<path d="M12 2 3 7v10l9 5 9-5V7z" />
|
|
1145
|
+
<path d="M3 7l9 5 9-5M12 12v10" />
|
|
1146
|
+
</svg>
|
|
1147
|
+
</span>
|
|
1148
|
+
<h3>Cloudflare ecosystem</h3>
|
|
1149
|
+
<p>Auth, mail, storage, AI, payments — one deploy.</p>
|
|
1150
|
+
</div>
|
|
1151
|
+
<span class="arrow">
|
|
1152
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1153
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1154
|
+
</svg>
|
|
1155
|
+
</span>
|
|
1156
|
+
</a>
|
|
1157
|
+
</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
|
|
1160
|
+
<div class="lw-foot">Running on Lunora · Vite + Svelte</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
`;
|
|
1164
|
+
const VANILLA_MAIN = `import "./style.css";
|
|
1165
|
+
|
|
1166
|
+
import { LunoraClient } from "lunorash/client";
|
|
1167
|
+
|
|
1168
|
+
import { api } from "#lunora/_generated/api.js";
|
|
1169
|
+
|
|
1170
|
+
// \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
|
|
1171
|
+
// \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
|
|
1172
|
+
// the client against production data.
|
|
1173
|
+
const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
|
|
1174
|
+
const client = new LunoraClient({ url });
|
|
1175
|
+
|
|
1176
|
+
const MOON_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>';
|
|
1177
|
+
const SUN_ICON =
|
|
1178
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" /></svg>';
|
|
1179
|
+
|
|
1180
|
+
const welcomeHtml = \`
|
|
1181
|
+
<div class="lw-bg">
|
|
1182
|
+
<div class="arc a1"></div>
|
|
1183
|
+
<div class="arc a2"></div>
|
|
1184
|
+
<div class="glow"></div>
|
|
1185
|
+
</div>
|
|
1186
|
+
|
|
1187
|
+
<button class="lw-toggle" type="button" aria-label="Toggle color theme"></button>
|
|
1188
|
+
|
|
1189
|
+
<div class="lw-wrap">
|
|
1190
|
+
<div class="brand">
|
|
1191
|
+
<svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
|
|
1192
|
+
<path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
|
|
1193
|
+
</svg>
|
|
1194
|
+
<span class="word">Lunora</span>
|
|
1195
|
+
</div>
|
|
1196
|
+
|
|
1197
|
+
<div class="grid">
|
|
1198
|
+
<a class="card feature" href="https://lunora.sh/docs">
|
|
1199
|
+
<div class="shot" aria-hidden="true">
|
|
1200
|
+
<div class="top">
|
|
1201
|
+
<span class="wm"><i></i> Lunora</span>
|
|
1202
|
+
<span class="search"></span>
|
|
1203
|
+
<span class="ver">v0.1</span>
|
|
1204
|
+
</div>
|
|
1205
|
+
<div class="body">
|
|
1206
|
+
<div class="nav">
|
|
1207
|
+
<i style="width: 80%"></i>
|
|
1208
|
+
<i style="width: 60%"></i>
|
|
1209
|
+
<i style="width: 72%"></i>
|
|
1210
|
+
<i style="width: 50%"></i>
|
|
1211
|
+
<i style="width: 66%"></i>
|
|
1212
|
+
<i style="width: 44%"></i>
|
|
1213
|
+
<i style="width: 58%"></i>
|
|
1214
|
+
</div>
|
|
1215
|
+
<div class="doc">
|
|
1216
|
+
<span class="h"></span>
|
|
1217
|
+
<i style="width: 92%"></i>
|
|
1218
|
+
<i style="width: 88%"></i>
|
|
1219
|
+
<span class="accent"></span>
|
|
1220
|
+
<i style="width: 80%"></i>
|
|
1221
|
+
<i style="width: 90%"></i>
|
|
1222
|
+
<i style="width: 72%"></i>
|
|
1223
|
+
<i style="width: 84%"></i>
|
|
1224
|
+
<i style="width: 78%"></i>
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
<div class="info">
|
|
1229
|
+
<span class="ic">
|
|
1230
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1231
|
+
<path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
|
|
1232
|
+
<path d="M14 3v5h5" />
|
|
1233
|
+
</svg>
|
|
1234
|
+
</span>
|
|
1235
|
+
<h2>Documentation</h2>
|
|
1236
|
+
<div class="row">
|
|
1237
|
+
<p>
|
|
1238
|
+
Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
|
|
1239
|
+
you'll have a live app fast.
|
|
1240
|
+
</p>
|
|
1241
|
+
<span class="arrow">
|
|
1242
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1243
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1244
|
+
</svg>
|
|
1245
|
+
</span>
|
|
1246
|
+
</div>
|
|
1247
|
+
</div>
|
|
1248
|
+
</a>
|
|
1249
|
+
|
|
1250
|
+
<div class="stack">
|
|
1251
|
+
<a class="card mini" href="https://lunora.sh/blog">
|
|
1252
|
+
<div class="mc">
|
|
1253
|
+
<span class="ic">
|
|
1254
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1255
|
+
<path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
|
|
1256
|
+
</svg>
|
|
1257
|
+
</span>
|
|
1258
|
+
<h3>Blog</h3>
|
|
1259
|
+
<p>Product updates, deep dives, and what's new in Lunora.</p>
|
|
1260
|
+
</div>
|
|
1261
|
+
<span class="arrow">
|
|
1262
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1263
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1264
|
+
</svg>
|
|
1265
|
+
</span>
|
|
1266
|
+
</a>
|
|
1267
|
+
<a class="card mini" href="/_lunora">
|
|
1268
|
+
<div class="mc">
|
|
1269
|
+
<span class="ic">
|
|
1270
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1271
|
+
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
1272
|
+
<path d="M3 9h18M9 21V9" />
|
|
1273
|
+
</svg>
|
|
1274
|
+
</span>
|
|
1275
|
+
<h3>Lunora Studio</h3>
|
|
1276
|
+
<p>Local admin for schema, data, logs, and advisors.</p>
|
|
1277
|
+
</div>
|
|
1278
|
+
<span class="arrow">
|
|
1279
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1280
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1281
|
+
</svg>
|
|
1282
|
+
</span>
|
|
1283
|
+
</a>
|
|
1284
|
+
<a class="card mini" href="https://lunora.sh/packages">
|
|
1285
|
+
<div class="mc">
|
|
1286
|
+
<span class="ic">
|
|
1287
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
|
1288
|
+
<path d="M12 2 3 7v10l9 5 9-5V7z" />
|
|
1289
|
+
<path d="M3 7l9 5 9-5M12 12v10" />
|
|
1290
|
+
</svg>
|
|
1291
|
+
</span>
|
|
1292
|
+
<h3>Cloudflare ecosystem</h3>
|
|
1293
|
+
<p>Auth, mail, storage, AI, payments — one deploy.</p>
|
|
1294
|
+
</div>
|
|
1295
|
+
<span class="arrow">
|
|
1296
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1297
|
+
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
1298
|
+
</svg>
|
|
1299
|
+
</span>
|
|
1300
|
+
</a>
|
|
1301
|
+
</div>
|
|
1302
|
+
</div>
|
|
1303
|
+
|
|
1304
|
+
<div class="lw-foot">Running on Lunora · Vite + Vanilla · <span id="lw-count">0</span> messages</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
\`;
|
|
1307
|
+
|
|
1308
|
+
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
1309
|
+
root.classList.add("lunora-welcome");
|
|
1310
|
+
root.innerHTML = welcomeHtml;
|
|
1311
|
+
|
|
1312
|
+
// Theme toggle: flip the root's data-theme + swap the button's icon/label.
|
|
1313
|
+
const toggleButton = root.querySelector<HTMLButtonElement>(".lw-toggle")!;
|
|
1314
|
+
|
|
1315
|
+
const paintToggle = (theme: "dark" | "light"): void => {
|
|
1316
|
+
root.dataset.theme = theme;
|
|
1317
|
+
toggleButton.innerHTML = \`\${theme === "light" ? MOON_ICON : SUN_ICON}<span>\${theme === "light" ? "Ivory" : "Night"}</span>\`;
|
|
311
1318
|
};
|
|
312
1319
|
|
|
1320
|
+
paintToggle(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
|
1321
|
+
toggleButton.addEventListener("click", () => {
|
|
1322
|
+
paintToggle(root.dataset.theme === "light" ? "dark" : "light");
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// Live demo: the message count of a demo channel re-renders on every delta.
|
|
1326
|
+
const count = document.querySelector<HTMLSpanElement>("#lw-count")!;
|
|
1327
|
+
|
|
1328
|
+
client.onUpdate(api.messages.list, { channelId: "channel:demo" }, (result) => {
|
|
1329
|
+
count.textContent = String(result.messages.length);
|
|
1330
|
+
});
|
|
1331
|
+
`;
|
|
1332
|
+
|
|
313
1333
|
const READ_URL = `const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;`;
|
|
314
1334
|
const REACT_MAIN = `import "./index.css";
|
|
315
1335
|
|
|
@@ -396,47 +1416,57 @@ const app = mount(Root, { target: document.getElementById("app")! });
|
|
|
396
1416
|
|
|
397
1417
|
export default app;
|
|
398
1418
|
`;
|
|
399
|
-
const VANILLA_MAIN = `import "./style.css";
|
|
400
|
-
|
|
401
|
-
import { LunoraClient } from "lunorash/client";
|
|
402
|
-
|
|
403
|
-
import { api } from "../lunora/_generated/api";
|
|
404
|
-
|
|
405
|
-
// Vanilla starter: no framework provider — talk to Lunora through the client
|
|
406
|
-
// directly. \`@lunora/vite\` runs the Worker on the same origin as Vite.
|
|
407
|
-
${READ_URL}
|
|
408
|
-
const client = new LunoraClient({ url });
|
|
409
|
-
|
|
410
|
-
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
411
|
-
|
|
412
|
-
const heading = document.createElement("h1");
|
|
413
|
-
heading.textContent = "Vite + Lunora";
|
|
414
|
-
|
|
415
|
-
const output = document.createElement("pre");
|
|
416
|
-
root.replaceChildren(heading, output);
|
|
417
|
-
|
|
418
|
-
const render = (messages: unknown): void => {
|
|
419
|
-
// textContent (not innerHTML) — never inject server data as markup.
|
|
420
|
-
output.textContent = JSON.stringify(messages, null, 2);
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
// Live subscription: the list re-renders on every server delta.
|
|
424
|
-
client.onUpdate(api.messages.list, { channelId: "channel:demo" }, render);
|
|
425
|
-
`;
|
|
426
1419
|
const ADAPTERS = {
|
|
427
|
-
react: {
|
|
428
|
-
|
|
1420
|
+
react: {
|
|
1421
|
+
adapter: "@lunora/react",
|
|
1422
|
+
createViteTemplate: "react-ts",
|
|
1423
|
+
files: [
|
|
1424
|
+
{ contents: REACT_MAIN, path: "src/main.tsx" },
|
|
1425
|
+
{ contents: REACT_APP, path: "src/App.tsx" },
|
|
1426
|
+
{ contents: WELCOME_CSS, path: "src/index.css" }
|
|
1427
|
+
],
|
|
1428
|
+
label: "React"
|
|
1429
|
+
},
|
|
1430
|
+
solid: {
|
|
1431
|
+
adapter: "@lunora/solid",
|
|
1432
|
+
createViteTemplate: "solid",
|
|
1433
|
+
files: [
|
|
1434
|
+
{ contents: SOLID_INDEX, path: "src/index.tsx" },
|
|
1435
|
+
{ contents: SOLID_APP, path: "src/App.tsx" },
|
|
1436
|
+
{ contents: WELCOME_CSS, path: "src/index.css" }
|
|
1437
|
+
],
|
|
1438
|
+
label: "Solid"
|
|
1439
|
+
},
|
|
429
1440
|
svelte: {
|
|
430
1441
|
adapter: "@lunora/svelte",
|
|
431
1442
|
createViteTemplate: "svelte-ts",
|
|
432
1443
|
files: [
|
|
433
1444
|
{ contents: SVELTE_ROOT, path: "src/Root.svelte" },
|
|
434
|
-
{ contents: SVELTE_MAIN, path: "src/main.ts" }
|
|
1445
|
+
{ contents: SVELTE_MAIN, path: "src/main.ts" },
|
|
1446
|
+
{ contents: SVELTE_APP, path: "src/App.svelte" },
|
|
1447
|
+
{ contents: WELCOME_CSS, path: "src/app.css" }
|
|
435
1448
|
],
|
|
436
1449
|
label: "Svelte"
|
|
437
1450
|
},
|
|
438
|
-
vanilla: {
|
|
439
|
-
|
|
1451
|
+
vanilla: {
|
|
1452
|
+
adapter: "lunorash/client",
|
|
1453
|
+
createViteTemplate: "vanilla-ts",
|
|
1454
|
+
files: [
|
|
1455
|
+
{ contents: VANILLA_MAIN, path: "src/main.ts" },
|
|
1456
|
+
{ contents: WELCOME_CSS, path: "src/style.css" }
|
|
1457
|
+
],
|
|
1458
|
+
label: "Vanilla"
|
|
1459
|
+
},
|
|
1460
|
+
vue: {
|
|
1461
|
+
adapter: "@lunora/vue",
|
|
1462
|
+
createViteTemplate: "vue-ts",
|
|
1463
|
+
files: [
|
|
1464
|
+
{ contents: VUE_MAIN, path: "src/main.ts" },
|
|
1465
|
+
{ contents: VUE_APP, path: "src/App.vue" },
|
|
1466
|
+
{ contents: WELCOME_CSS, path: "src/style.css" }
|
|
1467
|
+
],
|
|
1468
|
+
label: "Vue"
|
|
1469
|
+
}
|
|
440
1470
|
};
|
|
441
1471
|
const isOverlayFramework = (value) => Object.hasOwn(ADAPTERS, value);
|
|
442
1472
|
|
|
@@ -451,15 +1481,39 @@ export default defineSchema({
|
|
|
451
1481
|
.index("by_channel", ["channelId"]),
|
|
452
1482
|
});
|
|
453
1483
|
`;
|
|
454
|
-
const LUNORA_MESSAGES = `import {
|
|
1484
|
+
const LUNORA_MESSAGES = `import { RateLimiter, rateLimit } from "@lunora/ratelimit";
|
|
1485
|
+
|
|
1486
|
+
import { mutation, query, v } from "#lunora/_generated/server.js";
|
|
455
1487
|
|
|
456
|
-
|
|
457
|
-
|
|
1488
|
+
/**
|
|
1489
|
+
* One in-memory limiter so the public \`send\` mutation isn't an open flood target
|
|
1490
|
+
* out of the box. The default store is in-memory (per-isolate, resets on
|
|
1491
|
+
* eviction) — fine for a starter; run \`lunora add ratelimit\` for the durable,
|
|
1492
|
+
* \`ctx.db\`-backed store when you ship to production.
|
|
1493
|
+
*/
|
|
1494
|
+
const limiter = new RateLimiter({
|
|
1495
|
+
config: {
|
|
1496
|
+
send: { kind: "token bucket", period: 60_000, rate: 30 },
|
|
1497
|
+
},
|
|
458
1498
|
});
|
|
459
1499
|
|
|
460
|
-
export const
|
|
461
|
-
|
|
1500
|
+
export const list = query.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), limit: v.optional(v.number()) }).query(async ({ args, ctx }) => {
|
|
1501
|
+
const messages = await ctx.db
|
|
1502
|
+
.query("messages")
|
|
1503
|
+
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
|
1504
|
+
.take(args.limit ?? 50);
|
|
1505
|
+
|
|
1506
|
+
return { channelId: args.channelId, messages };
|
|
462
1507
|
});
|
|
1508
|
+
|
|
1509
|
+
export const send = mutation
|
|
1510
|
+
.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), text: v.string().meta({ schema: { maxLength: 4096 } }) })
|
|
1511
|
+
.use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId ?? "anon" }))
|
|
1512
|
+
.mutation(async ({ args, ctx }) => {
|
|
1513
|
+
const id = await ctx.db.insert("messages", { channelId: args.channelId, text: args.text });
|
|
1514
|
+
|
|
1515
|
+
return { channelId: args.channelId, id, text: args.text };
|
|
1516
|
+
});
|
|
463
1517
|
`;
|
|
464
1518
|
const SERVER_ENTRY = `import type { ShardNamespaceLike } from "lunorash/runtime";
|
|
465
1519
|
|
|
@@ -489,7 +1543,13 @@ const WRANGLER = `{
|
|
|
489
1543
|
"observability": { "enabled": true, "head_sampling_rate": 1 },
|
|
490
1544
|
}
|
|
491
1545
|
`;
|
|
492
|
-
const GITIGNORE_ADDITIONS = [".wrangler", ".lunora/", ".lunora-cache", "lunora/_generated"];
|
|
1546
|
+
const GITIGNORE_ADDITIONS = [".wrangler", ".env", ".env.*", "!.env.example", ".lunora/", ".lunora-cache", "lunora/_generated"];
|
|
1547
|
+
const ENV_EXAMPLE = `# Lunora endpoint for the browser client.
|
|
1548
|
+
# Vite statically replaces \`import.meta.env.VITE_LUNORA_URL\` at \`vite dev\` / build.
|
|
1549
|
+
# Leave it unset to use the page origin; set it to point at a deployed Worker:
|
|
1550
|
+
#
|
|
1551
|
+
# VITE_LUNORA_URL=https://my-app.example.workers.dev
|
|
1552
|
+
`;
|
|
493
1553
|
const COMMON_DEV_DEPENDENCIES = {
|
|
494
1554
|
"@cloudflare/workers-types": "^4.20260611.1",
|
|
495
1555
|
wrangler: "^4.100.0"
|
|
@@ -514,15 +1574,17 @@ const ensureGitignore = (target) => {
|
|
|
514
1574
|
${missing.join("\n")}
|
|
515
1575
|
`, "utf8");
|
|
516
1576
|
};
|
|
517
|
-
const
|
|
1577
|
+
const isLunoraDep$1 = (name) => name === "lunorash" || name.startsWith("@lunora/");
|
|
1578
|
+
const stampRange = (name, range, distTag, versions) => isLunoraDep$1(name) ? versions?.get(name) ?? distTag : range;
|
|
518
1579
|
const withDependency = (map, name, range, distTag) => {
|
|
519
1580
|
return { ...map, [name]: stampRange(name, range, distTag) };
|
|
520
1581
|
};
|
|
521
|
-
const restampLunora = (map, distTag) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag)]));
|
|
522
|
-
const patchPackageJson = (target, name, adapter, distTag) => {
|
|
1582
|
+
const restampLunora = (map, distTag, versions) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag, versions)]));
|
|
1583
|
+
const patchPackageJson = async (target, name, adapter, distTag) => {
|
|
523
1584
|
const path = join$1(target, "package.json");
|
|
524
1585
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
525
1586
|
let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
|
|
1587
|
+
dependencies = withDependency(dependencies, "@lunora/ratelimit", distTag, distTag);
|
|
526
1588
|
if (adapter.adapter.startsWith("@lunora/")) {
|
|
527
1589
|
dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
|
|
528
1590
|
}
|
|
@@ -530,12 +1592,16 @@ const patchPackageJson = (target, name, adapter, distTag) => {
|
|
|
530
1592
|
dependencies = withDependency(dependencies, depName, range, distTag);
|
|
531
1593
|
}
|
|
532
1594
|
let devDependencies = withDependency(parsed.devDependencies ?? {}, "@lunora/vite", distTag, distTag);
|
|
1595
|
+
devDependencies = withDependency(devDependencies, "@lunora/studio", distTag, distTag);
|
|
533
1596
|
for (const [depName, range] of Object.entries(COMMON_DEV_DEPENDENCIES)) {
|
|
534
1597
|
devDependencies = withDependency(devDependencies, depName, range, distTag);
|
|
535
1598
|
}
|
|
1599
|
+
const lunoraNames = [...Object.keys(dependencies), ...Object.keys(devDependencies)].filter((depName) => isLunoraDep$1(depName));
|
|
1600
|
+
const versions = await resolveTagVersions(lunoraNames, distTag);
|
|
536
1601
|
parsed.name = name;
|
|
537
|
-
parsed.
|
|
538
|
-
parsed.
|
|
1602
|
+
parsed.imports = { ...parsed.imports, "#lunora/*": "./lunora/*" };
|
|
1603
|
+
parsed.dependencies = restampLunora(dependencies, distTag, versions);
|
|
1604
|
+
parsed.devDependencies = restampLunora(devDependencies, distTag, versions);
|
|
539
1605
|
parsed.scripts = { ...parsed.scripts, codegen: "lunora codegen", deploy: "vite build && lunora deploy" };
|
|
540
1606
|
writeFileSync(path, `${JSON.stringify(parsed, void 0, 4)}
|
|
541
1607
|
`, "utf8");
|
|
@@ -551,22 +1617,123 @@ const patchBaseViteConfig = (target, logger) => {
|
|
|
551
1617
|
writeFileSync(candidate, result.code, "utf8");
|
|
552
1618
|
}
|
|
553
1619
|
};
|
|
554
|
-
const applyLunoraOverlay = (options) => {
|
|
1620
|
+
const applyLunoraOverlay = async (options) => {
|
|
555
1621
|
const { adapter, distTag, logger, name, target } = options;
|
|
556
1622
|
const written = [];
|
|
557
1623
|
writeFile(target, join$1("lunora", "schema.ts"), LUNORA_SCHEMA, written);
|
|
558
1624
|
writeFile(target, join$1("lunora", "messages.ts"), LUNORA_MESSAGES, written);
|
|
559
1625
|
writeFile(target, join$1("src", "server.ts"), SERVER_ENTRY, written);
|
|
560
1626
|
writeFile(target, "wrangler.jsonc", WRANGLER.replaceAll("__NAME__", name), written);
|
|
1627
|
+
writeFile(target, ".env.example", ENV_EXAMPLE, written);
|
|
561
1628
|
for (const file of adapter.files) {
|
|
562
1629
|
writeFile(target, file.path, file.contents, written);
|
|
563
1630
|
}
|
|
564
1631
|
patchBaseViteConfig(target, logger);
|
|
565
|
-
patchPackageJson(target, name, adapter, distTag);
|
|
1632
|
+
await patchPackageJson(target, name, adapter, distTag);
|
|
566
1633
|
ensureGitignore(target);
|
|
567
1634
|
return written;
|
|
568
1635
|
};
|
|
569
1636
|
|
|
1637
|
+
const ADJECTIVES = [
|
|
1638
|
+
"lunar",
|
|
1639
|
+
"silver",
|
|
1640
|
+
"silent",
|
|
1641
|
+
"waning",
|
|
1642
|
+
"waxing",
|
|
1643
|
+
"crescent",
|
|
1644
|
+
"cosmic",
|
|
1645
|
+
"stellar",
|
|
1646
|
+
"orbital",
|
|
1647
|
+
"gibbous",
|
|
1648
|
+
"twilight",
|
|
1649
|
+
"midnight",
|
|
1650
|
+
"shimmering",
|
|
1651
|
+
"drifting",
|
|
1652
|
+
"weightless"
|
|
1653
|
+
];
|
|
1654
|
+
const NOUNS = [
|
|
1655
|
+
"moon",
|
|
1656
|
+
"tide",
|
|
1657
|
+
"crater",
|
|
1658
|
+
"comet",
|
|
1659
|
+
"eclipse",
|
|
1660
|
+
"halo",
|
|
1661
|
+
"orbit",
|
|
1662
|
+
"nebula",
|
|
1663
|
+
"voyager",
|
|
1664
|
+
"lander",
|
|
1665
|
+
"rover",
|
|
1666
|
+
"beacon",
|
|
1667
|
+
"harbor",
|
|
1668
|
+
"meadow",
|
|
1669
|
+
"fox"
|
|
1670
|
+
];
|
|
1671
|
+
const pick = (items) => (
|
|
1672
|
+
// eslint-disable-next-line sonarjs/pseudo-random -- cosmetic default project name, not a security decision.
|
|
1673
|
+
items[Math.floor(Math.random() * items.length)]
|
|
1674
|
+
);
|
|
1675
|
+
const generateProjectName = () => `${pick(ADJECTIVES)}-${pick(NOUNS)}`;
|
|
1676
|
+
|
|
1677
|
+
const isOnline = async () => dns.lookup("github.com").then(
|
|
1678
|
+
() => true,
|
|
1679
|
+
() => false
|
|
1680
|
+
);
|
|
1681
|
+
const GITHUB_SOURCE = /^(?:gh|github):([^/]+)\/([^#/]+)(?:\/[^#]*)?(?:#(.+))?$/;
|
|
1682
|
+
const parseGitHubSource = (source) => {
|
|
1683
|
+
const match = GITHUB_SOURCE.exec(source);
|
|
1684
|
+
if (match === null) {
|
|
1685
|
+
return void 0;
|
|
1686
|
+
}
|
|
1687
|
+
const [, owner, repo, ref] = match;
|
|
1688
|
+
if (owner === void 0 || repo === void 0) {
|
|
1689
|
+
return void 0;
|
|
1690
|
+
}
|
|
1691
|
+
return { owner, ref: ref ?? "HEAD", repo };
|
|
1692
|
+
};
|
|
1693
|
+
const templateRefExists = async (source) => {
|
|
1694
|
+
const parsed = parseGitHubSource(source);
|
|
1695
|
+
if (parsed === void 0) {
|
|
1696
|
+
return void 0;
|
|
1697
|
+
}
|
|
1698
|
+
const url = `https://codeload.github.com/${parsed.owner}/${parsed.repo}/tar.gz/${parsed.ref}`;
|
|
1699
|
+
try {
|
|
1700
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
1701
|
+
if (response.status === 404) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
return response.ok ? true : void 0;
|
|
1705
|
+
} catch {
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
const verifyRemoteTemplate = async (params) => {
|
|
1710
|
+
if (params.isLocal) {
|
|
1711
|
+
return true;
|
|
1712
|
+
}
|
|
1713
|
+
const isGitHubBacked = params.source === void 0 || parseGitHubSource(params.source) !== void 0;
|
|
1714
|
+
if (!isGitHubBacked) {
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
if (!await isOnline()) {
|
|
1718
|
+
params.logger.error("you appear to be offline — connect to the internet and try again, or scaffold from a local template with `--from <dir>`.");
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
if (params.source !== void 0 && await templateRefExists(params.source) === false) {
|
|
1722
|
+
params.logger.error(`template source not found: ${params.source} — double-check --ref / --source, or browse the templates at https://lunora.sh/docs.`);
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
return true;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
const COPY = {
|
|
1729
|
+
extras: "Let's finish setting up your app.",
|
|
1730
|
+
framework: "Which framework should we launch?",
|
|
1731
|
+
git: "Initialize a new git repository? (optional)",
|
|
1732
|
+
install: "Install dependencies now?",
|
|
1733
|
+
name: "Where should we land your project?",
|
|
1734
|
+
nextHeader: "Liftoff confirmed — explore your project!",
|
|
1735
|
+
packageManager: "Which package manager?"
|
|
1736
|
+
};
|
|
570
1737
|
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
|
|
571
1738
|
const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
572
1739
|
const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
@@ -609,7 +1776,27 @@ const isTextFile = (filePath) => {
|
|
|
609
1776
|
};
|
|
610
1777
|
const substitute = (content, name) => content.replaceAll("{{name}}", name);
|
|
611
1778
|
const isLunoraDep = (name) => name === "lunorash" || name.startsWith("@lunora/");
|
|
612
|
-
const
|
|
1779
|
+
const resolveLunoraVersions = async (files, distTag) => {
|
|
1780
|
+
const names = /* @__PURE__ */ new Set();
|
|
1781
|
+
for (const file of files) {
|
|
1782
|
+
if (basename(file) !== "package.json") {
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
1787
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
1788
|
+
for (const name of Object.keys(parsed[section] ?? {})) {
|
|
1789
|
+
if (isLunoraDep(name)) {
|
|
1790
|
+
names.add(name);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return resolveTagVersions(names, distTag);
|
|
1798
|
+
};
|
|
1799
|
+
const stampLunoraDeps = (packageJsonText, distTag, versions) => {
|
|
613
1800
|
let parsed;
|
|
614
1801
|
try {
|
|
615
1802
|
parsed = JSON.parse(packageJsonText);
|
|
@@ -622,23 +1809,38 @@ const stampLunoraDeps = (packageJsonText, distTag) => {
|
|
|
622
1809
|
if (!isLunoraDep(name)) {
|
|
623
1810
|
continue;
|
|
624
1811
|
}
|
|
625
|
-
const
|
|
1812
|
+
const pin = versions.get(name) ?? distTag;
|
|
1813
|
+
const edits = modify(text, [section, name], pin, { formattingOptions: { insertSpaces: true, tabSize: 4 } });
|
|
626
1814
|
text = applyEdits(text, edits);
|
|
627
1815
|
}
|
|
628
1816
|
}
|
|
629
1817
|
return text;
|
|
630
1818
|
};
|
|
1819
|
+
const PNPM_BUILT_DEPENDENCIES = ["esbuild", "sharp", "workerd"];
|
|
1820
|
+
const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
|
|
1821
|
+
const pnpmWorkspaceYaml = () => [
|
|
1822
|
+
"# pnpm reads its settings from here (the package.json `pnpm` field is no longer read).",
|
|
1823
|
+
"# Pre-approve the toolchain's native build scripts so `pnpm install` runs them",
|
|
1824
|
+
"# without the interactive `pnpm approve-builds` step.",
|
|
1825
|
+
"allowBuilds:",
|
|
1826
|
+
...PNPM_BUILT_DEPENDENCIES.map((name) => ` ${name}: true`),
|
|
1827
|
+
""
|
|
1828
|
+
].join("\n");
|
|
631
1829
|
const collectFiles = (directory) => {
|
|
632
1830
|
const out = [];
|
|
633
1831
|
for (const entry of walkSync(directory, { includeDirs: false, includeFiles: true })) {
|
|
1832
|
+
if (lstatSync(entry.path).isSymbolicLink()) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
634
1835
|
out.push(entry.path);
|
|
635
1836
|
}
|
|
636
1837
|
return out;
|
|
637
1838
|
};
|
|
638
|
-
const copyTemplate = (sourceDirectory, target, name) => {
|
|
1839
|
+
const copyTemplate = async (sourceDirectory, target, name) => {
|
|
639
1840
|
const files = collectFiles(sourceDirectory);
|
|
640
1841
|
const written = [];
|
|
641
1842
|
const distTag = resolveDistTag();
|
|
1843
|
+
const versions = await resolveLunoraVersions(files, distTag);
|
|
642
1844
|
for (const source of files) {
|
|
643
1845
|
const relativePath = relative(sourceDirectory, source);
|
|
644
1846
|
const destination = join$1(target, relativePath);
|
|
@@ -646,7 +1848,7 @@ const copyTemplate = (sourceDirectory, target, name) => {
|
|
|
646
1848
|
const raw = readFileSync(source);
|
|
647
1849
|
let text = isTextFile(source) ? substitute(raw.toString("utf8"), name) : void 0;
|
|
648
1850
|
if (text !== void 0 && basename(source) === "package.json") {
|
|
649
|
-
text = stampLunoraDeps(text, distTag);
|
|
1851
|
+
text = stampLunoraDeps(text, distTag, versions);
|
|
650
1852
|
}
|
|
651
1853
|
if (text === void 0) {
|
|
652
1854
|
writeFileSync(destination, raw);
|
|
@@ -669,68 +1871,153 @@ const isSafeSource = (source) => {
|
|
|
669
1871
|
}
|
|
670
1872
|
return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
|
|
671
1873
|
};
|
|
1874
|
+
const logWould = (logger, action) => {
|
|
1875
|
+
logger.info(`[dry-run] would ${action}`);
|
|
1876
|
+
};
|
|
672
1877
|
const logScaffoldSuccess = (logger, written, target) => {
|
|
1878
|
+
if (isInteractive()) {
|
|
1879
|
+
process.stdout.write("\n");
|
|
1880
|
+
}
|
|
673
1881
|
logger.success(`scaffolded ${String(written.length)} files into ${target}`);
|
|
674
1882
|
};
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
1883
|
+
const installCommand = (manager, packages) => {
|
|
1884
|
+
const verb = manager === "npm" ? "install" : "add";
|
|
1885
|
+
return `${manager} ${verb} ${packages.join(" ")}`;
|
|
1886
|
+
};
|
|
1887
|
+
const isInsideMonorepo = (startDirectory) => {
|
|
1888
|
+
let directory = resolve(startDirectory);
|
|
1889
|
+
for (; ; ) {
|
|
1890
|
+
if (existsSync(join$1(directory, "pnpm-workspace.yaml"))) {
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
const packagePath = join$1(directory, "package.json");
|
|
1894
|
+
if (existsSync(packagePath)) {
|
|
1895
|
+
try {
|
|
1896
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1897
|
+
if (parsed.workspaces !== void 0) {
|
|
1898
|
+
return true;
|
|
1899
|
+
}
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const parent = dirname$1(directory);
|
|
1904
|
+
if (parent === directory) {
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
directory = parent;
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
const isInsideGitRepo = (startDirectory) => {
|
|
1911
|
+
let directory = resolve(startDirectory);
|
|
1912
|
+
for (; ; ) {
|
|
1913
|
+
if (existsSync(join$1(directory, ".git"))) {
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1916
|
+
const parent = dirname$1(directory);
|
|
1917
|
+
if (parent === directory) {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
directory = parent;
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
const maybeOfferGit = async (options, target) => {
|
|
1924
|
+
if (options.yes === true || !isInteractive() || isInsideGitRepo(dirname$1(target))) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (!await tuiConfirm(COPY.git, { badge: BADGES.git, defaultYes: false })) {
|
|
1928
|
+
await tuiInfo("Sounds good! You can always run git init manually.");
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (options.dryRun === true) {
|
|
1932
|
+
logWould(options.logger, "initialize a git repository");
|
|
1933
|
+
return;
|
|
678
1934
|
}
|
|
679
|
-
|
|
680
|
-
|
|
1935
|
+
const spawner = options.spawner ?? defaultSpawner;
|
|
1936
|
+
const result = await withTuiSpinner("Initializing a git repository…", () => spawner({ args: ["init"], command: "git", cwd: target }));
|
|
1937
|
+
if (result.code === 0) {
|
|
1938
|
+
await emitStep("git", "Initialized an empty git repository.");
|
|
1939
|
+
} else {
|
|
1940
|
+
options.logger.warn("`git init` failed — initialize it yourself later with `git init`.");
|
|
681
1941
|
}
|
|
682
|
-
return `${manager} ${script}`;
|
|
683
1942
|
};
|
|
684
|
-
const printNextSteps = (
|
|
1943
|
+
const printNextSteps = async (name, installed, insideMonorepo) => {
|
|
685
1944
|
const manager = installed ?? "pnpm";
|
|
686
|
-
|
|
687
|
-
logger.info(` cd ${name}`);
|
|
1945
|
+
const steps = [{ code: `cd ./${name}`, lead: "Enter your project directory using" }];
|
|
688
1946
|
if (installed === void 0) {
|
|
689
|
-
|
|
1947
|
+
steps.push({ code: `${manager} install`, lead: "Install dependencies with", tail: insideMonorepo ? " from the workspace root" : void 0 });
|
|
1948
|
+
}
|
|
1949
|
+
steps.push(
|
|
1950
|
+
{ code: runScriptCommand(manager, "dev"), lead: "Run", tail: " to start the dev server." },
|
|
1951
|
+
{ code: "lunora add", lead: "Add features like auth or storage using" }
|
|
1952
|
+
);
|
|
1953
|
+
const help = [
|
|
1954
|
+
{ code: "https://lunora.sh/docs", lead: "Read the docs at" },
|
|
1955
|
+
{ code: "https://lunora.sh/chat", lead: "Stuck? Join the chat at" }
|
|
1956
|
+
];
|
|
1957
|
+
if (isInteractive()) {
|
|
1958
|
+
await tuiNextSteps(BADGES.next, COPY.nextHeader, steps, help);
|
|
1959
|
+
return;
|
|
690
1960
|
}
|
|
691
|
-
|
|
1961
|
+
const lines = steps.map((step) => `${step.lead} ${step.code}${step.tail ?? ""}`);
|
|
1962
|
+
lines.push("", ...help.map((line) => `${line.lead} ${line.code}${line.tail ?? ""}`));
|
|
1963
|
+
await emitStep("next", COPY.nextHeader, lines.join("\n"));
|
|
692
1964
|
};
|
|
693
1965
|
const offerInstallIsInteractive = (options) => options.yes !== true && (options.installPrompt !== void 0 || isInteractive());
|
|
694
1966
|
const maybeOfferInstall = async (options, target) => {
|
|
695
1967
|
if (!offerInstallIsInteractive(options)) {
|
|
696
1968
|
return void 0;
|
|
697
1969
|
}
|
|
1970
|
+
if (isInsideMonorepo(dirname$1(target))) {
|
|
1971
|
+
return void 0;
|
|
1972
|
+
}
|
|
698
1973
|
const managers = detectInstalledManagers(options.packageManagerProbe);
|
|
699
1974
|
const [defaultManager] = managers;
|
|
700
1975
|
if (defaultManager === void 0) {
|
|
701
1976
|
return void 0;
|
|
702
1977
|
}
|
|
703
|
-
const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm(
|
|
1978
|
+
const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm(COPY.install, { badge: BADGES.deps, defaultYes: true }));
|
|
704
1979
|
if (!await confirm()) {
|
|
1980
|
+
await tuiInfo("No problem! Remember to install dependencies after setup.");
|
|
705
1981
|
return void 0;
|
|
706
1982
|
}
|
|
707
1983
|
let manager = defaultManager;
|
|
708
1984
|
if (managers.length > 1) {
|
|
709
1985
|
manager = options.installPrompt ? await options.installPrompt.selectManager(managers) : await tuiSelect(
|
|
710
|
-
|
|
1986
|
+
COPY.packageManager,
|
|
711
1987
|
managers.map((candidate) => {
|
|
712
1988
|
return { label: candidate, value: candidate };
|
|
713
1989
|
}),
|
|
714
|
-
{ default: defaultManager }
|
|
1990
|
+
{ badge: BADGES.deps, default: defaultManager }
|
|
715
1991
|
) ?? defaultManager;
|
|
716
1992
|
}
|
|
1993
|
+
if (options.dryRun === true) {
|
|
1994
|
+
logWould(options.logger, `install dependencies with ${manager}`);
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
if (manager === "pnpm") {
|
|
1998
|
+
const workspacePath = join$1(target, PNPM_WORKSPACE_FILENAME);
|
|
1999
|
+
if (!existsSync(workspacePath)) {
|
|
2000
|
+
writeFileSync(workspacePath, pnpmWorkspaceYaml(), "utf8");
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
717
2003
|
const spawner = options.spawner ?? defaultSpawner;
|
|
718
2004
|
const { args, command } = installArgsFor(manager);
|
|
719
|
-
|
|
2005
|
+
await emitStep("deps", `Installing dependencies with ${manager}…`);
|
|
2006
|
+
const result = await spawner({ args, command, cwd: target });
|
|
720
2007
|
if (result.code !== 0) {
|
|
721
2008
|
options.logger.warn(`\`${command} install\` exited with code ${String(result.code)} — run it yourself in ${basename(target)}/.`);
|
|
722
2009
|
return void 0;
|
|
723
2010
|
}
|
|
724
|
-
|
|
2011
|
+
await emitStep("deps", `Dependencies installed with ${manager}.`);
|
|
725
2012
|
return manager;
|
|
726
2013
|
};
|
|
727
|
-
const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
2014
|
+
const scaffoldFromLocal = async (fromRoot, templateType, target, name, logger) => {
|
|
728
2015
|
const templateDirectory = join$1(fromRoot, templateType);
|
|
729
2016
|
if (!existsSync(templateDirectory)) {
|
|
730
2017
|
logger.error(`template not found in local source: ${templateDirectory}`);
|
|
731
2018
|
return { code: 1, files: [], target };
|
|
732
2019
|
}
|
|
733
|
-
const written = copyTemplate(templateDirectory, target, name);
|
|
2020
|
+
const written = await copyTemplate(templateDirectory, target, name);
|
|
734
2021
|
logScaffoldSuccess(logger, written, target);
|
|
735
2022
|
return { code: 0, files: written, target };
|
|
736
2023
|
};
|
|
@@ -739,30 +2026,55 @@ const scaffoldFromRemote = async (options) => {
|
|
|
739
2026
|
const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-init-fetch-"));
|
|
740
2027
|
const stagingDirectory = join$1(stagingRoot, "template");
|
|
741
2028
|
try {
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
2029
|
+
const pinnedRef = source !== void 0 && source.length > 0 ? ref : await resolvePinnedSourceRef(ref, logger);
|
|
2030
|
+
const remote = resolveTemplateSource(templateType, source, pinnedRef);
|
|
2031
|
+
let downloaded;
|
|
2032
|
+
let written = [];
|
|
2033
|
+
await tuiTasks(
|
|
2034
|
+
[
|
|
2035
|
+
{
|
|
2036
|
+
label: `${templateType} template fetched`,
|
|
2037
|
+
run: async () => {
|
|
2038
|
+
downloaded = await downloadTemplate(remote, {
|
|
2039
|
+
cwd: stagingRoot,
|
|
2040
|
+
dir: stagingDirectory,
|
|
2041
|
+
force: true,
|
|
2042
|
+
install: false,
|
|
2043
|
+
silent: true
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
label: `files copied into ${name}/`,
|
|
2049
|
+
run: async () => {
|
|
2050
|
+
written = await copyTemplate(stagingDirectory, target, name);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
],
|
|
2054
|
+
{ end: "Project initialized!", start: "Project initializing…" }
|
|
752
2055
|
);
|
|
753
2056
|
const staged = collectFiles(stagingDirectory);
|
|
2057
|
+
if (isInteractive()) {
|
|
2058
|
+
process.stdout.write("\n");
|
|
2059
|
+
}
|
|
754
2060
|
logger.info(
|
|
755
|
-
downloaded
|
|
756
|
-
);
|
|
757
|
-
const written = await withTuiSpinner(
|
|
758
|
-
`Scaffolding ${String(staged.length)} files into ${name}/…`,
|
|
759
|
-
() => Promise.resolve(copyTemplate(stagingDirectory, target, name))
|
|
2061
|
+
downloaded?.commit ? `template: ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} files)` : `template: ${downloaded?.source ?? remote} (${String(staged.length)} files)`
|
|
760
2062
|
);
|
|
761
2063
|
logScaffoldSuccess(logger, written, target);
|
|
762
2064
|
return { code: 0, files: written, target };
|
|
763
2065
|
} catch (error) {
|
|
764
|
-
|
|
765
|
-
|
|
2066
|
+
if (error instanceof PromptCancelledError) {
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
const { hints, message } = describeDownloadFailure(error, {
|
|
2070
|
+
ref: resolveSourceRef(ref),
|
|
2071
|
+
remote: resolveTemplateSource(templateType, source, ref),
|
|
2072
|
+
templateType
|
|
2073
|
+
});
|
|
2074
|
+
logger.error(message);
|
|
2075
|
+
for (const hint of hints) {
|
|
2076
|
+
logger.warn(hint);
|
|
2077
|
+
}
|
|
766
2078
|
return { code: 1, files: [], target };
|
|
767
2079
|
} finally {
|
|
768
2080
|
rmSync(stagingRoot, { force: true, recursive: true });
|
|
@@ -781,30 +2093,44 @@ const scaffoldViteOverlay = async (options) => {
|
|
|
781
2093
|
const adapter = ADAPTERS[framework];
|
|
782
2094
|
const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-vite-base-"));
|
|
783
2095
|
try {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
await withTuiSpinner(
|
|
788
|
-
`Fetching the ${adapter.label} (create-vite) base…`,
|
|
789
|
-
() => downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true })
|
|
790
|
-
);
|
|
791
|
-
renameCreateViteDotfiles(stagingDirectory);
|
|
792
|
-
cpSync(stagingDirectory, target, { recursive: true });
|
|
793
|
-
} else {
|
|
794
|
-
const localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
|
|
2096
|
+
let localBase;
|
|
2097
|
+
if (overlayBaseFrom !== void 0) {
|
|
2098
|
+
localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
|
|
795
2099
|
if (!existsSync(localBase)) {
|
|
796
2100
|
logger.error(`create-vite base not found on disk: ${localBase}`);
|
|
797
2101
|
return { code: 1, files: [], target };
|
|
798
2102
|
}
|
|
799
|
-
cpSync(localBase, target, { recursive: true });
|
|
800
2103
|
}
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
2104
|
+
const copyBase = async () => {
|
|
2105
|
+
if (localBase !== void 0) {
|
|
2106
|
+
cpSync(localBase, target, { recursive: true });
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const stagingDirectory = join$1(stagingRoot, "base");
|
|
2110
|
+
const remote = `github:vitejs/vite/packages/create-vite/template-${adapter.createViteTemplate}#main`;
|
|
2111
|
+
await downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true });
|
|
2112
|
+
renameCreateViteDotfiles(stagingDirectory);
|
|
2113
|
+
cpSync(stagingDirectory, target, { recursive: true });
|
|
2114
|
+
};
|
|
2115
|
+
let written = [];
|
|
2116
|
+
await tuiTasks(
|
|
2117
|
+
[
|
|
2118
|
+
{ label: `create-vite (${adapter.label}) base ready`, run: copyBase },
|
|
2119
|
+
{
|
|
2120
|
+
label: `Lunora overlay applied (${adapter.label})`,
|
|
2121
|
+
run: async () => {
|
|
2122
|
+
written = await applyLunoraOverlay({ adapter, distTag: resolveDistTag(), logger, name, target });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
],
|
|
2126
|
+
{ end: "Project initialized!", start: "Project initializing…" }
|
|
804
2127
|
);
|
|
805
2128
|
logScaffoldSuccess(logger, written, target);
|
|
806
2129
|
return { code: 0, files: [...collectFiles(target)], target };
|
|
807
2130
|
} catch (error) {
|
|
2131
|
+
if (error instanceof PromptCancelledError) {
|
|
2132
|
+
throw error;
|
|
2133
|
+
}
|
|
808
2134
|
const message = error instanceof Error ? error.message : String(error);
|
|
809
2135
|
logger.error(`failed to scaffold the ${adapter.label} base: ${message}`);
|
|
810
2136
|
return { code: 1, files: [], target };
|
|
@@ -872,12 +2198,12 @@ const scaffoldLunoraDirectory = (cwd, logger) => {
|
|
|
872
2198
|
}
|
|
873
2199
|
return written;
|
|
874
2200
|
};
|
|
875
|
-
const printFrameworkNextSteps = (detection, logger) => {
|
|
2201
|
+
const printFrameworkNextSteps = (detection, manager, logger) => {
|
|
876
2202
|
const { adapter, class: frameworkClass, framework } = detection;
|
|
877
2203
|
logger.info("");
|
|
878
2204
|
logger.info(`detected framework: ${framework} (class ${frameworkClass})`);
|
|
879
2205
|
logger.info("next steps:");
|
|
880
|
-
logger.info(` 1. install the adapter:
|
|
2206
|
+
logger.info(` 1. install the adapter: ${installCommand(manager, [adapter, "@lunora/client", "@lunora/runtime", "@lunora/server"])}`);
|
|
881
2207
|
logger.info(" 2. run codegen: lunora codegen");
|
|
882
2208
|
if (frameworkClass === "A") {
|
|
883
2209
|
logger.info(" 3. compose one worker: wrap your worker entry with");
|
|
@@ -923,48 +2249,77 @@ const runInPlaceInit = (cwd, logger) => {
|
|
|
923
2249
|
return viteResult;
|
|
924
2250
|
}
|
|
925
2251
|
const scaffolded = scaffoldLunoraDirectory(cwd, logger);
|
|
926
|
-
printFrameworkNextSteps(detection, logger);
|
|
2252
|
+
printFrameworkNextSteps(detection, detectPackageManager(cwd), logger);
|
|
927
2253
|
return { code: 0, files: [...viteResult.files, ...scaffolded], target: cwd };
|
|
928
2254
|
};
|
|
929
2255
|
const offerIsInteractive = (options) => options.yes !== true && (options.prompt !== void 0 || (options.interactive ?? isInteractive()));
|
|
930
2256
|
const maybeOfferExtras = async (options, projectDirectory) => {
|
|
931
2257
|
const interactive = offerIsInteractive(options);
|
|
932
|
-
const
|
|
2258
|
+
const preselected = options.add === void 0 ? [] : parseFeatureList(options.add, (message) => {
|
|
2259
|
+
options.logger.warn(message);
|
|
2260
|
+
});
|
|
2261
|
+
const applyAll = async (plans) => {
|
|
2262
|
+
if (plans.length === 0) {
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
if (options.dryRun === true) {
|
|
2266
|
+
logWould(options.logger, `add ${plans.map((plan) => plan.label).join(", ")}`);
|
|
2267
|
+
return true;
|
|
2268
|
+
}
|
|
2269
|
+
const buffered = [];
|
|
933
2270
|
const applyLogger = isInteractive() ? {
|
|
934
2271
|
error: (message) => {
|
|
935
|
-
|
|
2272
|
+
buffered.push({ level: "error", message });
|
|
936
2273
|
},
|
|
937
2274
|
info: () => {
|
|
938
2275
|
},
|
|
939
2276
|
success: () => {
|
|
940
2277
|
},
|
|
941
2278
|
warn: (message) => {
|
|
942
|
-
|
|
2279
|
+
buffered.push({ level: "warn", message });
|
|
943
2280
|
}
|
|
944
2281
|
} : options.logger;
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
2282
|
+
const steps = plans.map((plan) => {
|
|
2283
|
+
return {
|
|
2284
|
+
running: `adding ${plan.label}…`,
|
|
2285
|
+
task: () => runAddCommand({
|
|
2286
|
+
allowUnsafeSource: options.allowUnsafeSource,
|
|
2287
|
+
cwd: projectDirectory,
|
|
2288
|
+
from: options.registryFrom,
|
|
2289
|
+
logger: applyLogger,
|
|
2290
|
+
names: [...plan.names],
|
|
2291
|
+
ref: options.ref,
|
|
2292
|
+
source: options.registrySource,
|
|
2293
|
+
transformManifest: plan.transformManifest,
|
|
2294
|
+
yes: true
|
|
2295
|
+
})
|
|
2296
|
+
};
|
|
2297
|
+
});
|
|
2298
|
+
const done = `added ${plans.map((plan) => plan.label).join(", ")}`;
|
|
2299
|
+
const results = await withTuiBadgeProgress(BADGES.add, steps, done);
|
|
2300
|
+
for (const { level, message } of buffered) {
|
|
2301
|
+
options.logger[level](message);
|
|
2302
|
+
}
|
|
2303
|
+
return results.every((result) => result.code === 0);
|
|
959
2304
|
};
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
apply,
|
|
2305
|
+
const deps = {
|
|
2306
|
+
applyAll,
|
|
963
2307
|
interactive,
|
|
964
2308
|
logger: options.logger,
|
|
965
|
-
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
|
|
966
|
-
|
|
967
|
-
|
|
2309
|
+
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, { ...settings, badge: BADGES.add })),
|
|
2310
|
+
preselected: preselected.length > 0 ? preselected : void 0,
|
|
2311
|
+
projectName: basename(projectDirectory),
|
|
2312
|
+
select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, { ...settings, badge: BADGES.add })),
|
|
2313
|
+
text: options.prompt?.text ?? ((message, settings) => tuiText(message, { ...settings, badge: BADGES.add }))
|
|
2314
|
+
};
|
|
2315
|
+
if (preselected.length > 0) {
|
|
2316
|
+
await offerRegistryExtras(deps);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (isInteractive()) {
|
|
2320
|
+
await tuiHeadline(COPY.extras);
|
|
2321
|
+
}
|
|
2322
|
+
await offerRegistryExtras(deps);
|
|
968
2323
|
};
|
|
969
2324
|
const DEFAULT_FRAMEWORK = "react";
|
|
970
2325
|
const FRAMEWORK_CHOICES = [
|
|
@@ -994,7 +2349,7 @@ const resolveScaffoldChoice = async (options) => {
|
|
|
994
2349
|
if (!isInteractive() || options.yes === true) {
|
|
995
2350
|
return { framework: DEFAULT_FRAMEWORK, kind: "overlay" };
|
|
996
2351
|
}
|
|
997
|
-
return toScaffoldChoice(await tuiSelect(
|
|
2352
|
+
return toScaffoldChoice(await tuiSelect(COPY.framework, FRAMEWORK_CHOICES, { badge: BADGES.tmpl, default: DEFAULT_FRAMEWORK }) ?? DEFAULT_FRAMEWORK);
|
|
998
2353
|
};
|
|
999
2354
|
const nonInteractiveInitError = (options) => {
|
|
1000
2355
|
if (isInteractive() || options.yes === true) {
|
|
@@ -1012,65 +2367,137 @@ const nonInteractiveInitError = (options) => {
|
|
|
1012
2367
|
}
|
|
1013
2368
|
return `lunora init can't prompt in a non-interactive terminal — provide ${missing.join(" and ")}, or pass --yes to accept the defaults.`;
|
|
1014
2369
|
};
|
|
1015
|
-
const
|
|
1016
|
-
|
|
2370
|
+
const scaffoldOverlayPath = async (options, framework, name, target) => {
|
|
2371
|
+
if (!isOverlayFramework(framework)) {
|
|
2372
|
+
options.logger.error(`init: unknown framework "${framework}". Supported overlays: ${Object.keys(ADAPTERS).join(", ")}.`);
|
|
2373
|
+
return { code: 1, files: [], target };
|
|
2374
|
+
}
|
|
2375
|
+
if (!await verifyRemoteTemplate({ isLocal: options.overlayBaseFrom !== void 0, logger: options.logger })) {
|
|
2376
|
+
return { code: 1, files: [], target };
|
|
2377
|
+
}
|
|
2378
|
+
mkdirSync(target, { recursive: true });
|
|
2379
|
+
return scaffoldViteOverlay({ framework, logger: options.logger, name, overlayBaseFrom: options.overlayBaseFrom, target });
|
|
2380
|
+
};
|
|
2381
|
+
const scaffoldTemplatePath = async (options, templateType, name, target) => {
|
|
2382
|
+
if (templateType === "next") {
|
|
2383
|
+
options.logger.warn('template "next" is not yet available — re-run with `--vite react` or `-t standalone`.');
|
|
2384
|
+
return { code: 1, files: [], target };
|
|
2385
|
+
}
|
|
2386
|
+
if (options.from !== void 0) {
|
|
2387
|
+
return await scaffoldFromLocal(options.from, templateType, target, name, options.logger);
|
|
2388
|
+
}
|
|
2389
|
+
if (options.source !== void 0 && options.source.length > 0 && !options.allowUnsafeSource && !isSafeSource(options.source)) {
|
|
2390
|
+
options.logger.error(
|
|
2391
|
+
`init: refusing --source ${options.source} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`
|
|
2392
|
+
);
|
|
2393
|
+
return { code: 1, files: [], target };
|
|
2394
|
+
}
|
|
2395
|
+
if (!await verifyRemoteTemplate({ isLocal: false, logger: options.logger, source: resolveTemplateSource(templateType, options.source, options.ref) })) {
|
|
2396
|
+
return { code: 1, files: [], target };
|
|
2397
|
+
}
|
|
2398
|
+
return scaffoldFromRemote({ logger: options.logger, name, ref: options.ref, source: options.source, target, templateType });
|
|
2399
|
+
};
|
|
2400
|
+
const scaffoldNewProject = async (options, cwd, recordTarget) => {
|
|
2401
|
+
await tuiMoonrise("realtime backend on Cloudflare Workers + Durable Objects");
|
|
1017
2402
|
const blocked = nonInteractiveInitError(options);
|
|
1018
2403
|
if (blocked !== void 0) {
|
|
1019
2404
|
options.logger.error(blocked);
|
|
1020
2405
|
return { code: 1, files: [], target: "" };
|
|
1021
2406
|
}
|
|
1022
|
-
const
|
|
2407
|
+
const suggestedName = generateProjectName();
|
|
2408
|
+
const rawName = options.name ?? await tuiText(COPY.name, { badge: BADGES.dir, default: suggestedName, placeholder: suggestedName });
|
|
1023
2409
|
const choice = await resolveScaffoldChoice(options);
|
|
2410
|
+
const name = rawName.trim();
|
|
2411
|
+
if (name.length === 0) {
|
|
2412
|
+
options.logger.error(`init: refusing an empty project name — pass a directory name (e.g. \`lunora init my-app\`).`);
|
|
2413
|
+
return { code: 1, files: [], target: "" };
|
|
2414
|
+
}
|
|
1024
2415
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
1025
2416
|
options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
|
|
1026
2417
|
return { code: 1, files: [], target: "" };
|
|
1027
2418
|
}
|
|
1028
2419
|
const target = resolve(cwd, name);
|
|
1029
|
-
|
|
2420
|
+
const targetPreExisted = existsSync(target);
|
|
2421
|
+
if (targetPreExisted) {
|
|
1030
2422
|
const entries = readdirSync(target);
|
|
1031
2423
|
if (entries.length > 0) {
|
|
1032
2424
|
options.logger.error(`target directory not empty: ${target}`);
|
|
1033
2425
|
return { code: 1, files: [], target };
|
|
1034
2426
|
}
|
|
1035
2427
|
}
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
2428
|
+
if (options.dryRun === true) {
|
|
2429
|
+
const what = choice.kind === "overlay" ? `the ${choice.framework} create-vite overlay` : `the ${choice.templateType} template`;
|
|
2430
|
+
logWould(options.logger, `scaffold ${what} into ${target}`);
|
|
2431
|
+
return { code: 0, files: [], target };
|
|
2432
|
+
}
|
|
2433
|
+
recordTarget(target, targetPreExisted);
|
|
2434
|
+
return choice.kind === "overlay" ? scaffoldOverlayPath(options, choice.framework, name, target) : scaffoldTemplatePath(options, choice.templateType, name, target);
|
|
2435
|
+
};
|
|
2436
|
+
const resetScaffoldOnCancel = (cleanup, logger) => {
|
|
2437
|
+
const { target, targetPreExisted } = cleanup;
|
|
2438
|
+
if (target === void 0 || !existsSync(target)) {
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
if (targetPreExisted === true) {
|
|
2442
|
+
for (const entry of readdirSync(target)) {
|
|
2443
|
+
rmSync(join$1(target, entry), { force: true, recursive: true });
|
|
1040
2444
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
2445
|
+
} else {
|
|
2446
|
+
rmSync(target, { force: true, recursive: true });
|
|
1043
2447
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
2448
|
+
logger.info(`removed the partially-created project at ${target}`);
|
|
2449
|
+
};
|
|
2450
|
+
const runScaffoldStep = async (options, cwd, recordTarget) => {
|
|
2451
|
+
if (options.inPlace !== true) {
|
|
2452
|
+
return scaffoldNewProject(options, cwd, recordTarget);
|
|
1048
2453
|
}
|
|
1049
|
-
if (options.
|
|
1050
|
-
|
|
2454
|
+
if (options.dryRun === true) {
|
|
2455
|
+
logWould(options.logger, `configure Lunora into ${cwd}`);
|
|
2456
|
+
return { code: 0, files: [], target: cwd };
|
|
1051
2457
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
2458
|
+
return runInPlaceInit(cwd, options.logger);
|
|
2459
|
+
};
|
|
2460
|
+
const runPostScaffold = async (options, result, cwd) => {
|
|
2461
|
+
await maybeOfferExtras(options, result.target);
|
|
2462
|
+
const installedManager = options.inPlace === true ? void 0 : await maybeOfferInstall(options, result.target);
|
|
2463
|
+
if (options.inPlace !== true) {
|
|
2464
|
+
await maybeOfferGit(options, result.target);
|
|
2465
|
+
await printNextSteps(basename(result.target), installedManager, isInsideMonorepo(cwd));
|
|
2466
|
+
await emitMascot(options.logger);
|
|
1057
2467
|
}
|
|
1058
|
-
|
|
2468
|
+
};
|
|
2469
|
+
const scaffoldCiPipeline = (options, result, cwd) => {
|
|
2470
|
+
if (result.code !== 0 || options.ci === void 0) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
if (options.dryRun === true) {
|
|
2474
|
+
logWould(options.logger, `scaffold a ${options.ci} CI deploy pipeline`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
|
|
1059
2478
|
};
|
|
1060
2479
|
const runInitCommand = async (options) => {
|
|
1061
2480
|
const cwd = options.cwd ?? process.cwd();
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
2481
|
+
const cleanup = {};
|
|
2482
|
+
let result;
|
|
2483
|
+
try {
|
|
2484
|
+
result = await runScaffoldStep(options, cwd, (target, preExisted) => {
|
|
2485
|
+
cleanup.target = target;
|
|
2486
|
+
cleanup.targetPreExisted = preExisted;
|
|
2487
|
+
});
|
|
2488
|
+
if (result.code === 0 && result.target !== "") {
|
|
2489
|
+
cleanup.target = void 0;
|
|
2490
|
+
await runPostScaffold(options, result, cwd);
|
|
1069
2491
|
}
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
if (error instanceof PromptCancelledError) {
|
|
2494
|
+
resetScaffoldOnCancel(cleanup, options.logger);
|
|
2495
|
+
process.stdout.write("\n ✖ Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n");
|
|
2496
|
+
return { code: 130, files: [], target: "" };
|
|
2497
|
+
}
|
|
2498
|
+
throw error;
|
|
1070
2499
|
}
|
|
1071
|
-
|
|
1072
|
-
scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
|
|
1073
|
-
}
|
|
2500
|
+
scaffoldCiPipeline(options, result, cwd);
|
|
1074
2501
|
return result;
|
|
1075
2502
|
};
|
|
1076
2503
|
const isTemplate = (value) => value === "analog" || value === "astro" || value === "next" || value === "nuxt" || value === "react-router" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid";
|
|
@@ -1087,9 +2514,11 @@ const resolveCiProvider = (raw, logger) => {
|
|
|
1087
2514
|
const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
1088
2515
|
const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
|
|
1089
2516
|
return runInitCommand({
|
|
2517
|
+
add: options.add,
|
|
1090
2518
|
allowUnsafeSource: options.allowUnsafeSource === true,
|
|
1091
2519
|
cwd,
|
|
1092
2520
|
ci: resolveCiProvider(options.ci, logger),
|
|
2521
|
+
dryRun: options.dryRun === true,
|
|
1093
2522
|
from: options.from,
|
|
1094
2523
|
inPlace: options.here === true,
|
|
1095
2524
|
interactive: options.interactive === true ? true : void 0,
|