quickbase-js 0.1.0-beta.10 → 0.1.0-beta.12

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 (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +56 -24
  3. package/dist/esm/quickbase.d.ts +1 -0
  4. package/dist/esm/quickbase.js +125 -87
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +125 -87
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/examples/quicbase-jsCDN.html +2 -2
  13. package/package.json +1 -1
  14. package/src/ThrottleBucket.ts +52 -16
  15. package/src/authorizationStrategy.ts +62 -35
  16. package/src/generated-unified/QuickbaseClient.ts +1 -1
  17. package/src/invokeMethod.ts +33 -66
  18. package/src/quickbaseClient.ts +12 -7
  19. package/src/rateLimiter.ts +8 -3
  20. package/src/tokenCache.ts +4 -0
  21. package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +208 -0
  22. package/tests/vitest/qb/ThrottleBucket.test.ts +77 -0
  23. package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +68 -0
  24. package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +63 -0
  25. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
  26. package/tests/vitest/unit/ThrottleBucket.test.ts +16 -10
  27. package/tests/vitest/unit/apps/getAppConcurrency.test.ts +151 -0
  28. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +20 -2
  29. package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +62 -0
  30. package/tests/vitest/unit/index.ts +1 -1
  31. package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +3 -3
  32. package/tools/log-gen.ts +1 -1
  33. package/tools/log-gen.yaml +130 -78
  34. package/api-examples.md +0 -49
  35. package/markdown-examples.md +0 -85
@@ -0,0 +1,62 @@
1
+ // tests/vitest/unit/auth/userTokenConcurrency.test.ts
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+ import { QB_APP_ID, QB_REALM, QB_USER_TOKEN, mockFetch } from "@tests/setup.ts";
5
+
6
+ describe("QuickbaseClient Unit - UserTokenStrategy Concurrency", () => {
7
+ it("handles concurrent getApp requests with static user token", async () => {
8
+ mockFetch.mockClear();
9
+ const client = quickbase({
10
+ realm: QB_REALM,
11
+ userToken: QB_USER_TOKEN,
12
+ debug: true,
13
+ fetchApi: mockFetch,
14
+ });
15
+
16
+ mockFetch.mockResolvedValue({
17
+ ok: true,
18
+ status: 200,
19
+ json: () =>
20
+ Promise.resolve({
21
+ id: QB_APP_ID,
22
+ name: "test-app",
23
+ created: "2025-02-13T18:22:33Z",
24
+ updated: "2025-03-22T04:00:18Z",
25
+ }),
26
+ });
27
+
28
+ const concurrentRequests = [
29
+ client.getApp({ appId: QB_APP_ID }),
30
+ client.getApp({ appId: QB_APP_ID }),
31
+ client.getApp({ appId: QB_APP_ID }),
32
+ ];
33
+
34
+ const results = await Promise.all(concurrentRequests);
35
+
36
+ expect(results).toHaveLength(3);
37
+ results.forEach((result) =>
38
+ expect(result).toEqual({
39
+ id: QB_APP_ID,
40
+ name: "test-app",
41
+ created: expect.any(Date),
42
+ updated: expect.any(Date),
43
+ })
44
+ );
45
+
46
+ const appCalls = mockFetch.mock.calls.filter((call) =>
47
+ call[0].includes(`/apps/${QB_APP_ID}`)
48
+ );
49
+ expect(appCalls).toHaveLength(3);
50
+ appCalls.forEach((call) =>
51
+ expect(call[1].headers).toMatchObject({
52
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
53
+ })
54
+ );
55
+
56
+ // No token fetches occur with UserTokenStrategy
57
+ const tokenFetchCalls = mockFetch.mock.calls.filter((call) =>
58
+ call[0].includes("/auth")
59
+ );
60
+ expect(tokenFetchCalls).toHaveLength(0);
61
+ });
62
+ });
@@ -9,7 +9,7 @@ export * from "./apps/getApp.test.ts";
9
9
  export * from "./auth/fetchTempToken401.test.ts";
10
10
  export * from "./auth/getTempToken.test.ts";
11
11
  export * from "./auth/retryOn401.test.ts";
12
- export * from "./auth/retryTempTokenTwo401.test.ts";
12
+ export * from "./auth/retryTempTokenMaxRetries.test.ts";
13
13
 
14
14
  // Fields
15
15
  export * from "./fields/getFields.test.ts";
@@ -1,13 +1,13 @@
1
1
  // tests/vitest/unit/quickbaseClient.test.ts
2
2
 
3
3
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
- import { quickbase } from "../../../src/quickbaseClient";
4
+ import { quickbase } from "../../../src/quickbaseClient.ts";
5
5
  import { mockFetch, QB_APP_ID, QB_REALM } from "../../setup.ts";
6
6
  import {
7
7
  UpdateAppRequest,
8
8
  UpdateApp200Response,
9
- } from "../../../src/generated/models";
10
- import { ChangesetSolution200Response } from "../../../src/generated/models";
9
+ } from "../../../src/generated/models/index.ts";
10
+ import { ChangesetSolution200Response } from "../../../src/generated/models/index.ts";
11
11
 
