astro 4.2.2 → 4.2.4

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.
Files changed (32) hide show
  1. package/dist/@types/astro.d.ts +1 -1
  2. package/dist/cli/db/index.js +5 -3
  3. package/dist/core/app/index.js +11 -14
  4. package/dist/core/app/ssrPipeline.d.ts +0 -10
  5. package/dist/core/app/ssrPipeline.js +0 -19
  6. package/dist/core/build/buildPipeline.js +0 -4
  7. package/dist/core/build/generate.js +2 -1
  8. package/dist/core/constants.js +1 -1
  9. package/dist/core/dev/dev.js +1 -1
  10. package/dist/core/logger/core.d.ts +1 -1
  11. package/dist/core/messages.js +2 -2
  12. package/dist/core/pipeline.d.ts +0 -8
  13. package/dist/core/pipeline.js +1 -29
  14. package/dist/core/routing/manifest/create.js +46 -17
  15. package/dist/prerender/utils.js +1 -1
  16. package/dist/runtime/client/dev-toolbar/apps/audit/a11y.js +1 -1
  17. package/dist/runtime/client/dev-toolbar/apps/settings.js +2 -2
  18. package/dist/runtime/client/dev-toolbar/entrypoint.js +1 -1
  19. package/dist/runtime/client/dev-toolbar/settings.d.ts +4 -1
  20. package/dist/runtime/client/dev-toolbar/settings.js +8 -1
  21. package/dist/runtime/client/dev-toolbar/toolbar.d.ts +1 -0
  22. package/dist/runtime/client/dev-toolbar/toolbar.js +28 -6
  23. package/dist/runtime/server/endpoint.js +4 -7
  24. package/dist/vite-plugin-astro-server/devPipeline.js +0 -4
  25. package/dist/vite-plugin-dev-toolbar/vite-plugin-dev-toolbar.d.ts +1 -1
  26. package/dist/vite-plugin-dev-toolbar/vite-plugin-dev-toolbar.js +39 -2
  27. package/dist/vite-plugin-html/transform/escape.js +12 -5
  28. package/dist/vite-plugin-html/transform/slots.js +6 -2
  29. package/dist/vite-plugin-html/transform/utils.d.ts +2 -2
  30. package/dist/vite-plugin-html/transform/utils.js +26 -7
  31. package/dist/vite-plugin-markdown/images.js +4 -1
  32. package/package.json +2 -2
@@ -2401,7 +2401,7 @@ export interface ClientDirectiveConfig {
2401
2401
  export interface DevToolbarApp {
2402
2402
  id: string;
2403
2403
  name: string;
2404
- icon: Icon;
2404
+ icon?: Icon;
2405
2405
  init?(canvas: ShadowRoot, eventTarget: EventTarget): void | Promise<void>;
2406
2406
  beforeTogglingOff?(canvas: ShadowRoot): boolean | Promise<boolean>;
2407
2407
  }
@@ -1,5 +1,6 @@
1
- import { createLoggerFromFlags } from "../flags.js";
1
+ import { createLoggerFromFlags, flagsToAstroInlineConfig } from "../flags.js";
2
2
  import { getPackage } from "../install-package.js";
3
+ import { resolveConfig } from "../../core/config/config.js";
3
4
  async function db({ flags }) {
4
5
  const logger = createLoggerFromFlags(flags);
5
6
  const getPackageOpts = { skipAsk: flags.yes || flags.y, cwd: flags.root };
@@ -12,8 +13,9 @@ async function db({ flags }) {
12
13
  return;
13
14
  }
14
15
  const { cli } = dbPackage;
15
- const [command, ...args] = flags._.slice(3).map((v) => v.toString());
16
- await cli(command, args);
16
+ const inlineConfig = flagsToAstroInlineConfig(flags);
17
+ const { astroConfig } = await resolveConfig(inlineConfig, "build");
18
+ await cli({ flags, config: astroConfig });
17
19
  }
18
20
  export {
19
21
  db
@@ -17,7 +17,7 @@ import {
17
17
  createStylesheetElementSet
18
18
  } from "../render/ssr-element.js";
19
19
  import { matchRoute } from "../routing/match.js";
20
- import { EndpointNotFoundError, SSRRoutePipeline } from "./ssrPipeline.js";
20
+ import { SSRRoutePipeline } from "./ssrPipeline.js";
21
21
  import { deserializeManifest } from "./common.js";
22
22
  const localsSymbol = Symbol.for("astro.locals");
23
23
  const clientAddressSymbol = Symbol.for("astro.clientAddress");
@@ -187,20 +187,17 @@ class App {
187
187
  }
188
188
  response = await this.#pipeline.renderRoute(renderContext, pageModule);
189
189
  } catch (err) {
190
- if (err instanceof EndpointNotFoundError) {
191
- return this.#renderError(request, { status: 404, response: err.originalResponse });
192
- } else {
193
- this.#logger.error(null, err.stack || err.message || String(err));
194
- return this.#renderError(request, { status: 500 });
195
- }
190
+ this.#logger.error(null, err.stack || err.message || String(err));
191
+ return this.#renderError(request, { status: 500 });
196
192
  }
197
- if (routeData.type === "page" || routeData.type === "redirect") {
198
- if (REROUTABLE_STATUS_CODES.has(response.status)) {
199
- return this.#renderError(request, {
200
- response,
201
- status: response.status
202
- });
203
- }
193
+ if (REROUTABLE_STATUS_CODES.has(response.status) && response.headers.get("X-Astro-Reroute") !== "no") {
194
+ return this.#renderError(request, {
195
+ response,
196
+ status: response.status
197
+ });
198
+ }
199
+ if (response.headers.has("X-Astro-Reroute")) {
200
+ response.headers.delete("X-Astro-Reroute");
204
201
  }
