Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ def getAssembleReleaseBuildArguments = { ->
if (onlyX86) {
arguments.add("-PonlyX86")
}
if (project.hasProperty("abis")) {
arguments.add("-Pabis=${project.property('abis')}")
}
if (useCCache) {
arguments.add("-PuseCCache")
}
Expand Down Expand Up @@ -461,6 +464,9 @@ def getRunTestsBuildArguments = { taskName ->
if (onlyX86) {
arguments.add("-PonlyX86")
}
if (project.hasProperty("abis")) {
arguments.add("-Pabis=${project.property('abis')}")
}
if (useCCache) {
arguments.add("-PuseCCache")
}
Expand Down
52 changes: 52 additions & 0 deletions docs/ns-builtin-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,58 @@ Rules:
versions for readability; it is intended for humans and must not be parsed
programmatically.

### `ns:runtime` (v1)

Runtime-level configuration. Keys, value domains, and scope are defined and
validated natively; the module surface is a thin frozen wrapper.

| export | description |
|---|---|
| `setConfig(key, value)` | Sets a runtime config key. Throws `TypeError` on an unknown key, an invalid value, or (for process-wide keys) when called from a worker isolate. |
| `getConfig(key)` | Returns the current value of a config key. Throws `TypeError` on an unknown key. Readable from any isolate. |

Config keys:

| key | values | scope | default |
|---|---|---|---|
| `logScriptLoading` | `true` \| `false` | process-wide (main-isolate writes only; read live by every isolate) | `false`, or the `logScriptLoading` value from nativescript.config / package.json at boot |
| `httpFetchUrlLog` | `true` \| `false` | process-wide (main-isolate writes only; read live by every isolate) | `false`, or the `httpFetchUrlLog` value from nativescript.config / package.json at boot |

Remote-module security (`security.allowRemoteModules`,
`security.remoteModuleAllowlist`) is **not** part of this surface. Those
values are read once from nativescript.config / package.json the first time
the HTTP loader gates a fetch, and they cannot be inspected or changed
through `getConfig` / `setConfig`.

iOS additionally registers `releasedObjectPolicy`; Android does not (it has
no released-native-counterpart machinery).

### `ns:module` (v1)

The module-loader control surface consumed by development tooling
(`@nativescript/vite`). Mechanism only: every policy concern (boot
orchestration, `import.meta.hot`, full reload, CSS apply, worker teardown,
WebSocket protocol) lives in the tooling.

| export | description |
|---|---|
| `configureLoader(config)` | Install loader policy before the session imports anything: `importMap` (bare specifier → URL, consulted inside the synchronous resolver), `volatilePatterns` (URL substrings always re-fetched), `canonicalization` (`stripParams`/`forPathPrefixes`/`preserveQueryFor` vocabulary for registry keying). Each present section replaces its state wholesale. |
| `invalidateModules(urls)` | Evict the given URLs (canonicalized) from the module registry and mark them bust-next-fetch, so the next network fetch bypasses every HTTP cache layer. |
| `getLoadedModuleUrls()` | URL-like keys currently in the module registry (used to compute full-reload eviction sets). |
| `setDevBootComplete(value?)` | Flip the dev-boot-complete signal (defaults to `true`); disarms cold-boot-only behaviors. |

Debug builds additionally carry `canonicalizeHttpUrlKey(url)`, a pure test
diagnostic; release builds omit it. Missing members are simply absent —
never present-but-throwing — so feature checks work. The module is
registered in every build; the security boundary for remote module loading
sits at the network layer (`security.allowRemoteModules` in
nativescript.config, enforced inside `HttpLoader`), not the module
registry and not `ns:runtime` getConfig/setConfig.

Note: `ns:module` (loader policy, structured, boot-time) is deliberately
separate from `ns:runtime` (live key-value runtime flags, `setConfig`/
`getConfig`).

## `node:` compatibility shims

The same registry serves the `node:` scheme with **compatibility shims** so
Expand Down
105 changes: 105 additions & 0 deletions test-app/app/src/main/assets/app/tests/testNsModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
describe("ns:module", function () {
it("should expose the dev-loader primitives via the ns:module builtin", function () {
var nsModule = require("ns:module");
expect(Object.isFrozen(nsModule)).toBe(true);
expect(typeof nsModule.configureLoader).toBe("function");
expect(typeof nsModule.invalidateModules).toBe("function");
expect(typeof nsModule.getLoadedModuleUrls).toBe("function");
expect(typeof nsModule.setDevBootComplete).toBe("function");
expect(nsModule.terminateAllWorkers).toBeUndefined();
expect(global.__NS_DEV__).toBeUndefined();
});

it("exposes exactly the declared surface", function () {
var nsModule = require("ns:module");
var expected = ["configureLoader", "getLoadedModuleUrls", "invalidateModules", "setDevBootComplete"];
if (typeof nsModule.canonicalizeHttpUrlKey === "function") {
expected.push("canonicalizeHttpUrlKey");
}
expect(Object.keys(nsModule).sort()).toEqual(expected.sort());
});

it("resolves ns:module to the same members for require and import()", function (done) {
var nsModule = require("ns:module");
import("ns:module").then(function (ns) {
expect(ns.default).toBe(nsModule);
expect(ns.invalidateModules).toBe(nsModule.invalidateModules);
expect(ns.configureLoader).toBe(nsModule.configureLoader);
done();
}).catch(function (error) {
fail("import('ns:module') rejected: " + error.message);
done();
});
});

it("setDevBootComplete flips the JS-visible boot-complete global", function () {
var nsModule = require("ns:module");
nsModule.setDevBootComplete(true);
expect(global.__NS_HMR_BOOT_COMPLETE__).toBe(true);
nsModule.setDevBootComplete(false);
expect(global.__NS_HMR_BOOT_COMPLETE__).toBe(false);
nsModule.setDevBootComplete();
expect(global.__NS_HMR_BOOT_COMPLETE__).toBe(true);
nsModule.setDevBootComplete(false);
});
Comment thread
NathanWalker marked this conversation as resolved.
});

describe("HTTP canonical key (ns:module canonicalizeHttpUrlKey)", function () {
function getCanon() {
return require("ns:module").canonicalizeHttpUrlKey;
}

function checkKey(input, expected) {
var canon = getCanon();
if (typeof canon !== "function") {
pending("ns:module.canonicalizeHttpUrlKey not exposed (release build)");
return;
}
expect(canon(input)).toBe(expected);
}

it("is exposed as a function in debug builds", function () {
var canon = getCanon();
if (typeof canon !== "function") {
pending("ns:module.canonicalizeHttpUrlKey not exposed (release build)");
return;
}
expect(typeof canon).toBe("function");
});

it("drops dev cache-busters (t/v/import) but keeps real query params", function () {
checkKey("http://h/ns/core?p=x&t=123&v=9&import=1", "http://h/ns/core?p=x");
});

it("leaves public (non-dev, non-volatile) URLs untouched", function () {
checkKey("https://cdn.example.com/lib.js?token=abc", "https://cdn.example.com/lib.js?token=abc");
});

it("treats module identity as literally the URL — no path-tag collapses", function () {
checkKey("http://h/ns/m/foo.js", "http://h/ns/m/foo.js");
checkKey("http://h/ns/rt", "http://h/ns/rt");
checkKey("http://h/ns/core", "http://h/ns/core");
});

it("ignores URL fragments for dev endpoints", function () {
checkKey("http://h/ns/m/foo.js#frag", "http://h/ns/m/foo.js");
});

it("honors a client-supplied canonicalization vocabulary via configureLoader", function () {
var canon = getCanon();
if (typeof canon !== "function") {
pending("ns:module.canonicalizeHttpUrlKey not exposed (release build)");
return;
}
require("ns:module").configureLoader({
canonicalization: {
stripParams: ["t", "v", "import"],
forPathPrefixes: ["/ns/", "/node_modules/.vite/", "/@id/", "/@fs/"],
preserveQueryFor: ["/@ng/component"],
},
});
expect(canon("http://h/ns/core?p=x&t=123&v=9&import=1")).toBe("http://h/ns/core?p=x");
expect(canon("http://h/ns/m/comp/@ng/component?c=a&t=42")).toBe("http://h/ns/m/comp/@ng/component?c=a&t=42");
expect(canon("https://cdn.example.com/lib.js?token=abc")).toBe("https://cdn.example.com/lib.js?token=abc");
});
});
67 changes: 67 additions & 0 deletions test-app/app/src/main/assets/app/tests/testNsRuntime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
describe("ns:runtime", function () {
var runtime = require("ns:runtime");

it("exposes frozen exports", function () {
expect(Object.isFrozen(runtime)).toBe(true);
expect(typeof runtime.setConfig).toBe("function");
expect(typeof runtime.getConfig).toBe("function");
});

it("exposes exactly the declared surface", function () {
expect(Object.keys(runtime).sort()).toEqual(["getConfig", "setConfig"]);
});

it("rejects unknown keys", function () {
expect(function () {
runtime.setConfig("noSuchKey", 1);
}).toThrowError(TypeError, /Unknown runtime config key/);
expect(function () {
runtime.getConfig("noSuchKey");
}).toThrowError(TypeError, /Unknown runtime config key/);
});

it("defaults logScriptLoading and httpFetchUrlLog from app config", function () {
expect(runtime.getConfig("logScriptLoading")).toBe(false);
expect(runtime.getConfig("httpFetchUrlLog")).toBe(false);
});

it("round-trips logScriptLoading and httpFetchUrlLog", function () {
runtime.setConfig("logScriptLoading", true);
expect(runtime.getConfig("logScriptLoading")).toBe(true);
runtime.setConfig("logScriptLoading", false);
expect(runtime.getConfig("logScriptLoading")).toBe(false);

runtime.setConfig("httpFetchUrlLog", true);
expect(runtime.getConfig("httpFetchUrlLog")).toBe(true);
runtime.setConfig("httpFetchUrlLog", false);
expect(runtime.getConfig("httpFetchUrlLog")).toBe(false);
});

it("rejects non-boolean log flag values and keeps the current one", function () {
expect(function () {
runtime.setConfig("logScriptLoading", "yes");
}).toThrowError(TypeError, /must be a boolean/);
expect(runtime.getConfig("logScriptLoading")).toBe(false);
expect(function () {
runtime.setConfig("httpFetchUrlLog", 1);
}).toThrowError(TypeError, /must be a boolean/);
expect(runtime.getConfig("httpFetchUrlLog")).toBe(false);
});

it("does not expose remote-module security through getConfig or setConfig", function () {
["security", "allowRemoteModules", "remoteModuleAllowlist"].forEach(function (key) {
expect(function () {
runtime.getConfig(key);
}).toThrowError(TypeError, /Unknown runtime config key/);
expect(function () {
runtime.setConfig(key, true);
}).toThrowError(TypeError, /Unknown runtime config key/);
});
});

it("does not expose releasedObjectPolicy (iOS-only)", function () {
expect(function () {
runtime.getConfig("releasedObjectPolicy");
}).toThrowError(TypeError, /Unknown runtime config key/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ describe("Remote Module Security", function() {
// In debug mode, this returns true because debug bypasses allowlist
expect(isAllowed).toBe(true);
});

it("should refuse lookalike-host prefixes at a URL-component boundary (Java helper)", function() {
// The Java helper is the production-path twin of the native gate.
// Debug still short-circuits to true, so this only asserts the
// helper exists and debug bypass still holds; production matching
// is covered by the native RemoteUrlMatchesAllowlistEntry logic.
expect(typeof com.tns.Runtime.isRemoteUrlAllowed).toBe("function");
expect(com.tns.Runtime.isRemoteUrlAllowed("https://cdn.example.com.attacker.com/x.js")).toBe(true);
});
Comment thread
NathanWalker marked this conversation as resolved.
});

describe("Static Import HTTP Loading", function() {
Expand Down
8 changes: 6 additions & 2 deletions test-app/runtests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def getBuildArguments = { ->
if (onlyX86) {
arguments.add("-PonlyX86")
}
if (project.hasProperty("abis")) {
arguments.add("-Pabis=${project.property('abis')}")
}
if (useCCache) {
arguments.add("-PuseCCache")
}
Expand Down Expand Up @@ -64,13 +67,14 @@ task runAdbAsRoot(type: Exec) {
}

task deletePreviousResultXml(type: Exec) {
ignoreExitValue = true
doFirst {
println "Removing previous android_unit_test_results.xml"

if (isWinOs) {
commandLine "cmd", "/c", "adb", runOnDeviceOrEmulator, "-e", "shell", "rm", "-rf", "/data/data/com.tns.testapplication/android_unit_test_results.xml"
commandLine "cmd", "/c", "adb", runOnDeviceOrEmulator, "-e", "shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"
} else {
commandLine "adb", runOnDeviceOrEmulator, "-e", "shell", "rm", "-rf", "/data/data/com.tns.testapplication/android_unit_test_results.xml"
commandLine "adb", runOnDeviceOrEmulator, "-e", "shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"
Comment thread
NathanWalker marked this conversation as resolved.
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ set(RUNTIME_BUILTIN_JS
${RUNTIME_BUILTIN_JS_DIR}/json-helper.js
${RUNTIME_BUILTIN_JS_DIR}/message-loop-timer.js
${RUNTIME_BUILTIN_JS_DIR}/node-util.js
${RUNTIME_BUILTIN_JS_DIR}/ns-module.js
${RUNTIME_BUILTIN_JS_DIR}/ns-runtime.js
${RUNTIME_BUILTIN_JS_DIR}/ns-util.js
${RUNTIME_BUILTIN_JS_DIR}/performance.js
${RUNTIME_BUILTIN_JS_DIR}/primordials.js
Expand Down Expand Up @@ -207,8 +209,7 @@ add_library(
src/main/cpp/URLImpl.cpp
src/main/cpp/URLSearchParamsImpl.cpp
src/main/cpp/URLPatternImpl.cpp
src/main/cpp/HMRSupport.cpp
src/main/cpp/DevFlags.cpp
src/main/cpp/HttpLoader.cpp

${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp

Expand Down
14 changes: 14 additions & 0 deletions test-app/runtime/src/main/cpp/ConcurrentQueue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ void ConcurrentQueue::Push(std::shared_ptr<worker::Message> message) {
}
}

void ConcurrentQueue::Signal() {
std::unique_lock<std::mutex> lock(initializationMutex_);
if (terminated_ || this->fd_ == -1) {
return;
}
uint64_t value = 1;
write(this->fd_, &value, sizeof(value));
}

bool ConcurrentQueue::IsEmpty() {
std::unique_lock<std::mutex> mlock(this->mutex_);
return this->messagesQueue_.empty();
}

std::vector<std::shared_ptr<worker::Message>> ConcurrentQueue::PopAll() {
std::unique_lock<std::mutex> mlock(this->mutex_);
std::vector<std::shared_ptr<worker::Message>> messages;
Expand Down
2 changes: 2 additions & 0 deletions test-app/runtime/src/main/cpp/ConcurrentQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct ConcurrentQueue {
public:
void Initialize(ALooper* looper, ALooper_callbackFunc performWork, void* data);
void Push(std::shared_ptr<worker::Message> message);
void Signal();
bool IsEmpty();
std::vector<std::shared_ptr<worker::Message>> PopAll();
void Terminate();

Expand Down
Loading
Loading