syntaxase 0.0.0 → 0.1.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 CHANGED
@@ -1,6 +1,62 @@
1
1
  # Syntaxase
2
2
 
3
- > [!WARNING]
4
- > Work in progress. This package is not ready for use yet.
3
+ Lightning-fast type stripping and JSX lowering.
5
4
 
6
- A tiny syntax-only transformer for TypeScript and JSX.
5
+ Named like an enzyme, Syntaxase removes erasable TypeScript syntax and lowers
6
+ supported TypeScript and JSX runtime constructs.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install syntaxase
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```js
17
+ import { stripTypes, transform } from "syntaxase";
18
+
19
+ const stripped = stripTypes(`
20
+ interface User { name: string }
21
+ const user: User = { name: "Ada" };
22
+ `);
23
+
24
+ const transformed = transform(`const view: JSX.Element = <h1>Hello</h1>;`, { jsx: true });
25
+ ```
26
+
27
+ ## TypeScript syntax
28
+
29
+ `stripTypes` erases fixed-width TypeScript syntax while preserving source
30
+ length and line layout. Use it with source accepted by TypeScript's
31
+ [`erasableSyntaxOnly`](https://www.typescriptlang.org/tsconfig/erasableSyntaxOnly.html)
32
+ option. It does not validate that constraint: runtime TypeScript constructs such
33
+ as enums may remain unchanged or lose TypeScript-only modifiers without gaining
34
+ the JavaScript code needed to preserve their semantics.
35
+
36
+ `transform` additionally lowers supported runtime TypeScript constructs and,
37
+ when enabled, JSX.
38
+
39
+ ## JSX options
40
+
41
+ | `options.jsx` | Behavior | Defaults and supported fields |
42
+ | -------------------------- | ------------------------------------------------- | --------------------------------------------------------------- |
43
+ | omitted or `false` | Parse TypeScript without JSX | None |
44
+ | `true` | Lower JSX with the automatic production runtime | `importSource: "react"` |
45
+ | `{ runtime: "automatic" }` | Lower JSX with the automatic runtime | `development: false`, `importSource: "react"` |
46
+ | `{ runtime: "classic" }` | Lower JSX to factory calls without adding imports | `pragma: "React.createElement"`, `pragmaFrag: "React.Fragment"` |
47
+ | `{ runtime: "preserve" }` | Parse JSX and leave it in the output | No additional fields |
48
+
49
+ Automatic development mode imports `jsxDEV` from
50
+ `<importSource>/jsx-dev-runtime`; production mode imports `jsx` and `jsxs` from
51
+ `<importSource>/jsx-runtime`. The default therefore requires React's JSX runtime
52
+ to be installed. Set `importSource` for another compatible runtime.
53
+
54
+ `development` is supported only by the automatic runtime. `pragma` and
55
+ `pragmaFrag` are supported only by the classic runtime.
56
+
57
+ ## Diagnostics and errors
58
+
59
+ The JavaScript API returns transformed strings only. Parser recovery diagnostics
60
+ are not exposed, so malformed input can still produce output. Use a TypeScript
61
+ checker or another parser when validation is required. Invalid API arguments and
62
+ internal transform failures throw.
package/binding.js ADDED
@@ -0,0 +1,63 @@
1
+ import { createRequire } from 'node:module';
2
+ import { readFileSync } from 'node:fs';
3
+ import { execSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const { platform, arch } = process;
10
+
11
+ const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-');
12
+
13
+ function isMusl() {
14
+ if (platform !== 'linux') return false;
15
+
16
+ try {
17
+ if (readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')) return true;
18
+ } catch {}
19
+
20
+ try {
21
+ const report = typeof process.report?.getReport === 'function'
22
+ ? process.report.getReport()
23
+ : null;
24
+ if (report) {
25
+ const header = typeof report === 'string' ? JSON.parse(report).header : report.header;
26
+ if (header?.glibcVersionRuntime) return false;
27
+ if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isFileMusl)) return true;
28
+ }
29
+ } catch {}
30
+
31
+ try {
32
+ return execSync('ldd --version', { encoding: 'utf8' }).includes('musl');
33
+ } catch {}
34
+
35
+ return false;
36
+ }
37
+
38
+ function loadBinding() {
39
+ const errors = [];
40
+ const libc = platform === 'linux' ? (isMusl() ? '-musl' : '-gnu') : '';
41
+ const suffix = `${platform}-${arch}${libc}`;
42
+
43
+ try {
44
+ return require(join(__dirname, '@syntaxase', 'binding-' + suffix, 'syntaxase.node'));
45
+ } catch (e) {
46
+ errors.push(e);
47
+ }
48
+
49
+ try {
50
+ return require('@syntaxase/binding-' + suffix + '/syntaxase.node');
51
+ } catch (e) {
52
+ errors.push(e);
53
+ }
54
+
55
+ throw new Error(
56
+ `Failed to load native binding for ${platform}-${arch}.\n` +
57
+ `If this persists, try removing node_modules and reinstalling.\n` +
58
+ errors.map(e => ` - ${e.message}`).join('\n'),
59
+ { cause: errors[errors.length - 1] }
60
+ );
61
+ }
62
+
63
+ export default loadBinding();
package/index.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /** JSX lowering strategy used by every Syntaxase JavaScript package. */
2
+ export type JSXRuntime = "automatic" | "classic" | "preserve";
3
+
4
+ export type JSXConfig =
5
+ | {
6
+ runtime?: "automatic";
7
+ development?: boolean;
8
+ importSource?: string;
9
+ pragma?: never;
10
+ pragmaFrag?: never;
11
+ }
12
+ | {
13
+ runtime: "classic";
14
+ development?: never;
15
+ importSource?: never;
16
+ pragma?: string;
17
+ pragmaFrag?: string;
18
+ }
19
+ | {
20
+ runtime: "preserve";
21
+ development?: never;
22
+ importSource?: never;
23
+ pragma?: never;
24
+ pragmaFrag?: never;
25
+ };
26
+
27
+ export interface TransformOptions {
28
+ jsx?: boolean | JSXConfig;
29
+ }
30
+
31
+ /** Transform erasable TypeScript and supported runtime TypeScript/JSX syntax to JavaScript. */
32
+ export function transform(sourceText: string, options?: TransformOptions): string;
33
+
34
+ /** Erase only fixed-width TypeScript syntax while preserving source length and line layout. */
35
+ export function stripTypes(sourceText: string): string;
package/index.js CHANGED
@@ -1 +1,25 @@
1
- // WIP
1
+ import binding from "./binding.js";
2
+ import { assertSourceText, MODE_STRIP_TYPES, resolveTransformOptions } from "./options.js";
3
+
4
+ const encoder = new TextEncoder();
5
+ const empty = new Uint8Array();
6
+
7
+ /** Transform erasable TypeScript and supported runtime TypeScript/JSX syntax to JavaScript. */
8
+ export function transform(sourceText, options = {}) {
9
+ assertSourceText(sourceText);
10
+ const normalized = resolveTransformOptions(options);
11
+ return invoke(sourceText, normalized.mode, normalized.first, normalized.second);
12
+ }
13
+
14
+ /** Erase only fixed-width TypeScript syntax while preserving source length and line layout. */
15
+ export function stripTypes(sourceText) {
16
+ assertSourceText(sourceText);
17
+ return invoke(sourceText, MODE_STRIP_TYPES, "", "");
18
+ }
19
+
20
+ function invoke(sourceText, mode, firstOption, secondOption) {
21
+ const source = encoder.encode(sourceText);
22
+ const first = firstOption.length === 0 ? empty : encoder.encode(firstOption);
23
+ const second = secondOption.length === 0 ? empty : encoder.encode(secondOption);
24
+ return binding.run(source, mode, first, second);
25
+ }
package/options.js ADDED
@@ -0,0 +1,186 @@
1
+ // Canonical option normalization shared by all JavaScript package boundaries.
2
+ const MODE_TRANSFORM = 0;
3
+ const MODE_AUTOMATIC = 1;
4
+ const MODE_AUTOMATIC_DEVELOPMENT = 2;
5
+ const MODE_CLASSIC = 3;
6
+ const MODE_PRESERVE = 4;
7
+ export const MODE_STRIP_TYPES = 5;
8
+
9
+ const IDENTIFIER_NAME = /^[$_\p{ID_Start}][$\u200c\u200d_\p{ID_Continue}]*$/u;
10
+ const RESERVED_IDENTIFIER_REFERENCES = new Set([
11
+ "await",
12
+ "break",
13
+ "case",
14
+ "catch",
15
+ "class",
16
+ "const",
17
+ "continue",
18
+ "debugger",
19
+ "default",
20
+ "delete",
21
+ "do",
22
+ "else",
23
+ "enum",
24
+ "export",
25
+ "extends",
26
+ "false",
27
+ "finally",
28
+ "for",
29
+ "function",
30
+ "if",
31
+ "implements",
32
+ "import",
33
+ "in",
34
+ "instanceof",
35
+ "interface",
36
+ "let",
37
+ "new",
38
+ "null",
39
+ "package",
40
+ "private",
41
+ "protected",
42
+ "public",
43
+ "return",
44
+ "static",
45
+ "super",
46
+ "switch",
47
+ "this",
48
+ "throw",
49
+ "true",
50
+ "try",
51
+ "typeof",
52
+ "var",
53
+ "void",
54
+ "while",
55
+ "with",
56
+ "yield",
57
+ ]);
58
+ const JSX_CONFIG_KEYS = new Set(["runtime", "development", "importSource", "pragma", "pragmaFrag"]);
59
+ const JSX_RUNTIMES = new Set(["automatic", "classic", "preserve"]);
60
+
61
+ export function assertSourceText(sourceText) {
62
+ if (typeof sourceText !== "string") {
63
+ throw new TypeError("sourceText must be a string");
64
+ }
65
+ }
66
+
67
+ export function resolveTransformOptions(options) {
68
+ assertOptions(options);
69
+ return resolveJSXConfig(options.jsx);
70
+ }
71
+
72
+ function assertOptions(options) {
73
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
74
+ throw new TypeError("transform options must be an object");
75
+ }
76
+ for (const key of Reflect.ownKeys(options)) {
77
+ if (key !== "jsx") {
78
+ throw new TypeError(`transform options contains unknown option ${String(key)}`);
79
+ }
80
+ }
81
+ }
82
+
83
+ function resolveJSXConfig(value) {
84
+ if (value === undefined || value === false) {
85
+ return { mode: MODE_TRANSFORM, first: "", second: "" };
86
+ }
87
+ if (value === true) {
88
+ return { mode: MODE_AUTOMATIC, first: "react", second: "" };
89
+ }
90
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
91
+ throw new TypeError("transform options.jsx must be a boolean or an object");
92
+ }
93
+
94
+ assertKnownJSXKeys(value);
95
+ const runtime = readRuntime(value);
96
+ const importSource = readString(value, "importSource");
97
+ const pragma = readMemberExpression(value, "pragma");
98
+ const pragmaFrag = readMemberExpression(value, "pragmaFrag");
99
+
100
+ if (runtime === "automatic") {
101
+ const development = readBoolean(value, "development", false);
102
+ assertAbsent(value, "pragma", runtime);
103
+ assertAbsent(value, "pragmaFrag", runtime);
104
+ return {
105
+ mode: development ? MODE_AUTOMATIC_DEVELOPMENT : MODE_AUTOMATIC,
106
+ first: importSource ?? "react",
107
+ second: "",
108
+ };
109
+ }
110
+ if (runtime === "classic") {
111
+ assertAbsent(value, "development", runtime);
112
+ assertAbsent(value, "importSource", runtime);
113
+ return {
114
+ mode: MODE_CLASSIC,
115
+ first: pragma ?? "React.createElement",
116
+ second: pragmaFrag ?? "React.Fragment",
117
+ };
118
+ }
119
+
120
+ assertAbsent(value, "development", runtime);
121
+ assertAbsent(value, "importSource", runtime);
122
+ assertAbsent(value, "pragma", runtime);
123
+ assertAbsent(value, "pragmaFrag", runtime);
124
+ return { mode: MODE_PRESERVE, first: "", second: "" };
125
+ }
126
+
127
+ function assertKnownJSXKeys(config) {
128
+ for (const key of Reflect.ownKeys(config)) {
129
+ if (typeof key !== "string" || !JSX_CONFIG_KEYS.has(key)) {
130
+ throw new TypeError(`transform options.jsx contains unknown option ${String(key)}`);
131
+ }
132
+ }
133
+ }
134
+
135
+ function readRuntime(config) {
136
+ if (!Object.hasOwn(config, "runtime")) return "automatic";
137
+ const runtime = config.runtime;
138
+ if (typeof runtime !== "string" || !JSX_RUNTIMES.has(runtime)) {
139
+ throw new TypeError('transform options.jsx.runtime must be "automatic", "classic", or "preserve"');
140
+ }
141
+ return runtime;
142
+ }
143
+
144
+ function readBoolean(config, key, defaultValue) {
145
+ if (!Object.hasOwn(config, key)) return defaultValue;
146
+ const value = config[key];
147
+ if (typeof value !== "boolean") {
148
+ throw new TypeError(`transform options.jsx.${key} must be a boolean`);
149
+ }
150
+ return value;
151
+ }
152
+
153
+ function readString(config, key) {
154
+ if (!Object.hasOwn(config, key)) return undefined;
155
+ const value = config[key];
156
+ if (typeof value !== "string" || value.length === 0) {
157
+ throw new TypeError(`transform options.jsx.${key} must be a non-empty string`);
158
+ }
159
+ return value;
160
+ }
161
+
162
+ function readMemberExpression(config, key) {
163
+ if (!Object.hasOwn(config, key)) return undefined;
164
+ const value = config[key];
165
+ if (typeof value !== "string" || !isDottedMemberExpression(value)) {
166
+ throw new TypeError(`transform options.jsx.${key} must be an identifier or dotted member expression`);
167
+ }
168
+ return value;
169
+ }
170
+
171
+ function isDottedMemberExpression(value) {
172
+ const segments = value.split(".");
173
+ const root = segments[0];
174
+ if (root === undefined || !isIdentifierReference(root)) return false;
175
+ return segments.every((segment) => IDENTIFIER_NAME.test(segment));
176
+ }
177
+
178
+ function isIdentifierReference(value) {
179
+ return IDENTIFIER_NAME.test(value) && !RESERVED_IDENTIFIER_REFERENCES.has(value);
180
+ }
181
+
182
+ function assertAbsent(config, key, runtime) {
183
+ if (Object.hasOwn(config, key)) {
184
+ throw new TypeError(`transform options.jsx.${String(key)} is not supported with ${runtime} runtime`);
185
+ }
186
+ }
package/package.json CHANGED
@@ -1,20 +1,48 @@
1
1
  {
2
2
  "name": "syntaxase",
3
- "version": "0.0.0",
4
- "description": "A tiny syntax-only transformer for TypeScript and JSX.",
3
+ "version": "0.1.0",
4
+ "description": "Lightning-fast type stripping and JSX lowering",
5
+ "license": "MIT OR Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/magic-akari/syntaxase.git"
9
+ },
5
10
  "type": "module",
