Skip to content

Commit 785217d

Browse files
committed
feat(unstable_createRoot): support unsafe_penetrate
1 parent 90ddc3c commit 785217d

File tree

7 files changed

+130
-31
lines changed

7 files changed

+130
-31
lines changed

etc/runtime.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export interface CreateRootOptions {
119119
portal?: HTMLElement;
120120
scope?: "page" | "fragment";
121121
unknownBricks?: "silent" | "throw";
122+
unsafe_penetrate?: boolean;
122123
}
123124

124125
// Warning: (ae-forgotten-export) The symbol "Runtime" needs to be exported by the entry point index.d.ts
@@ -458,6 +459,8 @@ interface RuntimeContext extends LegacyCompatibleRuntimeContext {
458459
tplStateStoreMap: Map<string, DataStore<"STATE">>;
459460
// (undocumented)
460461
tplStateStoreScope?: DataStore<"STATE">[];
462+
// (undocumented)
463+
unsafe_penetrate?: boolean;
461464
}
462465

463466
// @public (undocumented)
@@ -582,7 +585,7 @@ const symbolForRootRuntimeContext: unique symbol;
582585
function unmountUseBrick({ rendererContext }: RenderUseBrickResult, mountResult: MountUseBrickResult): void;
583586

584587
// @public (undocumented)
585-
export function unstable_createRoot(container: HTMLElement | DocumentFragment, { portal: _portal, scope, unknownBricks }?: CreateRootOptions): {
588+
export function unstable_createRoot(container: HTMLElement | DocumentFragment, { portal: _portal, scope, unknownBricks, unsafe_penetrate, }?: CreateRootOptions): {
586589
render(brick: BrickConf | BrickConf[], { theme, uiVersion, language, context, functions, templates, i18n: i18nData, url, app, }?: RenderOptions): Promise<void>;
587590
unmount(): void;
588591
};

packages/runtime/src/createRoot.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export interface CreateRootOptions {
4343
* Defaults to "throw".
4444
*/
4545
unknownBricks?: "silent" | "throw";
46+
47+
/**
48+
* Set unsafe_penetrate to true to allow accessing global variables
49+
* from an isolated root.
50+
*
51+
* It is unsafe, use it at your own risk.
52+
*/
53+
unsafe_penetrate?: boolean;
4654
}
4755

4856
export interface RenderOptions {
@@ -59,7 +67,12 @@ export interface RenderOptions {
5967

6068
export function unstable_createRoot(
6169
container: HTMLElement | DocumentFragment,
62-
{ portal: _portal, scope = "fragment", unknownBricks }: CreateRootOptions = {}
70+
{
71+
portal: _portal,
72+
scope = "fragment",
73+
unknownBricks,
74+
unsafe_penetrate,
75+
}: CreateRootOptions = {}
6376
) {
6477
let portal = _portal;
6578
let createPortal: RenderRoot["createPortal"];
@@ -110,6 +123,7 @@ export function unstable_createRoot(
110123
pendingPermissionsPreCheck: [],
111124
tplStateStoreMap: new Map<string, DataStore<"STATE">>(),
112125
formStateStoreMap: new Map<string, DataStore<"FORM_STATE">>(),
126+
unsafe_penetrate,
113127
} as Partial<RuntimeContext> as RuntimeContext;
114128

115129
if (url) {

packages/runtime/src/internal/compute/computeRealValue.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, test, expect } from "@jest/globals";
22
import { asyncComputeRealValue, computeRealValue } from "./computeRealValue.js";
33
import type { RuntimeContext } from "../interfaces.js";
4+
import { _internalApiGetRuntimeContext } from "../Runtime.js";
5+
6+
jest.mock("../Runtime.js", () => ({
7+
_internalApiGetRuntimeContext: jest.fn(),
8+
}));
49

510
const query = new URLSearchParams("q=foo");
611
const runtimeContext = {
@@ -13,6 +18,10 @@ const runtimeContextWithNoData = { query } as RuntimeContext;
1318
const fn = () => {};
1419

1520
describe("asyncComputeRealValue", () => {
21+
beforeEach(() => {
22+
(_internalApiGetRuntimeContext as jest.Mock).mockReset();
23+
});
24+
1625
test("useBrick", async () => {
1726
expect(
1827
await asyncComputeRealValue(
@@ -155,6 +164,10 @@ describe("asyncComputeRealValue", () => {
155164
});
156165

157166
describe("computeRealValue", () => {
167+
beforeEach(() => {
168+
(_internalApiGetRuntimeContext as jest.Mock).mockReset();
169+
});
170+
158171
test("useBrick with legacy transform", () => {
159172
expect(
160173
computeRealValue(
@@ -297,4 +310,23 @@ describe("computeRealValue", () => {
297310
},
298311
});
299312
});
313+
314+
test("without unsafe penetrate", () => {
315+
(_internalApiGetRuntimeContext as jest.Mock).mockReturnValue({
316+
query: new URLSearchParams("q=bar"),
317+
});
318+
expect(computeRealValue("Q: ${QUERY.q}", runtimeContext)).toEqual("Q: foo");
319+
});
320+
321+
test("with unsafe penetrate", () => {
322+
(_internalApiGetRuntimeContext as jest.Mock).mockReturnValue({
323+
query: new URLSearchParams("q=bar"),
324+
});
325+
expect(
326+
computeRealValue("Q: ${QUERY.q}", {
327+
...runtimeContext,
328+
unsafe_penetrate: true,
329+
})
330+
).toEqual("Q: bar");
331+
});
300332
});

packages/runtime/src/internal/compute/computeRealValue.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { omit, pick } from "lodash";
12
import { isEvaluable } from "@next-core/cook";
23
import { hasOwnProperty, isObject } from "@next-core/utils/general";
34
import { transformAndInject, transform, inject } from "@next-core/inject";
@@ -14,6 +15,16 @@ import {
1415
isLazyContentInUseBrick,
1516
} from "./getNextStateOfUseBrick.js";
1617
import { hasBeenComputed, markAsComputed } from "./markAsComputed.js";
18+
import { _internalApiGetRuntimeContext } from "../Runtime.js";
19+
20+
const penetrableCtxNames = [
21+
"app",
22+
"location",
23+
"query",
24+
"match",
25+
"flags",
26+
"sys",
27+
] as const;
1728

1829
export interface ComputeOptions {
1930
$$lazyForUseBrick?: boolean;
@@ -42,16 +53,25 @@ export async function asyncComputeRealValue(
4253
if (preEvaluated || isEvaluable(value as string)) {
4354
result = await asyncEvaluate(value, runtimeContext, { lazy });
4455
dismissMarkingComputed = shouldDismissMarkingComputed(value);
56+
} else if (lazy) {
57+
result = value;
4558
} else {
46-
result = lazy
47-
? value
48-
: (hasOwnProperty(runtimeContext, "data")
49-
? internalOptions.noInject
50-
? transform
51-
: transformAndInject
52-
: internalOptions.noInject
59+
const penetrableCtx = runtimeContext.unsafe_penetrate
60+
? ({
61+
...pick(_internalApiGetRuntimeContext(), penetrableCtxNames),
62+
...omit(runtimeContext, penetrableCtxNames),
63+
} as RuntimeContext)
64+
: runtimeContext;
65+
66+
result = (
67+
hasOwnProperty(penetrableCtx, "data")
68+
? internalOptions.noInject
69+
? transform
70+
: transformAndInject
71+
: internalOptions.noInject
5372
? identity
54-
: inject)(value, runtimeContext);
73+
: inject
74+
)(value, penetrableCtx);
5575
}
5676

5777
if (!dismissMarkingComputed) {
@@ -121,16 +141,25 @@ export function computeRealValue(
121141
if (preEvaluated || isEvaluable(value as string)) {
122142
result = evaluate(value, runtimeContext);
123143
dismissMarkingComputed = shouldDismissMarkingComputed(value);
144+
} else if (lazy) {
145+
result = value;
124146
} else {
125-
result = lazy
126-
? value
127-
: (hasOwnProperty(runtimeContext, "data")
128-
? internalOptions.noInject
129-
? transform
130-
: transformAndInject
131-
: internalOptions.noInject
147+
const penetrableCtx = runtimeContext.unsafe_penetrate
148+
? ({
149+
...pick(_internalApiGetRuntimeContext(), penetrableCtxNames),
150+
...omit(runtimeContext, penetrableCtxNames),
151+
} as RuntimeContext)
152+
: runtimeContext;
153+
154+
result = (
155+
hasOwnProperty(penetrableCtx, "data")
156+
? internalOptions.noInject
157+
? transform
158+
: transformAndInject
159+
: internalOptions.noInject
132160
? identity
133-
: inject)(value, runtimeContext);
161+
: inject
162+
)(value, penetrableCtx);
134163
}
135164

136165
if (!dismissMarkingComputed) {

packages/runtime/src/internal/compute/evaluate.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ formStateStore.define(
254254
const consoleError = jest.spyOn(console, "error");
255255

256256
describe("evaluate", () => {
257+
beforeEach(() => {
258+
(_internalApiGetRuntimeContext as jest.Mock).mockReset();
259+
});
260+
257261
test.each<[string, unknown]>([
258262
["<% [] %>", []],
259263
["<% EVENT.detail %>", "yes"],
@@ -406,7 +410,7 @@ describe("evaluate", () => {
406410
});
407411

408412
test("Access existed global CTX.DS", () => {
409-
(_internalApiGetRuntimeContext as jest.Mock).mockReturnValueOnce({
413+
(_internalApiGetRuntimeContext as jest.Mock).mockReturnValue({
410414
ctxStore: {
411415
has(key: string) {
412416
return key === "DS";
@@ -418,6 +422,26 @@ describe("evaluate", () => {
418422
});
419423
expect(evaluate("<% CTX.DS.demo %>", runtimeContext)).toBe("mocked-DS");
420424
});
425+
426+
test("unsafe penetrate", () => {
427+
const unsafeContext = {
428+
...runtimeContext,
429+
unsafe_penetrate: true,
430+
};
431+
(_internalApiGetRuntimeContext as jest.Mock).mockReturnValue({
432+
app: {
433+
id: "global",
434+
name: "Global",
435+
homepage: "/global",
436+
},
437+
flags: {
438+
unsafe: true,
439+
},
440+
});
441+
expect(evaluate("<% APP.id %>", unsafeContext)).toBe("global");
442+
expect(evaluate("<% FLAGS.unsafe %>", unsafeContext)).toBe(true);
443+
expect(evaluate("<% FLAGS.test %>", unsafeContext)).toBe(undefined);
444+
});
421445
});
422446

423447
describe("asyncEvaluate", () => {

packages/runtime/src/internal/compute/evaluate.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -303,18 +303,13 @@ function lowLevelEvaluate(
303303
return {
304304
blockingList,
305305
run() {
306-
const {
307-
app: currentApp,
308-
location,
309-
query,
310-
match,
311-
flags,
312-
sys,
313-
ctxStore,
314-
data,
315-
event,
316-
} = runtimeContext;
317-
const app = runtimeContext.overrideApp ?? currentApp;
306+
const { ctxStore, data, event, unsafe_penetrate } = runtimeContext;
307+
308+
const penetrableCtx = unsafe_penetrate
309+
? _internalApiGetRuntimeContext()!
310+
: runtimeContext;
311+
312+
const { app, location, query, match, flags, sys } = penetrableCtx;
318313

319314
for (const variableName of attemptToVisitGlobals) {
320315
switch (variableName) {

packages/runtime/src/internal/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export interface RuntimeContext extends LegacyCompatibleRuntimeContext {
3535
formStateStoreId?: string;
3636
formStateStoreScope?: DataStore<"FORM_STATE">[];
3737
inUseBrick?: boolean;
38+
39+
unsafe_penetrate?: boolean;
3840
}
3941

4042
export type AsyncPropertyEntry = [

0 commit comments

Comments
 (0)