@lukoweb/apitogo 0.2.41 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +330 -37
- package/dist/declarations/cli/platform/tools/lifecycle.d.ts +4 -0
- package/package.json +1 -1
- package/src/config/v2/full-server.ts +187 -0
- package/src/config/v2/manifest.ts +472 -452
- package/src/config/v2/services.ts +56 -20
- package/src/config/v2/wrapper.ts +60 -4
- package/src/vite/watch-ignore.ts +80 -70
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
} from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { ApitogoError } from "../../lib/util/invariant.js";
|
|
12
|
+
import {
|
|
13
|
+
FULL_SERVER_HEALTH_PATH,
|
|
14
|
+
FULL_SERVER_RUNTIME,
|
|
15
|
+
FULL_SERVER_RUNTIME_VERSION,
|
|
16
|
+
FULL_SERVER_SOURCE,
|
|
17
|
+
FULL_SPEC,
|
|
18
|
+
fullServerFiles,
|
|
19
|
+
} from "./full-server.js";
|
|
12
20
|
import { docsFromPaths, starterPage } from "./init.js";
|
|
13
21
|
import { detectExistingDocs } from "./layout.js";
|
|
14
22
|
import { loadConfigV2 } from "./load.js";
|
|
@@ -129,8 +137,11 @@ export type AddOptions = {
|
|
|
129
137
|
disabled?: boolean;
|
|
130
138
|
/** Include every docs folder found instead of asking. */
|
|
131
139
|
acceptAllDocs?: boolean;
|
|
132
|
-
/**
|
|
133
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Content to create: `blank` is an empty shell, `starter` a filled example (portal, gateway and plans), and `full`
|
|
142
|
+
* the starter plus a backend the platform runs, so the first deploy works without further steps.
|
|
143
|
+
*/
|
|
144
|
+
template?: "blank" | "starter" | "full";
|
|
134
145
|
portal?: {
|
|
135
146
|
name?: string;
|
|
136
147
|
docs?: string[] | false;
|
|
@@ -176,6 +187,10 @@ export type AddOptions = {
|
|
|
176
187
|
|
|
177
188
|
export type PlannedFile = { path: string; content: string };
|
|
178
189
|
|
|
190
|
+
/** The templates that fill the new files with examples. */
|
|
191
|
+
const isFilled = (template: AddOptions["template"]) =>
|
|
192
|
+
template === "starter" || template === "full";
|
|
193
|
+
|
|
179
194
|
export type ServiceEdit = {
|
|
180
195
|
service: ServiceName;
|
|
181
196
|
action: "add" | "remove";
|
|
@@ -397,7 +412,7 @@ async function buildPortal(
|
|
|
397
412
|
const pages = path.join(resolved.root, "portal", "pages");
|
|
398
413
|
if (template === "blank")
|
|
399
414
|
await addFile(context, path.join(pages, ".gitkeep"), "");
|
|
400
|
-
else if (options.template
|
|
415
|
+
else if (isFilled(options.template)) {
|
|
401
416
|
// The starter has a full home page of its own, so the docs start at /introduction, where the page links to.
|
|
402
417
|
await addFile(
|
|
403
418
|
context,
|
|
@@ -447,20 +462,30 @@ async function buildPortal(
|
|
|
447
462
|
|
|
448
463
|
/**
|
|
449
464
|
* A managed gateway runs the code in its source folder, and a new project has none. The folder is made (a `.gitkeep`
|
|
450
|
-
* keeps it in git) and the user is told it is empty
|
|
465
|
+
* keeps it in git) and the user is told it is empty; the full template writes a small Node.js server there instead.
|
|
466
|
+
* A folder that has files is left as it is. Returns whether the sample server was written.
|
|
451
467
|
*/
|
|
452
468
|
async function createSourceFolder(
|
|
453
469
|
context: Context,
|
|
454
470
|
sourcePath: string,
|
|
455
|
-
|
|
471
|
+
runtime: Runtime,
|
|
472
|
+
): Promise<boolean> {
|
|
456
473
|
const folder = path.join(context.dir, sourcePath);
|
|
457
474
|
const entries = await readdir(folder).catch(() => [] as string[]);
|
|
458
|
-
if (entries.some((name) => name !== ".gitkeep")) return;
|
|
475
|
+
if (entries.some((name) => name !== ".gitkeep")) return false;
|
|
476
|
+
if (context.options.template === "full" && runtime === FULL_SERVER_RUNTIME) {
|
|
477
|
+
for (const [file, content] of Object.entries(
|
|
478
|
+
fullServerFiles(context.resolved.project.name),
|
|
479
|
+
))
|
|
480
|
+
await addFile(context, path.join(folder, file), content);
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
459
483
|
if (entries.length === 0)
|
|
460
484
|
await addFile(context, path.join(folder, ".gitkeep"), "");
|
|
461
485
|
context.notes.push(
|
|
462
486
|
`The folder ${sourcePath}/ is empty: put your backend code there before you deploy.`,
|
|
463
487
|
);
|
|
488
|
+
return false;
|
|
464
489
|
}
|
|
465
490
|
|
|
466
491
|
async function buildGateway(
|
|
@@ -469,9 +494,11 @@ async function buildGateway(
|
|
|
469
494
|
const { options, dir, resolved } = context;
|
|
470
495
|
const o = options.gateway ?? {};
|
|
471
496
|
const ask = options.ask;
|
|
497
|
+
// The full template runs a backend of its own, so it asks nothing about the origin.
|
|
498
|
+
const full = options.template === "full" && !o.url && o.origin !== "proxy";
|
|
472
499
|
let mode =
|
|
473
500
|
o.origin ??
|
|
474
|
-
(o.url ? "proxy" : o.source || o.runtime ? "managed" : undefined);
|
|
501
|
+
(o.url ? "proxy" : o.source || o.runtime || full ? "managed" : undefined);
|
|
475
502
|
if (!mode && ask) {
|
|
476
503
|
mode = await ask.choose("How does the gateway reach your API?", [
|
|
477
504
|
{
|
|
@@ -492,6 +519,8 @@ async function buildGateway(
|
|
|
492
519
|
);
|
|
493
520
|
}
|
|
494
521
|
let origin: NonNullable<ConfigV2["gateway"]>["origin"];
|
|
522
|
+
// The sample server was written, so the sample OpenAPI file can describe its endpoints.
|
|
523
|
+
let sampleServer = false;
|
|
495
524
|
if (mode === "proxy") {
|
|
496
525
|
const url =
|
|
497
526
|
o.url ??
|
|
@@ -506,6 +535,7 @@ async function buildGateway(
|
|
|
506
535
|
} else {
|
|
507
536
|
const source =
|
|
508
537
|
o.source ??
|
|
538
|
+
(full ? FULL_SERVER_SOURCE : undefined) ??
|
|
509
539
|
(ask
|
|
510
540
|
? await ask.text("Folder with your code, relative to the project", {
|
|
511
541
|
default: "server",
|
|
@@ -513,6 +543,7 @@ async function buildGateway(
|
|
|
513
543
|
: undefined);
|
|
514
544
|
const runtime =
|
|
515
545
|
o.runtime ??
|
|
546
|
+
(full ? FULL_SERVER_RUNTIME : undefined) ??
|
|
516
547
|
(ask
|
|
517
548
|
? await ask.choose("Runtime of your code", [
|
|
518
549
|
{ value: "nodejs" as const, label: "Node.js" },
|
|
@@ -523,16 +554,18 @@ async function buildGateway(
|
|
|
523
554
|
const runtimeVersion =
|
|
524
555
|
o.runtimeVersion !== undefined && runtime
|
|
525
556
|
? runtimeVersionSlug(runtime, o.runtimeVersion)
|
|
526
|
-
:
|
|
527
|
-
?
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
557
|
+
: full && runtime === FULL_SERVER_RUNTIME
|
|
558
|
+
? FULL_SERVER_RUNTIME_VERSION
|
|
559
|
+
: ask && runtime
|
|
560
|
+
? await ask.choose(
|
|
561
|
+
"Runtime version",
|
|
562
|
+
RUNTIME_VERSIONS[runtime].map((choice, index) => ({
|
|
563
|
+
value: choice.value as string,
|
|
564
|
+
label:
|
|
565
|
+
index === 0 ? `${choice.label} (suggested)` : choice.label,
|
|
566
|
+
})),
|
|
567
|
+
)
|
|
568
|
+
: undefined;
|
|
536
569
|
if (!source || !runtime || !runtimeVersion) {
|
|
537
570
|
throw new ServiceEditError(
|
|
538
571
|
"--origin managed needs --source, --runtime and --runtime-version.",
|
|
@@ -542,14 +575,15 @@ async function buildGateway(
|
|
|
542
575
|
);
|
|
543
576
|
}
|
|
544
577
|
const sourcePath = insideProject(dir, source, "Source folder");
|
|
578
|
+
sampleServer = await createSourceFolder(context, sourcePath, runtime);
|
|
545
579
|
origin = {
|
|
546
580
|
mode,
|
|
547
581
|
source: sourcePath,
|
|
548
582
|
runtime,
|
|
549
583
|
runtimeVersion,
|
|
550
584
|
...(o.exclude?.length ? { exclude: o.exclude } : {}),
|
|
585
|
+
...(sampleServer ? { healthPath: FULL_SERVER_HEALTH_PATH } : {}),
|
|
551
586
|
};
|
|
552
|
-
await createSourceFolder(context, sourcePath);
|
|
553
587
|
}
|
|
554
588
|
|
|
555
589
|
let specs: string[];
|
|
@@ -591,7 +625,9 @@ async function buildGateway(
|
|
|
591
625
|
target,
|
|
592
626
|
options.template === "blank"
|
|
593
627
|
? BLANK_SPEC(resolved.project.name)
|
|
594
|
-
:
|
|
628
|
+
: sampleServer
|
|
629
|
+
? FULL_SPEC(resolved.project.name)
|
|
630
|
+
: STARTER_SPEC(resolved.project.name),
|
|
595
631
|
);
|
|
596
632
|
specs = [rel(dir, target)];
|
|
597
633
|
} else {
|
|
@@ -706,7 +742,7 @@ async function buildPlans(
|
|
|
706
742
|
o.template ??
|
|
707
743
|
(options.template === "blank"
|
|
708
744
|
? "none"
|
|
709
|
-
: options.template
|
|
745
|
+
: isFilled(options.template)
|
|
710
746
|
? "starter"
|
|
711
747
|
: "free");
|
|
712
748
|
// Only paid plans need a billing provider. A catalog of free plans takes no payment, so its default is `none` and the
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -69,7 +69,8 @@ export type PortalPlan = {
|
|
|
69
69
|
};
|
|
70
70
|
/** Pricing, subscriptions, API keys and usage pages. */
|
|
71
71
|
plansPlugin?: {
|
|
72
|
-
|
|
72
|
+
/** The plans backend; absent in a demo preview, where the pages show sample data. */
|
|
73
|
+
apiBaseUrl?: string;
|
|
73
74
|
keysApiBaseUrl?: string;
|
|
74
75
|
/** The billing provider of the plans service (`none`, `stripe`, ...). `none`: the pages offer no payment. */
|
|
75
76
|
billing?: string;
|
|
@@ -84,6 +85,11 @@ export type PortalPlan = {
|
|
|
84
85
|
skipped: { piece: string; reason: string }[];
|
|
85
86
|
/** True only for a stub manifest. Production builds refuse it. */
|
|
86
87
|
preview: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* A preview with no deployed identity: the portal signs in with the demo account instead, and the account and plans
|
|
90
|
+
* pages show sample data. Never set outside a preview.
|
|
91
|
+
*/
|
|
92
|
+
demo?: boolean;
|
|
87
93
|
};
|
|
88
94
|
|
|
89
95
|
/** npm packages the generated site needs, beyond `@lukoweb/apitogo`, for what the plan registers. */
|
|
@@ -203,6 +209,20 @@ export function planPortal(
|
|
|
203
209
|
return plan;
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
// Before the first deploy there is no identity to sign in with. A preview signs in with the demo account instead,
|
|
213
|
+
// and the account and plans pages show sample data, so the console can be tried locally.
|
|
214
|
+
if (plan.preview && identity && !identity.issuer) {
|
|
215
|
+
plan.demo = true;
|
|
216
|
+
plan.userPanel = true;
|
|
217
|
+
skip("sign-in", "preview: the demo account signs in");
|
|
218
|
+
if (plans)
|
|
219
|
+
plan.plansPlugin = {
|
|
220
|
+
billing: "none",
|
|
221
|
+
links: plan.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {},
|
|
222
|
+
};
|
|
223
|
+
return plan;
|
|
224
|
+
}
|
|
225
|
+
|
|
206
226
|
if (!identity) {
|
|
207
227
|
skip("sign-in", "services.json has no identity entry");
|
|
208
228
|
} else if (!serviceReady(identity)) {
|
|
@@ -274,6 +294,10 @@ export function buildSiteConfig(
|
|
|
274
294
|
extras: {
|
|
275
295
|
redirects?: HomeRedirect[];
|
|
276
296
|
docsNavigation?: unknown[];
|
|
297
|
+
/** The first docs page, linked from the header next to the API reference. */
|
|
298
|
+
docsPath?: string;
|
|
299
|
+
/** A demo preview's plan catalog, from `plans.json`; the pages read it from the site config. */
|
|
300
|
+
demoPlans?: unknown[];
|
|
277
301
|
protectedRoutes?: string[];
|
|
278
302
|
} = {},
|
|
279
303
|
): Record<string, unknown> {
|
|
@@ -346,22 +370,29 @@ export function buildSiteConfig(
|
|
|
346
370
|
}
|
|
347
371
|
: {}),
|
|
348
372
|
...(navigation.length > 0 ? { navigation } : {}),
|
|
349
|
-
// The sidebar link above is only seen inside the docs; the header is what every page shows.
|
|
373
|
+
// The sidebar link above is only seen inside the docs; the header is what every page shows. A header navigation
|
|
374
|
+
// replaces the docs tab, so the docs get a header link of their own.
|
|
350
375
|
...(plan.apiReference
|
|
351
376
|
? {
|
|
352
377
|
header: {
|
|
353
378
|
navigation: [
|
|
379
|
+
...(plan.docs && extras.docsPath
|
|
380
|
+
? [{ label: "Documentation", to: extras.docsPath }]
|
|
381
|
+
: []),
|
|
354
382
|
{ label: "API Reference", to: `/${API_REFERENCE_PATH}` },
|
|
355
383
|
],
|
|
356
384
|
},
|
|
357
385
|
}
|
|
358
386
|
: {}),
|
|
387
|
+
...(extras.demoPlans?.length ? { plans: extras.demoPlans } : {}),
|
|
359
388
|
...(redirects.length > 0 ? { redirects } : {}),
|
|
360
389
|
...(extras.protectedRoutes?.length
|
|
361
390
|
? { protectedRoutes: extras.protectedRoutes }
|
|
362
391
|
: {}),
|
|
363
392
|
...(theme !== undefined ? { theme } : {}),
|
|
364
|
-
...(plan.auth
|
|
393
|
+
...(plan.auth || plan.demo
|
|
394
|
+
? { authentication: authenticationFor(plan) }
|
|
395
|
+
: {}),
|
|
365
396
|
...(plan.preview ? { preview: true } : {}),
|
|
366
397
|
};
|
|
367
398
|
}
|
|
@@ -373,6 +404,8 @@ export function buildSiteConfig(
|
|
|
373
404
|
* form. Without the plans service there is no backend, and the portal signs in with the identity directly (OpenID).
|
|
374
405
|
*/
|
|
375
406
|
function authenticationFor(plan: PortalPlan): Record<string, unknown> {
|
|
407
|
+
// The demo form: demo@apitogo.local / password, only in a local run.
|
|
408
|
+
if (plan.demo) return { type: "dev-portal", demoMode: true };
|
|
376
409
|
if (plan.plansPlugin)
|
|
377
410
|
return {
|
|
378
411
|
type: "dev-portal",
|
|
@@ -389,7 +422,7 @@ function authenticationFor(plan: PortalPlan): Record<string, unknown> {
|
|
|
389
422
|
|
|
390
423
|
/** Shown on every page of a preview run, so a stand-in manifest is never mistaken for a deployed portal. */
|
|
391
424
|
export const PREVIEW_BANNER_MESSAGE =
|
|
392
|
-
"Preview mode: sign
|
|
425
|
+
"Preview mode: sign in with demo@apitogo.local / password to try the console. Run `apitogo deploy` for the real services.";
|
|
393
426
|
|
|
394
427
|
/** The wrapper source. Imports exist only for what the plan registers. */
|
|
395
428
|
export function renderWrapper(plan: PortalPlan): string {
|
|
@@ -423,6 +456,24 @@ export function renderWrapper(plan: PortalPlan): string {
|
|
|
423
456
|
].join("\n");
|
|
424
457
|
}
|
|
425
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The plans of the project's `plans.json`, for the pricing and account pages of a demo preview: with no plans backend
|
|
461
|
+
* they read their catalog from the site config. A missing or unreadable file gives no plans; `check` reports it.
|
|
462
|
+
*/
|
|
463
|
+
async function readDemoPlans(
|
|
464
|
+
file: string | undefined,
|
|
465
|
+
): Promise<unknown[] | undefined> {
|
|
466
|
+
if (!file) return undefined;
|
|
467
|
+
try {
|
|
468
|
+
const parsed = JSON.parse(await readFile(file, "utf8")) as {
|
|
469
|
+
plans?: unknown;
|
|
470
|
+
};
|
|
471
|
+
return Array.isArray(parsed.plans) ? parsed.plans : undefined;
|
|
472
|
+
} catch {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
426
477
|
/** The portal theme file, when it exists and parses. `check` reports an invalid one; the build just skips it. */
|
|
427
478
|
async function readTheme(resolved: ResolvedConfigV2): Promise<unknown> {
|
|
428
479
|
const file = resolved.portal?.theme;
|
|
@@ -567,6 +618,11 @@ export async function preparePortal(
|
|
|
567
618
|
{
|
|
568
619
|
redirects: home ? [home] : [],
|
|
569
620
|
docsNavigation: docsNavigation(hostedDocRoutes),
|
|
621
|
+
docsPath: pickLandingRoute(hostedDocRoutes),
|
|
622
|
+
demoPlans:
|
|
623
|
+
plan.demo && plan.plansPlugin
|
|
624
|
+
? await readDemoPlans(resolved.plans?.file)
|
|
625
|
+
: undefined,
|
|
570
626
|
protectedRoutes: protectedRoutesFor({
|
|
571
627
|
requireAuth: resolved.portal?.requireAuth,
|
|
572
628
|
hasAuth: !!plan.auth,
|
package/src/vite/watch-ignore.ts
CHANGED
|
@@ -1,70 +1,80 @@
|
|
|
1
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A config version 2 portal builds from `<state>/generated/`, where `content` is a link to the project folder.
|
|
6
|
-
* That folder holds the state folder itself (`.apitogo/` or `apitogo/.state/`), which holds `generated/content`
|
|
7
|
-
* again, so a file watcher that follows the link goes round forever (ENAMETOOLONG).
|
|
8
|
-
*/
|
|
9
|
-
const STATE_INSIDE_CONTENT =
|
|
10
|
-
/[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A config version 2 portal builds from `<state>/generated/`, where `content` is a link to the project folder.
|
|
6
|
+
* That folder holds the state folder itself (`.apitogo/` or `apitogo/.state/`), which holds `generated/content`
|
|
7
|
+
* again, so a file watcher that follows the link goes round forever (ENAMETOOLONG).
|
|
8
|
+
*/
|
|
9
|
+
const STATE_INSIDE_CONTENT =
|
|
10
|
+
/[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* On Windows the site root can arrive as `C:/...` while the watcher reports `C:\...`, so both sides are resolved to
|
|
14
|
+
* the platform's form first; otherwise nothing matched and the watcher followed `content` until ELOOP.
|
|
15
|
+
*/
|
|
16
|
+
const within = (base: string, file: string) => {
|
|
17
|
+
const resolvedBase = path.resolve(base);
|
|
18
|
+
const resolvedFile = path.resolve(file);
|
|
19
|
+
return (
|
|
20
|
+
resolvedFile === resolvedBase ||
|
|
21
|
+
resolvedFile.startsWith(resolvedBase + path.sep)
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const unique = (paths: string[]) => [...new Set(paths)];
|
|
26
|
+
|
|
27
|
+
/** Whether `root` is the site root (`<state>/generated`) of a config version 2 workspace: it has the `content` link. */
|
|
28
|
+
function isWorkspace(root: string): boolean {
|
|
29
|
+
try {
|
|
30
|
+
return (
|
|
31
|
+
path.basename(root) === "generated" &&
|
|
32
|
+
lstatSync(path.join(root, "content")).isSymbolicLink()
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The tenant's own folder of a config version 2 workspace (what `content` links to), or `undefined` for any other
|
|
41
|
+
* project. The site root sits inside the state folder, so the folder that holds `node_modules` and the pages is
|
|
42
|
+
* outside it and the dev server has to be told it may serve files from there.
|
|
43
|
+
*/
|
|
44
|
+
export function tenantDir(root: string): string | undefined {
|
|
45
|
+
return isWorkspace(root)
|
|
46
|
+
? realpathSync(path.join(root, "content"))
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What the dev server's watcher must leave alone in a config version 2 workspace, or `undefined` for any other
|
|
52
|
+
* project. The watcher meets the state folder twice: below the `content` link and, after resolving the link,
|
|
53
|
+
* by its real path. So the whole state folder is skipped, except `generated/` itself (the site root) and the
|
|
54
|
+
* tenant's own files.
|
|
55
|
+
*/
|
|
56
|
+
export function stateIgnore(
|
|
57
|
+
root: string,
|
|
58
|
+
): ((file: string) => boolean) | undefined {
|
|
59
|
+
if (!isWorkspace(root)) return undefined;
|
|
60
|
+
|
|
61
|
+
const state = path.dirname(root);
|
|
62
|
+
const states = unique([state, realpathSync(state)]);
|
|
63
|
+
const roots = unique([root, realpathSync(root)]);
|
|
64
|
+
|
|
65
|
+
return (file) => {
|
|
66
|
+
if (!states.some((base) => within(base, file))) return false;
|
|
67
|
+
if (!roots.some((base) => within(base, file))) return true;
|
|
68
|
+
return STATE_INSIDE_CONTENT.test(file);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The folders the dev server may serve files from. The site root of a config version 2 workspace sits inside the
|
|
74
|
+
* state folder, so the package (`node_modules`) and the tenant's pages are outside it. Without them the page's main
|
|
75
|
+
* script fails with "Failed to load url ... Does the file exist?" and the server stops.
|
|
76
|
+
*/
|
|
77
|
+
export function devServerAllow(root: string, moduleDir: string): string[] {
|
|
78
|
+
const tenant = tenantDir(root);
|
|
79
|
+
return [root, path.dirname(root), moduleDir, ...(tenant ? [tenant] : [])];
|
|
80
|
+
}
|