6
- "exports": "./index.js",
11
+ "main": "index.js",
12
+ "types": "index.d.ts",
7
13
  "files": [
8
- "index.js"
14
+ "index.js",
15
+ "index.d.ts",
16
+ "binding.js",
17
+ "options.js"
9
18
  ],
10
- "engines": {
11
- "node": ">=18"
19
+ "optionalDependencies": {
20
+ "@syntaxase/binding-linux-x64-gnu": "0.1.0",
21
+ "@syntaxase/binding-linux-arm64-gnu": "0.1.0",
22
+ "@syntaxase/binding-linux-arm-gnu": "0.1.0",
23
+ "@syntaxase/binding-linux-x64-musl": "0.1.0",
24
+ "@syntaxase/binding-linux-arm64-musl": "0.1.0",
25
+ "@syntaxase/binding-linux-arm-musl": "0.1.0",
26
+ "@syntaxase/binding-darwin-x64": "0.1.0",
27
+ "@syntaxase/binding-darwin-arm64": "0.1.0",
28
+ "@syntaxase/binding-win32-x64": "0.1.0",
29
+ "@syntaxase/binding-win32-arm64": "0.1.0",
30
+ "@syntaxase/binding-freebsd-x64": "0.1.0"
12
31
  },
32
+ "author": "magic-akari <akari.ccino@gmail.com>",
33
+ "homepage": "https://github.com/magic-akari/syntaxase",
13
34
  "keywords": [
14
35
  "typescript",
36
+ "tsx",
15
37
  "jsx",
16
38
  "transformer",
17
- "transpiler"
39
+ "strip-types"
18
40
  ],
19
- "license": "MIT"
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "funding": {
45
+ "type": "github",
46
+ "url": "https://github.com/sponsors/magic-akari"
47
+ }
20
48
  }