diff --git a/.appveyor.yml b/.appveyor.yml
index 8a2822a..aac253a 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -27,6 +27,7 @@ after_build:
- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Launcher\bin\x86\%CONFIGURATION%\* artifacts\
- cmd: nuget pack WelsonJS.Toolkit\WelsonJS.Toolkit\ -properties Configuration=%CONFIGURATION% -properties Platform=x86 -OutputDirectory artifacts\
- ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/welsonjs_setup_unsigned.exe" -Destination "artifacts\welsonjs_setup.exe"
+ - ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/chakracore-build/x86_release/ChakraCore.dll" -Destination "artifacts\ChakraCore.dll"
- cmd: 7z a artifacts.zip artifacts\*
artifacts:
@@ -45,4 +46,4 @@ notifications:
- gnh1201@catswords.re.kr
subject: "Build {{status}}: {{projectName}} {{buildVersion}}"
on_build_success: false
- on_build_failure: true
+ on_build_failure: true
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..317e9ac
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "WelsonJS.Toolkit/ChakraCore"]
+ path = WelsonJS.Toolkit/ChakraCore
+ url = https://github.com/chakra-core/ChakraCore
diff --git a/WelsonJS.Toolkit/ChakraCore b/WelsonJS.Toolkit/ChakraCore
new file mode 160000
index 0000000..36becec
--- /dev/null
+++ b/WelsonJS.Toolkit/ChakraCore
@@ -0,0 +1 @@
+Subproject commit 36becec43348f259e8bee08cf2fcd171bfe56f42
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs
index 74b323f..16d4edb 100644
--- a/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs
@@ -10,8 +10,8 @@ namespace WelsonJS.Launcher
{
public interface ICompatibleLogger
{
- void Info(string message);
- void Warn(string message);
- void Error(string message);
+ void Info(params object[] args);
+ void Warn(params object[] args);
+ void Error(params object[] args);
}
}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs
new file mode 100644
index 0000000..53af2b5
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs
@@ -0,0 +1,71 @@
+// JsCore.cs
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
+// https://github.com/gnh1201/welsonjs
+//
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+namespace WelsonJS.Launcher
+{
+ public sealed class JsCore : IDisposable
+ {
+ private JsNative.JsRuntime _rt;
+ private JsNative.JsContext _ctx;
+ private bool _disposed;
+
+ public JsCore()
+ {
+ Check(JsNative.JsCreateRuntime(JsNative.JsRuntimeAttributes.None, null, out _rt), "JsCreateRuntime");
+ Check(JsNative.JsCreateContext(_rt, out _ctx), "JsCreateContext");
+ Check(JsNative.JsSetCurrentContext(_ctx), "JsSetCurrentContext");
+ }
+
+ public string EvaluateToString(string script, string sourceUrl = "repl")
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(JsCore));
+ if (script == null) throw new ArgumentNullException(nameof(script));
+
+ JsNative.JsValue result;
+ Check(JsNative.JsRunScript(script, UIntPtr.Zero, sourceUrl, out result), "JsRunScript");
+
+ JsNative.JsValue jsStr;
+ Check(JsNative.JsConvertValueToString(result, out jsStr), "JsConvertValueToString");
+
+ IntPtr p;
+ UIntPtr len;
+ Check(JsNative.JsStringToPointer(jsStr, out p, out len), "JsStringToPointer");
+
+ int chars = checked((int)len);
+ return Marshal.PtrToStringUni(p, chars);
+ }
+
+ private static void Check(JsNative.JsErrorCode code, string op)
+ {
+ if (code != JsNative.JsErrorCode.JsNoError)
+ throw new InvalidOperationException(op + " failed: " + code + " (0x" + ((int)code).ToString("X", CultureInfo.InvariantCulture) + ")");
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ // Unset current context
+ JsNative.JsSetCurrentContext(new JsNative.JsContext { Handle = IntPtr.Zero });
+ }
+ catch { /* ignore */ }
+ finally
+ {
+ if (_rt.Handle != IntPtr.Zero)
+ JsNative.JsDisposeRuntime(_rt);
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~JsCore() { Dispose(); }
+ }
+}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs
new file mode 100644
index 0000000..3f44823
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace WelsonJS.Launcher
+{
+ public static class JsNative
+ {
+ // === Enums / handles ===
+ [Flags]
+ public enum JsRuntimeAttributes : uint
+ {
+ None = 0x00000000,
+ DisableBackgroundWork = 0x00000001,
+ AllowScriptInterrupt = 0x00000002,
+ EnableIdleProcessing = 0x00000004,
+ DisableNativeCodeGeneration = 0x00000008,
+ EnableExperimentalFeatures = 0x00000010,
+ }
+
+ // ChakraCore typedefs are opaque pointers; represent as IntPtr
+ public struct JsRuntime { public IntPtr Handle; }
+ public struct JsContext { public IntPtr Handle; }
+ public struct JsValue { public IntPtr Handle; }
+
+ // JsErrorCode (essential subset; expand as needed)
+ public enum JsErrorCode
+ {
+ JsNoError = 0,
+
+ // Usage
+ JsErrorInvalidArgument = 0x10001,
+ JsErrorNullArgument = 0x10002,
+ JsErrorNoCurrentContext = 0x10003,
+ JsErrorInExceptionState = 0x10004,
+ JsErrorNotImplemented = 0x10005,
+ JsErrorWrongThread = 0x10006,
+ JsErrorRuntimeInUse = 0x10007,
+
+ // Script
+ JsErrorScriptException = 0x30001,
+ JsErrorScriptCompile = 0x30002,
+ JsErrorScriptTerminated = 0x30003,
+
+ // Engine
+ JsErrorOutOfMemory = 0x20001,
+
+ // Fatal
+ JsErrorFatal = 0x40001,
+ }
+
+ // Thread service callback: __stdcall
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ public delegate JsErrorCode JsThreadServiceCallback(IntPtr callback, IntPtr callbackState);
+
+ // ======= FIXED SIGNATURES (StdCall + Unicode) =======
+
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsCreateRuntime(
+ JsRuntimeAttributes attributes,
+ JsThreadServiceCallback threadService, // pass null if unused
+ out JsRuntime runtime);
+
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsCreateContext(
+ JsRuntime runtime,
+ out JsContext newContext);
+
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsSetCurrentContext(JsContext context);
+
+ // JsSourceContext is size_t → UIntPtr; strings are wide-char
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]
+ public static extern JsErrorCode JsRunScript(
+ string script,
+ UIntPtr sourceContext,
+ string sourceUrl,
+ out JsValue result);
+
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsConvertValueToString(JsValue value, out JsValue stringValue);
+
+ // Returns pointer to UTF-16 buffer + length (size_t) for a JsString value
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsStringToPointer(
+ JsValue value,
+ out IntPtr buffer,
+ out UIntPtr length);
+
+ // Unset by passing "invalid" context (JS_INVALID_REFERENCE is typically null)
+ [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern JsErrorCode JsDisposeRuntime(JsRuntime runtime);
+ }
+}
diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
new file mode 100644
index 0000000..5eddd2d
--- /dev/null
+++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
@@ -0,0 +1,295 @@
+// JsSerializer.cs
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
+// https://github.com/gnh1201/welsonjs
+//
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace WelsonJS.Launcher
+{
+ public sealed class JsSerializer : IDisposable
+ {
+ private readonly JsCore _core;
+ private readonly bool _ownsCore;
+
+ public JsSerializer() : this(new JsCore(), true) { }
+
+ public JsSerializer(JsCore core, bool ownsCore)
+ {
+ if (core == null) throw new ArgumentNullException("core");
+ _core = core;
+ _ownsCore = ownsCore;
+ }
+
+ public bool IsValid(string json)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ string script =
+ "(function(){try{JSON.parse(" + Q(json) + ");return '1';}catch(_){return '0';}})()";
+ string r = _core.EvaluateToString(script);
+ return r == "1";
+ }
+
+ public string Minify(string json)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ string script = "JSON.stringify(JSON.parse(" + Q(json) + "))";
+ return _core.EvaluateToString(script);
+ }
+
+ public string Pretty(string json, int space)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ space = Clamp(space, 0, 10);
+ string script = "JSON.stringify(JSON.parse(" + Q(json) + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")";
+ return _core.EvaluateToString(script);
+ }
+
+ public string Normalize(string json)
+ {
+ return Minify(json);
+ }
+
+ ///
+ /// Extracts a value by a simple path of property names (numeric segment as string = array index).
+ /// Returns the selected value as a JSON string.
+ ///
+ public string Extract(string json, params string[] path)
+ {
+ if (path == null) path = new string[0];
+ object[] mixed = new object[path.Length];
+ for (int i = 0; i < path.Length; i++) mixed[i] = path[i];
+ return Extract(json, mixed);
+ }
+
+ ///
+ /// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices).
+ /// Returns the selected value as a JSON string (e.g., a JS string returns with quotes).
+ /// Usage: Extract(json, "items", 0, "name")
+ ///
+ public string Extract(string json, params object[] path)
+ {
+ if (json == null) throw new ArgumentNullException("json");
+ if (path == null) path = new object[0];
+
+ string jsPath = BuildJsPath(path);
+
+ var sb = new StringBuilder();
+ sb.Append("(function(){var v=JSON.parse(").Append(Q(json)).Append(");");
+ sb.Append("var p=").Append(jsPath).Append(";");
+ sb.Append("for(var i=0;i
(ReferenceEqualityComparer.Instance), 0);
+ string script = "JSON.stringify((" + expr + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")";
+ return _core.EvaluateToString(script);
+ }
+
+ private static int Clamp(int v, int min, int max)
+ {
+ if (v < min) return min;
+ if (v > max) return max;
+ return v;
+ }
+
+ ///
+ /// Encode a .NET string as a JS double-quoted string literal.
+ ///
+ private static string Q(string s)
+ {
+ if (s == null) return "null";
+ var sb = new StringBuilder(s.Length + 16);
+ sb.Append('"');
+ for (int i = 0; i < s.Length; i++)
+ {
+ char ch = s[i];
+ switch (ch)
+ {
+ case '\\': sb.Append(@"\\"); break;
+ case '"': sb.Append("\\\""); break;
+ case '\b': sb.Append(@"\b"); break;
+ case '\f': sb.Append(@"\f"); break;
+ case '\n': sb.Append(@"\n"); break;
+ case '\r': sb.Append(@"\r"); break;
+ case '\t': sb.Append(@"\t"); break;
+ case '\u2028': sb.Append("\\u2028"); break;
+ case '\u2029': sb.Append("\\u2029"); break;
+ default:
+ if (char.IsControl(ch))
+ {
+ sb.Append("\\u");
+ sb.Append(((int)ch).ToString("X4"));
+ }
+ else
+ {
+ sb.Append(ch);
+ }
+ break;
+ }
+ }
+ sb.Append('"');
+ return sb.ToString();
+ }
+
+ ///
+ /// Builds a JS array literal representing the path.
+ /// Numeric segments are emitted as numbers; others as strings.
+ ///
+ private static string BuildJsPath(object[] segments)
+ {
+ if (segments == null || segments.Length == 0) return "[]";
+
+ var sb = new StringBuilder();
+ sb.Append('[');
+ for (int i = 0; i < segments.Length; i++)
+ {
+ if (i > 0) sb.Append(',');
+
+ object seg = segments[i];
+
+ // Treat integral types as numbers for array indexing
+ if (seg is sbyte || seg is byte ||
+ seg is short || seg is ushort ||
+ seg is int || seg is uint ||
+ seg is long || seg is ulong)
+ {
+ sb.Append(Convert.ToString(seg, CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ string str = (seg == null) ? string.Empty : Convert.ToString(seg, CultureInfo.InvariantCulture);
+ sb.Append(Q(str));
+ }
+ }
+ sb.Append(']');
+ return sb.ToString();
+ }
+
+ private static bool IsNumeric(object v)
+ {
+ if (v == null) return false;
+ Type t = v.GetType();
+ t = Nullable.GetUnderlyingType(t) ?? t;
+ return t == typeof(byte) || t == typeof(sbyte) ||
+ t == typeof(short) || t == typeof(ushort) ||
+ t == typeof(int) || t == typeof(uint) ||
+ t == typeof(long) || t == typeof(ulong) ||
+ t == typeof(float) || t == typeof(double) ||
+ t == typeof(decimal);
+ }
+
+ private static bool IsImmutableLike(object v)
+ {
+ return v is string || v is bool ||
+ v is byte || v is sbyte ||
+ v is short || v is ushort ||
+ v is int || v is uint ||
+ v is long || v is ulong ||
+ v is float || v is double || v is decimal ||
+ v is Guid || v is DateTime || v is DateTimeOffset;
+ }
+
+ ///
+ /// Builds a safe JS expression for a .NET value (no engine calls here).
+ /// Engine will stringify the produced expression via JSON.stringify.
+ ///
+ private static string BuildJsExpression(object value, HashSet