@intentic/extension-manifest 1.317.0 → 1.319.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +219 -7
- package/dist/bundle.js.map +1 -1
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +9 -2
- package/dist/manifest.js.map +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +45 -10
- package/dist/permissions.js.map +1 -1
- package/dist/points/capabilities.d.ts +2 -0
- package/dist/points/capabilities.d.ts.map +1 -1
- package/dist/points/capabilities.js +4 -0
- package/dist/points/capabilities.js.map +1 -1
- package/dist/points/index.d.ts +16 -0
- package/dist/points/index.d.ts.map +1 -1
- package/dist/points/index.js +3 -0
- package/dist/points/index.js.map +1 -1
- package/dist/points/side-views.d.ts +17 -0
- package/dist/points/side-views.d.ts.map +1 -0
- package/dist/points/side-views.js +20 -0
- package/dist/points/side-views.js.map +1 -0
- package/dist/points/viewers.js +1 -1
- package/dist/points/viewers.js.map +1 -1
- package/intentic-extension.schema.json +44 -1
- package/package.json +8 -3
- package/src/bundle.test.ts +91 -0
- package/src/bundle.ts +288 -9
- package/src/manifest.ts +13 -2
- package/src/permissions.test.ts +98 -0
- package/src/permissions.ts +72 -13
- package/src/points/capabilities.ts +7 -0
- package/src/points/index.ts +3 -0
- package/src/points/side-views.ts +34 -0
- package/src/points/viewers.ts +1 -1
- package/src/powers.test.ts +212 -0
package/dist/points/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/points/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,cAAc,YAAY,CAAC;AAC3B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAK3B,MAAM,CAAC,MAAM,mBAAmB,GAAG;IAC/B,UAAU;IACV,UAAU;IACV,YAAY;IACZ,cAAc;IACd,aAAa;IACb,aAAa;IACb,cAAc;IACd,UAAU;IACV,gBAAgB;IAChB,iBAAiB;IACjB,aAAa;IACb,wBAAwB;IACxB,QAAQ;IACR,UAAU;CACmC,CAAC;AAUlD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CACrC,MAAM,CAAC,WAAW,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAqB,CAChJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/points/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,cAAc,YAAY,CAAC;AAC3B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAK3B,MAAM,CAAC,MAAM,mBAAmB,GAAG;IAC/B,UAAU;IACV,UAAU;IACV,YAAY;IACZ,cAAc;IACd,cAAc;IACd,aAAa;IACb,aAAa;IACb,cAAc;IACd,UAAU;IACV,gBAAgB;IAChB,iBAAiB;IACjB,aAAa;IACb,wBAAwB;IACxB,QAAQ;IACR,UAAU;CACmC,CAAC;AAUlD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CACrC,MAAM,CAAC,WAAW,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAqB,CAChJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SideViewContributionSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
label: z.ZodString;
|
|
5
|
+
links: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type SideViewContribution = z.infer<typeof SideViewContributionSchema>;
|
|
8
|
+
export declare const sideViewsPoint: {
|
|
9
|
+
readonly name: "sideViews";
|
|
10
|
+
readonly description: "Things this extension can show in the editor's side panel, one input at a time, beside whatever the reader is doing. Each entry reserves an id; the extension supplies the component with api.sideViews.register and opens one with api.sideViews.open, and the host refuses any id this list does not cover.";
|
|
11
|
+
readonly schema: z.ZodArray<z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
label: z.ZodString;
|
|
14
|
+
links: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=side-views.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-views.d.ts","sourceRoot":"","sources":["../../src/points/side-views.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,0BAA0B;;;;iBAmByD,CAAC;AACjG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,cAAc;mBACjB,WAAW;0BAEb,+SAA+S;;;;;;CAEjR,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const SideViewContributionSchema = z.object({
|
|
3
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
4
|
+
label: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("What one of these is called (\"CI run\"), shown in the install dialog and on a tab whose own title could not be read. Each tab's title is the extension's to say for the thing it shows."),
|
|
8
|
+
links: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Allow this side view to take links the chat renders: a link it recognises (its registration's `claim`) opens beside the chat instead of in a new browser tab. Declared because it changes what the reader's click does; leave it out and the host never asks.")
|
|
12
|
+
.meta({ power: { key: "side-view-links:${id}", sentence: 'opens links it recognises as "${label}" beside the chat' } }),
|
|
13
|
+
})
|
|
14
|
+
.meta({ power: { key: "side-view:${id}", sentence: 'shows "${label}" in the side panel' } });
|
|
15
|
+
export const sideViewsPoint = {
|
|
16
|
+
name: "sideViews",
|
|
17
|
+
description: "Things this extension can show in the editor's side panel, one input at a time, beside whatever the reader is doing. Each entry reserves an id; the extension supplies the component with api.sideViews.register and opens one with api.sideViews.open, and the host refuses any id this list does not cover.",
|
|
18
|
+
schema: z.array(SideViewContributionSchema),
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=side-views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-views.js","sourceRoot":"","sources":["../../src/points/side-views.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,sBAAsB,CAAC;IAC5C,KAAK,EAAE,CAAC;SACH,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACL,0LAA0L,CAC7L;IAIL,KAAK,EAAE,CAAC;SACH,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,QAAQ,CACL,+PAA+P,CAClQ;SACA,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,QAAQ,EAAE,yDAAyD,EAAE,EAAE,CAAC;CAC9H,CAAC;KACG,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,QAAQ,EAAE,oCAAoC,EAAE,EAAE,CAAC,CAAC;AAGjG,MAAM,CAAC,MAAM,cAAc,GAAG;IAC1B,IAAI,EAAE,WAAW;IACjB,WAAW,EACP,+SAA+S;IACnT,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,0BAA0B,CAAC;CACT,CAAC"}
|
package/dist/points/viewers.js
CHANGED
|
@@ -7,7 +7,7 @@ export const ViewerContributionSchema = z.object({
|
|
|
7
7
|
.describe('Bare file extensions, no dot: e.g. ["docx", "xlsx"].'),
|
|
8
8
|
fetch: z
|
|
9
9
|
.enum(["text", "blob", "url", "path"])
|
|
10
|
-
.describe("How much of the file the host hands you. `text` for a format that is text (svg, a subtitle track). `blob` for one that must be parsed end to end before any of it shows (a .docx, a spreadsheet), bounded by the daemon's raw-read cap. `url` for anything range-read rather than parsed (audio, video): your component gets a streaming URL to point an element at, never the bytes. `path` for a viewer whose own backend reads and writes the file: you get the workspace path and the scope it is viewed in,
|
|
10
|
+
.describe("How much of the file the host hands you. `text` for a format that is text (svg, a subtitle track). `blob` for one that must be parsed end to end before any of it shows (a .docx, a spreadsheet), bounded by the daemon's raw-read cap. `url` for anything range-read rather than parsed (audio, video): your component gets a streaming URL to point an element at, never the bytes. `path` for a viewer whose own backend reads and writes the file: you get the workspace path and the scope it is viewed in, plus `readOnly` where the window may not write the file and, in a desktop app's local window, a `text` slot holding the document's text for while your own view can't show it. Emit `dirty` (a boolean) whenever you start or stop holding edits the file doesn't have, so a window closing over them can ask first."),
|
|
11
11
|
edit: z
|
|
12
12
|
.boolean()
|
|
13
13
|
.optional()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewers.js","sourceRoot":"","sources":["../../src/points/viewers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,sBAAsB,CAAC;IAC5C,UAAU,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,sDAAsD,CAAC;IASrE,KAAK,EAAE,CAAC;SACH,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;SACrC,QAAQ,CACL,
|
|
1
|
+
{"version":3,"file":"viewers.js","sourceRoot":"","sources":["../../src/points/viewers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,sBAAsB,CAAC;IAC5C,UAAU,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,sDAAsD,CAAC;IASrE,KAAK,EAAE,CAAC;SACH,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;SACrC,QAAQ,CACL,uyBAAuyB,CAC1yB;IAGL,IAAI,EAAE,CAAC;SACF,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,QAAQ,CACL,mKAAmK,CACtK;IAGL,OAAO,EAAE,CAAC;SACL,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,QAAQ,CACL,oOAAoO,CACvO;CACR,CAAC;KACG,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,4FAA4F,EAAE,EAAE,CAAC,CAAC;AAGtJ,MAAM,CAAC,MAAM,YAAY,GAAG;IACxB,IAAI,EAAE,SAAS;IACf,WAAW,EACP,6OAA6O;IACjP,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC;CACP,CAAC"}
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
"url",
|
|
205
205
|
"path"
|
|
206
206
|
],
|
|
207
|
-
"description": "How much of the file the host hands you. `text` for a format that is text (svg, a subtitle track). `blob` for one that must be parsed end to end before any of it shows (a .docx, a spreadsheet), bounded by the daemon's raw-read cap. `url` for anything range-read rather than parsed (audio, video): your component gets a streaming URL to point an element at, never the bytes. `path` for a viewer whose own backend reads and writes the file: you get the workspace path and the scope it is viewed in,
|
|
207
|
+
"description": "How much of the file the host hands you. `text` for a format that is text (svg, a subtitle track). `blob` for one that must be parsed end to end before any of it shows (a .docx, a spreadsheet), bounded by the daemon's raw-read cap. `url` for anything range-read rather than parsed (audio, video): your component gets a streaming URL to point an element at, never the bytes. `path` for a viewer whose own backend reads and writes the file: you get the workspace path and the scope it is viewed in, plus `readOnly` where the window may not write the file and, in a desktop app's local window, a `text` slot holding the document's text for while your own view can't show it. Emit `dirty` (a boolean) whenever you start or stop holding edits the file doesn't have, so a window closing over them can ask first."
|
|
208
208
|
},
|
|
209
209
|
"edit": {
|
|
210
210
|
"description": "Whether this viewer writes the file back. An editing viewer is chosen over a render-only viewer claiming the same extension, whatever order the two activated in.",
|
|
@@ -255,6 +255,41 @@
|
|
|
255
255
|
},
|
|
256
256
|
"description": "Per-directory documents this extension can offer. Your provider marks the rows in the Workspace tree it has something to say about, and the host opens your component as a tab."
|
|
257
257
|
},
|
|
258
|
+
"sideViews": {
|
|
259
|
+
"type": "array",
|
|
260
|
+
"items": {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"id": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
266
|
+
},
|
|
267
|
+
"label": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"minLength": 1,
|
|
270
|
+
"description": "What one of these is called (\"CI run\"), shown in the install dialog and on a tab whose own title could not be read. Each tab's title is the extension's to say for the thing it shows."
|
|
271
|
+
},
|
|
272
|
+
"links": {
|
|
273
|
+
"description": "Allow this side view to take links the chat renders: a link it recognises (its registration's `claim`) opens beside the chat instead of in a new browser tab. Declared because it changes what the reader's click does; leave it out and the host never asks.",
|
|
274
|
+
"power": {
|
|
275
|
+
"key": "side-view-links:${id}",
|
|
276
|
+
"sentence": "opens links it recognises as \"${label}\" beside the chat"
|
|
277
|
+
},
|
|
278
|
+
"type": "boolean"
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"required": [
|
|
282
|
+
"id",
|
|
283
|
+
"label"
|
|
284
|
+
],
|
|
285
|
+
"power": {
|
|
286
|
+
"key": "side-view:${id}",
|
|
287
|
+
"sentence": "shows \"${label}\" in the side panel"
|
|
288
|
+
},
|
|
289
|
+
"additionalProperties": false
|
|
290
|
+
},
|
|
291
|
+
"description": "Things this extension can show in the editor's side panel, one input at a time, beside whatever the reader is doing. Each entry reserves an id; the extension supplies the component with api.sideViews.register and opens one with api.sideViews.open, and the host refuses any id this list does not cover."
|
|
292
|
+
},
|
|
258
293
|
"commands": {
|
|
259
294
|
"type": "array",
|
|
260
295
|
"items": {
|
|
@@ -687,6 +722,14 @@
|
|
|
687
722
|
],
|
|
688
723
|
"additionalProperties": false
|
|
689
724
|
},
|
|
725
|
+
"hosts": {
|
|
726
|
+
"description": "The hosts this card's credential is meant for, as templates over the fields like `env` (`api.github.com`, `*.githubusercontent.com`, `${url}`); a value that comes out as a URL counts as its host. The sandbox limits the credential's `{{secret:…}}` reference to them by default, so a use aimed anywhere else asks a person first. The owner can change or lift the list on the Secrets view.",
|
|
727
|
+
"type": "array",
|
|
728
|
+
"items": {
|
|
729
|
+
"type": "string",
|
|
730
|
+
"minLength": 1
|
|
731
|
+
}
|
|
732
|
+
},
|
|
690
733
|
"mcp": {
|
|
691
734
|
"description": "Deprecated: declare `contributes.tools` with `perCard` naming this card instead, and serve the tools with `api.tools.serve`. A path in this extension's backend (`server`) answering MCP over Streamable HTTP; every turn granted a card of this kind gets it as a server named by the card's id, each request arriving at `<path>/<card id>`.",
|
|
692
735
|
"effect": "mcp",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/extension-manifest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.319.0",
|
|
4
4
|
"description": "What an intentic extension DECLARES, the intentic-extension.json schema and the sandbox-route allowlist rule",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,18 +32,23 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@intentic/base": "1.
|
|
35
|
+
"@intentic/base": "1.319.0",
|
|
36
36
|
"tslib": "2.8.1",
|
|
37
37
|
"zod": "4.6.5"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
+
"@intentic/testing": "0.0.0",
|
|
40
41
|
"@intentic/tsconfig": "0.0.0",
|
|
42
|
+
"@types/bun": "1.4.2",
|
|
41
43
|
"@types/node": "24.13.6",
|
|
42
44
|
"@typescript/native-preview": "7.0.0-dev.20260707.2"
|
|
43
45
|
},
|
|
44
46
|
"scripts": {
|
|
45
47
|
"build": "tsgo",
|
|
48
|
+
"typecheck": "tsgo --noEmit -p tsconfig.test.json",
|
|
46
49
|
"watch": "tsgo --build --watch",
|
|
47
|
-
"schema": "tsgo && node scripts/write-schema.mjs"
|
|
50
|
+
"schema": "tsgo && node scripts/write-schema.mjs",
|
|
51
|
+
"test": "suites",
|
|
52
|
+
"test:watch": "suites --watch"
|
|
48
53
|
}
|
|
49
54
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { bundleProblem, bundleSpecifiers, HOST_PUBLISHED_SPECIFIERS } from "./bundle.js";
|
|
2
|
+
|
|
3
|
+
// Every case is a whole bundle and the specifiers it names, in source order. The minified ones are how a bundler
|
|
4
|
+
// actually emits: no whitespace a line-anchored scan could lean on.
|
|
5
|
+
const CASES: readonly (readonly [string, string, readonly string[]])[] = [
|
|
6
|
+
["a minified bundle's every import, on one line", `import{ref as e}from"vue";import{x as y}from"lodash";e(y);`, ["vue", "lodash"]],
|
|
7
|
+
["a default import beside named ones", `import x,{y}from"z";`, ["z"]],
|
|
8
|
+
["a namespace import", `import*as n from"z";`, ["z"]],
|
|
9
|
+
["a default and a namespace import", `import d,*as n from"z";`, ["z"]],
|
|
10
|
+
["a multi-line import", `import {\n a,\n b as c,\n} from "@intentic/sandbox";\n`, ["@intentic/sandbox"]],
|
|
11
|
+
["a statement after a closing brace", `function f(){}import{a}from"b";`, ["b"]],
|
|
12
|
+
["export * from", `export * from "./chunk.js";`, ["./chunk.js"]],
|
|
13
|
+
["export * as from", `export*as ns from"ns-lib";`, ["ns-lib"]],
|
|
14
|
+
["export {a} from", `export {a} from "a-lib";export{b as c}from'b-lib'`, ["a-lib", "b-lib"]],
|
|
15
|
+
["export {} from", `export{}from"empty"`, ["empty"]],
|
|
16
|
+
["a local export names nothing", `const a=1;export{a};export default a;export const b=2;`, []],
|
|
17
|
+
["a local export, then an import after a line break", `export {a}\nimport "next"`, ["next"]],
|
|
18
|
+
["a side-effect import", `import "polyfill";import'other';`, ["polyfill", "other"]],
|
|
19
|
+
["a dynamic import", `const m=await import("./lazy.js");`, ["./lazy.js"]],
|
|
20
|
+
["a dynamic import of a hole-less template", "import(`lit`).then(f)", ["lit"]],
|
|
21
|
+
["a dynamic import with options", `import("data", { with: { type: "json" } })`, ["data"]],
|
|
22
|
+
["a dynamic import of an expression names no module", `import(name);import("pre"+fix);import(\`t\${x}\`)`, []],
|
|
23
|
+
["import attributes", `import cfg from "./cfg.json" with { type: "json" };`, ["./cfg.json"]],
|
|
24
|
+
["quoted binding names", `import {"a-b" as c} from "q";export {c as "d-e"} from "r";`, ["q", "r"]],
|
|
25
|
+
["a binding called from", `import from from "f";import {from} from "g";`, ["f", "g"]],
|
|
26
|
+
["a specifier named twice counts once", `import a from"vue";import{b}from"vue";export*from"vue"`, ["vue"]],
|
|
27
|
+
["the word inside a string", `const s = "import x from 'fake'";const t = 'export * from "fake"';`, []],
|
|
28
|
+
["the word inside a template", "const s = `import x from 'fake';\nimport('fake')`;", []],
|
|
29
|
+
["the word inside a template's hole's string", "const s = `a${\"import 'fake'\"}b`;import \"real\";", ["real"]],
|
|
30
|
+
["an import inside a template's hole", "const s = `a${await import(\"inner\")}b`;", ["inner"]],
|
|
31
|
+
["nested template holes", "const s=`a${`b${c}d`}e${{f:1}.f}g`;import\"after\";", ["after"]],
|
|
32
|
+
["the word inside a line comment", `// import x from "fake"\nimport "real";`, ["real"]],
|
|
33
|
+
["the word inside a block comment", `/* import x from "fake";\n export * from "fake2" */import "real";`, ["real"]],
|
|
34
|
+
["a regex holding the word and a quote", `const r=/import x from "fake"/g;import "real";`, ["real"]],
|
|
35
|
+
["a regex holding a lone quote", `if(/["']/.test(s))x();import{a}from"real";`, ["real"]],
|
|
36
|
+
["a regex after a keyword", `function f(s){return/'/.test(s)}import"real";`, ["real"]],
|
|
37
|
+
["a regex with a slash in a class", `const r=/[/"]/;import"real";`, ["real"]],
|
|
38
|
+
["division is not a regex", `const a=b/2,c=d/3;import"real";`, ["real"]],
|
|
39
|
+
["division after a call and an index", `x=f(1)/g[0]/2;import"real";`, ["real"]],
|
|
40
|
+
["a method called import", `loader.import("fake");obj?.import("fake2");class A{import(p){}}`, []],
|
|
41
|
+
["import.meta", `const u=import.meta.url;import "real";`, ["real"]],
|
|
42
|
+
["an object key called import or export", `const o={import:"fake",export:"fake2"};`, []],
|
|
43
|
+
["a private field called import", `class A{#import=1;f(){return this.#import}}`, []],
|
|
44
|
+
["a hashbang line", `#!/usr/bin/env node\nimport "real";`, ["real"]],
|
|
45
|
+
["escaped quotes in a string", `const s="\\"import 'fake'\\"";import "real";`, ["real"]],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
describe("bundleSpecifiers", () => {
|
|
49
|
+
test.each(CASES)("%s", (_name, source, expected) => {
|
|
50
|
+
expect(bundleSpecifiers(source)).toEqual([...expected]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("bundleProblem", () => {
|
|
55
|
+
test("a bundle importing only what the host publishes loads", () => {
|
|
56
|
+
const source = HOST_PUBLISHED_SPECIFIERS.map((specifier, index) => `import*as m${index} from"${specifier}";`).join("");
|
|
57
|
+
expect(bundleProblem(source)).toBe(undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a bundle importing nothing loads", () => {
|
|
61
|
+
expect(bundleProblem(`export const activate=()=>{};`)).toBe(undefined);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test.each([
|
|
65
|
+
["a relative static import", `import{a}from"vue";import{b}from"./chunk.js";`, "./chunk.js"],
|
|
66
|
+
["a relative re-export", `export*from"../shared.js"`, "../shared.js"],
|
|
67
|
+
["an absolute dynamic import", `import("/assets/lazy.js")`, "/assets/lazy.js"],
|
|
68
|
+
])("%s is a second file", (_name, source, specifier) => {
|
|
69
|
+
expect(bundleProblem(source)).toBe(
|
|
70
|
+
`imports a second file (${specifier}): a bundle is imported from a blob URL, so nothing relative to it can resolve`,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("every relative specifier is named, ahead of any unpublished bare one", () => {
|
|
75
|
+
expect(bundleProblem(`import"lodash";import"./a.js";export*from"./b.js"`)).toBe(
|
|
76
|
+
"imports a second file (./a.js, ./b.js): a bundle is imported from a blob URL, so nothing relative to it can resolve",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("an unpublished bare specifier, found past the first import of a minified bundle, is named", () => {
|
|
81
|
+
expect(bundleProblem(`import{ref as e}from"vue";import{x as y}from"lodash";import("dayjs")`)).toBe(
|
|
82
|
+
"imports lodash, dayjs, which the host does not publish, bundle it in, or use one of: vue, @intentic/extension-api, @intentic/extension-ui, @tanstack/vue-query",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a published package's subpath is not the package", () => {
|
|
87
|
+
expect(bundleProblem(`import{h}from"vue/dist/vue.esm-bundler.js"`)).toBe(
|
|
88
|
+
"imports vue/dist/vue.esm-bundler.js, which the host does not publish, bundle it in, or use one of: vue, @intentic/extension-api, @intentic/extension-ui, @tanstack/vue-query",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/bundle.ts
CHANGED
|
@@ -5,15 +5,294 @@
|
|
|
5
5
|
// What the shell's import map publishes to a bundle.
|
|
6
6
|
export const HOST_PUBLISHED_SPECIFIERS = ["vue", "@intentic/extension-api", "@intentic/extension-ui", "@tanstack/vue-query"] as const;
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
];
|
|
8
|
+
// The tokens the scan tells apart. A string carries its contents, and so does a template without a `${}` hole.
|
|
9
|
+
type Token =
|
|
10
|
+
| { readonly kind: "ident" | "punct"; readonly text: string }
|
|
11
|
+
| { readonly kind: "string"; readonly value: string }
|
|
12
|
+
| { readonly kind: "template"; readonly value: string | undefined }
|
|
13
|
+
| { readonly kind: "number" | "regex" };
|
|
14
|
+
|
|
15
|
+
// Words after which an expression starts, so a `/` there opens a regex rather than dividing.
|
|
16
|
+
const BEFORE_EXPRESSION = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw", "case", "do", "else", "yield", "await", "extends"]);
|
|
17
|
+
|
|
18
|
+
// Whether a `/` after `last` opens a regex. After a value it divides. A `}` counts as a statement's end, since a block
|
|
19
|
+
// closes far more often than an object literal anything could divide. A misread regex is bounded by its line, see
|
|
20
|
+
// regexEnd.
|
|
21
|
+
const regexMayFollow = (last: Token | undefined): boolean => {
|
|
22
|
+
if (last === undefined) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (last.kind === "ident") {
|
|
26
|
+
return BEFORE_EXPRESSION.has(last.text);
|
|
27
|
+
}
|
|
28
|
+
return last.kind === "punct" && ![")", "]", "++", "--"].includes(last.text);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const IDENT = /[#$_\\\p{ID_Start}][$\\\u200C\u200D\p{ID_Continue}]*/uy;
|
|
32
|
+
const NUMBER = /\.?\d[\w.]*/y;
|
|
33
|
+
const LINE_BREAK = /[\n\r\u2028\u2029]/u;
|
|
34
|
+
const SPACE = /\s/u;
|
|
35
|
+
|
|
36
|
+
// Where a sticky `pattern` matching at `at` ends, or -1 when it does not match there.
|
|
37
|
+
const matchEnd = (pattern: RegExp, source: string, at: number): number => {
|
|
38
|
+
pattern.lastIndex = at;
|
|
39
|
+
return pattern.test(source) ? pattern.lastIndex : -1;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const lineEnd = (source: string, at: number): number => {
|
|
43
|
+
const offset = source.slice(at).search(LINE_BREAK);
|
|
44
|
+
return offset < 0 ? source.length : at + offset;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface Read {
|
|
48
|
+
readonly end: number;
|
|
49
|
+
readonly value: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The quoted string opening at `at`: where it ends and what it holds. An unterminated one ends at its line.
|
|
53
|
+
const readString = (source: string, at: number): Read => {
|
|
54
|
+
const quote = source[at];
|
|
55
|
+
for (let i = at + 1; i < source.length; i++) {
|
|
56
|
+
const char = source[i];
|
|
57
|
+
if (char === "\\") {
|
|
58
|
+
i++;
|
|
59
|
+
} else if (char === quote) {
|
|
60
|
+
return { end: i + 1, value: source.slice(at + 1, i) };
|
|
61
|
+
} else if (char === "\n" || char === "\r") {
|
|
62
|
+
return { end: i, value: source.slice(at + 1, i) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { end: source.length, value: source.slice(at + 1) };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Where the regex opening at `at` ends, flags included, or -1 when its line ends first: then the slash was division.
|
|
69
|
+
const regexEnd = (source: string, at: number): number => {
|
|
70
|
+
let inClass = false;
|
|
71
|
+
for (let i = at + 1; i < source.length; i++) {
|
|
72
|
+
const char = source[i] ?? "";
|
|
73
|
+
if (LINE_BREAK.test(char)) {
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
if (char === "\\") {
|
|
77
|
+
i++;
|
|
78
|
+
} else if (char === "[" || char === "]") {
|
|
79
|
+
inClass = char === "[";
|
|
80
|
+
} else if (char === "/" && !inClass) {
|
|
81
|
+
return Math.max(i + 1, matchEnd(IDENT, source, i + 1));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return -1;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
interface Span {
|
|
88
|
+
readonly end: number;
|
|
89
|
+
// Whether it stops at a `${` rather than at its closing backtick.
|
|
90
|
+
readonly hole: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A template's text from `at` (past its backtick, or past the `}` closing a hole) to its closing backtick or its next
|
|
94
|
+
// `${`.
|
|
95
|
+
const templateSpan = (source: string, at: number): Span => {
|
|
96
|
+
for (let i = at; i < source.length; i++) {
|
|
97
|
+
const char = source[i];
|
|
98
|
+
if (char === "\\") {
|
|
99
|
+
i++;
|
|
100
|
+
} else if (char === "`") {
|
|
101
|
+
return { end: i + 1, hole: false };
|
|
102
|
+
} else if (char === "$" && source[i + 1] === "{") {
|
|
103
|
+
return { end: i + 2, hole: true };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { end: source.length, hole: false };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const punctAt = (source: string, at: number): string => {
|
|
110
|
+
if (source.startsWith("...", at)) {
|
|
111
|
+
return "...";
|
|
112
|
+
}
|
|
113
|
+
const two = source.slice(at, at + 2);
|
|
114
|
+
if (two === "++" || two === "--" || (two === "?." && !/\d/u.test(source[at + 2] ?? ""))) {
|
|
115
|
+
return two;
|
|
116
|
+
}
|
|
117
|
+
return source[at] ?? "";
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
interface Lexer {
|
|
121
|
+
at: number;
|
|
122
|
+
last: Token | undefined;
|
|
123
|
+
// Code brace depth, and the depth each open template hole hands back to its template at.
|
|
124
|
+
braces: number;
|
|
125
|
+
readonly holes: number[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Past whitespace and comments from the lexer's position.
|
|
129
|
+
const skipTrivia = (source: string, lexer: Lexer): void => {
|
|
130
|
+
for (;;) {
|
|
131
|
+
if (SPACE.test(source[lexer.at] ?? "")) {
|
|
132
|
+
lexer.at++;
|
|
133
|
+
} else if (source.startsWith("//", lexer.at)) {
|
|
134
|
+
lexer.at = lineEnd(source, lexer.at);
|
|
135
|
+
} else if (source.startsWith("/*", lexer.at)) {
|
|
136
|
+
const close = source.indexOf("*/", lexer.at + 2);
|
|
137
|
+
lexer.at = close < 0 ? source.length : close + 2;
|
|
138
|
+
} else {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// A template's text from `from` onward: one token for a template closing here, a `${` for one opening a hole.
|
|
145
|
+
const readTemplate = (source: string, lexer: Lexer, from: number, head: boolean): Token => {
|
|
146
|
+
const span = templateSpan(source, from);
|
|
147
|
+
lexer.at = span.end;
|
|
148
|
+
if (span.hole) {
|
|
149
|
+
lexer.holes.push(lexer.braces);
|
|
150
|
+
return { kind: "punct", text: "${" };
|
|
151
|
+
}
|
|
152
|
+
return { kind: "template", value: head ? source.slice(from, span.end - 1) : undefined };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// The token at the lexer's position, which is past any trivia and before the end.
|
|
156
|
+
const readToken = (source: string, lexer: Lexer): Token => {
|
|
157
|
+
const { at } = lexer;
|
|
158
|
+
const char = source[at] ?? "";
|
|
159
|
+
const regex = char === "/" && regexMayFollow(lexer.last) ? regexEnd(source, at) : -1;
|
|
160
|
+
if (regex >= 0) {
|
|
161
|
+
lexer.at = regex;
|
|
162
|
+
return { kind: "regex" };
|
|
163
|
+
}
|
|
164
|
+
if (char === '"' || char === "'") {
|
|
165
|
+
const { end, value } = readString(source, at);
|
|
166
|
+
lexer.at = end;
|
|
167
|
+
return { kind: "string", value };
|
|
168
|
+
}
|
|
169
|
+
if (char === "`") {
|
|
170
|
+
return readTemplate(source, lexer, at + 1, true);
|
|
171
|
+
}
|
|
172
|
+
if (char === "}" && lexer.holes.at(-1) === lexer.braces) {
|
|
173
|
+
lexer.holes.pop();
|
|
174
|
+
return readTemplate(source, lexer, at + 1, false);
|
|
175
|
+
}
|
|
176
|
+
const ident = matchEnd(IDENT, source, at);
|
|
177
|
+
if (ident >= 0) {
|
|
178
|
+
lexer.at = ident;
|
|
179
|
+
return { kind: "ident", text: source.slice(at, ident) };
|
|
180
|
+
}
|
|
181
|
+
const number = matchEnd(NUMBER, source, at);
|
|
182
|
+
if (number >= 0) {
|
|
183
|
+
lexer.at = number;
|
|
184
|
+
return { kind: "number" };
|
|
185
|
+
}
|
|
186
|
+
const text = punctAt(source, at);
|
|
187
|
+
lexer.braces += text === "{" ? 1 : text === "}" ? -1 : 0;
|
|
188
|
+
lexer.at += text.length;
|
|
189
|
+
return { kind: "punct", text };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Tokenizes an ES module just far enough to see its imports: string, template, comment and regex contents are skipped
|
|
193
|
+
// rather than read as code, and a template's `${}` holes are read as code again.
|
|
194
|
+
const tokenize = (source: string, emit: (token: Token) => void): void => {
|
|
195
|
+
const lexer: Lexer = { at: source.startsWith("#!") ? lineEnd(source, 0) : 0, last: undefined, braces: 0, holes: [] };
|
|
196
|
+
for (skipTrivia(source, lexer); lexer.at < source.length; skipTrivia(source, lexer)) {
|
|
197
|
+
lexer.last = readToken(source, lexer);
|
|
198
|
+
emit(lexer.last);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const isPunct = (token: Token, text: string): boolean => token.kind === "punct" && token.text === text;
|
|
203
|
+
|
|
204
|
+
// Where the reading of an import or re-export stands. A clause is what sits between `import`/`export` and `from`:
|
|
205
|
+
// bindings, braces, `*`, `as`, commas, quoted names; `closed` once its braces have closed, when only `from` may follow.
|
|
206
|
+
type State =
|
|
207
|
+
| { readonly at: "idle" | "import" | "export" | "dynamic" | "from" }
|
|
208
|
+
| { readonly at: "argument"; readonly value: string }
|
|
209
|
+
| { readonly at: "clause"; readonly depth: number; readonly closed: boolean };
|
|
210
|
+
|
|
211
|
+
const IDLE: State = { at: "idle" };
|
|
212
|
+
|
|
213
|
+
// The state a clause moves to on `token`, or undefined when the token cannot sit in the clause.
|
|
214
|
+
const clauseNext = (depth: number, closed: boolean, token: Token): State | undefined => {
|
|
215
|
+
if (depth === 0 && token.kind === "ident" && token.text === "from") {
|
|
216
|
+
return { at: "from" };
|
|
217
|
+
}
|
|
218
|
+
if (closed) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
if (isPunct(token, "{")) {
|
|
222
|
+
return { at: "clause", depth: depth + 1, closed: false };
|
|
223
|
+
}
|
|
224
|
+
if (isPunct(token, "}") && depth > 0) {
|
|
225
|
+
return { at: "clause", depth: depth - 1, closed: depth === 1 };
|
|
226
|
+
}
|
|
227
|
+
const bindingPart = token.kind === "ident" || token.kind === "string" || isPunct(token, ",") || isPunct(token, "*");
|
|
228
|
+
return bindingPart ? { at: "clause", depth, closed } : undefined;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// A literal module name: a string, or a template with no hole (which only `import()` accepts).
|
|
232
|
+
const literalOf = (token: Token): string | undefined => (token.kind === "string" || token.kind === "template" ? token.value : undefined);
|
|
233
|
+
|
|
234
|
+
// The state after `token`, recording what it completes into `found`, or undefined when the token breaks the pattern
|
|
235
|
+
// under way (and must be read again from idle, since it may start the next one).
|
|
236
|
+
const transition = (state: State, token: Token, found: Set<string>): State | undefined => {
|
|
237
|
+
switch (state.at) {
|
|
238
|
+
case "idle":
|
|
239
|
+
return token.kind === "ident" && (token.text === "import" || token.text === "export") ? { at: token.text } : IDLE;
|
|
240
|
+
case "import":
|
|
241
|
+
if (isPunct(token, "(")) {
|
|
242
|
+
return { at: "dynamic" };
|
|
243
|
+
}
|
|
244
|
+
if (token.kind === "string") {
|
|
245
|
+
found.add(token.value);
|
|
246
|
+
return IDLE;
|
|
247
|
+
}
|
|
248
|
+
return clauseNext(0, false, token);
|
|
249
|
+
case "export":
|
|
250
|
+
return isPunct(token, "*") || isPunct(token, "{") ? clauseNext(0, false, token) : undefined;
|
|
251
|
+
case "dynamic": {
|
|
252
|
+
const value = literalOf(token);
|
|
253
|
+
return value === undefined ? undefined : { at: "argument", value };
|
|
254
|
+
}
|
|
255
|
+
case "argument":
|
|
256
|
+
// `import("x")` or `import("x", { with: … })`; `import("x" + y)` names no one module.
|
|
257
|
+
if (isPunct(token, ")") || isPunct(token, ",")) {
|
|
258
|
+
found.add(state.value);
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
case "from":
|
|
262
|
+
if (token.kind === "string") {
|
|
263
|
+
found.add(token.value);
|
|
264
|
+
return IDLE;
|
|
265
|
+
}
|
|
266
|
+
// `import from from "x"`: the first `from` was a binding.
|
|
267
|
+
return clauseNext(0, false, token);
|
|
268
|
+
case "clause":
|
|
269
|
+
return clauseNext(state.depth, state.closed, token);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Reads the specifiers off a token stream: `import … from "x"`, `import "x"`, `export … from "x"` and `import("x")`.
|
|
274
|
+
const specifierReader = (found: Set<string>): ((token: Token) => void) => {
|
|
275
|
+
let state: State = IDLE;
|
|
276
|
+
let previous: Token | undefined;
|
|
277
|
+
return (token) => {
|
|
278
|
+
// `x.import(…)` is a method and `x.export` a property, never the keyword. No pattern accepts a `.`, so one
|
|
279
|
+
// before this token has already left the reader idle.
|
|
280
|
+
const member = previous !== undefined && (isPunct(previous, ".") || isPunct(previous, "?."));
|
|
281
|
+
previous = token;
|
|
282
|
+
if (!member) {
|
|
283
|
+
state = transition(state, token, found) ?? transition(IDLE, token, found) ?? IDLE;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Every specifier a single-file ESM bundle names, in source order: static imports, re-exports, bare side-effect
|
|
289
|
+
// imports and dynamic import() of a literal, wherever they sit (a minified bundle puts them all on one line), never
|
|
290
|
+
// counting the word inside a string, a template, a comment or a regex.
|
|
291
|
+
export const bundleSpecifiers = (source: string): string[] => {
|
|
292
|
+
const found = new Set<string>();
|
|
293
|
+
tokenize(source, specifierReader(found));
|
|
294
|
+
return [...found];
|
|
295
|
+
};
|
|
17
296
|
|
|
18
297
|
// Why this bundle cannot load, or undefined when it can, naming the offending specifiers.
|
|
19
298
|
export const bundleProblem = (source: string): string | undefined => {
|
package/src/manifest.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MARK_FIELDS } from "./mark.js";
|
|
3
|
+
import { permissionProblem } from "./permissions.js";
|
|
3
4
|
import { contributesSchema } from "./points/index.js";
|
|
4
5
|
|
|
5
6
|
// The extension manifest (`intentic-extension.json`, not under .claude-plugin/): the approval surface the install
|
|
6
7
|
// dialog renders, and the host refuses any runtime registration whose id it didn't declare here. This file is the
|
|
7
8
|
// envelope only; contributions are assembled from points/.
|
|
8
9
|
|
|
10
|
+
// One "<METHOD> <path-glob>" entry, refused at parse when malformed (permissions.ts), so a bad manifest fails where it is
|
|
11
|
+
// read instead of inside the route gate on its first call.
|
|
12
|
+
const permissionEntry = (): z.ZodString =>
|
|
13
|
+
z.string().superRefine((entry, ctx) => {
|
|
14
|
+
const problem = permissionProblem(entry);
|
|
15
|
+
if (problem !== undefined) {
|
|
16
|
+
ctx.addIssue({ code: "custom", message: problem });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
9
20
|
const ManifestShape = z.object({
|
|
10
21
|
// Declared so it survives the parse instead of being silently stripped; nothing at runtime reads it.
|
|
11
22
|
$schema: z.string().optional().describe("The authoring schema, for editor completion and validation. Nothing at runtime reads it."),
|
|
@@ -54,11 +65,11 @@ const ManifestShape = z.object({
|
|
|
54
65
|
permissions: z
|
|
55
66
|
.object({
|
|
56
67
|
sandbox: z
|
|
57
|
-
.array(
|
|
68
|
+
.array(permissionEntry().meta({ power: { key: "sandbox:${value}", sentence: "its UI calls the sandbox route ${value}" } }))
|
|
58
69
|
.optional()
|
|
59
70
|
.describe("Daemon routes your UI half may call. Your own backend namespace needs no entry: its backend is your own code."),
|
|
60
71
|
daemon: z
|
|
61
|
-
.array(
|
|
72
|
+
.array(permissionEntry().meta({ power: { key: "daemon:${value}", sentence: "its backend calls the daemon route ${value}" } }))
|
|
62
73
|
.optional()
|
|
63
74
|
.describe(
|
|
64
75
|
"Daemon routes your SERVER half may call. Separate from `sandbox` because the two halves run as different principals: the UI as the owner's session, the backend as a minted per-extension token, so a grant to one must never quietly widen the other.",
|