Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3fd1476
format, ruff, lint
amol- Feb 6, 2026
8358cee
start migration to quickjs
amol- Feb 6, 2026
3d40efe
Migrate toward pyproject.toml
amol- Feb 6, 2026
94a10c9
Moving toward quickjs
amol- Feb 6, 2026
f35c703
Implement module loading
amol- Feb 6, 2026
9b8961f
Remove duktape
amol- Feb 6, 2026
85f8f27
Remove compatibility code
amol- Feb 6, 2026
8d87d8a
Lint and format
amol- Feb 6, 2026
66e8dd2
fix handling of null and undefined return values
amol- Feb 7, 2026
5d19ac5
fix double free
amol- Feb 7, 2026
d25a0a0
Format script too
amol- Feb 7, 2026
28c4bb7
enable/disable module mode on an explicit flag
amol- Feb 7, 2026
8421896
Improve exception reporting and map null and undefined to None
amol- Feb 7, 2026
82be5f8
Add logo
amol- Feb 7, 2026
4240c62
Tweak pytest invocation, for windows
amol- Feb 7, 2026
fcd2cf1
Fix build on Windows
amol- Feb 7, 2026
cee6a7f
quickjs migration phase 2
amol- May 8, 2026
0201599
Prevent nested calls of evaljs to avoid leaking promise queues on return
amol- May 10, 2026
82c40a2
refactoring before publish
amol- May 10, 2026
1d1911b
aggregate runtime files
amol- May 11, 2026
517edaf
lint and format
amol- May 13, 2026
a0be781
refactoring commonjs support
amol- May 24, 2026
7cfb24e
format
amol- May 25, 2026
34ca9b1
Fix tests for Windows
amol- May 25, 2026
663ca7c
remove fake windows code
amol- Jun 13, 2026
3183c9c
consolidate code
amol- Jun 14, 2026
bc991e5
format
amol- Jun 14, 2026
e02fa33
put back compatibility layer tests
amol- Jun 14, 2026
81e5568
track what changed
amol- Jun 14, 2026
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
9 changes: 8 additions & 1 deletion .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install build
run: python -m pip install build

- name: Build sdist
run: python setup.py sdist
run: python -m build --sdist --outdir dist

- uses: actions/upload-artifact@v4
with:
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: lint
on:
pull_request:
workflow_dispatch:
push:
branches:
- master

permissions:
contents: read

jobs:
pre-commit:
name: pre-commit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install pre-commit
run: python -m pip install --upgrade pre-commit
- name: Run pre-commit
run: pre-commit run --all-files --show-diff-on-failure
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use native integration, not local and you won't need to install ruff externally + run it in https://pre-commit.ci. Pro tip: with a double run, you can normalize trailing commas nicely — https://github.com/cherrypy/cheroot/blob/662cd9dcf426253536f921c618b480e65a429cb1/.pre-commit-config.yaml#L62-L87

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ci:
autofix_commit_msg: "style: apply pre-commit fixes"
autoupdate_schedule: monthly

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
123 changes: 123 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Changes in 0.6.0

## New features and capabilities

- JavaScript engine migrated from Duktape to QuickJS-NG v0.11.0.
- Better modern JavaScript syntax support.
- Native Promise/job-queue support.

- New public file runner API:
- `dukpy.run(path, **kwargs)`
- `JSInterpreter.run(path, **kwargs)`
- The `dukpy` CLI now uses this path-based runner.

- Native ES module support for file entrypoints.
- `.mjs` runs as native ESM.
- `.cjs` runs as CommonJS.
- `.js` follows nearest `package.json` `"type": "module"` / `"commonjs"`.
- Package-less ambiguous `.js` files are probed instead of source-scanned.

- ESM features now supported through `run()`:
- static `import`
- `export`
- `import.meta.url`
- `import.meta.main`
- top-level `await`