12
12
  describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
13
13
  let clientTemp: ReturnType<typeof quickbase>;
package/tools/log-gen.ts CHANGED
@@ -53,7 +53,7 @@ const includeFiles: string[] = [
53
53
  "src/generated-unified/QuickbaseClient.ts",
54
54
  "src/authorizationStrategy.ts",
55
55
  "src/rateLimiter.ts",
56
- // "src/RateLimitError.ts",
56
+ "src/RateLimitError.ts",
57
57
  "src/ThrottleBucket.ts",
58
58
  "src/tokenCache.ts",
59
59
  "src/utils.ts",
@@ -1,7 +1,7 @@
1
1
  project: >-
2
2
  quickbase-js
3
3
  date: >-
4
- 3/21/2025
4
+ 3/23/2025
5
5
  root: >-
6
6
  /home/drew/Projects/quickbase-js
7
7
  goals:
@@ -727,10 +727,6 @@ tree:
727
727
  README.md
728
728
  type: >-
729
729
  file
730
- - name: >-
731
- api-examples.md
732
- type: >-
733
- file
734
730
  - name: >-
735
731
  build-common.js
736
732
  type: >-
@@ -1056,10 +1052,6 @@ tree:
1056
1052
  index.md
1057
1053
  type: >-
1058
1054
  file
1059
- - name: >-
1060
- markdown-examples.md
1061
- type: >-
1062
- file
1063
1055
  - name: >-
1064
1056
  open-api
1065
1057
  type: >-
@@ -1194,6 +1186,26 @@ tree:
1194
1186
  RateLimitError.ts
1195
1187
  type: >-
1196
1188
  file
1189
+ contents: >
1190
+ //
1191
+ src/RateLimitError.ts
1192
+
1193
+ export
1194
+ class
1195
+ RateLimitError
1196
+ extends
1197
+ Error
1198
+ {
1199
+ public readonly status: number;
1200
+ public readonly retryAfter?: number;
1201
+
1202
+ constructor(message: string, status: number, retryAfter?: number) {
1203
+ super(message);
1204
+ this.name = "RateLimitError";
1205
+ this.status = status;
1206
+ this.retryAfter = retryAfter; // Seconds from Retry-After header, if present
1207
+ }
1208
+ }
1197
1209
  - name: >-
1198
1210
  ThrottleBucket.ts
1199
1211
  type: >-
@@ -1253,7 +1265,6 @@ tree:
1253
1265
  //
1254
1266
  src/authorizationStrategy.ts
1255
1267
 
1256
-
1257
1268
  import
1258
1269
  {
1259
1270
  TokenCache
@@ -1279,18 +1290,14 @@ tree:
1279
1290
  }
1280
1291
 
1281
1292
 
1282
- //
1283
- src/authorizationStrategy.ts
1284
- (partial
1285
- update)
1286
-
1287
-
1288
1293
  export
1289
1294
  class
1290
1295
  TempTokenStrategy
1291
1296
  implements
1292
1297
  AuthorizationStrategy
1293
1298
  {
1299
+ private pendingFetches: Map<string, Promise<string>> = new Map();
1300
+
1294
1301
  constructor(
1295
1302
  private tokenCache: TokenCache,
1296
1303
  private initialTempToken: string | undefined,
@@ -1334,7 +1341,15 @@ tree:
1334
1341
  async getToken(dbid: string): Promise<string | undefined> {
1335
1342
  let token = this.tokenCache.get(dbid) || this.initialTempToken;
1336
1343
  if (!token && dbid) {
1337
- token = await this.fetchTempToken(dbid);
1344
+ if (this.pendingFetches.has(dbid)) {
1345
+ console.log(`[getToken] Waiting for existing fetch for dbid: ${dbid}`);
1346
+ return this.pendingFetches.get(dbid);
1347
+ }
1348
+ const fetchPromise = this.fetchTempToken(dbid).finally(() =>
1349
+ this.pendingFetches.delete(dbid)
1350
+ );
1351
+ this.pendingFetches.set(dbid, fetchPromise);
1352
+ token = await fetchPromise;
1338
1353
  }
1339
1354
  return token;
1340
1355
  }
@@ -1369,9 +1384,12 @@ tree:
1369
1384
  return null;
1370
1385
  }
1371
1386
  if (debug) console.log(`Refreshing temp token for dbid: ${dbid}`);
1372
- const newToken = await this.fetchTempToken(dbid);
1373
- this.tokenCache.set(dbid, newToken);
1374
- return newToken;
1387
+ const newToken = await this.getToken(dbid);
1388
+ if (newToken) {
1389
+ this.tokenCache.set(dbid, newToken);
1390
+ return newToken;
1391
+ }
1392
+ return null;
1375
1393
  }
1376
1394
  }
