@knighted/jsx 1.2.1 → 1.3.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 +10 -2
- package/dist/cjs/cli/init.cjs +259 -0
- package/dist/cjs/cli/init.d.cts +36 -0
- package/dist/cli/init.d.ts +36 -0
- package/dist/cli/init.js +266 -0
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ A runtime JSX template tag backed by the [`oxc-parser`](https://github.com/oxc-p
|
|
|
20
20
|
- [Node / SSR usage](#node--ssr-usage)
|
|
21
21
|
- [Next.js integration](#nextjs-integration)
|
|
22
22
|
- [Browser usage](#browser-usage)
|
|
23
|
+
- [Component testing](docs/testing.md)
|
|
23
24
|
- [Testing & demos](#testing)
|
|
25
|
+
- [CLI setup](docs/cli.md)
|
|
24
26
|
|
|
25
27
|
## Installation
|
|
26
28
|
|
|
@@ -34,12 +36,14 @@ npm install @knighted/jsx
|
|
|
34
36
|
> [!NOTE]
|
|
35
37
|
> Planning to use the React runtime (`@knighted/jsx/react`)? Install `react@>=18` and `react-dom@>=18` alongside this package so the helper can create elements and render them through ReactDOM.
|
|
36
38
|
|
|
37
|
-
The parser automatically uses native bindings when it runs in Node.js. To enable the WASM binding for browser builds you also need the `@oxc-parser/binding-wasm32-wasi` package.
|
|
39
|
+
The parser automatically uses native bindings when it runs in Node.js. To enable the WASM binding for browser builds you also need the `@oxc-parser/binding-wasm32-wasi` package. The quickest path is:
|
|
38
40
|
|
|
39
41
|
```sh
|
|
40
|
-
|
|
42
|
+
npx @knighted/jsx init
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
See [docs/cli.md](docs/cli.md) for flags, dry runs, and package-manager overrides. If you prefer manual install, run `npm_config_ignore_platform=true npm install @oxc-parser/binding-wasm32-wasi`.
|
|
46
|
+
|
|
43
47
|
> [!TIP]
|
|
44
48
|
> Public CDNs such as `esm.sh` or `jsdelivr` already publish bundles that include the WASM binding, so you can import this package directly from those endpoints in `<script type="module">` blocks without any extra setup.
|
|
45
49
|
|
|
@@ -334,6 +338,10 @@ Each lite subpath ships the same API as its standard counterpart but is pre-mini
|
|
|
334
338
|
|
|
335
339
|
## Testing
|
|
336
340
|
|
|
341
|
+
Looking for guidance on testing your own components with `jsx` or `reactJsx`? See
|
|
342
|
+
[docs/testing.md](docs/testing.md) for DOM and React runtime examples. The commands
|
|
343
|
+
below cover the library's internal test suites.
|
|
344
|
+
|
|
337
345
|
Run the Vitest suite (powered by jsdom) to exercise the DOM runtime and component support:
|
|
338
346
|
|
|
339
347
|
```sh
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseArgs = parseArgs;
|
|
7
|
+
exports.detectPackageManager = detectPackageManager;
|
|
8
|
+
exports.ensurePackageJson = ensurePackageJson;
|
|
9
|
+
exports.runNpmPack = runNpmPack;
|
|
10
|
+
exports.parsePackageName = parsePackageName;
|
|
11
|
+
exports.installBinding = installBinding;
|
|
12
|
+
exports.installRuntimeDeps = installRuntimeDeps;
|
|
13
|
+
exports.isDependencyInstalled = isDependencyInstalled;
|
|
14
|
+
exports.persistBindingSpec = persistBindingSpec;
|
|
15
|
+
exports.verifyBinding = verifyBinding;
|
|
16
|
+
exports.promptYesNo = promptYesNo;
|
|
17
|
+
exports.maybeHandleConfigPrompt = maybeHandleConfigPrompt;
|
|
18
|
+
exports.main = main;
|
|
19
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const promises_1 = __importDefault(require("node:readline/promises"));
|
|
23
|
+
const node_module_1 = require("node:module");
|
|
24
|
+
const node_url_1 = require("node:url");
|
|
25
|
+
const tar_1 = require("tar");
|
|
26
|
+
const DEFAULT_BINDING_SPEC = process.env.WASM_BINDING_PACKAGE ?? '@oxc-parser/binding-wasm32-wasi@^0.99.0';
|
|
27
|
+
const RUNTIME_DEPS = ['@napi-rs/wasm-runtime', '@emnapi/runtime', '@emnapi/core'];
|
|
28
|
+
const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const options = {
|
|
31
|
+
cwd: process.cwd(),
|
|
32
|
+
dryRun: false,
|
|
33
|
+
verbose: false,
|
|
34
|
+
force: false,
|
|
35
|
+
skipConfig: true,
|
|
36
|
+
wasmPackage: DEFAULT_BINDING_SPEC,
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (arg === '--dry-run') {
|
|
41
|
+
options.dryRun = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === '--verbose') {
|
|
44
|
+
options.verbose = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '--force' || arg === '--yes') {
|
|
47
|
+
options.force = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg === '--skip-config') {
|
|
50
|
+
options.skipConfig = true;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === '--config') {
|
|
53
|
+
options.skipConfig = false;
|
|
54
|
+
}
|
|
55
|
+
else if (arg === '--package-manager' || arg === '--pm') {
|
|
56
|
+
const pm = argv[i + 1];
|
|
57
|
+
if (!pm)
|
|
58
|
+
throw new Error('Missing value for --package-manager');
|
|
59
|
+
if (!SUPPORTED_PACKAGE_MANAGERS.includes(pm)) {
|
|
60
|
+
throw new Error(`Unsupported package manager: ${pm}`);
|
|
61
|
+
}
|
|
62
|
+
options.packageManager = pm;
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === '--wasm-package') {
|
|
66
|
+
const pkg = argv[i + 1];
|
|
67
|
+
if (!pkg)
|
|
68
|
+
throw new Error('Missing value for --wasm-package');
|
|
69
|
+
options.wasmPackage = pkg;
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (arg === '--help' || arg === '-h') {
|
|
73
|
+
printHelp();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return options;
|
|
78
|
+
}
|
|
79
|
+
function printHelp() {
|
|
80
|
+
console.log(`\nUsage: npx @knighted/jsx init [options]\n\nOptions:\n --package-manager, --pm <name> Choose npm | pnpm | yarn | bun\n --wasm-package <spec> Override binding package spec\n --config Prompt to help with loader config\n --skip-config Skip any loader config prompts (default)\n --dry-run Print actions without executing\n --force, --yes Assume yes for prompts\n --verbose Log extra detail\n -h, --help Show this help message\n`);
|
|
81
|
+
}
|
|
82
|
+
function log(message) {
|
|
83
|
+
console.log(message);
|
|
84
|
+
}
|
|
85
|
+
function logVerbose(message, verbose) {
|
|
86
|
+
if (verbose)
|
|
87
|
+
console.log(message);
|
|
88
|
+
}
|
|
89
|
+
function detectPackageManager(cwd, explicit) {
|
|
90
|
+
if (explicit)
|
|
91
|
+
return explicit;
|
|
92
|
+
const ua = process.env.npm_config_user_agent ?? '';
|
|
93
|
+
if (ua.startsWith('pnpm'))
|
|
94
|
+
return 'pnpm';
|
|
95
|
+
if (ua.startsWith('yarn'))
|
|
96
|
+
return 'yarn';
|
|
97
|
+
if (ua.startsWith('bun'))
|
|
98
|
+
return 'bun';
|
|
99
|
+
const lookups = {
|
|
100
|
+
pnpm: 'pnpm-lock.yaml',
|
|
101
|
+
yarn: 'yarn.lock',
|
|
102
|
+
bun: 'bun.lockb',
|
|
103
|
+
npm: 'package-lock.json',
|
|
104
|
+
};
|
|
105
|
+
for (const [pm, lockfile] of Object.entries(lookups)) {
|
|
106
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, lockfile)))
|
|
107
|
+
return pm;
|
|
108
|
+
}
|
|
109
|
+
return 'npm';
|
|
110
|
+
}
|
|
111
|
+
function ensurePackageJson(cwd) {
|
|
112
|
+
const pkgPath = node_path_1.default.join(cwd, 'package.json');
|
|
113
|
+
if (!node_fs_1.default.existsSync(pkgPath)) {
|
|
114
|
+
throw new Error('No package.json found. Run this inside a project with package.json.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function runNpmPack(spec, cwd, dryRun, verbose) {
|
|
118
|
+
logVerbose(`> npm pack ${spec}`, verbose);
|
|
119
|
+
if (dryRun)
|
|
120
|
+
return `${spec.replace(/\W+/g, '_')}.tgz`;
|
|
121
|
+
const output = (0, node_child_process_1.execFileSync)('npm', ['pack', spec], {
|
|
122
|
+
cwd,
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
125
|
+
}).trim();
|
|
126
|
+
const lines = output.split('\n').filter(Boolean);
|
|
127
|
+
return lines[lines.length - 1];
|
|
128
|
+
}
|
|
129
|
+
function parsePackageName(spec) {
|
|
130
|
+
const match = spec.match(/^(@?[^@\s]+\/[^@\s]+|@?[^@\s]+)(?:@(.+))?$/);
|
|
131
|
+
if (!match)
|
|
132
|
+
return { name: spec, version: undefined };
|
|
133
|
+
const [, name, version] = match;
|
|
134
|
+
return { name, version };
|
|
135
|
+
}
|
|
136
|
+
async function installBinding(spec, cwd, dryRun, verbose) {
|
|
137
|
+
const { name, version } = parsePackageName(spec);
|
|
138
|
+
const tarballName = runNpmPack(spec, cwd, dryRun, verbose);
|
|
139
|
+
const tarballPath = node_path_1.default.resolve(cwd, tarballName);
|
|
140
|
+
const targetDir = node_path_1.default.resolve(cwd, 'node_modules', ...name.split('/'));
|
|
141
|
+
log(`> Installing ${spec} into ${targetDir}`);
|
|
142
|
+
if (!dryRun) {
|
|
143
|
+
node_fs_1.default.rmSync(targetDir, { recursive: true, force: true });
|
|
144
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
145
|
+
await (0, tar_1.extract)({ file: tarballPath, cwd: targetDir, strip: 1 });
|
|
146
|
+
node_fs_1.default.rmSync(tarballPath, { force: true });
|
|
147
|
+
}
|
|
148
|
+
return { targetDir, tarballPath: dryRun ? undefined : tarballPath, name, version };
|
|
149
|
+
}
|
|
150
|
+
function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
|
|
151
|
+
const missing = deps.filter(dep => !isDependencyInstalled(dep, cwd));
|
|
152
|
+
if (missing.length === 0) {
|
|
153
|
+
log('> Runtime dependencies already present; skipping install');
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const commands = {
|
|
157
|
+
npm: ['npm', ['install', ...missing, '--save']],
|
|
158
|
+
pnpm: ['pnpm', ['add', ...missing]],
|
|
159
|
+
yarn: ['yarn', ['add', ...missing]],
|
|
160
|
+
bun: ['bun', ['add', ...missing]],
|
|
161
|
+
};
|
|
162
|
+
const [command, args] = commands[pm];
|
|
163
|
+
logVerbose(`> ${command} ${args.join(' ')}`, verbose);
|
|
164
|
+
if (!dryRun) {
|
|
165
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, { cwd, stdio: 'inherit' });
|
|
166
|
+
if (result.status !== 0) {
|
|
167
|
+
throw new Error(`Failed to install runtime dependencies with ${pm}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return missing;
|
|
171
|
+
}
|
|
172
|
+
function isDependencyInstalled(dep, cwd) {
|
|
173
|
+
try {
|
|
174
|
+
const requireFromCwd = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, 'package.json'));
|
|
175
|
+
requireFromCwd.resolve(dep);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function persistBindingSpec(cwd, name, version, dryRun, verbose) {
|
|
183
|
+
if (!version) {
|
|
184
|
+
logVerbose('> Skipping package.json update (no version parsed)', verbose);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const pkgPath = node_path_1.default.join(cwd, 'package.json');
|
|
188
|
+
const pkgRaw = node_fs_1.default.readFileSync(pkgPath, 'utf8');
|
|
189
|
+
const pkgJson = JSON.parse(pkgRaw);
|
|
190
|
+
pkgJson.optionalDependencies = pkgJson.optionalDependencies ?? {};
|
|
191
|
+
pkgJson.optionalDependencies[name] = version;
|
|
192
|
+
log(`> Recording optionalDependency ${name}@${version}`);
|
|
193
|
+
if (!dryRun) {
|
|
194
|
+
node_fs_1.default.writeFileSync(pkgPath, `${JSON.stringify(pkgJson, null, 2)}\n`, 'utf8');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function verifyBinding(name, cwd, verbose) {
|
|
198
|
+
const requireFromCwd = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, 'package.json'));
|
|
199
|
+
const resolved = requireFromCwd.resolve(name);
|
|
200
|
+
logVerbose(`> Resolved ${name} to ${resolved}`, verbose);
|
|
201
|
+
const imported = await import((0, node_url_1.pathToFileURL)(resolved).href);
|
|
202
|
+
if (!imported) {
|
|
203
|
+
throw new Error(`Imported ${name} is empty; verification failed`);
|
|
204
|
+
}
|
|
205
|
+
return resolved;
|
|
206
|
+
}
|
|
207
|
+
async function promptYesNo(prompt, defaultValue, force) {
|
|
208
|
+
if (!process.stdin.isTTY)
|
|
209
|
+
return defaultValue;
|
|
210
|
+
if (force)
|
|
211
|
+
return true;
|
|
212
|
+
const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
213
|
+
const suffix = defaultValue ? '[Y/n]' : '[y/N]';
|
|
214
|
+
const answer = await rl.question(`${prompt} ${suffix} `);
|
|
215
|
+
await rl.close();
|
|
216
|
+
if (!answer.trim())
|
|
217
|
+
return defaultValue;
|
|
218
|
+
const normalized = answer.trim().toLowerCase();
|
|
219
|
+
return normalized === 'y' || normalized === 'yes';
|
|
220
|
+
}
|
|
221
|
+
async function maybeHandleConfigPrompt(skipConfig, force) {
|
|
222
|
+
if (skipConfig) {
|
|
223
|
+
log('> Skipping loader config (default). Re-run with --config to opt in.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const wantsHelp = await promptYesNo('Do you want help adding loader configuration now?', false, force);
|
|
227
|
+
if (!wantsHelp) {
|
|
228
|
+
log('> Skipping loader config per your choice.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
log('> Loader assistance is interactive and not applied automatically yet. See docs at docs/cli.md for next steps.');
|
|
232
|
+
}
|
|
233
|
+
async function main() {
|
|
234
|
+
const options = parseArgs(process.argv.slice(2));
|
|
235
|
+
ensurePackageJson(options.cwd);
|
|
236
|
+
const packageManager = detectPackageManager(options.cwd, options.packageManager);
|
|
237
|
+
log(`> Using package manager: ${packageManager}`);
|
|
238
|
+
const installedRuntimeDeps = installRuntimeDeps(packageManager, RUNTIME_DEPS, options.cwd, options.dryRun, options.verbose);
|
|
239
|
+
const binding = await installBinding(options.wasmPackage, options.cwd, options.dryRun, options.verbose);
|
|
240
|
+
persistBindingSpec(options.cwd, binding.name, binding.version, options.dryRun, options.verbose);
|
|
241
|
+
let resolvedPath;
|
|
242
|
+
if (!options.dryRun) {
|
|
243
|
+
resolvedPath = await verifyBinding(binding.name, options.cwd, options.verbose);
|
|
244
|
+
log(`> Verified ${binding.name} at ${resolvedPath}`);
|
|
245
|
+
}
|
|
246
|
+
await maybeHandleConfigPrompt(options.skipConfig, options.force);
|
|
247
|
+
log('\nDone!');
|
|
248
|
+
log(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ''}`);
|
|
249
|
+
log(`- Target: ${binding.targetDir}`);
|
|
250
|
+
log(`- Runtime deps installed: ${installedRuntimeDeps.join(', ') || 'none (already present)'}`);
|
|
251
|
+
if (resolvedPath)
|
|
252
|
+
log(`- Verified import: ${resolvedPath}`);
|
|
253
|
+
}
|
|
254
|
+
if (process.env.KNIGHTED_JSX_CLI_TEST !== '1') {
|
|
255
|
+
main().catch(error => {
|
|
256
|
+
console.error('Failed to set up WASM binding:', error instanceof Error ? error.message : error);
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare const SUPPORTED_PACKAGE_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
|
|
2
|
+
type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number];
|
|
3
|
+
type CliOptions = {
|
|
4
|
+
cwd: string;
|
|
5
|
+
dryRun: boolean;
|
|
6
|
+
verbose: boolean;
|
|
7
|
+
force: boolean;
|
|
8
|
+
skipConfig: boolean;
|
|
9
|
+
packageManager?: PackageManager;
|
|
10
|
+
wasmPackage: string;
|
|
11
|
+
};
|
|
12
|
+
declare function parseArgs(argv: string[]): CliOptions;
|
|
13
|
+
declare function detectPackageManager(cwd: string, explicit?: PackageManager): PackageManager;
|
|
14
|
+
declare function ensurePackageJson(cwd: string): void;
|
|
15
|
+
declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean): string;
|
|
16
|
+
declare function parsePackageName(spec: string): {
|
|
17
|
+
name: string;
|
|
18
|
+
version: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
};
|
|
23
|
+
declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean): Promise<{
|
|
24
|
+
targetDir: string;
|
|
25
|
+
tarballPath?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
}>;
|
|
29
|
+
declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean): string[];
|
|
30
|
+
declare function isDependencyInstalled(dep: string, cwd: string): boolean;
|
|
31
|
+
declare function persistBindingSpec(cwd: string, name: string, version: string | undefined, dryRun: boolean, verbose: boolean): void;
|
|
32
|
+
declare function verifyBinding(name: string, cwd: string, verbose: boolean): Promise<string>;
|
|
33
|
+
declare function promptYesNo(prompt: string, defaultValue: boolean, force: boolean): Promise<boolean>;
|
|
34
|
+
declare function maybeHandleConfigPrompt(skipConfig: boolean, force: boolean): Promise<void>;
|
|
35
|
+
declare function main(): Promise<void>;
|
|
36
|
+
export { parseArgs, detectPackageManager, ensurePackageJson, runNpmPack, parsePackageName, installBinding, installRuntimeDeps, isDependencyInstalled, persistBindingSpec, verifyBinding, promptYesNo, maybeHandleConfigPrompt, main, };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare const SUPPORTED_PACKAGE_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
|
|
2
|
+
type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number];
|
|
3
|
+
type CliOptions = {
|
|
4
|
+
cwd: string;
|
|
5
|
+
dryRun: boolean;
|
|
6
|
+
verbose: boolean;
|
|
7
|
+
force: boolean;
|
|
8
|
+
skipConfig: boolean;
|
|
9
|
+
packageManager?: PackageManager;
|
|
10
|
+
wasmPackage: string;
|
|
11
|
+
};
|
|
12
|
+
declare function parseArgs(argv: string[]): CliOptions;
|
|
13
|
+
declare function detectPackageManager(cwd: string, explicit?: PackageManager): PackageManager;
|
|
14
|
+
declare function ensurePackageJson(cwd: string): void;
|
|
15
|
+
declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean): string;
|
|
16
|
+
declare function parsePackageName(spec: string): {
|
|
17
|
+
name: string;
|
|
18
|
+
version: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
};
|
|
23
|
+
declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean): Promise<{
|
|
24
|
+
targetDir: string;
|
|
25
|
+
tarballPath?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
}>;
|
|
29
|
+
declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean): string[];
|
|
30
|
+
declare function isDependencyInstalled(dep: string, cwd: string): boolean;
|
|
31
|
+
declare function persistBindingSpec(cwd: string, name: string, version: string | undefined, dryRun: boolean, verbose: boolean): void;
|
|
32
|
+
declare function verifyBinding(name: string, cwd: string, verbose: boolean): Promise<string>;
|
|
33
|
+
declare function promptYesNo(prompt: string, defaultValue: boolean, force: boolean): Promise<boolean>;
|
|
34
|
+
declare function maybeHandleConfigPrompt(skipConfig: boolean, force: boolean): Promise<void>;
|
|
35
|
+
declare function main(): Promise<void>;
|
|
36
|
+
export { parseArgs, detectPackageManager, ensurePackageJson, runNpmPack, parsePackageName, installBinding, installRuntimeDeps, isDependencyInstalled, persistBindingSpec, verifyBinding, promptYesNo, maybeHandleConfigPrompt, main, };
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync, execFileSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import readline from 'readline/promises';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import { pathToFileURL } from 'url';
|
|
8
|
+
import { extract } from 'tar';
|
|
9
|
+
|
|
10
|
+
var DEFAULT_BINDING_SPEC = process.env.WASM_BINDING_PACKAGE ?? "@oxc-parser/binding-wasm32-wasi@^0.99.0";
|
|
11
|
+
var RUNTIME_DEPS = ["@napi-rs/wasm-runtime", "@emnapi/runtime", "@emnapi/core"];
|
|
12
|
+
var SUPPORTED_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const options = {
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
dryRun: false,
|
|
17
|
+
verbose: false,
|
|
18
|
+
force: false,
|
|
19
|
+
skipConfig: true,
|
|
20
|
+
wasmPackage: DEFAULT_BINDING_SPEC
|
|
21
|
+
};
|
|
22
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
if (arg === "--dry-run") {
|
|
25
|
+
options.dryRun = true;
|
|
26
|
+
} else if (arg === "--verbose") {
|
|
27
|
+
options.verbose = true;
|
|
28
|
+
} else if (arg === "--force" || arg === "--yes") {
|
|
29
|
+
options.force = true;
|
|
30
|
+
} else if (arg === "--skip-config") {
|
|
31
|
+
options.skipConfig = true;
|
|
32
|
+
} else if (arg === "--config") {
|
|
33
|
+
options.skipConfig = false;
|
|
34
|
+
} else if (arg === "--package-manager" || arg === "--pm") {
|
|
35
|
+
const pm = argv[i + 1];
|
|
36
|
+
if (!pm) throw new Error("Missing value for --package-manager");
|
|
37
|
+
if (!SUPPORTED_PACKAGE_MANAGERS.includes(pm)) {
|
|
38
|
+
throw new Error(`Unsupported package manager: ${pm}`);
|
|
39
|
+
}
|
|
40
|
+
options.packageManager = pm;
|
|
41
|
+
i += 1;
|
|
42
|
+
} else if (arg === "--wasm-package") {
|
|
43
|
+
const pkg = argv[i + 1];
|
|
44
|
+
if (!pkg) throw new Error("Missing value for --wasm-package");
|
|
45
|
+
options.wasmPackage = pkg;
|
|
46
|
+
i += 1;
|
|
47
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return options;
|
|
53
|
+
}
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(
|
|
56
|
+
`
|
|
57
|
+
Usage: npx @knighted/jsx init [options]
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--package-manager, --pm <name> Choose npm | pnpm | yarn | bun
|
|
61
|
+
--wasm-package <spec> Override binding package spec
|
|
62
|
+
--config Prompt to help with loader config
|
|
63
|
+
--skip-config Skip any loader config prompts (default)
|
|
64
|
+
--dry-run Print actions without executing
|
|
65
|
+
--force, --yes Assume yes for prompts
|
|
66
|
+
--verbose Log extra detail
|
|
67
|
+
-h, --help Show this help message
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function log(message) {
|
|
72
|
+
console.log(message);
|
|
73
|
+
}
|
|
74
|
+
function logVerbose(message, verbose) {
|
|
75
|
+
if (verbose) console.log(message);
|
|
76
|
+
}
|
|
77
|
+
function detectPackageManager(cwd, explicit) {
|
|
78
|
+
if (explicit) return explicit;
|
|
79
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
80
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
81
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
82
|
+
if (ua.startsWith("bun")) return "bun";
|
|
83
|
+
const lookups = {
|
|
84
|
+
pnpm: "pnpm-lock.yaml",
|
|
85
|
+
yarn: "yarn.lock",
|
|
86
|
+
bun: "bun.lockb",
|
|
87
|
+
npm: "package-lock.json"
|
|
88
|
+
};
|
|
89
|
+
for (const [pm, lockfile] of Object.entries(lookups)) {
|
|
90
|
+
if (fs.existsSync(path.join(cwd, lockfile))) return pm;
|
|
91
|
+
}
|
|
92
|
+
return "npm";
|
|
93
|
+
}
|
|
94
|
+
function ensurePackageJson(cwd) {
|
|
95
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
96
|
+
if (!fs.existsSync(pkgPath)) {
|
|
97
|
+
throw new Error("No package.json found. Run this inside a project with package.json.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function runNpmPack(spec, cwd, dryRun, verbose) {
|
|
101
|
+
logVerbose(`> npm pack ${spec}`, verbose);
|
|
102
|
+
if (dryRun) return `${spec.replace(/\W+/g, "_")}.tgz`;
|
|
103
|
+
const output = execFileSync("npm", ["pack", spec], {
|
|
104
|
+
cwd,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
stdio: ["ignore", "pipe", "inherit"]
|
|
107
|
+
}).trim();
|
|
108
|
+
const lines = output.split("\n").filter(Boolean);
|
|
109
|
+
return lines[lines.length - 1];
|
|
110
|
+
}
|
|
111
|
+
function parsePackageName(spec) {
|
|
112
|
+
const match = spec.match(/^(@?[^@\s]+\/[^@\s]+|@?[^@\s]+)(?:@(.+))?$/);
|
|
113
|
+
if (!match) return { name: spec, version: void 0 };
|
|
114
|
+
const [, name, version] = match;
|
|
115
|
+
return { name, version };
|
|
116
|
+
}
|
|
117
|
+
async function installBinding(spec, cwd, dryRun, verbose) {
|
|
118
|
+
const { name, version } = parsePackageName(spec);
|
|
119
|
+
const tarballName = runNpmPack(spec, cwd, dryRun, verbose);
|
|
120
|
+
const tarballPath = path.resolve(cwd, tarballName);
|
|
121
|
+
const targetDir = path.resolve(cwd, "node_modules", ...name.split("/"));
|
|
122
|
+
log(`> Installing ${spec} into ${targetDir}`);
|
|
123
|
+
if (!dryRun) {
|
|
124
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
125
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
126
|
+
await extract({ file: tarballPath, cwd: targetDir, strip: 1 });
|
|
127
|
+
fs.rmSync(tarballPath, { force: true });
|
|
128
|
+
}
|
|
129
|
+
return { targetDir, tarballPath: dryRun ? void 0 : tarballPath, name, version };
|
|
130
|
+
}
|
|
131
|
+
function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
|
|
132
|
+
const missing = deps.filter((dep) => !isDependencyInstalled(dep, cwd));
|
|
133
|
+
if (missing.length === 0) {
|
|
134
|
+
log("> Runtime dependencies already present; skipping install");
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const commands = {
|
|
138
|
+
npm: ["npm", ["install", ...missing, "--save"]],
|
|
139
|
+
pnpm: ["pnpm", ["add", ...missing]],
|
|
140
|
+
yarn: ["yarn", ["add", ...missing]],
|
|
141
|
+
bun: ["bun", ["add", ...missing]]
|
|
142
|
+
};
|
|
143
|
+
const [command, args] = commands[pm];
|
|
144
|
+
logVerbose(`> ${command} ${args.join(" ")}`, verbose);
|
|
145
|
+
if (!dryRun) {
|
|
146
|
+
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
|
|
147
|
+
if (result.status !== 0) {
|
|
148
|
+
throw new Error(`Failed to install runtime dependencies with ${pm}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return missing;
|
|
152
|
+
}
|
|
153
|
+
function isDependencyInstalled(dep, cwd) {
|
|
154
|
+
try {
|
|
155
|
+
const requireFromCwd = createRequire(path.join(cwd, "package.json"));
|
|
156
|
+
requireFromCwd.resolve(dep);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function persistBindingSpec(cwd, name, version, dryRun, verbose) {
|
|
163
|
+
if (!version) {
|
|
164
|
+
logVerbose("> Skipping package.json update (no version parsed)", verbose);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
168
|
+
const pkgRaw = fs.readFileSync(pkgPath, "utf8");
|
|
169
|
+
const pkgJson = JSON.parse(pkgRaw);
|
|
170
|
+
pkgJson.optionalDependencies = pkgJson.optionalDependencies ?? {};
|
|
171
|
+
pkgJson.optionalDependencies[name] = version;
|
|
172
|
+
log(`> Recording optionalDependency ${name}@${version}`);
|
|
173
|
+
if (!dryRun) {
|
|
174
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkgJson, null, 2)}
|
|
175
|
+
`, "utf8");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function verifyBinding(name, cwd, verbose) {
|
|
179
|
+
const requireFromCwd = createRequire(path.join(cwd, "package.json"));
|
|
180
|
+
const resolved = requireFromCwd.resolve(name);
|
|
181
|
+
logVerbose(`> Resolved ${name} to ${resolved}`, verbose);
|
|
182
|
+
const imported = await import(pathToFileURL(resolved).href);
|
|
183
|
+
if (!imported) {
|
|
184
|
+
throw new Error(`Imported ${name} is empty; verification failed`);
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
async function promptYesNo(prompt, defaultValue, force) {
|
|
189
|
+
if (!process.stdin.isTTY) return defaultValue;
|
|
190
|
+
if (force) return true;
|
|
191
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
192
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
193
|
+
const answer = await rl.question(`${prompt} ${suffix} `);
|
|
194
|
+
await rl.close();
|
|
195
|
+
if (!answer.trim()) return defaultValue;
|
|
196
|
+
const normalized = answer.trim().toLowerCase();
|
|
197
|
+
return normalized === "y" || normalized === "yes";
|
|
198
|
+
}
|
|
199
|
+
async function maybeHandleConfigPrompt(skipConfig, force) {
|
|
200
|
+
if (skipConfig) {
|
|
201
|
+
log("> Skipping loader config (default). Re-run with --config to opt in.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const wantsHelp = await promptYesNo(
|
|
205
|
+
"Do you want help adding loader configuration now?",
|
|
206
|
+
false,
|
|
207
|
+
force
|
|
208
|
+
);
|
|
209
|
+
if (!wantsHelp) {
|
|
210
|
+
log("> Skipping loader config per your choice.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
log(
|
|
214
|
+
"> Loader assistance is interactive and not applied automatically yet. See docs at docs/cli.md for next steps."
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
async function main() {
|
|
218
|
+
const options = parseArgs(process.argv.slice(2));
|
|
219
|
+
ensurePackageJson(options.cwd);
|
|
220
|
+
const packageManager = detectPackageManager(options.cwd, options.packageManager);
|
|
221
|
+
log(`> Using package manager: ${packageManager}`);
|
|
222
|
+
const installedRuntimeDeps = installRuntimeDeps(
|
|
223
|
+
packageManager,
|
|
224
|
+
RUNTIME_DEPS,
|
|
225
|
+
options.cwd,
|
|
226
|
+
options.dryRun,
|
|
227
|
+
options.verbose
|
|
228
|
+
);
|
|
229
|
+
const binding = await installBinding(
|
|
230
|
+
options.wasmPackage,
|
|
231
|
+
options.cwd,
|
|
232
|
+
options.dryRun,
|
|
233
|
+
options.verbose
|
|
234
|
+
);
|
|
235
|
+
persistBindingSpec(
|
|
236
|
+
options.cwd,
|
|
237
|
+
binding.name,
|
|
238
|
+
binding.version,
|
|
239
|
+
options.dryRun,
|
|
240
|
+
options.verbose
|
|
241
|
+
);
|
|
242
|
+
let resolvedPath;
|
|
243
|
+
if (!options.dryRun) {
|
|
244
|
+
resolvedPath = await verifyBinding(binding.name, options.cwd, options.verbose);
|
|
245
|
+
log(`> Verified ${binding.name} at ${resolvedPath}`);
|
|
246
|
+
}
|
|
247
|
+
await maybeHandleConfigPrompt(options.skipConfig, options.force);
|
|
248
|
+
log("\nDone!");
|
|
249
|
+
log(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ""}`);
|
|
250
|
+
log(`- Target: ${binding.targetDir}`);
|
|
251
|
+
log(
|
|
252
|
+
`- Runtime deps installed: ${installedRuntimeDeps.join(", ") || "none (already present)"}`
|
|
253
|
+
);
|
|
254
|
+
if (resolvedPath) log(`- Verified import: ${resolvedPath}`);
|
|
255
|
+
}
|
|
256
|
+
if (process.env.KNIGHTED_JSX_CLI_TEST !== "1") {
|
|
257
|
+
main().catch((error) => {
|
|
258
|
+
console.error(
|
|
259
|
+
"Failed to set up WASM binding:",
|
|
260
|
+
error instanceof Error ? error.message : error
|
|
261
|
+
);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { detectPackageManager, ensurePackageJson, installBinding, installRuntimeDeps, isDependencyInstalled, main, maybeHandleConfigPrompt, parseArgs, parsePackageName, persistBindingSpec, promptYesNo, runNpmPack, verifyBinding };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/jsx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Runtime JSX tagged template that renders DOM or React trees anywhere without a build step.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsx runtime",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
|
+
"bin": {
|
|
21
|
+
"jsx": "./dist/cli/init.js"
|
|
22
|
+
},
|
|
20
23
|
"exports": {
|
|
21
24
|
".": {
|
|
22
25
|
"types": "./dist/index.d.ts",
|
|
@@ -65,11 +68,11 @@
|
|
|
65
68
|
"./package.json": "./package.json"
|
|
66
69
|
},
|
|
67
70
|
"engines": {
|
|
68
|
-
"node": ">=22.
|
|
71
|
+
"node": ">=22.17.0"
|
|
69
72
|
},
|
|
70
73
|
"engineStrict": true,
|
|
71
74
|
"scripts": {
|
|
72
|
-
"build": "duel && npm run build:lite",
|
|
75
|
+
"build": "duel && npm run build:lite && npm run build:cli",
|
|
73
76
|
"prepare": "husky",
|
|
74
77
|
"precheck-types": "npm run build",
|
|
75
78
|
"check-types": "npm run check-types:lib && npm run check-types:demo",
|
|
@@ -78,8 +81,8 @@
|
|
|
78
81
|
"lint": "eslint src test",
|
|
79
82
|
"prettier": "prettier -w .",
|
|
80
83
|
"prettier:check": "prettier --check .",
|
|
81
|
-
"test": "vitest run --coverage",
|
|
82
|
-
"test:watch": "vitest",
|
|
84
|
+
"test": "KNIGHTED_JSX_CLI_TEST=1 vitest run --coverage",
|
|
85
|
+
"test:watch": "KNIGHTED_JSX_CLI_TEST=1 vitest",
|
|
83
86
|
"test:e2e": "npm run build && npm run setup:wasm && npm run build:fixture && playwright test",
|
|
84
87
|
"build:fixture": "node scripts/build-rspack-fixture.mjs",
|
|
85
88
|
"demo:node-ssr": "node test/fixtures/node-ssr/render.mjs",
|
|
@@ -87,6 +90,7 @@
|
|
|
87
90
|
"build:demo": "vite build --config vite.config.ts",
|
|
88
91
|
"preview": "vite preview --config vite.config.ts",
|
|
89
92
|
"build:lite": "tsup --config tsup.config.ts",
|
|
93
|
+
"build:cli": "tsup --config tsup.cli.config.ts",
|
|
90
94
|
"setup:wasm": "node scripts/setup-wasm.mjs",
|
|
91
95
|
"prepack": "npm run build"
|
|
92
96
|
},
|
|
@@ -113,7 +117,6 @@
|
|
|
113
117
|
"prettier": "^3.7.3",
|
|
114
118
|
"react": "^19.0.0",
|
|
115
119
|
"react-dom": "^19.0.0",
|
|
116
|
-
"tar": "^7.4.3",
|
|
117
120
|
"tsup": "^8.5.1",
|
|
118
121
|
"typescript": "^5.9.3",
|
|
119
122
|
"typescript-eslint": "^8.48.0",
|
|
@@ -122,7 +125,8 @@
|
|
|
122
125
|
},
|
|
123
126
|
"dependencies": {
|
|
124
127
|
"magic-string": "^0.30.21",
|
|
125
|
-
"oxc-parser": "^0.99.0"
|
|
128
|
+
"oxc-parser": "^0.99.0",
|
|
129
|
+
"tar": "^7.4.3"
|
|
126
130
|
},
|
|
127
131
|
"peerDependencies": {
|
|
128
132
|
"jsdom": "*",
|