- CommonJS runtime rewritten for QuickJS.
- Supports `require`, `module`, `exports`.
- Supports `__filename` and `__dirname`.
- Has a module cache shared between global `require()` and ESM/CommonJS interop.
- Failed CommonJS modules are removed from cache so they can be retried.

- ESM importing CommonJS is supported.
- Default import maps to `module.exports`.
- Namespace exposes: `default`, `module`, `exports`, `require`.
- No named-export inference from CommonJS source.

- Node-like compatibility shims still shipped and tested:
- `fs`
- `path`
- `url`
- `querystring`
- `punycode`

- Promise/microtask behavior is now handled.
- Promise microtasks are drained before result serialization.
- Promise failures during evaluation/serialization propagate as `JSRuntimeError`.

- Improved Python callback bridge.
- Preserves argument order and JSON types.
- Supports Unicode function names and Unicode/emoji values.
- Python `None` callback returns become JavaScript `undefined`.
- Missing Python callbacks become catchable JS `ReferenceError`.
- Python exceptions become catchable JS `InternalError`.

- Result conversion now follows `JSON.stringify` more closely.
- `null`, `undefined`, `NaN`, `Infinity`, `-Infinity` map to Python `None`.
- JSON conversion failures like circular references and BigInt produce runtime errors.

- Improved runtime safety.
- Stack exhaustion and oversized allocations are reported as runtime errors.
- Python signal exceptions propagate.
- Blocking `Atomics.wait` is disabled.

- Installer hardening.
- npm registry access now uses HTTPS.
- Tarball URLs must be HTTPS.
- Rejects unsafe tar paths, path traversal, multiple archive roots, unsupported tar entries, and symlink destination escapes.
- Better errors for missing metadata, missing versions, missing tarball URLs.

- Packaging modernization.
- Moved project metadata to `pyproject.toml`.
- Declares `requires-python = ">=3.9"`.
- Adds Ruff/pre-commit lint setup.
- Builds sdist with `python -m build`.

## No longer available and behavior changes

- Duktape-specific behavior is gone.
- No Duktape engine.
- Code relying on the JS global `Duktape` or `Duktape.modSearch` will break.
- Module loading is now DukPy’s QuickJS/CommonJS shim instead of Duktape’s module system.

- Python versions below 3.9 are no longer supported.
- 0.6.0 declares Python `>=3.9`.
- Old Python 2 compatibility code is gone.

- `dukpy.run` changed shape.
- Old `dukpy/run.py` module was removed.
- New public API is `dukpy.run(...)` function.
- Code like `from dukpy.run import main` will break.
- The console command `dukpy` still exists, but now points to `dukpy.cli:main`.

- `evaljs()` remains script-only.
- It does not auto-detect ESM syntax.
- Static `import` / `export` should be run via `dukpy.run()` file entrypoints, not raw `evaljs()` source text.

- CommonJS module IDs may differ.
- New loader uses canonical file-like module IDs with extensions and forward slashes.
- Code depending on old Duktape/loader `module.id` or `require.id` exact strings may see changed values.

- `require()` no longer runs ES modules as CommonJS.
- `require('x.mjs')` errors.
- `require()` of `.js` files classified as ESM errors.
- This is intentional; use ESM `import` / `dukpy.run()` for modules.

- CommonJS named exports are not inferred.
- `import { name } from './commonjs.js'` is not supported unless the synthetic namespace has that name.
- Use default import for CommonJS exports.

- JavaScript error messages/stacks changed.
- Errors now use QuickJS wording and stack formatting, not Duktape wording.
- Tests expecting exact old Duktape messages will need updates.

- Result serialization changed for some edge values.
- Top-level functions/Symbols now raise `Invalid Result Value`.
- `undefined`, `NaN`, and infinities now map to `None`.
- Some values that previously looked like `{}` or Duktape-specific output may differ.

- Installer is stricter.
- HTTP registry/tarball URLs are rejected.
- Tarballs with symlinks, path traversal, multiple roots, or unsupported entries are rejected.
- Installing through symlinked destinations is rejected.
67 changes: 67 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Contributing