1377
1395
 
@@ -1382,7 +1400,10 @@ tree:
1382
1400
  implements
1383
1401
  AuthorizationStrategy
1384
1402
  {
1385
- constructor(private userToken: string) {}
1403
+ constructor(
1404
+ private userToken: string,
1405
+ private baseUrl: string = "https://api.quickbase.com/v1" // Added for consistency, unused
1406
+ ) {}
1386
1407
 
1387
1408
  async getToken(_dbid: string): Promise<string> {
1388
1409
  return this.userToken;
@@ -1419,23 +1440,32 @@ tree:
1419
1440
  AuthorizationStrategy
1420
1441
  {
1421
1442
  private currentToken: string | undefined;
1443
+ private pendingFetches: Map<string, Promise<string>> = new Map();
1422
1444
 
1423
1445
  constructor(
1424
1446
  private samlToken: string,
1425
1447
  private realm: string,
1426
1448
  private fetchApi: typeof fetch,
1427
- private debug: boolean = false
1449
+ private debug: boolean = false,
1450
+ private baseUrl: string = "https://api.quickbase.com/v1" // Added and used
1428
1451
  ) {}
1429
1452
 
1430
1453
  async getToken(_dbid: string): Promise<string | undefined> {
1431
1454
  if (!this.currentToken) {
1432
- this.currentToken = await this.fetchSsoToken({
1455
+ if (this.pendingFetches.has("sso")) {
1456
+ if (this.debug)
1457
+ console.log("[getToken] Waiting for existing SSO fetch");
1458
+ return this.pendingFetches.get("sso");
1459
+ }
1460
+ const fetchPromise = this.fetchSsoToken({
1433
1461
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
1434
1462
  requested_token_type:
1435
1463
  "urn:quickbase:params:oauth:token-type:temp_token",
1436
1464
  subject_token: this.samlToken,
1437
1465
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
1438
- });
1466
+ }).finally(() => this.pendingFetches.delete("sso"));
1467
+ this.pendingFetches.set("sso", fetchPromise);
1468
+ this.currentToken = await fetchPromise;
1439
1469
  }
1440
1470
  return this.currentToken;
1441
1471
  }
@@ -1478,18 +1508,15 @@ tree:
1478
1508
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
1479
1509
  };
1480
1510
 
1481
- const response = await this.fetchApi(
1482
- `https://api.quickbase.com/v1/auth/oauth/token`,
1483
- {
1484
- method: "POST",
1485
- headers: {
1486
- "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
1487
- "Content-Type": "application/json",
1488
- },
1489
- body: JSON.stringify(payload),
1490
- credentials: "omit",
1491
- }
1492
- );
1511
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
1512
+ method: "POST",
1513
+ headers: {
1514
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
1515
+ "Content-Type": "application/json",
1516
+ },
1517
+ body: JSON.stringify(payload),
1518
+ credentials: "omit",
1519
+ });
1493
1520
 