205
202
  if (addCookieHeader) {
206
203
  for (const setCookieHeaderValue of App.getSetCookieFromResponse(response)) {
@@ -1,13 +1,3 @@
1
1
  import { Pipeline } from '../pipeline.js';
2
- import type { Environment } from '../render/index.js';
3
- /**
4
- * Thrown when an endpoint contains a response with the header "X-Astro-Response" === 'Not-Found'
5
- */
6
- export declare class EndpointNotFoundError extends Error {
7
- originalResponse: Response;
8
- constructor(originalResponse: Response);
9
- }
10
2
  export declare class SSRRoutePipeline extends Pipeline {
11
- #private;
12
- constructor(env: Environment);
13
3
  }
@@ -1,25 +1,6 @@
1
1
  import { Pipeline } from "../pipeline.js";
2
- class EndpointNotFoundError extends Error {
3
- originalResponse;
4
- constructor(originalResponse) {
5
- super();
6
- this.originalResponse = originalResponse;
7
- }
8
- }
9
2
  class SSRRoutePipeline extends Pipeline {
10
- constructor(env) {
11
- super(env);
12
- this.setEndpointHandler(this.#ssrEndpointHandler);
13
- }
14
- // This function is responsible for handling the result coming from an endpoint.
15
- async #ssrEndpointHandler(request, response) {
16
- if (response.headers.get("X-Astro-Response") === "Not-Found") {
17
- throw new EndpointNotFoundError(response);
18
- }
19
- return response;
20
- }
21
3
  }
22
4
  export {
23
- EndpointNotFoundError,
24
5
  SSRRoutePipeline
25
6
  };
@@ -51,7 +51,6 @@ class BuildPipeline extends Pipeline {
51
51
  this.#internals = internals;
52
52
  this.#staticBuildOptions = staticBuildOptions;
53
53
  this.#manifest = manifest;
54
- this.setEndpointHandler(this.#handleEndpointResult);
55
54
  }
56
55
  getInternals() {
57
56
  return this.#internals;
@@ -143,9 +142,6 @@ class BuildPipeline extends Pipeline {
143
142
  }
144
143
  return pages;
145
144
  }
146
- async #handleEndpointResult(_, response) {
147
- return response;
148
- }
149
145
  }
150
146
  export {
151
147
  BuildPipeline
@@ -347,7 +347,8 @@ function getUrlForPath(pathname, base, origin, format, routeType) {
347
347
  async function generatePath(pathname, pipeline, gopts, route) {
348
348
  const { mod, scripts: hoistedScripts, styles: _styles } = gopts;
349
349
  const manifest = pipeline.getManifest();
350
- pipeline.getEnvironment().logger.debug("build", `Generating: ${pathname}`);
350
+ const logger = pipeline.getLogger();
351
+ logger.debug("build", `Generating: ${pathname}`);
351
352
  const links = /* @__PURE__ */ new Set();
352
353
  const scripts = createModuleScriptsSet(
353
354
  hoistedScripts ? [hoistedScripts] : [],
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "4.2.2";
1
+ const ASTRO_VERSION = "4.2.4";
2
2
  const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
3
3
  ".markdown",
4
4
  ".mdown",
@@ -23,7 +23,7 @@ async function dev(inlineConfig) {
23
23
  base: restart.container.settings.config.base
24
24
  })
25
25
  );
26
- const currentVersion = "4.2.2";
26
+ const currentVersion = "4.2.4";
27
27
  if (currentVersion.includes("-")) {
28
28
  logger.warn("SKIP_FORMAT", msg.prerelease({ currentVersion }));
29
29
  }
@@ -7,7 +7,7 @@ export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
7
7
  * rather than specific to a single command, function, use, etc. The label will be
8
8
  * shown in the log message to the user, so it should be relevant.
9
9
  */
10
- export type LoggerLabel = 'add' | 'build' | 'check' | 'config' | 'content' | 'deprecated' | 'markdown' | 'router' | 'types' | 'vite' | 'watch' | 'middleware' | 'preferences' | 'redirects' | 'SKIP_FORMAT';
10
+ export type LoggerLabel = 'add' | 'build' | 'check' | 'config' | 'content' | 'deprecated' | 'markdown' | 'router' | 'types' | 'vite' | 'watch' | 'middleware' | 'preferences' | 'redirects' | 'toolbar' | 'SKIP_FORMAT';
11
11
  export interface LogOptions {
12
12
  dest: LogWritable<LogMessage>;
13
13
  level: LoggerLevel;
@@ -36,7 +36,7 @@ function serverStart({
36
36
  host,
37
37
  base
38
38
  }) {
39
- const version = "4.2.2";
39
+ const version = "4.2.4";
40
40
  const localPrefix = `${dim("\u2503")} Local `;
41
41
  const networkPrefix = `${dim("\u2503")} Network `;
42
42
  const emptyPrefix = " ".repeat(11);
@@ -258,7 +258,7 @@ function printHelp({
258
258
  message.push(
259
259
  linebreak(),
260
260
  ` ${bgGreen(black(` ${commandName} `))} ${green(
261
- `v${"4.2.2"}`
261
+ `v${"4.2.4"}`
262
262
  )} ${headline}`
263
263
  );
264
264
  }
@@ -1,6 +1,5 @@
1
1
  import type { ComponentInstance, MiddlewareHandler } from '../@types/astro.js';
2
2
  import { type Environment, type RenderContext } from './render/index.js';
3
- type EndpointResultHandler = (originalRequest: Request, result: Response) => Promise<Response> | Response;
4
3
  export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void;
5
4
  /**
6
5
  * This is the basic class of a pipeline.
@@ -16,12 +15,6 @@ export declare class Pipeline {
16
15
  */
17
16
  constructor(env: Environment);
18
17
  setEnvironment(): void;
19
- /**
20
- * When rendering a route, an "endpoint" will a type that needs to be handled and transformed into a `Response`.
21
- *
22
- * Each consumer might have different needs; use this function to set up the handler.
23
- */
24
- setEndpointHandler(handler: EndpointResultHandler): void;
25
18
  /**
26
19
  * A middleware function that will be called before each request.
27
20
  */
@@ -44,4 +37,3 @@ export declare class Pipeline {
44
37
  */
45
38
  onBeforeRenderRoute(fn: PipelineHookFunction): void;
46
39
  }
47
- export {};
@@ -8,11 +8,6 @@ class Pipeline {
8
8
  #hooks = {
9
9
  before: []
10
10
  };
11
- /**
12
- * The handler accepts the *original* `Request` and result returned by the endpoint.
13
- * It must return a `Response`.
14
- */
15
- #endpointHandler;
16
11
  /**
17
12
  * When creating a pipeline, an environment is mandatory.
18
13
  * The environment won't change for the whole lifetime of the pipeline.
@@ -22,14 +17,6 @@ class Pipeline {
22
17
  }
23
18
  setEnvironment() {
24
19
  }
25
- /**
26
- * When rendering a route, an "endpoint" will a type that needs to be handled and transformed into a `Response`.
27
- *
28
- * Each consumer might have different needs; use this function to set up the handler.
29
- */
30
- setEndpointHandler(handler) {
31
- this.#endpointHandler = handler;
32
- }
33
20
  /**
34
21
  * A middleware function that will be called before each request.
35
22
  */
@@ -55,22 +42,7 @@ class Pipeline {
55
42
  for (const hook of this.#hooks.before) {
56
43
  hook(renderContext, componentInstance);
57
44
  }
58
- const result = await this.#tryRenderRoute(
59
- renderContext,
60
- this.env,
61
- componentInstance,
62
- this.#onRequest
63
- );
64
- if (renderContext.route.type === "endpoint") {
65
- if (!this.#endpointHandler) {
66
- throw new Error(
67
- "You created a pipeline that does not know how to handle the result coming from an endpoint."
68
- );
69
- }
70
- return this.#endpointHandler(renderContext.request, result);
71
- } else {
72
- return result;
73
- }
45
+ return await this.#tryRenderRoute(renderContext, this.env, componentInstance, this.#onRequest);
74
46
  }
75
47
  /**
76
48
  * It attempts to render a route. A route can be a:
@@ -99,28 +99,57 @@ function isSemanticallyEqualSegment(segmentA, segmentB) {
99
99
  return true;
100
100
  }
101
101
  function routeComparator(a, b) {
102
+ const commonLength = Math.min(a.segments.length, b.segments.length);
103
+ for (let index = 0; index < commonLength; index++) {
104
+ const aSegment = a.segments[index];
105
+ const bSegment = b.segments[index];
106
+ const aIsStatic = aSegment.every((part) => !part.dynamic && !part.spread);
107
+ const bIsStatic = bSegment.every((part) => !part.dynamic && !part.spread);
108
+ if (aIsStatic && bIsStatic) {
109
+ const aContent = aSegment.map((part) => part.content).join("");
110
+ const bContent = bSegment.map((part) => part.content).join("");
111
+ if (aContent !== bContent) {
112
+ return aContent.localeCompare(bContent);
113
+ }
114
+ }
115
+ if (aIsStatic !== bIsStatic) {
116
+ return aIsStatic ? -1 : 1;
117
+ }
118
+ const aHasSpread = aSegment.some((part) => part.spread);
119
+ const bHasSpread = bSegment.some((part) => part.spread);
120
+ if (aHasSpread !== bHasSpread) {
121
+ return aHasSpread ? 1 : -1;
122
+ }
123
+ }
124
+ if (Math.abs(a.segments.length - b.segments.length) === 1) {
125
+ const aEndsInRest = a.segments.at(-1)?.some((part) => part.spread);
126
+ const bEndsInRest = b.segments.at(-1)?.some((part) => part.spread);
127
+ if (a.segments.length > b.segments.length && !bEndsInRest) {
128
+ return 1;
129
+ }
130
+ if (b.segments.length > a.segments.length && !aEndsInRest) {
131
+ return -1;
132
+ }
133
+ }
134
+ if (a.isIndex !== b.isIndex) {
135
+ if (a.isIndex) {
136
+ const followingBSegment = b.segments.at(a.segments.length);
137
+ const followingBSegmentIsStatic = followingBSegment?.every(
138
+ (part) => !part.dynamic && !part.spread
139
+ );
140
+ return followingBSegmentIsStatic ? 1 : -1;
141
+ }
142
+ const followingASegment = a.segments.at(b.segments.length);
143
+ const followingASegmentIsStatic = followingASegment?.every(
144
+ (part) => !part.dynamic && !part.spread
145
+ );
146
+ return followingASegmentIsStatic ? -1 : 1;
147
+ }
102
148
  const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
103
149
  const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
104
150
  if (aLength !== bLength) {
105
151
  return aLength > bLength ? -1 : 1;
106
152
  }
107
- const aIsStatic = a.segments.every(
108
- (segment) => segment.every((part) => !part.dynamic && !part.spread)
109
- );
110
- const bIsStatic = b.segments.every(
111
- (segment) => segment.every((part) => !part.dynamic && !part.spread)
112
- );
113
- if (aIsStatic !== bIsStatic) {
114
- return aIsStatic ? -1 : 1;
115
- }
116
- const aHasSpread = a.segments.some((segment) => segment.some((part) => part.spread));
117
- const bHasSpread = b.segments.some((segment) => segment.some((part) => part.spread));
118
- if (aHasSpread !== bHasSpread) {
119
- return aHasSpread ? 1 : -1;
120
- }
121
- if (a.prerender !== b.prerender) {
122
- return a.prerender ? -1 : 1;
123
- }
124
153
  if (a.type === "endpoint" !== (b.type === "endpoint")) {
125
154
  return a.type === "endpoint" ? -1 : 1;
126
155
  }
@@ -3,7 +3,7 @@ function isServerLikeOutput(config) {
3
3
  return config.output === "server" || config.output === "hybrid";
4
4
  }
5
5
  function getPrerenderDefault(config) {
6
- return config.output === "hybrid";
6
+ return config.output !== "server";
7
7
  }
8
8
  function getOutputDirectory(config) {
9
9
  const ssr = isServerLikeOutput(config);
@@ -193,7 +193,7 @@ const ariaAttributes = new Set(
193
193
  )
194
194
  );
195
195
  const ariaRoles = new Set(
196
- "alert alertdialog application article banner button cell checkbox columnheader combobox complementary contentinfo definition dialog directory document feed figure form grid gridcell group heading img link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem".split(
196
+ "alert alertdialog application article banner button cell checkbox columnheader combobox complementary contentinfo definition dialog directory document feed figure form grid gridcell group heading img link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status switch tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem".split(
197
197
  " "
198
198
  )
199
199
  );
@@ -14,7 +14,7 @@ const settingsRows = [
14
14
  }
15
15
  settings.updateSetting("disableAppNotification", evt.currentTarget.checked);
16
16
  const action = evt.currentTarget.checked ? "disabled" : "enabled";
17
- settings.log(`App notification badges ${action}`);
17
+ settings.logger.verboseLog(`App notification badges ${action}`);
18
18
  }
19
19
  }
20
20
  },
@@ -27,7 +27,7 @@ const settingsRows = [
27
27
  if (evt.currentTarget instanceof HTMLInputElement) {
28
28
  settings.updateSetting("verbose", evt.currentTarget.checked);
29
29
  const action = evt.currentTarget.checked ? "enabled" : "disabled";
30
- settings.log(`Verbose logging ${action}`);
30
+ settings.logger.verboseLog(`Verbose logging ${action}`);
31
31
  }
32
32
  }
33
33
  }
@@ -173,7 +173,7 @@ document.addEventListener("DOMContentLoaded", async () => {
173
173
  button.setAttribute("data-app-id", app.id);
174
174
  const iconContainer = document.createElement("div");
175
175
  const iconElement = document.createElement("template");
176
- iconElement.innerHTML = getAppIcon(app.icon);
176
+ iconElement.innerHTML = app.icon ? getAppIcon(app.icon) : "?";
177
177
  iconContainer.append(iconElement.content.cloneNode(true));
178
178
  const notification = document.createElement("div");
179
179
  notification.classList.add("notification");
@@ -9,5 +9,8 @@ export declare const defaultSettings: {
9
9
  export declare const settings: {
10
10
  readonly config: Settings;
11
11
  updateSetting: (key: keyof Settings, value: boolean) => void;
12
- log: (message: string) => void;
12
+ logger: {
13
+ log: (message: string) => void;
14
+ verboseLog: (message: string) => void;
15
+ };
13
16
  };
@@ -30,7 +30,14 @@ function getSettings() {
30
30
  return _settings;
31
31
  },
32
32
  updateSetting,
33
- log
33
+ logger: {
34
+ log,
35
+ verboseLog: (message) => {
36
+ if (_settings.verbose) {
37
+ log(message);
38
+ }
39
+ }
40
+ }
34
41
  };
35
42
  }
36
43
  export {
@@ -28,6 +28,7 @@ export declare class AstroDevToolbar extends HTMLElement {
28
28
  getAppTemplate(app: DevToolbarApp): string;
29
29
  getAppById(id: string): DevToolbarApp | undefined;
30
30
  getAppCanvasById(id: string): HTMLElement | null;
31
+ getAppButtonById(id: string): HTMLElement | null;
31
32
  toggleAppStatus(app: DevToolbarApp): Promise<void>;
32
33
  setAppStatus(app: DevToolbarApp, newStatus: boolean): Promise<boolean>;
33
34
  isHidden(): boolean;
@@ -109,6 +109,11 @@ class AstroDevToolbar extends HTMLElement {
109
109
  outline-offset: -3px;
110
110
  }
111
111
 
112
+ #dev-bar #bar-container .item[data-app-error]:hover, #dev-bar #bar-container .item[data-app-error]:focus-visible {
113
+ cursor: not-allowed;
114
+ background: #ff252520;
115
+ }
116
+
112
117
  #dev-bar .item:first-of-type {
113
118
  border-top-left-radius: 9999px;
114
119
  border-bottom-left-radius: 9999px;
@@ -149,6 +154,10 @@ class AstroDevToolbar extends HTMLElement {
149
154
  border-top: 5px solid #343841;
150
155
  }
151
156
 
157
+ #dev-bar .item[data-app-error] .icon {
158
+ opacity: 0.35;
159
+ }
160
+
152
161
  #dev-bar .item:hover .item-tooltip, #dev-bar .item:not(.active):focus-visible .item-tooltip {
153
162
  transition: opacity 0.2s ease-in-out 200ms;
154
163
  opacity: 1;
@@ -229,8 +238,7 @@ class AstroDevToolbar extends HTMLElement {
229
238
  this.devToolbarContainer = this.shadowRoot.querySelector("#dev-toolbar-root");
230
239
  this.attachEvents();
231
240
  this.apps.forEach(async (app) => {
232
- if (settings.config.verbose)
233
- console.log(`Creating app canvas for ${app.id}`);
241
+ settings.logger.verboseLog(`Creating app canvas for ${app.id}`);
234
242
  const appCanvas = document.createElement("astro-dev-toolbar-app-canvas");
235
243
  appCanvas.dataset.appId = app.id;
236
244
  this.shadowRoot?.append(appCanvas);
@@ -311,8 +319,7 @@ class AstroDevToolbar extends HTMLElement {
311
319
  const shadowRoot = this.getAppCanvasById(app.id).shadowRoot;
312
320
  app.status = "loading";
313
321
  try {
314
- if (settings.config.verbose)
315
- console.info(`Initializing app ${app.id}`);
322
+ settings.logger.verboseLog(`Initializing app ${app.id}`);
316
323
  await app.init?.(shadowRoot, app.eventTarget);
317
324
  app.status = "ready";
318
325
  if (import.meta.hot) {
@@ -322,11 +329,23 @@ class AstroDevToolbar extends HTMLElement {
322
329
  } catch (e) {
323
330
  console.error(`Failed to init app ${app.id}, error: ${e}`);
324
331
  app.status = "error";
332
+ if (import.meta.hot) {
333
+ import.meta.hot.send("astro:devtoolbar:error:init", {
334
+ app,
335
+ error: e instanceof Error ? e.stack : e
336
+ });
337
+ }
338
+ const appButton = this.getAppButtonById(app.id);
339
+ const appTooltip = appButton?.querySelector(".item-tooltip");
340
+ if (appButton && appTooltip) {
341
+ appButton.toggleAttribute("data-app-error", true);
342
+ appTooltip.innerText = `Error initializing ${app.name}`;
343
+ }
325
344
  }
326
345
  }
327
346
  getAppTemplate(app) {
328
347
  return `<button class="item" data-app-id="${app.id}">
329
- <div class="icon">${getAppIcon(app.icon)}<div class="notification"></div></div>
348
+ <div class="icon">${app.icon ? getAppIcon(app.icon) : "?"}<div class="notification"></div></div>
330
349
  <span class="item-tooltip">${app.name}</span>
331
350
  </button>`;
332
351
  }
@@ -338,6 +357,9 @@ class AstroDevToolbar extends HTMLElement {
338
357
  `astro-dev-toolbar-app-canvas[data-app-id="${id}"]`
339
358
  );
340
359
  }
360
+ getAppButtonById(id) {
361
+ return this.shadowRoot.querySelector(`[data-app-id="${id}"]`);
362
+ }
341
363
  async toggleAppStatus(app) {
342
364
  const activeApp = this.getActiveApp();
343
365
  if (activeApp) {
@@ -361,7 +383,7 @@ class AstroDevToolbar extends HTMLElement {
361
383
  return false;
362
384
  }
363
385
  app.active = newStatus ?? !app.active;
364
- const mainBarButton = this.shadowRoot.querySelector(`[data-app-id="${app.id}"]`);
386
+ const mainBarButton = this.getAppButtonById(app.id);
365
387
  const moreBarButton = this.getAppCanvasById("astro:more")?.shadowRoot?.querySelector(
366
388
  `[data-app-id="${app.id}"]`
367
389
  );
@@ -19,14 +19,11 @@ Found handlers: ${Object.keys(mod).map((exp) => JSON.stringify(exp)).join(", ")}
19
19
  ` + ("all" in mod ? `One of the exported handlers is "all" (lowercase), did you mean to export 'ALL'?
20
20
  ` : "")
21
21
  );
22
- return new Response(null, {
23
- status: 404,
24
- headers: {
25
- "X-Astro-Response": "Not-Found"
26
- }
27
- });
22
+ return new Response(null, { status: 404 });
28
23
  }
29
- return handler.call(mod, context);
24
+ const response = await handler.call(mod, context);
25
+ response.headers.set("X-Astro-Reroute", "no");
26
+ return response;
30
27
  }
31
28
  export {
32
29
  renderEndpoint
@@ -18,7 +18,6 @@ class DevPipeline extends Pipeline {
18
18
  this.#devLogger = logger;
19
19
  this.#settings = settings;
20
20
  this.#loader = loader;
21
- this.setEndpointHandler(this.#handleEndpointResult);
22
21
  }
23
22
  clearRouteCache() {
24
23
  this.env.routeCache.clearAll();
@@ -58,9 +57,6 @@ class DevPipeline extends Pipeline {
58
57
  streaming: true
59
58
  });
60
59
  }
61
- async #handleEndpointResult(_, response) {
62
- return response;
63
- }
64
60
  async handleFallback() {
65
61
  }
66
62
  }
@@ -1,3 +1,3 @@
1
1
  import type * as vite from 'vite';
2
2
  import type { AstroPluginOptions } from '../@types/astro.js';
3
- export default function astroDevToolbar({ settings }: AstroPluginOptions): vite.Plugin;
3
+ export default function astroDevToolbar({ settings, logger }: AstroPluginOptions): vite.Plugin;
@@ -1,6 +1,6 @@
1
1
  const VIRTUAL_MODULE_ID = "astro:dev-toolbar";
2
2
  const resolvedVirtualModuleId = "\0" + VIRTUAL_MODULE_ID;
3
- function astroDevToolbar({ settings }) {
3
+ function astroDevToolbar({ settings, logger }) {
4
4
  return {
5
5
  name: "astro:dev-toolbar",
6
6
  config() {
@@ -16,12 +16,49 @@ function astroDevToolbar({ settings }) {
16
16
  return resolvedVirtualModuleId;
17
17
  }
18
18
  },
19
+ configureServer(server) {
20
+ server.ws.on("astro:devtoolbar:error:load", (args) => {
21
+ logger.error(
22
+ "toolbar",
23
+ `Failed to load dev toolbar app from ${args.entrypoint}: ${args.error}`
24
+ );
25
+ });
26
+ server.ws.on("astro:devtoolbar:error:init", (args) => {
27
+ logger.error(
28
+ "toolbar",
29
+ `Failed to initialize dev toolbar app ${args.app.name} (${args.app.id}):
30
+ ${args.error}`
31
+ );
32
+ });
33
+ },
19
34
  async load(id) {
20
35
  if (id === resolvedVirtualModuleId) {
21
36
  return `
22
37
  export const loadDevToolbarApps = async () => {
23
- return [${settings.devToolbarApps.map((plugin) => `(await import(${JSON.stringify(plugin)})).default`).join(",")}];
38
+ return (await Promise.all([${settings.devToolbarApps.map((plugin) => `safeLoadPlugin(${JSON.stringify(plugin)})`).join(",")}])).filter(app => app);
24
39
  };
40
+
41
+ async function safeLoadPlugin(entrypoint) {
42
+ try {
43
+ const app = (await import(/* @vite-ignore */ entrypoint)).default;
44
+
45
+ if (typeof app !== 'object' || !app.id || !app.name) {
46
+ throw new Error("Apps must default export an object with an id, and a name.");
47
+ }
48
+
49
+ return app;
50
+ } catch (err) {
51
+ console.error(\`Failed to load dev toolbar app from \${entrypoint}: \${err.message}\`);
52
+
53
+ if (import.meta.hot) {
54
+ import.meta.hot.send('astro:devtoolbar:error:load', { entrypoint: entrypoint, error: err.message })
55
+ }
56
+
57
+ return undefined;
58
+ }
59
+
60
+ return undefined;
61
+ }
25
62
  `;
26
63
  }
27
64
  }
@@ -1,16 +1,23 @@
1
1
  import { visit } from "unist-util-visit";
2
- import { escape, needsEscape, replaceAttribute } from "./utils.js";
2
+ import { escapeTemplateLiteralCharacters, needsEscape, replaceAttribute } from "./utils.js";
3
3
  const rehypeEscape = ({ s }) => {
4
4
  return (tree) => {
5
5
  visit(tree, (node) => {
6
6
  if (node.type === "text" || node.type === "comment") {
7
7
  if (needsEscape(node.value)) {
8
- s.overwrite(node.position.start.offset, node.position.end.offset, escape(node.value));
8
+ s.overwrite(
9
+ node.position.start.offset,
10
+ node.position.end.offset,
11
+ escapeTemplateLiteralCharacters(node.value)
12
+ );
9
13
  }
10
14
  } else if (node.type === "element") {
11
- for (const [key, value] of Object.entries(node.properties ?? {})) {
12
- const newKey = needsEscape(key) ? escape(key) : key;
13
- const newValue = needsEscape(value) ? escape(value) : value;
15
+ if (!node.properties)
16
+ return;
17
+ for (let [key, value] of Object.entries(node.properties)) {
18
+ key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
19
+ const newKey = needsEscape(key) ? escapeTemplateLiteralCharacters(key) : key;
20
+ const newValue = needsEscape(value) ? escapeTemplateLiteralCharacters(value) : value;
14
21
  if (newKey === key && newValue === value)
15
22
  continue;
16
23
  replaceAttribute(s, node, key, value === "" ? newKey : `${newKey}="${newValue}"`);
@@ -1,5 +1,5 @@
1
1
  import { visit } from "unist-util-visit";
2
- import { escape } from "./utils.js";
2
+ import { escapeTemplateLiteralCharacters } from "./utils.js";
3
3
  const rehypeSlots = ({ s }) => {
4
4
  return (tree, file) => {
5
5
  visit(tree, (node) => {
@@ -12,7 +12,11 @@ const rehypeSlots = ({ s }) => {
12
12
  const first = node.children.at(0) ?? node;
13
13
  const last = node.children.at(-1) ?? node;
14
14
  const text = file.value.slice(first.position?.start.offset ?? 0, last.position?.end.offset ?? 0).toString();
15
- s.overwrite(start, end, `\${${SLOT_PREFIX}["${name}"] ?? \`${escape(text).trim()}\`}`);
15
+ s.overwrite(
16
+ start,
17
+ end,
18
+ `\${${SLOT_PREFIX}["${name}"] ?? \`${escapeTemplateLiteralCharacters(text).trim()}\`}`
19
+ );
16
20
  }
17
21
  });
18
22
  };
@@ -1,5 +1,5 @@
1
1
  import type { Element } from 'hast';
2
2
  import type MagicString from 'magic-string';
3
- export declare function replaceAttribute(s: MagicString, node: Element, key: string, newValue: string): void;
3
+ export declare function replaceAttribute(s: MagicString, node: Element, key: string, newValue: string): MagicString | undefined;
4
4
  export declare function needsEscape(value: any): value is string;
5
- export declare function escape(value: string): string;
5
+ export declare function escapeTemplateLiteralCharacters(value: string): string;
@@ -10,20 +10,39 @@ function replaceAttribute(s, node, key, newValue) {
10
10
  const token = tokens[0].replace(/([^>])(\>[\s\S]*$)/gim, "$1");
11
11
  if (token.trim() === key) {
12
12
  const end = start + key.length;
13
- s.overwrite(start, end, newValue);
13
+ return s.overwrite(start, end, newValue, { contentOnly: true });
14
14
  } else {
15
- const end = start + `${key}=${tokens[2]}${tokens[3]}${tokens[2]}`.length;
16
- s.overwrite(start, end, newValue);
15
+ const length = token.length;
16
+ const end = start + length;
17
+ return s.overwrite(start, end, newValue, { contentOnly: true });
17
18
  }
18
19
  }
20
+ const NEEDS_ESCAPE_RE = /[`\\]|\$\{/g;
19
21
  function needsEscape(value) {
20
- return typeof value === "string" && (value.includes("`") || value.includes("${"));
22
+ NEEDS_ESCAPE_RE.lastIndex = 0;
23
+ return typeof value === "string" && NEEDS_ESCAPE_RE.test(value);
21
24
  }
22
- function escape(value) {
23
- return value.replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
25
+ function escapeTemplateLiteralCharacters(value) {
26
+ NEEDS_ESCAPE_RE.lastIndex = 0;
27
+ let char;
28
+ let startIndex = 0;
29
+ let segment = "";
30
+ let text = "";
31
+ while ([char] = NEEDS_ESCAPE_RE.exec(value) ?? []) {
32
+ if (!char) {
33
+ text += value.slice(startIndex);
34
+ break;
35
+ }
36
+ const endIndex = NEEDS_ESCAPE_RE.lastIndex - char.length;
37
+ const prefix = segment === "\\" ? "" : "\\";
38
+ segment = prefix + char;
39
+ text += value.slice(startIndex, endIndex) + segment;
40
+ startIndex = NEEDS_ESCAPE_RE.lastIndex;
41
+ }
42
+ return text;
24
43
  }
25
44
  export {
26
- escape,
45
+ escapeTemplateLiteralCharacters,
27
46
  needsEscape,
28
47
  replaceAttribute
29
48
  };
@@ -8,7 +8,10 @@ function getMarkdownCodeForImages(imagePaths, html) {
8
8
  ${imagePaths.map((entry) => {
9
9
  const rawUrl = JSON.stringify(entry.raw);
10
10
  return `{
11
- const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl} + '[^"]*)"', 'g');
11
+ const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl.replace(
12
+ /[.*+?^${}()|[\]\\]/g,
13
+ "\\\\$&"
14
+ )} + '[^"]*)"', 'g');
12
15
  let match;
13
16
  let occurrenceCounter = 0;
14
17
  while ((match = regex.exec(html)) !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "4.2.2",
3
+ "version": "4.2.4",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -156,7 +156,7 @@
156
156
  "tsconfck": "^3.0.0",
157
157
  "unist-util-visit": "^5.0.0",
158
158
  "vfile": "^6.0.1",
159
- "vite": "^5.0.10",
159
+ "vite": "^5.0.12",
160
160
  "vitefu": "^0.2.5",
161
161
  "which-pm": "^2.1.1",
162
162
  "yargs-parser": "^21.1.1",