feat: migrate to ESM

This commit is contained in:
Amaan Qureshi 2025-09-15 19:06:51 -04:00 committed by Amaan Qureshi
parent 67f50b85f5
commit 39a67eec61
59 changed files with 132 additions and 80 deletions

View file

@ -297,7 +297,7 @@ pub fn generate_grammar_files(
)
},
|path| {
let contents = fs::read_to_string(path)?
let mut contents = fs::read_to_string(path)?
.replace(
r#""node-addon-api": "^8.3.1"#,
r#""node-addon-api": "^8.5.0""#,
@ -311,6 +311,16 @@ pub fn generate_grammar_files(
"tree-sitter": "^0.22.4",
"tree-sitter-cli":"#},
);
if !contents.contains("module") {
eprintln!("Updating package.json");
contents = contents.replace(
indoc! {r#"
"repository": {"#},
indoc! {r#"
"type": "module",
"repository": {"#},
);
}
write_file(path, contents)?;
Ok(())
},
@ -318,9 +328,20 @@ pub fn generate_grammar_files(
// Do not create a grammar.js file in a repo with multiple language configs
if !tree_sitter_config.has_multiple_language_configs() {
missing_path(repo_path.join("grammar.js"), |path| {
generate_file(path, GRAMMAR_JS_TEMPLATE, language_name, &generate_opts)
})?;
missing_path_else(
repo_path.join("grammar.js"),
allow_update,
|path| generate_file(path, GRAMMAR_JS_TEMPLATE, language_name, &generate_opts),
|path| {
let mut contents = fs::read_to_string(path)?;
if contents.contains("module.exports") {
contents = contents.replace("module.exports =", "export default");
write_file(path, contents)?;
}
Ok(())
},
)?;
}
// Write .gitignore file
@ -410,7 +431,7 @@ pub fn generate_grammar_files(
|path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
|path| {
let contents = fs::read_to_string(path)?;
if !contents.contains("bun") {
if !contents.contains("new URL") {
eprintln!("Replacing index.js");
generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
}
@ -422,14 +443,31 @@ pub fn generate_grammar_files(
generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)
})?;
missing_path(path.join("binding_test.js"), |path| {
generate_file(
path,
BINDING_TEST_JS_TEMPLATE,
language_name,
&generate_opts,
)
})?;
missing_path_else(
path.join("binding_test.js"),
allow_update,
|path| {
generate_file(
path,
BINDING_TEST_JS_TEMPLATE,
language_name,
&generate_opts,
)
},
|path| {
let contents = fs::read_to_string(path)?;
if !contents.contains("import") {
eprintln!("Replacing binding_test.js");
generate_file(
path,
BINDING_TEST_JS_TEMPLATE,
language_name,
&generate_opts,
)?;
}
Ok(())
},
)?;
missing_path(path.join("binding.cc"), |path| {
generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)

View file

@ -1,9 +1,9 @@
const assert = require("node:assert");
const { test } = require("node:test");
const Parser = require("tree-sitter");
import assert from "node:assert";
import { test } from "node:test";
import Parser from "tree-sitter";
import language from "./index.js";
test("can load grammar", () => {
const parser = new Parser();
assert.doesNotThrow(() => parser.setLanguage(require(".")));
assert.doesNotThrow(() => parser.setLanguage(language));
});

View file

@ -7,7 +7,7 @@
/// <reference types="tree-sitter-cli/dsl" />
// @ts-check
module.exports = grammar({
export default grammar({
name: "LOWER_PARSER_NAME",
rules: {

View file

@ -1,11 +1,15 @@
const root = require("path").join(__dirname, "..", "..");
const root = new URL("../..", import.meta.url).pathname;
module.exports =
typeof process.versions.bun === "string"
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
? require(`../../prebuilds/${process.platform}-${process.arch}/tree-sitter-KEBAB_PARSER_NAME.node`)
: require("node-gyp-build")(root);
const binding = typeof process.versions.bun === "string"
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
? await import(`../../prebuilds/${process.platform}-${process.arch}/tree-sitter-KEBAB_PARSER_NAME.node`)
: await import("node-gyp-build");
const result = binding.default ? binding.default(root) : binding(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
const nodeTypeInfo = await import("../../src/node-types.json", {assert: {type: "json"}});
result.nodeTypeInfo = nodeTypeInfo.default;
} catch (_) {}
export default result;

View file

@ -2,6 +2,7 @@
"name": "tree-sitter-PARSER_NAME",
"version": "PARSER_VERSION",
"description": "PARSER_DESCRIPTION",
"type": "module",
"repository": {
"type": "git",
"url": "git+PARSER_URL.git"

View file

@ -428,6 +428,15 @@ pub fn load_grammar_file(
fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResult<String> {
let grammar_path = fs::canonicalize(grammar_path)?;
let grammar_uses_commonjs = fs::read_to_string(&grammar_path)?.contains("module.exports");
if grammar_uses_commonjs {
eprintln!("Warning: Your grammar.js uses CommonJS.");
eprintln!("Consider migrating to ES modules (export default) for better compatibility.");
eprintln!(
"See: https://tree-sitter.github.io/tree-sitter/creating-parsers/#the-grammar-file"
);
}
#[cfg(windows)]
let grammar_path = url::Url::from_file_path(grammar_path)
.expect("Failed to convert path to URL")

View file

@ -64,7 +64,7 @@ There should be a file called `grammar.js` with the following contents:
/// <reference types="tree-sitter-cli/dsl" />
// @ts-check
module.exports = grammar({
export default grammar({
name: 'LOWER_PARSER_NAME',
rules: {

View file

@ -313,7 +313,7 @@ A construct like `[x, y]` could be legitimately parsed as both an array literal
pattern (like in `let [x, y] = arr`).
```js
module.exports = grammar({
export default grammar({
name: "javascript",
rules: {

View file

@ -2,7 +2,7 @@
// shows that you can alias a rule that would otherwise be anonymous, and it will then appear as a
// named node.
module.exports = grammar({
export default grammar({
name: 'aliased_inlined_rules',
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'aliased_rules',
extras: $ => [

View file

@ -1,7 +1,7 @@
// This grammar shows that `ALIAS` rules can be applied directly to `TOKEN` and `IMMEDIATE_TOKEN`
// rules.
module.exports = grammar({
export default grammar({
name: 'aliased_token_rules',
extras: $ => [/\s/],

View file

@ -5,7 +5,7 @@
// their parent rule. In that situation, eliminating the invisible node could cause the alias to be
// incorrectly applied to its child.
module.exports = grammar({
export default grammar({
name: 'aliased_unit_reductions',
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'aliases_in_root',
extras: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'anonymous_error',
rules: {
document: $ => repeat(choice('ok', 'ERROR')),

View file

@ -4,7 +4,7 @@
// characters. This grammar tests that this escaping works. The test is basically that the generated
// parser compiles successfully.
module.exports = grammar({
export default grammar({
name: "anonymous_tokens_with_escaped_chars",
rules: {
first_rule: $ => choice(

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'associativity_left',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'associativity_missing',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'associativity_right',
rules: {

View file

@ -2,7 +2,7 @@
// parser generator in order to implement repetition. There is no way of referring to these rules in
// the grammar DSL, so these conflicts must be resolved by referring to their parent rules.
module.exports = grammar({
export default grammar({
name: 'conflict_in_repeat_rule',
rules: {

View file

@ -2,7 +2,7 @@
// after an external token is consumed. This tests that the logic for determining the repeat rule's
// "parent" rule works in the presence of external tokens.
module.exports = grammar({
export default grammar({
name: 'conflict_in_repeat_rule_after_external_token',
externals: $ => [
@ -29,4 +29,4 @@ module.exports = grammar({
identifier: $ => /[a-z]+/
}
});
});

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'conflicting_precedence',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "depends_on_column",
rules: {
x_is_at: ($) => seq(/[ \r\n]*/, choice($.odd_column, $.even_column), "x"),

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'dynamic_precedence',
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'epsilon_external_extra_tokens',
extras: $ => [/\s/, $.comment],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'epsilon_external_tokens',
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'epsilon_rules',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'external_and_internal_anonymous_tokens',
externals: $ => [

View file

@ -2,7 +2,7 @@
// validity of an *internal* token. This is done by including the names of that internal token
// (`line_break`) in the grammar's `externals` field.
module.exports = grammar({
export default grammar({
name: 'external_and_internal_tokens',
externals: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "external_extra_tokens",
externals: $ => [

View file

@ -2,7 +2,7 @@
// that track the nesting depth of parentheses, similar to Ruby's percent
// string literals.
module.exports = grammar({
export default grammar({
name: "external_tokens",
externals: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "external_unicode_column_alignment",
externals: $ => [

View file

@ -1,6 +1,6 @@
// This grammar has an "extra" rule, `comment`, that is a non-terminal.
module.exports = grammar({
export default grammar({
name: "extra_non_terminals",
extras: $ => [

View file

@ -1,7 +1,7 @@
// This grammar has a non-terminal extra rule `macro_statement` that contains
// child rules that are also used elsewhere in the grammar.
module.exports = grammar({
export default grammar({
name: "extra_non_terminals_with_shared_rules",
extras: $ => [/\s+/, $.macro_statement],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "get_col_eof",
externals: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'get_col_should_hang_not_crash',
externals: $ => [

View file

@ -3,7 +3,7 @@
// When there are *no* leading `extras`, an immediate token is preferred over a normal token which
// would otherwise match.
module.exports = grammar({
export default grammar({
name: "immediate_tokens",
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'indirect_recursive_in_single_symbol_transitions',
rules: {
source_file: $ => repeat($._statement),

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "inline_rules",
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "inlined_aliased_rules",
extras: $ => [/\s/],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "inverted_external_token",
externals: $ => [$.line_break],

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "invisible_start_rule",
rules: {
_value: $ => choice($.a, $.b),

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'lexical_conflicts_due_to_state_merging',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'named_precedences',
conflicts: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'named_rule_aliased_as_anonymous',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'nested_inlined_rules',
inline: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: "next_sibling_from_zwt",
extras: $ => [
/\s|\\\r?\n/,

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'partially_resolved_conflict',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'precedence_on_single_child_missing',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'precedence_on_single_child_negative',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'precedence_on_single_child_positive',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'precedence_on_subsequence',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'precedence_on_token',
extras: $ => [

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'readme_grammar',
// Things that can appear anywhere in the language, like comments

View file

@ -1,7 +1,7 @@
const RESERVED_NAMES = ["if", "while", "var"];
const RESERVED_PROPERTY_NAMES = ["var"];
module.exports = grammar({
export default grammar({
name: "reserved_words",
reserved: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'start_rule_is_blank',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'start_rule_is_token',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'unicode_classes',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'unused_rules',
rules: {

View file

@ -1,4 +1,4 @@
module.exports = grammar({
export default grammar({
name: 'uses_current_column',
externals: $ => [