1494
1521
  if (!response.ok) {
1495
1522
  const errorBody = await response.json().catch(() => ({}));
@@ -1519,18 +1546,15 @@ tree:
1519
1546
  subject_token: string;
1520
1547
  subject_token_type: string;
1521
1548
  }): Promise<string> {
1522
- const response = await this.fetchApi(
1523
- `https://api.quickbase.com/v1/auth/oauth/token`,
1524
- {
1525
- method: "POST",
1526
- headers: {
1527
- "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
1528
- "Content-Type": "application/json",
1529
- },
1530
- body: JSON.stringify(params),
1531
- credentials: "omit",
1532
- }
1533
- );
1549
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
1550
+ method: "POST",
1551
+ headers: {
1552
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
1553
+ "Content-Type": "application/json",
1554
+ },
1555
+ body: JSON.stringify(params),
1556
+ credentials: "omit",
1557
+ });
1534
1558
 
1535
1559
  if (!response.ok) {
1536
1560
  const errorBody = await response.json().catch(() => ({}));
@@ -2327,7 +2351,7 @@ tree:
2327
2351
  //
2328
2352
  Generated
2329
2353
  on
2330
- 2025-03-22T03:13:09.438Z
2354
+ 2025-03-23T16:58:27.294Z
2331
2355
 
2332
2356
  import
2333
2357
  {
@@ -3638,6 +3662,10 @@ tree:
3638
3662
  }
3639
3663
 
3640
3664
 
3665
+ //
3666
+ src/invokeMethod.ts
3667
+ (fixed)
3668
+
3641
3669
  export
3642
3670
  async
3643
3671
  function
@@ -3657,7 +3685,9 @@ tree:
3657
3685
  rateLimiter: RateLimiter,
3658
3686
  transformDates: (obj: any, convertStringsToDates: boolean) => any,
3659
3687
  debug: boolean | undefined,
3660
- convertDates: boolean
3688
+ convertDates: boolean,
3689
+ attempt: number = 0,
3690
+ maxAttempts: number = rateLimiter.maxRetries + 1
3661
3691
  ):
3662
3692
  Promise<ReturnType<QuickbaseClient[K]>>
3663
3693
  {
@@ -3781,9 +3811,6 @@ tree:
3781
3811
  return { message, status };
3782
3812
  }
3783
3813
 
3784
- let attempt = 0;
3785
- const maxAttempts = rateLimiter.maxRetries + 1;
3786
-
3787
3814
  while (attempt < maxAttempts) {
3788
3815
  console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
3789
3816
  try {
@@ -3810,17 +3837,7 @@ tree:
3810
3837
  ({ message, status } = await parseErrorResponse(response));
3811
3838
  } else {
3812
3839
  if (debug) console.log(`[${methodName}] Unexpected error:`, error);
3813
- // Attempt to handle as a fetch error with a response
3814
- if (error instanceof Error && "response" in error) {
3815
- response = (error as any).response;
3816
- if (response instanceof Response) {
3817
- ({ message, status } = await parseErrorResponse(response));
3818
- } else {
3819
- throw error; // Rethrow if no response
3820
- }
3821
- } else {
3822
- throw error; // Rethrow truly unexpected errors
3823
- }
3840
+ throw error; // Rethrow truly unexpected errors
3824
3841
  }
3825
3842
 
3826
3843
  if (debug)
@@ -3846,14 +3863,23 @@ tree:
3846
3863
  continue;
3847
3864
  }
