@lukoweb/apitogo 0.2.64 → 0.2.66

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 CHANGED
@@ -8519,10 +8519,13 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
8519
8519
  }
8520
8520
  ] : []
8521
8521
  ];
8522
+ const logo = portal?.logo ? siteLogo(portal) : void 0;
8522
8523
  return {
8524
+ // The browser tab's icon is the project's logo; without one, the portal's own icon (API-134).
8525
+ ...logo ? { metadata: { favicon: logo.src.light } } : {},
8523
8526
  site: {
8524
8527
  title: portal?.name ?? resolved.project.name,
8525
- ...portal?.logo ? { logo: siteLogo(portal) } : {},
8528
+ ...logo ? { logo } : {},
8526
8529
  ...plan2.preview ? { banner: { message: PREVIEW_BANNER_MESSAGE, color: "caution" } } : {}
8527
8530
  },
8528
8531
  modules: {
@@ -11430,7 +11433,7 @@ import {
11430
11433
  // package.json
11431
11434
  var package_default = {
11432
11435
  name: "@lukoweb/apitogo",
11433
- version: "0.2.64",
11436
+ version: "0.2.66",
11434
11437
  type: "module",
11435
11438
  sideEffects: [
11436
11439
  "**/*.css",
@@ -23190,6 +23193,9 @@ function waitForJobStepLabel(kind, moduleType) {
23190
23193
  return "Job";
23191
23194
  }
23192
23195
  }
23196
+ function deletionFinished(error) {
23197
+ return error instanceof ProjectGoneError || error instanceof PlatformApiError && error.status === 404;
23198
+ }
23193
23199
  function projectDeletionNotFoundResult(last, jobId) {
23194
23200
  return last ?? {
23195
23201
  status: "Completed",
@@ -23251,7 +23257,7 @@ function pollProvisionJob(poll, options) {
23251
23257
  stepLabel: options.stepLabel,
23252
23258
  progress: options.progress?.child(options.stepLabel),
23253
23259
  onPollError: options.kind === "projectDeletion" ? (error, last) => {
23254
- if (error instanceof PlatformApiError && error.status === 404) {
23260
+ if (deletionFinished(error)) {
23255
23261
  return {
23256
23262
  status: "succeeded",
23257
23263
  value: projectDeletionNotFoundResult(last, options.jobId),
@@ -115,6 +115,11 @@ var ProtectedRoutesSchema = z.pipe(
115
115
 
116
116
  // src/vite/prerender/utils.ts
117
117
  var prerenderFileFor = (urlPath) => urlPath === "/" ? "/index.html" : urlPath.endsWith("/index") ? `${urlPath}/index.html` : `${urlPath}.html`;
118
+ var SCRIPT_SAFE_LT = `${"\\"}u003c`;
119
+ var redirectPageHtml = (to) => (
120
+ // Every `<` of the target becomes SCRIPT_SAFE_LT, so a target can never close the script early.
121
+ `<!doctype html><link rel="icon" href="data:,"><script>window.location.href=${JSON.stringify(to).replaceAll("<", SCRIPT_SAFE_LT)};</script>`
122
+ );
118
123
 
119
124
  // src/vite/prerender/worker.ts
120
125
  var { template, distDir, serverConfigPath, entryServerPath, writeRedirects } = Piscina.workerData;
@@ -149,7 +154,7 @@ var renderPage = async ({ urlPath }) => {
149
154
  if ([301, 302, 303, 307, 308].includes(response.status)) {
150
155
  const redirectTo = response.headers.get("Location") ?? "";
151
156
  if (writeRedirects) {
152
- const redirectHtml = `<!doctype html><script>window.location.href=${JSON.stringify(redirectTo)};</script>`;
157
+ const redirectHtml = redirectPageHtml(redirectTo);
153
158
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
154
159
  await fs.writeFile(outputPath, redirectHtml);
155
160
  }
@@ -3,6 +3,7 @@ import { type ApitogoConfig } from "../config.js";
3
3
  import { recentJobMessages, type ProgressReporter } from "../helpers/progress.js";
4
4
  export type WaitForJobKind = "module" | "deployment" | "azureInfrastructure" | "projectDeletion" | "devPortalDeploy" | "devPortalBackendUpdate";
5
5
  export declare function waitForJobStepLabel(kind: WaitForJobKind, moduleType?: string): string;
6
+ export declare function deletionFinished(error: unknown): boolean;
6
7
  export declare function handleActivateModule(client: PlatformApiClient, config: ApitogoConfig, args: {
7
8
  projectId?: string;
8
9
  moduleType: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.64",
3
+ "version": "0.2.66",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -400,10 +400,13 @@ export function buildSiteConfig(
400
400
  ]
401
401
  : []),
402
402
  ];
403
+ const logo = portal?.logo ? siteLogo(portal) : undefined;
403
404
  return {
405
+ // The browser tab's icon is the project's logo; without one, the portal's own icon (API-134).
406
+ ...(logo ? { metadata: { favicon: logo.src.light } } : {}),
404
407
  site: {
405
408
  title: portal?.name ?? resolved.project.name,
406
- ...(portal?.logo ? { logo: siteLogo(portal) } : {}),
409
+ ...(logo ? { logo } : {}),
407
410
  ...(plan.preview
408
411
  ? { banner: { message: PREVIEW_BANNER_MESSAGE, color: "caution" } }
409
412
  : {}),
@@ -518,9 +521,10 @@ export const PREVIEW_BANNER_MESSAGE =
518
521
  * The header logo as the site takes it: a file of the portal's `public/` folder becomes its address on the site, an https
519
522
  * address stays as it is (API-129). `check` refuses a file outside `public/`.
520
523
  */
521
- function siteLogo(
522
- portal: NonNullable<ResolvedConfigV2["portal"]>,
523
- ): Record<string, unknown> {
524
+ function siteLogo(portal: NonNullable<ResolvedConfigV2["portal"]>): {
525
+ src: { light: string; dark: string };
526
+ alt: string;
527
+ } {
524
528
  const logo = portal.logo as NonNullable<typeof portal.logo>;
525
529
  const publicDir = path.join(portal.dir, "public");
526
530
  const src = (file: string) =>
@@ -1,46 +1,49 @@
1
- import { Helmet } from "@zudoku/react-helmet-async";
2
- import type { PropsWithChildren } from "react";
3
- import { useLocation } from "react-router";
4
- import { joinUrl } from "../util/joinUrl.js";
5
- import { useZudoku } from "./context/ZudokuContext.js";
6
-
7
- export const Meta = ({ children }: PropsWithChildren) => {
8
- const { options } = useZudoku();
9
- const { metadata: meta } = options;
10
- const location = useLocation();
11
-
12
- return (
13
- <>
14
- <Helmet titleTemplate={meta?.title} defaultTitle={meta?.defaultTitle}>
15
- {options.canonicalUrlOrigin && (
16
- <link
17
- rel="canonical"
18
- href={joinUrl(
19
- options.canonicalUrlOrigin,
20
- options.basePath,
21
- location.pathname,
22
- )}
23
- />
24
- )}
25
- {meta?.description && (
26
- <meta name="description" content={meta.description} />
27
- )}
28
- {meta?.favicon && <link rel="icon" href={meta.favicon} />}
29
- {meta?.generator && <meta name="generator" content={meta.generator} />}
30
- {meta?.applicationName && (
31
- <meta name="application-name" content={meta.applicationName} />
32
- )}
33
- {meta?.referrer && <meta name="referrer" content={meta.referrer} />}
34
- {meta?.keywords && meta.keywords.length > 0 && (
35
- <meta name="keywords" content={meta.keywords.join(", ")} />
36
- )}
37
- {meta?.authors?.map((author) => (
38
- <meta key={author} name="author" content={author} />
39
- ))}
40
- {meta?.creator && <meta name="creator" content={meta.creator} />}
41
- {meta?.publisher && <meta name="publisher" content={meta.publisher} />}
42
- </Helmet>
43
- {children}
44
- </>
45
- );
46
- };
1
+ import { Helmet } from "@zudoku/react-helmet-async";
2
+ import type { PropsWithChildren } from "react";
3
+ import { useLocation } from "react-router";
4
+ // A site without its own icon would leave the browser asking for /favicon.ico, a 404 and a console error on every page
5
+ // (API-134). The build publishes this file with a hashed name, so no project has to carry it.
6
+ import defaultFavicon from "../assets/powered-by-icon.png";
7
+ import { joinUrl } from "../util/joinUrl.js";
8
+ import { useZudoku } from "./context/ZudokuContext.js";
9
+
10
+ export const Meta = ({ children }: PropsWithChildren) => {
11
+ const { options } = useZudoku();
12
+ const { metadata: meta } = options;
13
+ const location = useLocation();
14
+
15
+ return (
16
+ <>
17
+ <Helmet titleTemplate={meta?.title} defaultTitle={meta?.defaultTitle}>
18
+ {options.canonicalUrlOrigin && (
19
+ <link
20
+ rel="canonical"
21
+ href={joinUrl(
22
+ options.canonicalUrlOrigin,
23
+ options.basePath,
24
+ location.pathname,
25
+ )}
26
+ />
27
+ )}
28
+ {meta?.description && (
29
+ <meta name="description" content={meta.description} />
30
+ )}
31
+ <link rel="icon" href={meta?.favicon ?? defaultFavicon} />
32
+ {meta?.generator && <meta name="generator" content={meta.generator} />}
33
+ {meta?.applicationName && (
34
+ <meta name="application-name" content={meta.applicationName} />
35
+ )}
36
+ {meta?.referrer && <meta name="referrer" content={meta.referrer} />}
37
+ {meta?.keywords && meta.keywords.length > 0 && (
38
+ <meta name="keywords" content={meta.keywords.join(", ")} />
39
+ )}
40
+ {meta?.authors?.map((author) => (
41
+ <meta key={author} name="author" content={author} />
42
+ ))}
43
+ {meta?.creator && <meta name="creator" content={meta.creator} />}
44
+ {meta?.publisher && <meta name="publisher" content={meta.publisher} />}
45
+ </Helmet>
46
+ {children}
47
+ </>
48
+ );
49
+ };
@@ -87,3 +87,15 @@ export const routesToPaths = (routes: RouteObject[]): string[] =>
87
87
 
88
88
  export const routesToRewrites = (routes: RouteObject[]): RouteRewrite[] =>
89
89
  collectRewrites(resolveRoutes(routes));
90
+
91
+ /** `<` as a JavaScript string escape: a backslash, then u003c. */
92
+ const SCRIPT_SAFE_LT = `${"\\"}u003c`;
93
+
94
+ /**
95
+ * The page written for a route that redirects, such as `/` of a portal without a home page of its own. It declares an
96
+ * empty icon: without one the browser asks for /favicon.ico before the script runs, a 404 and a console error on every
97
+ * visit of the home page (API-134).
98
+ */
99
+ export const redirectPageHtml = (to: string): string =>
100
+ // Every `<` of the target becomes SCRIPT_SAFE_LT, so a target can never close the script early.
101
+ `<!doctype html><link rel="icon" href="data:,"><script>window.location.href=${JSON.stringify(to).replaceAll("<", SCRIPT_SAFE_LT)};</script>`;
@@ -7,7 +7,7 @@ import type { ZudokuConfig } from "../../config/validators/ZudokuConfig.js";
7
7
  import { runPluginTransformConfig } from "../../lib/core/transform-config.js";
8
8
  import { joinUrl } from "../../lib/util/joinUrl.js";
9
9
  import type { WorkerResult } from "./prerender.js";
10
- import { prerenderFileFor } from "./utils.js";
10
+ import { prerenderFileFor, redirectPageHtml } from "./utils.js";
11
11
 
12
12
  type EntryServer = typeof import("../../app/entry.server.js");
13
13
 
@@ -75,7 +75,7 @@ const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
75
75
  const redirectTo = response.headers.get("Location") ?? "";
76
76
 
77
77
  if (writeRedirects) {
78
- const redirectHtml = `<!doctype html><script>window.location.href=${JSON.stringify(redirectTo)};</script>`;
78
+ const redirectHtml = redirectPageHtml(redirectTo);
79
79
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
80
80
  await fs.writeFile(outputPath, redirectHtml);
81
81
  }