-
Notifications
You must be signed in to change notification settings - Fork 42
migrate to QuickJS and prepare for 0.6.0 #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
amol-
wants to merge
29
commits into
master
Choose a base branch
from
0.6.0
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
3fd1476
format, ruff, lint
amol- 8358cee
start migration to quickjs
amol- 3d40efe
Migrate toward pyproject.toml
amol- 94a10c9
Moving toward quickjs
amol- f35c703
Implement module loading
amol- 9b8961f
Remove duktape
amol- 85f8f27
Remove compatibility code
amol- 8d87d8a
Lint and format
amol- 66e8dd2
fix handling of null and undefined return values
amol- 5d19ac5
fix double free
amol- d25a0a0
Format script too
amol- 28c4bb7
enable/disable module mode on an explicit flag
amol- 8421896
Improve exception reporting and map null and undefined to None
amol- 82be5f8
Add logo
amol- 4240c62
Tweak pytest invocation, for windows
amol- fcd2cf1
Fix build on Windows
amol- cee6a7f
quickjs migration phase 2
amol- 0201599
Prevent nested calls of evaljs to avoid leaking promise queues on return
amol- 82c40a2
refactoring before publish
amol- 1d1911b
aggregate runtime files
amol- 517edaf
lint and format
amol- a0be781
refactoring commonjs support
amol- 7cfb24e
format
amol- 34ca9b1
Fix tests for Windows
amol- 663ca7c
remove fake windows code
amol- 3183c9c
consolidate code
amol- bc991e5
format
amol- e02fa33
put back compatibility layer tests
amol- 81e5568
track what changed
amol- File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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**. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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* | ||
|
|
@@ -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 | ||
| ~~~~~~~~~~~~~~~~~ | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
ruffexternally + 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