3848
3865
 
3849
- const newToken = await authStrategy.handleError(
3850
- status,
3851
- params,
3852
- attempt,
3853
- maxAttempts,
3854
- debug,
3855
- methodName
3856
- );
3866
+ // Handle the error and decide whether to retry
3867
+ let newToken: string | null = null;
3868
+ try {
3869
+ newToken = await authStrategy.handleError(
3870
+ status,
3871
+ params,
3872
+ attempt,
3873
+ maxAttempts,
3874
+ debug,
3875
+ methodName
3876
+ );
3877
+ } catch (authError) {
3878
+ if (debug)
3879
+ console.log(`[${methodName}] Auth strategy error:`, authError);
3880
+ throw authError; // Propagate the fetchTempToken error and exit
3881
+ }
3882
+
3857
3883
  if (newToken) {
3858
3884
  token = newToken;
3859
3885
  authStrategy.applyHeaders(baseHeaders, token);
@@ -3879,7 +3905,6 @@ tree:
3879
3905
  //
3880
3906
  src/quickbaseClient.ts
3881
3907
 
3882
-
3883
3908
  import
3884
3909
  {
3885
3910
  QuickbaseClient
@@ -3920,9 +3945,6 @@ tree:
3920
3945
  }
3921
3946
  from
3922
3947
  "./utils";
3923
- //
3924
- Updated
3925
- import
3926
3948
 
3927
3949
  import
3928
3950
  {
@@ -4042,7 +4064,13 @@ tree:
4042
4064
  }
4043
4065
 
4044
4066
  const authStrategy: AuthorizationStrategy = useSso
4045
- ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
4067
+ ? new SsoTokenStrategy(
4068
+ samlToken || "",
4069
+ realm,
4070
+ effectiveFetch,
4071
+ debug,
4072
+ baseUrl
4073
+ )
4046
4074
  : useTempTokens
4047
4075
  ? new TempTokenStrategy(
4048
4076
  tokenCache,
@@ -4051,7 +4079,7 @@ tree:
4051
4079
  realm,
4052
4080
  baseUrl
4053
4081
  )
4054
- : new UserTokenStrategy(userToken || "");
4082
+ : new UserTokenStrategy(userToken || "", baseUrl);
4055
4083
 
4056
4084
  const baseHeaders: HTTPHeaders = {
4057
4085
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
@@ -4338,6 +4366,10 @@ tree:
4338
4366
  .last-run.json
4339
4367
  type: >-
4340
4368
  file
4369
+ - name: >-
4370
+ test-results.json
4371
+ type: >-
4372
+ file
4341
4373
  - name: >-
4342
4374
  tests
4343
4375
  type: >-
@@ -4370,6 +4402,10 @@ tree:
4370
4402
  getAppWithTempToken.test.ts
4371
4403
  type: >-
4372
4404
  file
4405
+ - name: >-
4406
+ getAppWithTempTokenConcurrency.test.ts
4407
+ type: >-
4408
+ file
4373
4409
  - name: >-
4374
4410
  getAppWithUserToken.test.ts
4375
4411
  type: >-
@@ -4442,10 +4478,18 @@ tree:
4442
4478
  retryUserToken401.test.ts
4443
4479
  type: >-
4444
4480
  file
4481
+ - name: >-
4482
+ ssoTokenConcurrency.test.ts
4483
+ type: >-
4484
+ file
4445
4485
  - name: >-
4446
4486
  ssoTokenIntegrationWithServer.test.ts
4447
4487
  type: >-
4448
4488
  file
4489
+ - name: >-
4490
+ userTokenConcurrency.test.ts
4491
+ type: >-
4492
+ file
4449
4493
  - name: >-
4450
4494
  fields
4451
4495
  type: >-
@@ -4580,6 +4624,10 @@ tree:
4580
4624
  getApp.test.ts
4581
4625
  type: >-
4582
4626
  file
4627
+ - name: >-
4628
+ getAppConcurrency.test.ts
4629
+ type: >-
4630
+ file
4583
4631
  - name: >-
4584
4632
  getAppEvents.test.ts
4585
4633
  type: >-
@@ -4617,6 +4665,10 @@ tree:
4617
4665
  ssoTokenRefresh.test.ts
4618
4666
  type: >-
4619
4667
  file
4668
+ - name: >-
4669
+ userTokenConcurrency.test.ts
4670
+ type: >-
4671
+ file
4620
4672
  - name: >-
4621
4673
  fields
4622
4674
  type: >-
package/api-examples.md DELETED
@@ -1,49 +0,0 @@
1
- ---
2
- outline: deep
3
- ---
4
-
5
- # Runtime API Examples
6
-
7
- This page demonstrates usage of some of the runtime APIs provided by VitePress.
8
-
9
- The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
10
-
11
- ```md
12
- <script setup>
13
- import { useData } from 'vitepress'
14
-
15
- const { theme, page, frontmatter } = useData()
16
- </script>
17
-
18
- ## Results
19
-
20
- ### Theme Data
21
- <pre>{{ theme }}</pre>
22
-
23
- ### Page Data
24
- <pre>{{ page }}</pre>
25
-
26
- ### Page Frontmatter
27
- <pre>{{ frontmatter }}</pre>
28
- ```
29
-
30
- <script setup>
31
- import { useData } from 'vitepress'
32
-
33
- const { site, theme, page, frontmatter } = useData()
34
- </script>
35
-
36
- ## Results
37
-
38
- ### Theme Data
39
- <pre>{{ theme }}</pre>
40
-
41
- ### Page Data
42
- <pre>{{ page }}</pre>
43
-
44
- ### Page Frontmatter
45
- <pre>{{ frontmatter }}</pre>
46
-
47
- ## More
48
-
49
- Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
@@ -1,85 +0,0 @@
1
- # Markdown Extension Examples
2
-
3
- This page demonstrates some of the built-in markdown extensions provided by VitePress.
4
-
5
- ## Syntax Highlighting
6
-
7
- VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
8
-
9
- **Input**
10
-
11
- ````md
12
- ```js{4}
13
- export default {
14
- data () {
15
- return {
16
- msg: 'Highlighted!'
17
- }
18
- }
19
- }
20
- ```
21
- ````
22
-
23
- **Output**
24
-
25
- ```js{4}
26
- export default {
27
- data () {
28
- return {
29
- msg: 'Highlighted!'
30
- }
31
- }
32
- }
33
- ```
34
-
35
- ## Custom Containers
36
-
37
- **Input**
38
-
39
- ```md
40
- ::: info
41
- This is an info box.
42
- :::
43
-
44
- ::: tip
45
- This is a tip.
46
- :::
47
-
48
- ::: warning
49
- This is a warning.
50
- :::
51
-
52
- ::: danger
53
- This is a dangerous warning.
54
- :::
55
-
56
- ::: details
57
- This is a details block.
58
- :::
59
- ```
60
-
61
- **Output**
62
-
63
- ::: info
64
- This is an info box.
65
- :::
66
-
67
- ::: tip
68
- This is a tip.
69
- :::
70
-
71
- ::: warning
72
- This is a warning.
73
- :::
74
-
75
- ::: danger
76
- This is a dangerous warning.
77
- :::
78
-
79
- ::: details
80
- This is a details block.
81
- :::
82
-
83
- ## More
84
-
85
- Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).