## Guidelines

### QuickJS owns JavaScript parsing

DukPy wraps QuickJS to provide JavaScript support in Python. It must stay as close
as possible to Node.js semantics for the JavaScript surface, within what QuickJS
itself allows.

Do **not** implement JavaScript parsing, lexing, or semantic detection in DukPy
code. In particular:

- Do not inspect JavaScript source character-by-character to infer syntax.
- Do not classify JavaScript as modules/scripts/CommonJS by scanning strings.
- Do not detect `import`, `export`, `await`, comments, strings, templates, or
identifiers with handwritten C or Python logic.
- Do not rewrite JavaScript source based on DukPy's own understanding of the
language grammar.

QuickJS is the JavaScript parser and evaluator. If we need to know whether
JavaScript is valid, whether it is a module, or how syntax should behave, route
that decision through QuickJS or through explicit user/API intent. DukPy may adapt
host integration around QuickJS, but it must not become a partial JavaScript
interpreter.

Compatibility shims, module loading, and CommonJS support must be designed around
clear boundaries: explicit modes, QuickJS parsing/evaluation, and runtime-level
JavaScript behavior. Any change that appears to require parsing JavaScript text in
DukPy should be treated as a design problem and discussed before implementation.

### Acceptance-test driven development

Major features and capabilities should be driven by acceptance tests.

Use `tests/acceptance/` for these tests. Each major feature gets its own
subdirectory containing:

- a dedicated JavaScript test case that demonstrates the expected user-facing
behavior;
- a Python test that loads and runs that JavaScript case through DukPy.

Prefer small, concrete JavaScript programs over prose specifications. The
JavaScript case should read like the behavior a user expects, while the Python
wrapper should stay thin and focused on running the case and asserting the
result.

Task tracking for architectural work should use probe-driven development: keep
small, explicit evolutions close to the code under change, validate each
capability with an acceptance case, and avoid separate BDD feature tracking.

### Code design style

Keep code simple, production-ready, and easy for a human to review.

