@astroscope/airlock 0.1.0 → 0.1.1
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 +5 -3
- package/dist/index.js +64 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @astroscope/airlock
|
|
2
2
|
|
|
3
|
+
> **Note:** This package is in active development. APIs may change between versions.
|
|
4
|
+
|
|
3
5
|
Strip excess props from hydrated Astro islands — prevents server data leaking to the client, reduces HTML payload size.
|
|
4
6
|
|
|
5
7
|
## Why?
|
|
@@ -100,15 +102,15 @@ Currently supports **React / Preact** components (`.tsx`, `.ts`, `.jsx`, `.js`).
|
|
|
100
102
|
|
|
101
103
|
The architecture uses a pluggable adapter pattern — Vue and Svelte adapters can be added without changing the core.
|
|
102
104
|
|
|
103
|
-
##
|
|
105
|
+
## Logging
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
Airlock always logs a summary during build:
|
|
106
108
|
|
|
107
109
|
```
|
|
108
110
|
[@astroscope/airlock] transformed 3 of 4 hydrated component usage(s)
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
Per-component details (ALLOW_ALL types, unresolved imports, etc.) are logged at debug level — visible with `--verbose`.
|
|
112
114
|
|
|
113
115
|
## License
|
|
114
116
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/vite-plugin.ts
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
+
import path3 from "path";
|
|
3
4
|
|
|
4
5
|
// src/adapters/react.ts
|
|
5
6
|
import path from "path";
|
|
@@ -8,34 +9,47 @@ import ts2 from "typescript";
|
|
|
8
9
|
// src/extractor.ts
|
|
9
10
|
import ts from "typescript";
|
|
10
11
|
function generateZodSchema(checker, propsType) {
|
|
11
|
-
const ctx = { visiting: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Map(), counter: 0 };
|
|
12
|
+
const ctx = { visiting: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Map(), counter: 0, depth: 0 };
|
|
12
13
|
const root = toZod(checker, propsType, ctx);
|
|
13
14
|
if (root === null) return null;
|
|
14
15
|
return {
|
|
15
16
|
root,
|
|
16
|
-
types: [...ctx.types.values()].map((t) => `const ${t.name}
|
|
17
|
+
types: [...ctx.types.values()].map((t) => `const ${t.name} = z.lazy(() => ${t.code});`)
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
function toZod(checker, type, ctx) {
|
|
21
|
+
ctx.depth++;
|
|
22
|
+
if (ctx.depth > 50) {
|
|
23
|
+
ctx.depth--;
|
|
24
|
+
return "z.any()";
|
|
25
|
+
}
|
|
20
26
|
if (ctx.visiting.has(type)) return getOrCreateLazyRef(checker, type, ctx);
|
|
21
27
|
const unwrapped = unwrapOptional(type);
|
|
28
|
+
if (ctx.visiting.has(unwrapped)) return getOrCreateLazyRef(checker, unwrapped, ctx);
|
|
22
29
|
if (isLeaf(unwrapped)) return null;
|
|
23
30
|
if (unwrapped.isUnion() && unwrapped.types.every((t) => isLeaf(t))) return null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
+
ctx.visiting.add(type);
|
|
32
|
+
ctx.visiting.add(unwrapped);
|
|
33
|
+
try {
|
|
34
|
+
if (checker.isArrayType(type) || checker.isArrayType(unwrapped)) {
|
|
35
|
+
return arrayToZod(checker, checker.isArrayType(type) ? type : unwrapped, ctx);
|
|
36
|
+
}
|
|
37
|
+
if (hasIndexSignature(checker, unwrapped)) return null;
|
|
38
|
+
if (unwrapped.isUnion()) {
|
|
39
|
+
const discriminant = findDiscriminant(checker, unwrapped);
|
|
40
|
+
if (discriminant) return discriminatedUnionToZod(checker, unwrapped, discriminant, ctx);
|
|
41
|
+
}
|
|
42
|
+
const properties = collectProperties(checker, unwrapped);
|
|
43
|
+
if (!properties) return null;
|
|
44
|
+
return propsToZodObject(checker, properties, ctx);
|
|
45
|
+
} finally {
|
|
46
|
+
ctx.visiting.delete(type);
|
|
47
|
+
ctx.visiting.delete(unwrapped);
|
|
48
|
+
ctx.depth--;
|
|
31
49
|
}
|
|
32
|
-
const properties = collectProperties(checker, unwrapped);
|
|
33
|
-
if (!properties) return null;
|
|
34
|
-
return propsToZodObject(checker, properties, type, ctx);
|
|
35
50
|
}
|
|
36
|
-
function propsToZodObject(checker, properties,
|
|
51
|
+
function propsToZodObject(checker, properties, ctx) {
|
|
37
52
|
if (properties.size === 0) return "z.object({})";
|
|
38
|
-
ctx.visiting.add(type);
|
|
39
53
|
const fields = [];
|
|
40
54
|
for (const [name, prop] of properties) {
|
|
41
55
|
const propType = checker.getTypeOfSymbol(prop);
|
|
@@ -45,7 +59,6 @@ function propsToZodObject(checker, properties, type, ctx) {
|
|
|
45
59
|
}
|
|
46
60
|
fields.push(`${safeKey(name)}: ${toZod(checker, propType, ctx) ?? "z.any()"}`);
|
|
47
61
|
}
|
|
48
|
-
ctx.visiting.delete(type);
|
|
49
62
|
return `z.object({${fields.join(", ")}})`;
|
|
50
63
|
}
|
|
51
64
|
function collectProperties(checker, type) {
|
|
@@ -421,23 +434,24 @@ async function transformCompiledOutput(code, schemas) {
|
|
|
421
434
|
const babel = await import("@babel/core");
|
|
422
435
|
const schemaMap = /* @__PURE__ */ new Map();
|
|
423
436
|
for (const m of schemas) {
|
|
424
|
-
schemaMap.set(
|
|
437
|
+
schemaMap.set(m.specifier, m.schemaId);
|
|
438
|
+
schemaMap.set(stripExt(m.resolvedPath), m.schemaId);
|
|
425
439
|
}
|
|
426
440
|
const matched = /* @__PURE__ */ new Set();
|
|
427
441
|
const usedSchemaIds = /* @__PURE__ */ new Set();
|
|
428
442
|
let renderComponentFound = false;
|
|
429
443
|
const plugin = ({ types: t }) => ({
|
|
430
444
|
visitor: {
|
|
431
|
-
ImportDeclaration(
|
|
432
|
-
if (!
|
|
433
|
-
for (const specifier of
|
|
445
|
+
ImportDeclaration(path4) {
|
|
446
|
+
if (!path4.node.source.value.includes("compiler-runtime")) return;
|
|
447
|
+
for (const specifier of path4.node.specifiers) {
|
|
434
448
|
if (specifier.type !== "ImportSpecifier") continue;
|
|
435
449
|
const imported = specifier.imported;
|
|
436
450
|
const importedName = "name" in imported ? imported.name : imported.value;
|
|
437
451
|
if (importedName !== "renderComponent") continue;
|
|
438
452
|
renderComponentFound = true;
|
|
439
453
|
const localName = specifier.local.name;
|
|
440
|
-
const binding =
|
|
454
|
+
const binding = path4.scope.getBinding(localName);
|
|
441
455
|
if (!binding) break;
|
|
442
456
|
for (const refPath of binding.referencePaths) {
|
|
443
457
|
const callPath = refPath.parentPath;
|
|
@@ -469,19 +483,19 @@ async function transformCompiledOutput(code, schemas) {
|
|
|
469
483
|
});
|
|
470
484
|
if (!renderComponentFound) {
|
|
471
485
|
throw new Error(
|
|
472
|
-
"
|
|
486
|
+
"could not find renderComponent import in compiled output. this may indicate a breaking change in Astro's compilation format. airlock refuses to continue to prevent potential data leaks."
|
|
473
487
|
);
|
|
474
488
|
}
|
|
475
489
|
for (const m of schemas) {
|
|
476
|
-
|
|
477
|
-
|
|
490
|
+
if (!matched.has(m.specifier) && !matched.has(stripExt(m.resolvedPath))) {
|
|
491
|
+
const foundPaths = [...matched].join(", ") || "(none)";
|
|
478
492
|
throw new Error(
|
|
479
|
-
`
|
|
493
|
+
`component "${m.specifier}" was detected in .astro source but not found in compiled output (matched: ${foundPaths}). airlock refuses to continue to prevent potential data leaks.`
|
|
480
494
|
);
|
|
481
495
|
}
|
|
482
496
|
}
|
|
483
497
|
if (!result?.code) {
|
|
484
|
-
throw new Error("
|
|
498
|
+
throw new Error("babel transform returned no output.");
|
|
485
499
|
}
|
|
486
500
|
const imports = [...usedSchemaIds, "__airlock_strip"];
|
|
487
501
|
const importLine = `import { ${imports.join(", ")} } from '${VIRTUAL_MODULE_ID}';`;
|
|
@@ -511,16 +525,35 @@ function airlockVitePlugin(options) {
|
|
|
511
525
|
let totalTransformed = 0;
|
|
512
526
|
let registry;
|
|
513
527
|
let server;
|
|
528
|
+
let root;
|
|
529
|
+
let isBuild = false;
|
|
514
530
|
return {
|
|
515
531
|
name: "@astroscope/airlock",
|
|
516
532
|
// note: no applyToEnvironment — handleHotUpdate must fire for all environments
|
|
517
533
|
// the transform hook checks for .astro files, so client env is skipped naturally
|
|
518
534
|
configResolved(config) {
|
|
519
|
-
|
|
535
|
+
root = config.root;
|
|
536
|
+
isBuild = config.command === "build";
|
|
537
|
+
registry = new SchemaRegistry([new ReactAdapter(root, logger)]);
|
|
520
538
|
},
|
|
521
539
|
configureServer(devServer) {
|
|
522
540
|
server = devServer;
|
|
523
541
|
},
|
|
542
|
+
async buildStart() {
|
|
543
|
+
if (!isBuild) return;
|
|
544
|
+
const srcDir = path3.join(root, "src");
|
|
545
|
+
const entries = await fs.readdir(srcDir, { recursive: true, withFileTypes: true });
|
|
546
|
+
for (const entry of entries) {
|
|
547
|
+
if (!entry.isFile() || !entry.name.endsWith(".astro")) continue;
|
|
548
|
+
const filePath = path3.join(entry.parentPath, entry.name);
|
|
549
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
550
|
+
const analyzed = await analyzeAstroSource(raw);
|
|
551
|
+
for (const comp of analyzed.hydratedComponents) {
|
|
552
|
+
if (!comp.importInfo) continue;
|
|
553
|
+
registry.resolve(comp.importInfo.specifier, comp.importInfo.exportName, filePath);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
524
557
|
resolveId(id) {
|
|
525
558
|
if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
|
|
526
559
|
},
|
|
@@ -555,7 +588,11 @@ function airlockVitePlugin(options) {
|
|
|
555
588
|
logger.debug(`<${comp.name}> (${comp.importInfo.specifier}) \u2014 ALLOW_ALL`);
|
|
556
589
|
continue;
|
|
557
590
|
}
|
|
558
|
-
componentsToWrap.push({
|
|
591
|
+
componentsToWrap.push({
|
|
592
|
+
specifier: comp.importInfo.specifier,
|
|
593
|
+
resolvedPath: resolved.resolvedPath,
|
|
594
|
+
schemaId: resolved.schemaId
|
|
595
|
+
});
|
|
559
596
|
}
|
|
560
597
|
if (componentsToWrap.length === 0) return;
|
|
561
598
|
if (server) {
|