emdash 0.9.0 → 0.11.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/dist/{adapters-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-BzltprvY.mjs → apply-Ded_1vng.mjs} +167 -254
- package/dist/apply-Ded_1vng.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +10 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +94 -43
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +12 -11
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
- package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
- package/dist/{byline-BSaNL1w7.mjs → byline-gFn1r0vA.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
- package/dist/cli/index.mjs +342 -95
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-8lOYF0pr.mjs → content-CERxPUN0.mjs} +14 -3
- package/dist/content-CERxPUN0.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +6 -4
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +19 -7
- package/dist/database/instrumentation.mjs.map +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-BFRaVcD6.d.mts → index-Cg-rC4Gj.d.mts} +110 -87
- package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -28
- package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
- package/dist/loader-ou_PXAjg.mjs.map +1 -0
- package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
- package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-BW32b4gi.mjs → media-1fFhub9c.mjs} +22 -10
- package/dist/media-1fFhub9c.mjs.map +1 -0
- package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
- package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
- package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
- package/dist/{query-Cg9ZKRQ0.mjs → query-8c_meo_K.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.mjs.map} +1 -1
- package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
- package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
- package/dist/{registry-Dw70ChxB.mjs → registry-Do34mz_P.mjs} +7 -6
- package/dist/registry-Do34mz_P.mjs.map +1 -0
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
- package/dist/request-cache-D4I69LeL.mjs.map +1 -0
- package/dist/request-context.d.mts +27 -1
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +16 -3
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
- package/dist/runner-DIcU2UCC.mjs.map +1 -0
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
- package/dist/search-DuWhx4NG.mjs.map +1 -0
- package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
- package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-Bw76xAxo.mjs +407 -0
- package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
- package/dist/taxonomy-D6NvlKo8.mjs +218 -0
- package/dist/taxonomy-D6NvlKo8.mjs.map +1 -0
- package/dist/{tokens-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
- package/dist/{tokens-D7zMmWi2.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
- package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
- package/dist/types-56BKbld_.mjs.map +1 -0
- package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
- package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
- package/dist/{types-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
- package/dist/types-BQx6ZXpR.d.mts.map +1 -0
- package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-DiI8NOG_.mjs +16 -0
- package/dist/types-DiI8NOG_.mjs.map +1 -0
- package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
- package/dist/types-IN5z_S3P.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
- package/dist/types-IZSZfEwv.d.mts.map +1 -0
- package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
- package/dist/validate-CO3JjFV5.d.mts.map +1 -0
- package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
- package/dist/validate-UK4Ja1uo.mjs.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
- package/dist/version-Bg31I_Ff.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
- package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
- package/package.json +9 -8
- package/src/api/errors.ts +5 -0
- package/src/api/handlers/content.ts +20 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/media-allowlist.ts +40 -0
- package/src/api/handlers/media.ts +1 -1
- package/src/api/handlers/menus.ts +400 -89
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/handlers/validate-media-fields.ts +125 -0
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/media.ts +23 -3
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/schema.ts +11 -2
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/middleware.ts +46 -11
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/media/upload-url.ts +10 -4
- package/src/astro/routes/api/media.ts +12 -4
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -6
- package/src/astro/types.ts +5 -1
- package/src/auth/rate-limit.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +81 -6
- package/src/cli/commands/bundle.ts +18 -15
- package/src/cli/commands/export-seed.ts +139 -24
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/instrumentation.ts +22 -8
- package/src/database/migrations/016_api_tokens.ts +18 -3
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/037_credential_algorithm.ts +18 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/media.ts +40 -10
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +12 -3
- package/src/emdash-runtime.ts +16 -3
- package/src/fields/file.ts +7 -6
- package/src/fields/image.ts +12 -11
- package/src/fields/types.ts +3 -0
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +1 -1
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +114 -26
- package/src/media/mime.ts +75 -0
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/plugins/types.ts +81 -191
- package/src/request-cache.ts +6 -2
- package/src/request-context.ts +42 -2
- package/src/schema/registry.ts +5 -5
- package/src/schema/types.ts +3 -2
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +157 -54
- package/src/seed/types.ts +18 -1
- package/src/seed/validate.ts +27 -13
- package/src/taxonomies/index.ts +230 -213
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-BzltprvY.mjs.map +0 -1
- package/dist/content-8lOYF0pr.mjs.map +0 -1
- package/dist/index-BFRaVcD6.d.mts.map +0 -1
- package/dist/loader-CKLbBnhK.mjs.map +0 -1
- package/dist/media-BW32b4gi.mjs.map +0 -1
- package/dist/registry-Dw70ChxB.mjs.map +0 -1
- package/dist/request-cache-B-bmkipQ.mjs.map +0 -1
- package/dist/runner-C7ADox5q.mjs.map +0 -1
- package/dist/search-dOGEccMa.mjs.map +0 -1
- package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
- package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
- package/dist/types-4fVtCIm0.mjs +0 -68
- package/dist/types-4fVtCIm0.mjs.map +0 -1
- package/dist/types-BSyXeCFW.d.mts.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CIOg5AR8.mjs.map +0 -1
- package/dist/types-CrtWgIvl.d.mts.map +0 -1
- package/dist/validate-Baqf0slj.mjs.map +0 -1
- package/dist/validate-BfQh_C_y.d.mts.map +0 -1
- package/dist/version-DoxrVdYf.mjs +0 -7
- package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single menu endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET /_emdash/api/menus/:name
|
|
5
|
-
* PUT /_emdash/api/menus/:name
|
|
6
|
-
* DELETE /_emdash/api/menus/:name
|
|
4
|
+
* GET /_emdash/api/menus/:name[?locale=xx]
|
|
5
|
+
* PUT /_emdash/api/menus/:name[?locale=xx]
|
|
6
|
+
* DELETE /_emdash/api/menus/:name[?locale=xx]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { APIRoute } from "astro";
|
|
@@ -11,20 +11,23 @@ import type { APIRoute } from "astro";
|
|
|
11
11
|
import { requirePerm } from "#api/authorize.js";
|
|
12
12
|
import { handleError, unwrapResult } from "#api/error.js";
|
|
13
13
|
import { handleMenuDelete, handleMenuGet, handleMenuUpdate } from "#api/handlers/menus.js";
|
|
14
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
15
|
-
import { updateMenuBody } from "#api/schemas.js";
|
|
14
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
15
|
+
import { localeFilterQuery, updateMenuBody } from "#api/schemas.js";
|
|
16
16
|
|
|
17
17
|
export const prerender = false;
|
|
18
18
|
|
|
19
|
-
export const GET: APIRoute = async ({ params, locals }) => {
|
|
19
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
20
20
|
const { emdash, user } = locals;
|
|
21
21
|
const name = params.name!;
|
|
22
22
|
|
|
23
23
|
const denied = requirePerm(user, "menus:read");
|
|
24
24
|
if (denied) return denied;
|
|
25
25
|
|
|
26
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
27
|
+
if (isParseError(query)) return query;
|
|
28
|
+
|
|
26
29
|
try {
|
|
27
|
-
const result = await handleMenuGet(emdash.db, name);
|
|
30
|
+
const result = await handleMenuGet(emdash.db, name, { locale: query.locale });
|
|
28
31
|
return unwrapResult(result);
|
|
29
32
|
} catch (error) {
|
|
30
33
|
return handleError(error, "Failed to fetch menu", "MENU_GET_ERROR");
|
|
@@ -38,26 +41,32 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
38
41
|
const denied = requirePerm(user, "menus:manage");
|
|
39
42
|
if (denied) return denied;
|
|
40
43
|
|
|
44
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
45
|
+
if (isParseError(query)) return query;
|
|
46
|
+
|
|
41
47
|
try {
|
|
42
48
|
const body = await parseBody(request, updateMenuBody);
|
|
43
49
|
if (isParseError(body)) return body;
|
|
44
50
|
|
|
45
|
-
const result = await handleMenuUpdate(emdash.db, name, body);
|
|
51
|
+
const result = await handleMenuUpdate(emdash.db, name, { ...body, locale: query.locale });
|
|
46
52
|
return unwrapResult(result);
|
|
47
53
|
} catch (error) {
|
|
48
54
|
return handleError(error, "Failed to update menu", "MENU_UPDATE_ERROR");
|
|
49
55
|
}
|
|
50
56
|
};
|
|
51
57
|
|
|
52
|
-
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
58
|
+
export const DELETE: APIRoute = async ({ params, request, locals }) => {
|
|
53
59
|
const { emdash, user } = locals;
|
|
54
60
|
const name = params.name!;
|
|
55
61
|
|
|
56
62
|
const denied = requirePerm(user, "menus:manage");
|
|
57
63
|
if (denied) return denied;
|
|
58
64
|
|
|
65
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
66
|
+
if (isParseError(query)) return query;
|
|
67
|
+
|
|
59
68
|
try {
|
|
60
|
-
const result = await handleMenuDelete(emdash.db, name);
|
|
69
|
+
const result = await handleMenuDelete(emdash.db, name, { locale: query.locale });
|
|
61
70
|
return unwrapResult(result);
|
|
62
71
|
} catch (error) {
|
|
63
72
|
return handleError(error, "Failed to delete menu", "MENU_DELETE_ERROR");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Menus list and create endpoints
|
|
3
3
|
*
|
|
4
|
-
* GET /_emdash/api/menus - List
|
|
5
|
-
* POST /_emdash/api/menus
|
|
4
|
+
* GET /_emdash/api/menus[?locale=xx] - List menus (optionally filtered by locale)
|
|
5
|
+
* POST /_emdash/api/menus - Create menu (body may include locale & translationOf)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
@@ -10,19 +10,22 @@ import type { APIRoute } from "astro";
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { handleError, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { handleMenuCreate, handleMenuList } from "#api/handlers/menus.js";
|
|
13
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
-
import { createMenuBody } from "#api/schemas.js";
|
|
13
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
|
+
import { createMenuBody, localeFilterQuery } from "#api/schemas.js";
|
|
15
15
|
|
|
16
16
|
export const prerender = false;
|
|
17
17
|
|
|
18
|
-
export const GET: APIRoute = async ({ locals }) => {
|
|
18
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
19
19
|
const { emdash, user } = locals;
|
|
20
20
|
|
|
21
21
|
const denied = requirePerm(user, "menus:read");
|
|
22
22
|
if (denied) return denied;
|
|
23
23
|
|
|
24
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
25
|
+
if (isParseError(query)) return query;
|
|
26
|
+
|
|
24
27
|
try {
|
|
25
|
-
const result = await handleMenuList(emdash.db);
|
|
28
|
+
const result = await handleMenuList(emdash.db, { locale: query.locale });
|
|
26
29
|
return unwrapResult(result);
|
|
27
30
|
} catch (error) {
|
|
28
31
|
return handleError(error, "Failed to fetch menus", "MENU_LIST_ERROR");
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term translation endpoints
|
|
3
|
+
*
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms/:slug/translations[?locale=xx]
|
|
5
|
+
* POST /_emdash/api/taxonomies/:name/terms/:slug/translations
|
|
6
|
+
* body: { locale, label?, slug? }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { APIRoute } from "astro";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { requirePerm } from "#api/authorize.js";
|
|
13
|
+
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
14
|
+
import {
|
|
15
|
+
handleTermCreate,
|
|
16
|
+
handleTermGet,
|
|
17
|
+
handleTermTranslations,
|
|
18
|
+
} from "#api/handlers/taxonomies.js";
|
|
19
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
20
|
+
import { localeFilterQuery } from "#api/schemas.js";
|
|
21
|
+
|
|
22
|
+
export const prerender = false;
|
|
23
|
+
|
|
24
|
+
const createTermTranslationBody = z
|
|
25
|
+
.object({
|
|
26
|
+
locale: z.string().min(1),
|
|
27
|
+
label: z.string().min(1).optional(),
|
|
28
|
+
slug: z.string().min(1).optional(),
|
|
29
|
+
})
|
|
30
|
+
.meta({ id: "CreateTermTranslationBody" });
|
|
31
|
+
|
|
32
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
33
|
+
const { emdash, user } = locals;
|
|
34
|
+
const { name, slug } = params;
|
|
35
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
36
|
+
|
|
37
|
+
const dbErr = requireDb(emdash?.db);
|
|
38
|
+
if (dbErr) return dbErr;
|
|
39
|
+
|
|
40
|
+
const denied = requirePerm(user, "taxonomies:read");
|
|
41
|
+
if (denied) return denied;
|
|
42
|
+
|
|
43
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
44
|
+
if (isParseError(query)) return query;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const anchor = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
48
|
+
if (!anchor.success) return unwrapResult(anchor);
|
|
49
|
+
const result = await handleTermTranslations(emdash.db, anchor.data.term.id);
|
|
50
|
+
return unwrapResult(result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return handleError(error, "Failed to list term translations", "TERM_TRANSLATIONS_ERROR");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
57
|
+
const { emdash, user } = locals;
|
|
58
|
+
const { name, slug } = params;
|
|
59
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
60
|
+
|
|
61
|
+
const dbErr = requireDb(emdash?.db);
|
|
62
|
+
if (dbErr) return dbErr;
|
|
63
|
+
|
|
64
|
+
const denied = requirePerm(user, "taxonomies:manage");
|
|
65
|
+
if (denied) return denied;
|
|
66
|
+
|
|
67
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
68
|
+
if (isParseError(query)) return query;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const body = await parseBody(request, createTermTranslationBody);
|
|
72
|
+
if (isParseError(body)) return body;
|
|
73
|
+
|
|
74
|
+
const source = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
75
|
+
if (!source.success) return unwrapResult(source);
|
|
76
|
+
|
|
77
|
+
const result = await handleTermCreate(emdash.db, name, {
|
|
78
|
+
slug: body.slug ?? source.data.term.slug,
|
|
79
|
+
label: body.label ?? source.data.term.label,
|
|
80
|
+
parentId: source.data.term.parentId,
|
|
81
|
+
description: source.data.term.description,
|
|
82
|
+
locale: body.locale,
|
|
83
|
+
translationOf: source.data.term.id,
|
|
84
|
+
});
|
|
85
|
+
return unwrapResult(result, 201);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return handleError(error, "Failed to create term translation", "TERM_TRANSLATION_CREATE_ERROR");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single term endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
* PUT
|
|
6
|
-
* DELETE /_emdash/api/taxonomies/:name/terms/:slug
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
5
|
+
* PUT /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
6
|
+
* DELETE /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { APIRoute } from "astro";
|
|
@@ -11,21 +11,18 @@ import type { APIRoute } from "astro";
|
|
|
11
11
|
import { requirePerm } from "#api/authorize.js";
|
|
12
12
|
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
13
13
|
import { handleTermDelete, handleTermGet, handleTermUpdate } from "#api/handlers/taxonomies.js";
|
|
14
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
15
|
-
import { updateTermBody } from "#api/schemas.js";
|
|
14
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
15
|
+
import { localeFilterQuery, updateTermBody } from "#api/schemas.js";
|
|
16
16
|
|
|
17
17
|
export const prerender = false;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Get a single term
|
|
21
21
|
*/
|
|
22
|
-
export const GET: APIRoute = async ({ params, locals }) => {
|
|
22
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
23
23
|
const { emdash, user } = locals;
|
|
24
24
|
const { name, slug } = params;
|
|
25
|
-
|
|
26
|
-
if (!name || !slug) {
|
|
27
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
28
|
-
}
|
|
25
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
29
26
|
|
|
30
27
|
const dbErr = requireDb(emdash?.db);
|
|
31
28
|
if (dbErr) return dbErr;
|
|
@@ -33,8 +30,11 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
33
30
|
const denied = requirePerm(user, "taxonomies:read");
|
|
34
31
|
if (denied) return denied;
|
|
35
32
|
|
|
33
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
34
|
+
if (isParseError(query)) return query;
|
|
35
|
+
|
|
36
36
|
try {
|
|
37
|
-
const result = await handleTermGet(emdash.db, name, slug);
|
|
37
|
+
const result = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
38
38
|
return unwrapResult(result);
|
|
39
39
|
} catch (error) {
|
|
40
40
|
return handleError(error, "Failed to get term", "TERM_GET_ERROR");
|
|
@@ -47,10 +47,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
47
47
|
export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
48
48
|
const { emdash, user } = locals;
|
|
49
49
|
const { name, slug } = params;
|
|
50
|
-
|
|
51
|
-
if (!name || !slug) {
|
|
52
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
53
|
-
}
|
|
50
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
54
51
|
|
|
55
52
|
const dbErr = requireDb(emdash?.db);
|
|
56
53
|
if (dbErr) return dbErr;
|
|
@@ -58,11 +55,14 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
58
55
|
const denied = requirePerm(user, "taxonomies:manage");
|
|
59
56
|
if (denied) return denied;
|
|
60
57
|
|
|
58
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
59
|
+
if (isParseError(query)) return query;
|
|
60
|
+
|
|
61
61
|
try {
|
|
62
62
|
const body = await parseBody(request, updateTermBody);
|
|
63
63
|
if (isParseError(body)) return body;
|
|
64
64
|
|
|
65
|
-
const result = await handleTermUpdate(emdash.db, name, slug, body);
|
|
65
|
+
const result = await handleTermUpdate(emdash.db, name, slug, body, { locale: query.locale });
|
|
66
66
|
return unwrapResult(result);
|
|
67
67
|
} catch (error) {
|
|
68
68
|
return handleError(error, "Failed to update term", "TERM_UPDATE_ERROR");
|
|
@@ -72,13 +72,10 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
72
72
|
/**
|
|
73
73
|
* Delete a term
|
|
74
74
|
*/
|
|
75
|
-
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
75
|
+
export const DELETE: APIRoute = async ({ params, request, locals }) => {
|
|
76
76
|
const { emdash, user } = locals;
|
|
77
77
|
const { name, slug } = params;
|
|
78
|
-
|
|
79
|
-
if (!name || !slug) {
|
|
80
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
81
|
-
}
|
|
78
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
82
79
|
|
|
83
80
|
const dbErr = requireDb(emdash?.db);
|
|
84
81
|
if (dbErr) return dbErr;
|
|
@@ -86,8 +83,11 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
|
86
83
|
const denied = requirePerm(user, "taxonomies:manage");
|
|
87
84
|
if (denied) return denied;
|
|
88
85
|
|
|
86
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
87
|
+
if (isParseError(query)) return query;
|
|
88
|
+
|
|
89
89
|
try {
|
|
90
|
-
const result = await handleTermDelete(emdash.db, name, slug);
|
|
90
|
+
const result = await handleTermDelete(emdash.db, name, slug, { locale: query.locale });
|
|
91
91
|
return unwrapResult(result);
|
|
92
92
|
} catch (error) {
|
|
93
93
|
return handleError(error, "Failed to delete term", "TERM_DELETE_ERROR");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Taxonomy terms list and create endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
* POST /_emdash/api/taxonomies/:name/terms
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms[?locale=xx] - List terms (tree for hierarchical)
|
|
5
|
+
* POST /_emdash/api/taxonomies/:name/terms - Create a new term (body may include locale & translationOf)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
@@ -10,21 +10,18 @@ import type { APIRoute } from "astro";
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { handleTermCreate, handleTermList } from "#api/handlers/taxonomies.js";
|
|
13
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
-
import { createTermBody } from "#api/schemas.js";
|
|
13
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
|
+
import { createTermBody, localeFilterQuery } from "#api/schemas.js";
|
|
15
15
|
|
|
16
16
|
export const prerender = false;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* List all terms for a taxonomy
|
|
20
20
|
*/
|
|
21
|
-
export const GET: APIRoute = async ({ params, locals }) => {
|
|
21
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
22
22
|
const { emdash, user } = locals;
|
|
23
23
|
const { name } = params;
|
|
24
|
-
|
|
25
|
-
if (!name) {
|
|
26
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
27
|
-
}
|
|
24
|
+
if (!name) return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
28
25
|
|
|
29
26
|
const dbErr = requireDb(emdash?.db);
|
|
30
27
|
if (dbErr) return dbErr;
|
|
@@ -32,8 +29,11 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
32
29
|
const denied = requirePerm(user, "taxonomies:read");
|
|
33
30
|
if (denied) return denied;
|
|
34
31
|
|
|
32
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
33
|
+
if (isParseError(query)) return query;
|
|
34
|
+
|
|
35
35
|
try {
|
|
36
|
-
const result = await handleTermList(emdash.db, name);
|
|
36
|
+
const result = await handleTermList(emdash.db, name, { locale: query.locale });
|
|
37
37
|
return unwrapResult(result);
|
|
38
38
|
} catch (error) {
|
|
39
39
|
return handleError(error, "Failed to list terms", "TERM_LIST_ERROR");
|
|
@@ -46,10 +46,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
46
46
|
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
47
47
|
const { emdash, user } = locals;
|
|
48
48
|
const { name } = params;
|
|
49
|
-
|
|
50
|
-
if (!name) {
|
|
51
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
52
|
-
}
|
|
49
|
+
if (!name) return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
53
50
|
|
|
54
51
|
const dbErr = requireDb(emdash?.db);
|
|
55
52
|
if (dbErr) return dbErr;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Taxonomy definitions endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET /_emdash/api/taxonomies - List
|
|
5
|
-
* POST /_emdash/api/taxonomies
|
|
4
|
+
* GET /_emdash/api/taxonomies[?locale=xx] - List taxonomy definitions
|
|
5
|
+
* POST /_emdash/api/taxonomies - Create a custom taxonomy definition
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
@@ -10,15 +10,15 @@ import type { APIRoute } from "astro";
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { handleTaxonomyCreate, handleTaxonomyList } from "#api/handlers/taxonomies.js";
|
|
13
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
-
import { createTaxonomyDefBody } from "#api/schemas.js";
|
|
13
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
|
+
import { createTaxonomyDefBody, localeFilterQuery } from "#api/schemas.js";
|
|
15
15
|
|
|
16
16
|
export const prerender = false;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* List taxonomy definitions
|
|
20
20
|
*/
|
|
21
|
-
export const GET: APIRoute = async ({ locals }) => {
|
|
21
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
22
22
|
const { emdash, user } = locals;
|
|
23
23
|
|
|
24
24
|
const dbErr = requireDb(emdash?.db);
|
|
@@ -27,8 +27,11 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
27
27
|
const denied = requirePerm(user, "taxonomies:read");
|
|
28
28
|
if (denied) return denied;
|
|
29
29
|
|
|
30
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
31
|
+
if (isParseError(query)) return query;
|
|
32
|
+
|
|
30
33
|
try {
|
|
31
|
-
const result = await handleTaxonomyList(emdash.db);
|
|
34
|
+
const result = await handleTaxonomyList(emdash.db, { locale: query.locale });
|
|
32
35
|
return unwrapResult(result);
|
|
33
36
|
} catch (error) {
|
|
34
37
|
return handleError(error, "Failed to list taxonomies", "TAXONOMY_LIST_ERROR");
|
package/src/astro/types.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface ManifestCollection {
|
|
|
43
43
|
* (e.g. a checkbox grid receiving its column definitions)
|
|
44
44
|
*/
|
|
45
45
|
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
|
|
46
|
+
/** The `_emdash_fields` row ID. Used by the admin to forward to upload/media-list API calls. */
|
|
47
|
+
id?: string;
|
|
48
|
+
/** Validation config for the field (e.g. `allowedMimeTypes` for file/image fields, subFields for repeater). */
|
|
49
|
+
validation?: Record<string, unknown>;
|
|
46
50
|
}
|
|
47
51
|
>;
|
|
48
52
|
}
|
|
@@ -292,7 +296,7 @@ export interface EmDashHandlers {
|
|
|
292
296
|
handleMediaList: (params: {
|
|
293
297
|
cursor?: string;
|
|
294
298
|
limit?: number;
|
|
295
|
-
mimeType?: string;
|
|
299
|
+
mimeType?: string | readonly string[];
|
|
296
300
|
}) => Promise<HandlerResponse>;
|
|
297
301
|
|
|
298
302
|
handleMediaGet: (id: string) => Promise<HandlerResponse>;
|
package/src/auth/rate-limit.ts
CHANGED
|
@@ -63,9 +63,9 @@ export async function checkRateLimit(
|
|
|
63
63
|
|
|
64
64
|
// Atomic upsert: insert or increment, return current count
|
|
65
65
|
const result = await sql<{ count: number }>`
|
|
66
|
-
INSERT INTO _emdash_rate_limits (key, window, count)
|
|
66
|
+
INSERT INTO _emdash_rate_limits (key, "window", count)
|
|
67
67
|
VALUES (${key}, ${windowStart}, 1)
|
|
68
|
-
ON CONFLICT (key, window)
|
|
68
|
+
ON CONFLICT (key, "window")
|
|
69
69
|
DO UPDATE SET count = _emdash_rate_limits.count + 1
|
|
70
70
|
RETURNING count
|
|
71
71
|
`.execute(db);
|
|
@@ -179,7 +179,7 @@ export async function cleanupExpiredRateLimits(
|
|
|
179
179
|
const cutoff = new Date(Date.now() - maxAgeSeconds * 1000).toISOString();
|
|
180
180
|
|
|
181
181
|
const result = await sql`
|
|
182
|
-
DELETE FROM _emdash_rate_limits WHERE window < ${cutoff}
|
|
182
|
+
DELETE FROM _emdash_rate_limits WHERE "window" < ${cutoff}
|
|
183
183
|
`.execute(db);
|
|
184
184
|
|
|
185
185
|
return Number(result.numAffectedRows ?? 0);
|
|
@@ -22,7 +22,12 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Bundle size caps per RFC 0001 §"Bundle size limits". These are decompressed
|
|
26
|
+
// sizes; the gzipped tarball is typically a fraction of MAX_BUNDLE_SIZE.
|
|
27
|
+
export const MAX_BUNDLE_SIZE = 256 * 1024;
|
|
28
|
+
export const MAX_FILE_SIZE = 128 * 1024;
|
|
29
|
+
export const MAX_FILE_COUNT = 20;
|
|
30
|
+
|
|
26
31
|
export const MAX_SCREENSHOTS = 5;
|
|
27
32
|
export const MAX_SCREENSHOT_WIDTH = 1920;
|
|
28
33
|
export const MAX_SCREENSHOT_HEIGHT = 1080;
|
|
@@ -251,23 +256,93 @@ export function findSourceExports(
|
|
|
251
256
|
// ── Directory helpers ────────────────────────────────────────────────────────
|
|
252
257
|
|
|
253
258
|
/**
|
|
254
|
-
*
|
|
259
|
+
* One file in a bundle: a tarball-relative path and its byte length.
|
|
260
|
+
* Produced by `collectBundleEntries` (from a staging dir) or by the publish
|
|
261
|
+
* flow (from tarball entries); consumed by `validateBundleSize`.
|
|
255
262
|
*/
|
|
256
|
-
export
|
|
257
|
-
|
|
263
|
+
export interface BundleFileEntry {
|
|
264
|
+
name: string;
|
|
265
|
+
bytes: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Recursively walk a staging directory and return a flat list of all files
|
|
270
|
+
* with sizes. Names are relative to `dir` so they match what would appear
|
|
271
|
+
* as the tarball entry name.
|
|
272
|
+
*/
|
|
273
|
+
export async function collectBundleEntries(dir: string): Promise<BundleFileEntry[]> {
|
|
274
|
+
const entries: BundleFileEntry[] = [];
|
|
275
|
+
await walkBundle(dir, "", entries);
|
|
276
|
+
return entries;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function walkBundle(dir: string, prefix: string, into: BundleFileEntry[]): Promise<void> {
|
|
258
280
|
const items = await readdir(dir, { withFileTypes: true });
|
|
259
281
|
for (const item of items) {
|
|
260
282
|
const fullPath = join(dir, item.name);
|
|
283
|
+
const relPath = prefix ? `${prefix}/${item.name}` : item.name;
|
|
261
284
|
if (item.isFile()) {
|
|
262
285
|
const s = await stat(fullPath);
|
|
263
|
-
|
|
286
|
+
into.push({ name: relPath, bytes: s.size });
|
|
264
287
|
} else if (item.isDirectory()) {
|
|
265
|
-
|
|
288
|
+
await walkBundle(fullPath, relPath, into);
|
|
266
289
|
}
|
|
267
290
|
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Sum the byte sizes of all entries.
|
|
295
|
+
*/
|
|
296
|
+
export function totalBundleBytes(entries: readonly BundleFileEntry[]): number {
|
|
297
|
+
let total = 0;
|
|
298
|
+
for (const e of entries) total += e.bytes;
|
|
268
299
|
return total;
|
|
269
300
|
}
|
|
270
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Check a bundle against the three size caps from RFC 0001:
|
|
304
|
+
* - total decompressed ≤ MAX_BUNDLE_SIZE
|
|
305
|
+
* - per-file decompressed ≤ MAX_FILE_SIZE
|
|
306
|
+
* - file count ≤ MAX_FILE_COUNT
|
|
307
|
+
*
|
|
308
|
+
* Returns a list of violation messages (empty if the bundle is within all
|
|
309
|
+
* caps). Messages are deterministic per input — the total/count violations
|
|
310
|
+
* come first, then oversized files in alphabetical order — so the same
|
|
311
|
+
* bundle always produces the same error text.
|
|
312
|
+
*/
|
|
313
|
+
export function validateBundleSize(entries: readonly BundleFileEntry[]): string[] {
|
|
314
|
+
const violations: string[] = [];
|
|
315
|
+
const total = totalBundleBytes(entries);
|
|
316
|
+
if (total > MAX_BUNDLE_SIZE) {
|
|
317
|
+
violations.push(
|
|
318
|
+
`Bundle size ${formatBytes(total)} exceeds maximum of ${formatBytes(MAX_BUNDLE_SIZE)}.`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (entries.length > MAX_FILE_COUNT) {
|
|
322
|
+
violations.push(
|
|
323
|
+
`Bundle contains ${entries.length} files, exceeds maximum of ${MAX_FILE_COUNT}.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const oversized = entries
|
|
327
|
+
.filter((e) => e.bytes > MAX_FILE_SIZE)
|
|
328
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
329
|
+
for (const e of oversized) {
|
|
330
|
+
violations.push(
|
|
331
|
+
`File ${e.name} is ${formatBytes(e.bytes)}, exceeds per-file maximum of ${formatBytes(MAX_FILE_SIZE)}.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return violations;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Render a byte count as a human-friendly string (e.g. "256.0 KB").
|
|
339
|
+
*/
|
|
340
|
+
export function formatBytes(n: number): string {
|
|
341
|
+
if (n < 1024) return `${n} B`;
|
|
342
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
343
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
344
|
+
}
|
|
345
|
+
|
|
271
346
|
// ── Tarball creation ─────────────────────────────────────────────────────────
|
|
272
347
|
|
|
273
348
|
/**
|
|
@@ -23,20 +23,22 @@ import consola from "consola";
|
|
|
23
23
|
import { CAPABILITY_RENAMES, isDeprecatedCapability } from "../../plugins/types.js";
|
|
24
24
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
collectBundleEntries,
|
|
27
|
+
createTarball,
|
|
28
28
|
extractManifest,
|
|
29
|
-
|
|
29
|
+
fileExists,
|
|
30
30
|
findBuildOutput,
|
|
31
|
+
findNodeBuiltinImports,
|
|
31
32
|
findSourceExports,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
createTarball,
|
|
35
|
-
MAX_BUNDLE_SIZE,
|
|
33
|
+
formatBytes,
|
|
34
|
+
ICON_SIZE,
|
|
36
35
|
MAX_SCREENSHOTS,
|
|
37
36
|
MAX_SCREENSHOT_WIDTH,
|
|
38
37
|
MAX_SCREENSHOT_HEIGHT,
|
|
39
|
-
|
|
38
|
+
readImageDimensions,
|
|
39
|
+
resolveSourceEntry,
|
|
40
|
+
totalBundleBytes,
|
|
41
|
+
validateBundleSize,
|
|
40
42
|
} from "./bundle-utils.js";
|
|
41
43
|
|
|
42
44
|
const TS_EXT_RE = /\.(tsx?|[mc]?js)$/;
|
|
@@ -596,15 +598,16 @@ export const bundleCommand = defineCommand({
|
|
|
596
598
|
}
|
|
597
599
|
}
|
|
598
600
|
|
|
599
|
-
//
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
// Bundle size caps (RFC 0001 §"Bundle size limits").
|
|
602
|
+
const bundleEntries = await collectBundleEntries(bundleDir);
|
|
603
|
+
const sizeViolations = validateBundleSize(bundleEntries);
|
|
604
|
+
if (sizeViolations.length > 0) {
|
|
605
|
+
for (const v of sizeViolations) consola.error(v);
|
|
604
606
|
hasErrors = true;
|
|
605
607
|
} else {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
+
consola.info(
|
|
609
|
+
`Bundle size: ${formatBytes(totalBundleBytes(bundleEntries))} across ${bundleEntries.length} file${bundleEntries.length === 1 ? "" : "s"}`,
|
|
610
|
+
);
|
|
608
611
|
}
|
|
609
612
|
|
|
610
613
|
if (hasErrors) {
|