- Prefer small, isolated changes with no effects at a distance.
- Prefer well-encapsulated deep modules over scattered behavior.
- Keep one capability understandable through one clear boundary whenever possible.
- Avoid unnecessary indirection, tiny single-use helpers, and temporary variables
that are used once.
- Keep functions concise when that improves clarity, but do not split code just
to satisfy style rules.
- Comments should explain what and why; code should explain how.
- Implement real behavior, not shortcuts that only satisfy current tests.
- Tests should validate meaningful user-facing behavior, not incidental details.
- Prefer standard-library solutions and existing project patterns before adding
abstractions or dependencies.
6 changes: 5 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
recursive-include src *.h
include src/*.c src/*.h
recursive-include src/quickjs *.c *.h VERSION VENDORING.json
recursive-include dukpy/jsruntime *.js
recursive-include dukpy/jscore *.js
recursive-include dukpy/jsmodules *.js
include LICENSE
include scripts/update_quickjs_vendor.py
global-exclude *.so *.pyd *.dll *.dylib
40 changes: 38 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ dukpy
.. image:: https://img.shields.io/pypi/v/dukpy.svg
:target: https://pypi.org/p/dukpy

.. raw:: html

<img align="left" width="100px" src="dukpy_logo.png" alt="DukPy logo">


DukPy is a simple JavaScript interpreter for Python **without any external
runtime dependency**.

The name comes from DukPy's original Duktape-based implementation and is kept
for package compatibility.

DukPy is a simple javascript interpreter for Python built on top of
duktape engine **without any external dependency**.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to leave a historic reference explaining the project name.

It comes with a bunch of common transpilers built-in for convenience:

- *CoffeeScript*
Expand Down Expand Up @@ -200,6 +208,34 @@ resulting value as far as it is possible to encode it in JSON.
If execution fails a ``dukpy.JSRuntimeError`` exception is raised
with the failure reason.

Running JavaScript files
~~~~~~~~~~~~~~~~~~~~~~~~

Use ``dukpy.run`` or, when reusing an interpreter, ``JSInterpreter.run`` to run a
JavaScript file entrypoint:

.. code:: python

>>> import dukpy
>>> dukpy.run("./main.mjs")
{}

``evaljs`` always evaluates source text as a script. ``run`` reads a file and
uses Node-like entrypoint classification: ``.mjs`` files are native ES modules,
``.cjs`` files are CommonJS, and ``.js`` files follow the nearest
``package.json`` ``type`` of ``module`` or ``commonjs``. Ambiguous ``.js``
entrypoints and dependencies are probed by QuickJS by compiling the CommonJS
wrapper first and falling back to native ES module compilation when that fails;
DukPy does not scan source text for ``import``, ``export``, ``await``, comments,
strings, or identifiers. Entrypoints use the same canonical module id as the
loader; for files under a registered loader path, ``import.meta.url`` and
CommonJS ``module.id`` may be relative to that path.

When ES modules import files classified as CommonJS, DukPy exposes a minimal
synthetic namespace: ``default`` is ``module.exports``, and the only named
exports are ``module``, ``exports``, and ``require``. DukPy does not infer named
exports from CommonJS source; use a default import for CommonJS API objects.

Passing Arguments
~~~~~~~~~~~~~~~~~

Expand Down
17 changes: 15 additions & 2 deletions dukpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
from .evaljs import evaljs, JSInterpreter
from .evaljs import evaljs, run, JSInterpreter
from ._dukpy import JSRuntimeError
from .install import install_jspackage

from .coffee import coffee_compile
from .babel import babel_compile, jsx_compile
from .tsc import typescript_compile
from .lessc import less_compile
from .lessc import less_compile

__all__ = [
"JSInterpreter",
"JSRuntimeError",
"babel_compile",
"coffee_compile",
"evaljs",
"run",
"install_jspackage",
"jsx_compile",
"less_compile",
"typescript_compile",
]
26 changes: 15 additions & 11 deletions dukpy/babel.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import os
from .evaljs import evaljs

BABEL_COMPILER = os.path.join(os.path.dirname(__file__), 'jsmodules', 'babel-6.26.0.min.js')
BABEL_COMPILER = os.path.join(
os.path.dirname(__file__), "jsmodules", "babel-6.26.0.min.js"
)


def babel_compile(source, **kwargs):
"""Compiles the given ``source`` from ES6 to ES5 using Babeljs"""
presets = kwargs.get('presets')
presets = kwargs.get("presets")
if not presets:
kwargs['presets'] = ["es2015"]
with open(BABEL_COMPILER, 'rb') as babel_js:
kwargs["presets"] = ["es2015"]
with open(BABEL_COMPILER, "rb") as babel_js:
return evaljs(
(babel_js.read().decode('utf-8'),
'var bres, res;'
'bres = Babel.transform(dukpy.es6code, dukpy.babel_options);',
'res = {map: bres.map, code: bres.code};'),
(
babel_js.read().decode("utf-8"),
"var bres, res;"
"bres = Babel.transform(dukpy.es6code, dukpy.babel_options);",
"res = {map: bres.map, code: bres.code};",
),
es6code=source,
babel_options=kwargs
babel_options=kwargs,
)


def jsx_compile(source, **kwargs):
kwargs['presets'] = ['es2015', 'react']
return babel_compile(source, **kwargs)['code']
kwargs["presets"] = ["es2015", "react"]
return babel_compile(source, **kwargs)["code"]
13 changes: 13 additions & 0 deletions dukpy/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import argparse
import sys

import dukpy


def main():
parser = argparse.ArgumentParser(description="Run a javascript script")
parser.add_argument("filename", help="path of the script to run")
args = parser.parse_args(sys.argv[1:])

dukpy.run(args.filename)
Loading
Loading