@atelic-action/ui 0.17.1 → 1.0.0
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/README.md +429 -45
- package/bin/atelic-check-base-path.ts +5 -0
- package/bin/atelic-clean-sitemap.ts +5 -0
- package/bin/atelic-contact-canary.ts +5 -0
- package/bin/atelic-print-sheets.sh +1 -1
- package/bin/atelic-seal-document.mjs +1 -1
- package/bin/atelic-serve-static.ts +1 -1
- package/dist/gate/index.d.ts +1 -1
- package/dist/gate/index.js +1 -1
- package/dist/gate/middleware.d.ts +4 -1
- package/dist/gate/middleware.js +14 -5
- package/dist/gate/wall.d.ts +25 -0
- package/dist/gate/wall.js +41 -2
- package/dist/routing/basePath.d.ts +24 -0
- package/dist/routing/basePath.js +54 -0
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.js +3 -2
- package/dist/vite/marketing-config.d.ts +50 -0
- package/dist/vite/marketing-config.js +83 -0
- package/package.json +164 -5
- package/src/app/AppProvider.tsx +41 -0
- package/src/app/AuthGuard.tsx +27 -0
- package/src/app/CardErrorBoundary.tsx +39 -0
- package/src/app/ErrorFallback.tsx +39 -0
- package/src/app/GoogleGlyph.tsx +22 -0
- package/src/app/config.ts +54 -0
- package/src/app/configure.ts +18 -0
- package/src/app/context/AppContext.tsx +52 -0
- package/src/app/context/SidebarContext.tsx +124 -0
- package/src/app/context/UserContext.tsx +139 -0
- package/src/app/hooks/useDebounce.ts +12 -0
- package/src/app/hooks/useIsDesktop.ts +31 -0
- package/src/app/hooks/useWakeLock.ts +49 -0
- package/src/app/index.ts +64 -0
- package/src/app/layout/ActionButton.tsx +67 -0
- package/src/app/layout/AppLayout.tsx +110 -0
- package/src/app/layout/AppPage.tsx +268 -0
- package/src/app/layout/Breadcrumbs.tsx +57 -0
- package/src/app/layout/DesktopRail.tsx +168 -0
- package/src/app/layout/FormActionButtons.tsx +69 -0
- package/src/app/layout/MobileMenu.tsx +156 -0
- package/src/app/layout/MobileTabBar.tsx +43 -0
- package/src/app/layout/PageError.tsx +30 -0
- package/src/app/layout/PageSkeleton.tsx +65 -0
- package/src/app/layout/SectionCards.tsx +33 -0
- package/src/app/layout/UserMenu.tsx +104 -0
- package/src/app/layout/identity.ts +22 -0
- package/src/app/layout/index.ts +20 -0
- package/src/app/layout/nav.ts +52 -0
- package/src/app/lib/dates.ts +57 -0
- package/src/app/lib/env.ts +37 -0
- package/src/app/lib/format.ts +61 -0
- package/src/app/lib/http.ts +205 -0
- package/src/app/lib/lastAuth.ts +45 -0
- package/src/app/lib/oauth.ts +32 -0
- package/src/app/lib/publicRoutes.ts +33 -0
- package/src/app/lib/queryClient.ts +38 -0
- package/src/app/lib/search.ts +17 -0
- package/src/app/lib/session.ts +36 -0
- package/src/app/lib/tokenRefresh.ts +137 -0
- package/src/app/lib/transformKeys.ts +60 -0
- package/src/app/lib/units.ts +228 -0
- package/src/app/lib/unwrap.ts +48 -0
- package/src/app/lib/utils.ts +10 -0
- package/src/app/stores/authStore.ts +80 -0
- package/src/app/stores/storage.ts +40 -0
- package/src/app/stores/unitsStore.ts +54 -0
- package/src/app/types/index.ts +72 -0
- package/src/app/types/query.ts +11 -0
- package/src/app/ui/accordion.tsx +54 -0
- package/src/app/ui/alert-dialog.tsx +114 -0
- package/src/app/ui/alert.tsx +49 -0
- package/src/app/ui/avatar.tsx +45 -0
- package/src/app/ui/badge-list.tsx +51 -0
- package/src/app/ui/badge.tsx +33 -0
- package/src/app/ui/button.tsx +49 -0
- package/src/app/ui/callout.tsx +22 -0
- package/src/app/ui/card.tsx +55 -0
- package/src/app/ui/checkbox.tsx +26 -0
- package/src/app/ui/clear-button.tsx +42 -0
- package/src/app/ui/command.tsx +140 -0
- package/src/app/ui/country-dropdown.tsx +184 -0
- package/src/app/ui/dialog.tsx +104 -0
- package/src/app/ui/dropdown-menu.tsx +185 -0
- package/src/app/ui/form.tsx +172 -0
- package/src/app/ui/index.ts +36 -0
- package/src/app/ui/input.tsx +22 -0
- package/src/app/ui/label.tsx +19 -0
- package/src/app/ui/option-select.tsx +56 -0
- package/src/app/ui/pagination.tsx +90 -0
- package/src/app/ui/popover.tsx +29 -0
- package/src/app/ui/progress.tsx +25 -0
- package/src/app/ui/search-input.tsx +63 -0
- package/src/app/ui/select.tsx +151 -0
- package/src/app/ui/separator.tsx +24 -0
- package/src/app/ui/sheet.tsx +119 -0
- package/src/app/ui/slider.tsx +23 -0
- package/src/app/ui/switch.tsx +27 -0
- package/src/app/ui/textarea.tsx +21 -0
- package/src/app/ui/tooltip.tsx +32 -0
- package/src/artifact/ArtifactShell.tsx +2 -2
- package/src/artifact/Gate.tsx +2 -2
- package/src/artifact/Note.tsx +1 -1
- package/src/artifact/Shot.tsx +8 -2
- package/src/artifact/Writeup.tsx +7 -7
- package/src/chrome/SiteShell.tsx +141 -0
- package/src/chrome/index.ts +2 -0
- package/src/components/Disclosure.tsx +80 -14
- package/src/components/GateLogin.tsx +1 -1
- package/src/components/Watermark.tsx +1 -1
- package/src/components/index.ts +6 -2
- package/src/gate/index.ts +1 -1
- package/src/gate/middleware.ts +17 -6
- package/src/gate/wall.ts +60 -2
- package/src/lib/basePathAudit.ts +96 -0
- package/src/lib/displayDate.ts +25 -0
- package/src/lib/honeypot.ts +11 -0
- package/src/lib/hours.ts +28 -0
- package/src/lib/sitemap.ts +34 -0
- package/src/lib/trackEvent.ts +26 -0
- package/src/lib/useOpenHeight.ts +28 -0
- package/src/primitives/Button.tsx +3 -1
- package/src/primitives/Chip.tsx +3 -1
- package/src/primitives/ImageFrame.tsx +48 -0
- package/src/primitives/ProseBlocks.tsx +67 -0
- package/src/primitives/QuoteCard.tsx +20 -0
- package/src/primitives/StarRow.tsx +37 -0
- package/src/primitives/TickList.tsx +24 -0
- package/src/primitives/index.ts +5 -0
- package/src/routing/BasePathProvider.tsx +47 -0
- package/src/routing/basePath.ts +57 -0
- package/src/routing/index.ts +8 -0
- package/src/scripts/check-base-path.ts +130 -0
- package/src/scripts/clean-sitemap.ts +27 -0
- package/src/scripts/contact-canary.ts +98 -0
- package/src/scripts/serve-static.ts +29 -3
- package/src/sections/BlogIndex.tsx +57 -0
- package/src/sections/BlogPost.tsx +65 -0
- package/src/sections/CTABanner.tsx +93 -0
- package/src/sections/CTASection.tsx +60 -0
- package/src/sections/ComparisonTable.tsx +114 -0
- package/src/sections/ContactForm.tsx +163 -0
- package/src/sections/ContactInfoCard.tsx +92 -0
- package/src/sections/CredentialList.tsx +28 -0
- package/src/sections/DeviceShowcase.tsx +39 -0
- package/src/sections/ExpandList.tsx +58 -0
- package/src/sections/FAQBrowser.tsx +283 -0
- package/src/sections/FeatureDetail.tsx +60 -0
- package/src/sections/FeatureGrid.tsx +83 -0
- package/src/sections/Gallery.tsx +84 -0
- package/src/sections/Hero.tsx +110 -0
- package/src/sections/HoursTable.tsx +23 -0
- package/src/sections/JumpRail.tsx +35 -0
- package/src/sections/LegalPage.tsx +43 -0
- package/src/sections/LocationSection.tsx +66 -0
- package/src/sections/Locations.tsx +71 -0
- package/src/sections/LogoMarquee.tsx +27 -0
- package/src/sections/MapEmbed.tsx +75 -0
- package/src/sections/MediaBand.tsx +20 -0
- package/src/sections/MenuList.tsx +44 -0
- package/src/sections/PageHero.tsx +25 -0
- package/src/sections/PressQuotes.tsx +51 -0
- package/src/sections/PricingBand.tsx +43 -0
- package/src/sections/PricingCard.tsx +36 -0
- package/src/sections/ProcessSteps.tsx +45 -0
- package/src/sections/ProfileCards.tsx +73 -0
- package/src/sections/RevealSlider.tsx +156 -0
- package/src/sections/ReviewCarousel.tsx +223 -0
- package/src/sections/ReviewWall.tsx +220 -0
- package/src/sections/SocialStrip.tsx +83 -0
- package/src/sections/SplitSection.tsx +56 -0
- package/src/sections/index.ts +76 -0
- package/src/sections/siteConfig.ts +201 -0
- package/src/styles/app.css +673 -0
- package/src/styles/artifact.css +583 -397
- package/src/styles/base.css +49 -47
- package/src/styles/chrome.css +71 -71
- package/src/styles/components.css +150 -140
- package/src/styles/gate.css +29 -29
- package/src/styles/primitives.css +190 -25
- package/src/styles/sections.css +2652 -0
- package/src/styles/star-rating.css +2 -2
- package/src/vite/index.ts +9 -2
- package/src/vite/marketing-config.ts +128 -0
- package/src/components/Collapsible.tsx +0 -43
- /package/src/scripts/{build-gate-payload.mjs → seal-document.mjs} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Print an artifact page to PDF with headless Chrome and report its sheet
|
|
3
3
|
# count. The writeup flows across as many letter pages as it needs, its
|
|
4
|
-
#
|
|
4
|
+
# native disclosures printed open, so its count is a record of the print and not a
|
|
5
5
|
# layout check. The monthly page is one sheet by construction, so pass 1 for
|
|
6
6
|
# it and the script fails when it prints as anything else.
|
|
7
7
|
#
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// atelic-seal-document <config.json> [out.json]: seals a document for its readers.
|
|
3
3
|
import { argv } from "node:process";
|
|
4
|
-
import { cli } from "../src/scripts/
|
|
4
|
+
import { cli } from "../src/scripts/seal-document.mjs";
|
|
5
5
|
|
|
6
6
|
await cli(argv.slice(2));
|
package/dist/gate/index.d.ts
CHANGED
package/dist/gate/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type WallOpenings } from "./wall.js";
|
|
1
2
|
export interface MiddlewareOptions {
|
|
2
|
-
/** Whether a path needs a session once the gate is armed. */
|
|
3
|
+
/** Whether a path needs a session once the gate is armed. Wins over `open`. */
|
|
3
4
|
needsSession?: (pathname: string) => boolean;
|
|
5
|
+
/** Paths the site opens beyond the wall's own, added to its allowlist. */
|
|
6
|
+
open?: WallOpenings;
|
|
4
7
|
}
|
|
5
8
|
export declare function createMiddleware(options?: MiddlewareOptions): (request: Request) => Promise<Response | undefined>;
|
|
6
9
|
declare const _default: (request: Request) => Promise<Response | undefined>;
|
package/dist/gate/middleware.js
CHANGED
|
@@ -16,13 +16,22 @@
|
|
|
16
16
|
*
|
|
17
17
|
* That matcher keeps only the two sign in functions away from the middleware,
|
|
18
18
|
* so any other function a host adds is held like a page. The default holds
|
|
19
|
-
* back everything `needsSession` in wall.ts does. A site
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
19
|
+
* back everything `needsSession` in wall.ts does. A marketing site, whose
|
|
20
|
+
* other functions (the contact relay) are public by design, uses the simple
|
|
21
|
+
* matcher instead, everything but `api/` and the login page:
|
|
22
|
+
*
|
|
23
|
+
* export const config = { matcher: ["/((?!api/|login$|login/).*)"] };
|
|
24
|
+
*
|
|
25
|
+
* and leaves the deciding here: a request passes with no session only when
|
|
26
|
+
* the wall's open exact paths and open prefixes name it, so a page is held by
|
|
27
|
+
* default whatever its path looks like (`/blog/release-2.0` included), where
|
|
28
|
+
* a matcher that let file extensions through let it escape. A site with an
|
|
29
|
+
* open file of its own passes `open`; one whose matcher already leaves its
|
|
30
|
+
* open paths out may still pass its own `needsSession`, which wins over both.
|
|
22
31
|
*/
|
|
23
32
|
import { isAllowed } from "./config.js";
|
|
24
33
|
import { verifyToken } from "./tokens.js";
|
|
25
|
-
import {
|
|
34
|
+
import { createNeedsSession } from "./wall.js";
|
|
26
35
|
function readCookie(header, name) {
|
|
27
36
|
const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
28
37
|
if (!match)
|
|
@@ -35,7 +44,7 @@ function readCookie(header, name) {
|
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
export function createMiddleware(options = {}) {
|
|
38
|
-
const needsSession = options.needsSession ??
|
|
47
|
+
const needsSession = options.needsSession ?? createNeedsSession(options.open);
|
|
39
48
|
return async function middleware(request) {
|
|
40
49
|
const secret = process.env.GATE_SESSION_SECRET;
|
|
41
50
|
if (!secret)
|
package/dist/gate/wall.d.ts
CHANGED
|
@@ -30,9 +30,28 @@
|
|
|
30
30
|
* it stays open on a private host too: a prospect reads the prototype
|
|
31
31
|
* without a sign in while the documents beside it stay behind the wall.
|
|
32
32
|
*
|
|
33
|
+
* A marketing site's login page loads the same kinds of file (its hashed
|
|
34
|
+
* assets, its fonts, its favicon), so the same lists hold a website by
|
|
35
|
+
* default: any path they do not name, a page whose last segment carries a
|
|
36
|
+
* dot (`/blog/release-2.0`) included, needs a session. A site with an open
|
|
37
|
+
* file of its own names it through `createNeedsSession` (or the middleware's
|
|
38
|
+
* `open` option), which adds to these lists and never opens the held corner.
|
|
39
|
+
*
|
|
33
40
|
* Pure and dependency free, so the middleware imports it and the tests call
|
|
34
41
|
* it directly.
|
|
35
42
|
*/
|
|
43
|
+
/** What the wall leaves open with no session, exact paths and prefixes, before any a site adds. */
|
|
44
|
+
export declare const OPEN_PATHS: {
|
|
45
|
+
readonly exact: readonly string[];
|
|
46
|
+
readonly prefixes: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
/** Paths a site opens beyond the wall's own, for a file its login page needs. */
|
|
49
|
+
export interface WallOpenings {
|
|
50
|
+
/** Paths opened as written, each starting with "/" (`/apple-touch-icon.png`). */
|
|
51
|
+
exact?: readonly string[];
|
|
52
|
+
/** Folders opened whole, each starting and ending with "/" and never "/" alone (`/icons/`). */
|
|
53
|
+
prefixes?: readonly string[];
|
|
54
|
+
}
|
|
36
55
|
/**
|
|
37
56
|
* Whether a request for this path needs a session when the wall is armed.
|
|
38
57
|
* The path is read the way a file server would read it (decoded, repeated
|
|
@@ -46,3 +65,9 @@
|
|
|
46
65
|
* like an open one names some other file.
|
|
47
66
|
*/
|
|
48
67
|
export declare function needsSession(pathname: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `needsSession` with a site's own open paths added to the wall's, read the
|
|
70
|
+
* same way. Throws on an entry that is not a plain path, or a prefix that
|
|
71
|
+
* would open the whole host, so a typo cannot quietly unlock a site.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createNeedsSession(open?: WallOpenings): (pathname: string) => boolean;
|
package/dist/gate/wall.js
CHANGED
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
* it stays open on a private host too: a prospect reads the prototype
|
|
31
31
|
* without a sign in while the documents beside it stay behind the wall.
|
|
32
32
|
*
|
|
33
|
+
* A marketing site's login page loads the same kinds of file (its hashed
|
|
34
|
+
* assets, its fonts, its favicon), so the same lists hold a website by
|
|
35
|
+
* default: any path they do not name, a page whose last segment carries a
|
|
36
|
+
* dot (`/blog/release-2.0`) included, needs a session. A site with an open
|
|
37
|
+
* file of its own names it through `createNeedsSession` (or the middleware's
|
|
38
|
+
* `open` option), which adds to these lists and never opens the held corner.
|
|
39
|
+
*
|
|
33
40
|
* Pure and dependency free, so the middleware imports it and the tests call
|
|
34
41
|
* it directly.
|
|
35
42
|
*/
|
|
@@ -45,6 +52,8 @@ const OPEN_EXACT = new Set([
|
|
|
45
52
|
const OPEN_PREFIXES = ["/api/auth/", "/assets/", "/brand/", "/fonts/", "/proto/"];
|
|
46
53
|
/** The one corner of an open prefix that stays behind the wall. */
|
|
47
54
|
const HELD_PREFIXES = ["/assets/doc/"];
|
|
55
|
+
/** What the wall leaves open with no session, exact paths and prefixes, before any a site adds. */
|
|
56
|
+
export const OPEN_PATHS = { exact: [...OPEN_EXACT], prefixes: [...OPEN_PREFIXES] };
|
|
48
57
|
/**
|
|
49
58
|
* Whether a request for this path needs a session when the wall is armed.
|
|
50
59
|
* The path is read the way a file server would read it (decoded, repeated
|
|
@@ -58,6 +67,9 @@ const HELD_PREFIXES = ["/assets/doc/"];
|
|
|
58
67
|
* like an open one names some other file.
|
|
59
68
|
*/
|
|
60
69
|
export function needsSession(pathname) {
|
|
70
|
+
return judge(pathname, OPEN_EXACT, OPEN_PREFIXES);
|
|
71
|
+
}
|
|
72
|
+
function judge(pathname, exact, prefixes) {
|
|
61
73
|
let path;
|
|
62
74
|
try {
|
|
63
75
|
path = decodeURIComponent(pathname);
|
|
@@ -71,7 +83,34 @@ export function needsSession(pathname) {
|
|
|
71
83
|
const folded = path.toLowerCase();
|
|
72
84
|
if (HELD_PREFIXES.some((prefix) => folded.startsWith(prefix)))
|
|
73
85
|
return true;
|
|
74
|
-
if (
|
|
86
|
+
if (exact.has(path))
|
|
75
87
|
return false;
|
|
76
|
-
return !
|
|
88
|
+
return !prefixes.some((prefix) => path.startsWith(prefix));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `needsSession` with a site's own open paths added to the wall's, read the
|
|
92
|
+
* same way. Throws on an entry that is not a plain path, or a prefix that
|
|
93
|
+
* would open the whole host, so a typo cannot quietly unlock a site.
|
|
94
|
+
*/
|
|
95
|
+
export function createNeedsSession(open = {}) {
|
|
96
|
+
const extraExact = open.exact ?? [];
|
|
97
|
+
const extraPrefixes = open.prefixes ?? [];
|
|
98
|
+
for (const path of extraExact) {
|
|
99
|
+
if (!path.startsWith("/") || path.includes("..") || path.includes("%")) {
|
|
100
|
+
throw new Error(`An open path must be a plain path starting with "/": ${JSON.stringify(path)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const prefix of extraPrefixes) {
|
|
104
|
+
if (prefix === "/" ||
|
|
105
|
+
!prefix.startsWith("/") ||
|
|
106
|
+
!prefix.endsWith("/") ||
|
|
107
|
+
prefix.includes("..")) {
|
|
108
|
+
throw new Error(`An open prefix must be a folder such as "/icons/", never "/" alone: ${JSON.stringify(prefix)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (extraExact.length === 0 && extraPrefixes.length === 0)
|
|
112
|
+
return needsSession;
|
|
113
|
+
const exact = new Set([...OPEN_EXACT, ...extraExact]);
|
|
114
|
+
const prefixes = [...OPEN_PREFIXES, ...extraPrefixes];
|
|
115
|
+
return (pathname) => judge(pathname, exact, prefixes);
|
|
77
116
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base path: the prefix a site is served under when it does not own the
|
|
3
|
+
* root of its host (a website build previewed at `/proto` on the business's
|
|
4
|
+
* artifact host). A site sets it once; `BasePathProvider` hands it to every
|
|
5
|
+
* component below, and `marketingConfig` turns it into Vite's `base` and the
|
|
6
|
+
* router's `basepath`.
|
|
7
|
+
*
|
|
8
|
+
* Normalized form: "" for the root, otherwise a leading slash and no trailing
|
|
9
|
+
* slash ("/proto"). These helpers are pure and take the base explicitly, so
|
|
10
|
+
* the Vite config, the build commands, and the tests use them as they are.
|
|
11
|
+
* Plain TypeScript with no imports, since the built Vite entry imports it.
|
|
12
|
+
*/
|
|
13
|
+
/** Turns a configured value ("", "/", "proto", "/proto/") into "" or "/proto". Throws on anything else. */
|
|
14
|
+
export declare function normalizeBasePath(raw: string | undefined): string;
|
|
15
|
+
/** True when the path already carries the base (so applying it again is a no op). */
|
|
16
|
+
export declare function hasBase(base: string, path: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Prefixes a root absolute path with the base. External URLs, protocol links
|
|
19
|
+
* (mailto:, tel:), fragments, relative paths, and paths that already carry the
|
|
20
|
+
* base come back unchanged, so it is safe on any href and idempotent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function joinBase(base: string, path: string): string;
|
|
23
|
+
/** Removes the base from a path that carries it ("/proto/faqs" to "/faqs", "/proto" to "/"). */
|
|
24
|
+
export declare function stripBase(base: string, path: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base path: the prefix a site is served under when it does not own the
|
|
3
|
+
* root of its host (a website build previewed at `/proto` on the business's
|
|
4
|
+
* artifact host). A site sets it once; `BasePathProvider` hands it to every
|
|
5
|
+
* component below, and `marketingConfig` turns it into Vite's `base` and the
|
|
6
|
+
* router's `basepath`.
|
|
7
|
+
*
|
|
8
|
+
* Normalized form: "" for the root, otherwise a leading slash and no trailing
|
|
9
|
+
* slash ("/proto"). These helpers are pure and take the base explicitly, so
|
|
10
|
+
* the Vite config, the build commands, and the tests use them as they are.
|
|
11
|
+
* Plain TypeScript with no imports, since the built Vite entry imports it.
|
|
12
|
+
*/
|
|
13
|
+
const SEGMENT = /^(\/[A-Za-z0-9._~-]+)+$/;
|
|
14
|
+
/** Turns a configured value ("", "/", "proto", "/proto/") into "" or "/proto". Throws on anything else. */
|
|
15
|
+
export function normalizeBasePath(raw) {
|
|
16
|
+
const trimmed = (raw ?? "").trim();
|
|
17
|
+
if (trimmed === "" || trimmed === "/")
|
|
18
|
+
return "";
|
|
19
|
+
const base = `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
|
|
20
|
+
if (!SEGMENT.test(base)) {
|
|
21
|
+
throw new Error(`basePath must be a path such as "/proto" (letters, digits, dot, dash, underscore, tilde): got ${JSON.stringify(raw)}`);
|
|
22
|
+
}
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
/** True for a path the base applies to: root absolute, not protocol relative. */
|
|
26
|
+
function isRootAbsolute(path) {
|
|
27
|
+
return path.startsWith("/") && !path.startsWith("//");
|
|
28
|
+
}
|
|
29
|
+
/** True when the path already carries the base (so applying it again is a no op). */
|
|
30
|
+
export function hasBase(base, path) {
|
|
31
|
+
if (!base)
|
|
32
|
+
return true;
|
|
33
|
+
if (!path.startsWith(base))
|
|
34
|
+
return false;
|
|
35
|
+
const next = path.charAt(base.length);
|
|
36
|
+
return next === "" || next === "/" || next === "?" || next === "#";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Prefixes a root absolute path with the base. External URLs, protocol links
|
|
40
|
+
* (mailto:, tel:), fragments, relative paths, and paths that already carry the
|
|
41
|
+
* base come back unchanged, so it is safe on any href and idempotent.
|
|
42
|
+
*/
|
|
43
|
+
export function joinBase(base, path) {
|
|
44
|
+
if (!base || !isRootAbsolute(path) || hasBase(base, path))
|
|
45
|
+
return path;
|
|
46
|
+
return `${base}${path}`;
|
|
47
|
+
}
|
|
48
|
+
/** Removes the base from a path that carries it ("/proto/faqs" to "/faqs", "/proto" to "/"). */
|
|
49
|
+
export function stripBase(base, path) {
|
|
50
|
+
if (!base || !isRootAbsolute(path) || !path.startsWith(base) || !hasBase(base, path))
|
|
51
|
+
return path;
|
|
52
|
+
const rest = path.slice(base.length);
|
|
53
|
+
return rest === "" || !rest.startsWith("/") ? `/${rest}` : rest;
|
|
54
|
+
}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a site's vite.config.ts imports (`@atelic-action/ui/vite`): the
|
|
3
|
-
* configuration of an artifact site as one call, and the
|
|
4
|
-
* for a site that writes its own.
|
|
3
|
+
* configuration of an artifact site or a marketing site as one call, and the
|
|
4
|
+
* plugin inside the artifact one for a site that writes its own.
|
|
5
5
|
*/
|
|
6
6
|
export { type ArtifactConfigOptions, type ArtifactPage, artifactConfig, artifactPages, } from "./artifact-config.js";
|
|
7
|
+
export { type MarketingConfigOptions, type MarketingPage, marketingBasePath, marketingConfig, NOT_FOUND_PAGE, } from "./marketing-config.js";
|
|
7
8
|
export { DOC_DIR, isPrivateContent, privateChunks } from "./private-chunks.js";
|
package/dist/vite/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a site's vite.config.ts imports (`@atelic-action/ui/vite`): the
|
|
3
|
-
* configuration of an artifact site as one call, and the
|
|
4
|
-
* for a site that writes its own.
|
|
3
|
+
* configuration of an artifact site or a marketing site as one call, and the
|
|
4
|
+
* plugin inside the artifact one for a site that writes its own.
|
|
5
5
|
*/
|
|
6
6
|
export { artifactConfig, artifactPages, } from "./artifact-config.js";
|
|
7
|
+
export { marketingBasePath, marketingConfig, NOT_FOUND_PAGE, } from "./marketing-config.js";
|
|
7
8
|
export { DOC_DIR, isPrivateContent, privateChunks } from "./private-chunks.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type UserConfig } from "vite";
|
|
2
|
+
/** One entry of the prerender list, as TanStack Start takes it. */
|
|
3
|
+
export interface MarketingPage {
|
|
4
|
+
path: string;
|
|
5
|
+
prerender?: {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
};
|
|
8
|
+
sitemap?: {
|
|
9
|
+
exclude: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface MarketingConfigOptions {
|
|
13
|
+
/**
|
|
14
|
+
* The two keys of the site's config the build reads: `url`, the canonical
|
|
15
|
+
* URL (with the base path on it for a based cut), and `basePath`.
|
|
16
|
+
*/
|
|
17
|
+
site: {
|
|
18
|
+
url: string;
|
|
19
|
+
basePath?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Pages the link crawler cannot reach on its own: a post on a dynamic
|
|
23
|
+
* route, a page nothing links to. The 404 page is always added.
|
|
24
|
+
*/
|
|
25
|
+
pages?: readonly MarketingPage[];
|
|
26
|
+
/**
|
|
27
|
+
* The host the sitemap's URLs carry. Defaults to the origin of `site.url`.
|
|
28
|
+
*/
|
|
29
|
+
sitemapHost?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The escape hatch for a site with a real need of its own: merged over
|
|
32
|
+
* the result with Vite's `mergeConfig`.
|
|
33
|
+
*/
|
|
34
|
+
extend?: UserConfig;
|
|
35
|
+
/** The environment to read `VITE_BASE_PATH` from; a test passes its own. */
|
|
36
|
+
env?: Record<string, string | undefined>;
|
|
37
|
+
}
|
|
38
|
+
/** The 404 page, prerendered and copied to 404.html by the build, never in the sitemap. */
|
|
39
|
+
export declare const NOT_FOUND_PAGE: MarketingPage;
|
|
40
|
+
/**
|
|
41
|
+
* The base the build uses: `VITE_BASE_PATH` when set (a rehearsal build,
|
|
42
|
+
* which CI runs under "/proto"), the config's `basePath` otherwise. Throws
|
|
43
|
+
* when a based config's `url` does not end with its base, since canonical and
|
|
44
|
+
* Open Graph URLs are built from it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function marketingBasePath(site: {
|
|
47
|
+
url: string;
|
|
48
|
+
basePath?: string;
|
|
49
|
+
}, env?: Record<string, string | undefined>): string;
|
|
50
|
+
export declare function marketingConfig({ site, pages, sitemapHost, extend, env, }: MarketingConfigOptions): UserConfig;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole Vite configuration of a marketing site, as one call. Every site
|
|
3
|
+
* cut from template-marketing carried its own copy of this configuration;
|
|
4
|
+
* here it is written once (0.19.0, ATE-643), and a site's vite.config.ts
|
|
5
|
+
* hands over its config and the pages the crawler cannot find:
|
|
6
|
+
*
|
|
7
|
+
* import { marketingConfig } from "@atelic-action/ui/vite";
|
|
8
|
+
* import { site } from "./src/site.config";
|
|
9
|
+
*
|
|
10
|
+
* export default marketingConfig({ site, pages: [{ path: "/blog/first-post" }] });
|
|
11
|
+
*
|
|
12
|
+
* The site's own paths (the `@` alias to its src) are found from the
|
|
13
|
+
* directory Vite is run in, which is the site's root.
|
|
14
|
+
*/
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
17
|
+
import react from "@vitejs/plugin-react";
|
|
18
|
+
import { mergeConfig } from "vite";
|
|
19
|
+
import { normalizeBasePath } from "../routing/basePath.js";
|
|
20
|
+
/** The 404 page, prerendered and copied to 404.html by the build, never in the sitemap. */
|
|
21
|
+
export const NOT_FOUND_PAGE = {
|
|
22
|
+
path: "/404",
|
|
23
|
+
prerender: { enabled: true },
|
|
24
|
+
sitemap: { exclude: true },
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The base the build uses: `VITE_BASE_PATH` when set (a rehearsal build,
|
|
28
|
+
* which CI runs under "/proto"), the config's `basePath` otherwise. Throws
|
|
29
|
+
* when a based config's `url` does not end with its base, since canonical and
|
|
30
|
+
* Open Graph URLs are built from it.
|
|
31
|
+
*/
|
|
32
|
+
export function marketingBasePath(site, env = process.env) {
|
|
33
|
+
const override = env.VITE_BASE_PATH;
|
|
34
|
+
const basePath = normalizeBasePath(override ?? site.basePath);
|
|
35
|
+
if (override === undefined && basePath && !site.url.endsWith(basePath)) {
|
|
36
|
+
throw new Error(`site.url (${site.url}) must end with basePath (${basePath}): canonical and Open Graph URLs are built from it`);
|
|
37
|
+
}
|
|
38
|
+
return basePath;
|
|
39
|
+
}
|
|
40
|
+
export function marketingConfig({ site, pages = [], sitemapHost, extend, env = process.env, }) {
|
|
41
|
+
const basePath = marketingBasePath(site, env);
|
|
42
|
+
const listed = pages.filter((page) => page.path !== NOT_FOUND_PAGE.path);
|
|
43
|
+
const config = {
|
|
44
|
+
// The base becomes Vite's `base`, so every built asset URL and every
|
|
45
|
+
// rebased `url(/...)` in the CSS carries it, and the router's
|
|
46
|
+
// `basepath`, so the router and the prerender both work under it.
|
|
47
|
+
base: `${basePath}/`,
|
|
48
|
+
// No `build.cssCodeSplit: false`. Every sheet a site carries, the
|
|
49
|
+
// package's included, rides in the one base sheet its root route links
|
|
50
|
+
// (src/styles/base.css), so no component imports a stylesheet and there
|
|
51
|
+
// is no component bundle to fold. With the flag off, TanStack Start
|
|
52
|
+
// looks for the `style.css` that folding emits, finds none, and fails
|
|
53
|
+
// the build (ATE-643, as artifactConfig found first). A site that gives
|
|
54
|
+
// a component its own stylesheet again sets the flag through `extend`.
|
|
55
|
+
plugins: [
|
|
56
|
+
tanstackStart({
|
|
57
|
+
...(basePath ? { router: { basepath: basePath } } : {}),
|
|
58
|
+
prerender: { enabled: true, crawlLinks: true },
|
|
59
|
+
pages: [NOT_FOUND_PAGE, ...listed],
|
|
60
|
+
sitemap: {
|
|
61
|
+
// A based cut is a preview under another project's host, noindex
|
|
62
|
+
// by that host's header rule, so it ships no sitemap.
|
|
63
|
+
enabled: !basePath,
|
|
64
|
+
host: sitemapHost ?? new URL(site.url).origin,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
react(),
|
|
68
|
+
],
|
|
69
|
+
resolve: {
|
|
70
|
+
alias: {
|
|
71
|
+
// The site's src, from the directory Vite is run in: this file
|
|
72
|
+
// lives in the package, so nothing here can be found relative to it.
|
|
73
|
+
"@": resolve(process.cwd(), "./src"),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
// @atelic-action/ui ships TSX and CSS as written, so the prerender's
|
|
77
|
+
// server build has to compile it rather than hand it to Node.
|
|
78
|
+
ssr: {
|
|
79
|
+
noExternal: ["@atelic-action/ui"],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return extend ? mergeConfig(config, extend) : config;
|
|
83
|
+
}
|