Skip to content

Commit 79fa5d5

Browse files
committed
Initial functionality
1 parent 125dee2 commit 79fa5d5

File tree

17 files changed

+2463
-10
lines changed

17 files changed

+2463
-10
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,8 @@ hatch_js/labextension
151151

152152
# Rust
153153
target
154+
155+
# Test parts
156+
hatch_js/tests/test_project_basic/js/dist
157+
hatch_js/tests/test_project_basic/js/node_modules
158+
hatch_js/tests/test_project_basic/project/extension

hatch_js/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
__version__ = "0.1.0"
2+
3+
from .hooks import hatch_register_build_hook
4+
from .plugin import HatchJsBuildHook
5+
from .structs import *

hatch_js/hooks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from typing import Type
2+
3+
from hatchling.plugin import hookimpl
4+
5+
from .plugin import HatchJsBuildHook
6+
7+
8+
@hookimpl
9+
def hatch_register_build_hook() -> Type[HatchJsBuildHook]:
10+
return HatchJsBuildHook

hatch_js/plugin.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
from logging import getLogger
4+
from os import getenv
5+
from typing import Any
6+
7+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
8+
9+
from .structs import HatchJsBuildConfig, HatchJsBuildPlan
10+
from .utils import import_string
11+
12+
__all__ = ("HatchJsBuildHook",)
13+
14+
15+
class HatchJsBuildHook(BuildHookInterface[HatchJsBuildConfig]):
16+
"""The hatch-js build hook."""
17+
18+
PLUGIN_NAME = "hatch-js"
19+
_logger = getLogger(__name__)
20+
21+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
22+
"""Initialize the plugin."""
23+
# Log some basic information
24+
project_name = self.metadata.config["project"]["name"]
25+
self._logger.info("Initializing hatch-js plugin version %s", version)
26+
self._logger.info(f"Running hatch-js: {project_name}")
27+
28+
# Only run if creating wheel
29+
# TODO: Add support for specify sdist-plan
30+
if self.target_name != "wheel":
31+
self._logger.info("ignoring target name %s", self.target_name)
32+
return
33+
34+
# Skip if SKIP_HATCH_JS is set
35+
# TODO: Support CLI once https://github.com/pypa/hatch/pull/1743
36+
if getenv("SKIP_HATCH_JS"):
37+
self._logger.info("Skipping the build hook since SKIP_HATCH_JS was set")
38+
return
39+
40+
# Get build config class or use default
41+
build_config_class = import_string(self.config["build-config-class"]) if "build-config-class" in self.config else HatchJsBuildConfig
42+
43+
# Instantiate build config
44+
config = build_config_class(name=project_name, **self.config)
45+
46+
# Get build plan class or use default
47+
build_plan_class = import_string(self.config["build-plan-class"]) if "build-plan-class" in self.config else HatchJsBuildPlan
48+
49+
# Instantiate builder
50+
build_plan = build_plan_class(**config.model_dump())
51+
52+
# Generate commands
53+
build_plan.generate()
54+
55+
# Log commands if in verbose mode
56+
if config.verbose:
57+
for command in build_plan.commands:
58+
self._logger.warning(command)
59+
60+
# Execute build plan
61+
build_plan.execute()
62+
63+
# Perform any cleanup actions
64+
build_plan.cleanup()
65+
66+
# if build_plan.libraries:
67+
# # force include libraries
68+
# for library in build_plan.libraries:
69+
# name = library.get_qualified_name(build_plan.platform.platform)
70+
# build_data["force_include"][name] = name
71+
72+
# build_data["pure_python"] = False
73+
# machine = platform_machine()
74+
# version_major = version_info.major
75+
# version_minor = version_info.minor
76+
# if "darwin" in sys_platform:
77+
# os_name = "macosx_11_0"
78+
# elif "linux" in sys_platform:
79+
# os_name = "linux"
80+
# else:
81+
# os_name = "win"
82+
# if all([lib.py_limited_api for lib in build_plan.libraries]):
83+
# build_data["tag"] = f"cp{version_major}{version_minor}-abi3-{os_name}_{machine}"
84+
# else:
85+
# build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"
86+
# else:
87+
# build_data["pure_python"] = False
88+
# machine = platform_machine()
89+
# version_major = version_info.major
90+
# version_minor = version_info.minor
91+
# # TODO abi3
92+
# if "darwin" in sys_platform:
93+
# os_name = "macosx_11_0"
94+
# elif "linux" in sys_platform:
95+
# os_name = "linux"
96+
# else:
97+
# os_name = "win"
98+
# build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"
99+
100+
# # force include libraries
101+
# for path in Path(".").rglob("*"):
102+
# if path.is_dir():
103+
# continue
104+
# if str(path).startswith(str(build_plan.cmake.build)) or str(path).startswith("dist"):
105+
# continue
106+
# if path.suffix in (".pyd", ".dll", ".so", ".dylib"):
107+
# build_data["force_include"][str(path)] = str(path)
108+
109+
# for path in build_data["force_include"]:
110+
# self._logger.warning(f"Force include: {path}")

hatch_js/structs.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
from os import chdir, curdir, system as system_call
4+
from pathlib import Path
5+
from shutil import which
6+
from typing import List, Literal, Optional
7+
8+
from pydantic import BaseModel, Field, field_validator
9+
10+
__all__ = (
11+
"HatchJsBuildConfig",
12+
"HatchJsBuildPlan",
13+
)
14+
15+
Toolchain = Literal["npm", "yarn", "pnpm", "jlpm"]
16+
17+
18+
class HatchJsBuildConfig(BaseModel):
19+
"""Build config values for Hatch Js Builder."""
20+
21+
name: Optional[str] = Field(default=None)
22+
verbose: Optional[bool] = Field(default=False)
23+
24+
path: Optional[Path] = Field(default=None, description="Path to the JavaScript project. Defaults to the current directory.")
25+
tool: Optional[Toolchain] = Field(default="npm", description="Command to run for building the project, e.g., 'npm', 'yarn', 'pnpm'")
26+
27+
install_cmd: Optional[str] = Field(
28+
default=None, description="Custom command to run for installing dependencies. If specified, overrides the default install command."
29+
)
30+
build_cmd: Optional[str] = Field(
31+
default="build", description="Custom command to run for building the project. If specified, overrides the default build command."
32+
)
33+
34+
targets: Optional[List[str]] = Field(default_factory=list, description="List of ensured targets to build")
35+
36+
# Check that tool exists
37+
@field_validator("tool", mode="before")
38+
@classmethod
39+
def _check_tool_exists(cls, tool: Toolchain) -> Toolchain:
40+
if not which(tool):
41+
raise ValueError(f"Tool '{tool}' not found in PATH. Please install it or specify a different tool.")
42+
return tool
43+
44+
# Validate path
45+
@field_validator("path", mode="before")
46+
@classmethod
47+
def validate_path(cls, path: Optional[Path]) -> Path:
48+
if path is None:
49+
return Path.cwd()
50+
if not isinstance(path, Path):
51+
path = Path(path)
52+
if not path.is_dir():
53+
raise ValueError(f"Path '{path}' is not a valid directory.")
54+
return path
55+
56+
57+
class HatchJsBuildPlan(HatchJsBuildConfig):
58+
commands: List[str] = Field(default_factory=list)
59+
60+
def generate(self):
61+
self.commands = []
62+
63+
# Run installation
64+
if self.tool in ("npm", "pnpm"):
65+
if self.install_cmd:
66+
self.commands.append(f"{self.tool} {self.install_cmd}")
67+
else:
68+
self.commands.append(f"{self.tool} install")
69+
elif self.tool in ("yarn", "jlpm"):
70+
if self.install_cmd:
71+
self.commands.append(f"{self.tool} {self.install_cmd}")
72+
else:
73+
self.commands.append(f"{self.tool}")
74+
75+
# Run build command
76+
if self.tool in ("npm", "pnpm"):
77+
self.commands.append(f"{self.tool} run {self.build_cmd}")
78+
elif self.tool in ("yarn", "jlpm"):
79+
self.commands.append(f"{self.tool} {self.build_cmd}")
80+
81+
return self.commands
82+
83+
def execute(self):
84+
"""Execute the build commands."""
85+
86+
# First navigate to the project path
87+
cwd = Path(curdir).resolve()
88+
chdir(self.path)
89+
90+
for command in self.commands:
91+
system_call(command)
92+
93+
# Check that all targets exist
94+
# Go back to original path
95+
chdir(str(cwd))
96+
for target in self.targets:
97+
if not Path(target).resolve().exists():
98+
raise FileNotFoundError(f"Target '{target}' does not exist after build. Please check your build configuration.")
99+
return self.commands
100+
101+
def cleanup(self):
102+
# No-op
103+
...

hatch_js/tests/test_all.py

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { NodeModulesExternal } from "@finos/perspective-esbuild-plugin/external.js";
2+
import { build } from "@finos/perspective-esbuild-plugin/build.js";
3+
import { BuildCss } from "@prospective.co/procss/target/cjs/procss.js";
4+
import fs from "fs";
5+
import cpy from "cpy";
6+
import path_mod from "path";
7+
8+
const COMMON_DEFINE = {
9+
global: "window",
10+
};
11+
12+
const BUILD = [
13+
{
14+
define: COMMON_DEFINE,
15+
entryPoints: ["src/js/index.js"],
16+
plugins: [NodeModulesExternal()],
17+
format: "esm",
18+
loader: {
19+
".css": "text",
20+
".html": "text",
21+
},
22+
outfile: "dist/esm/index.js",
23+
},
24+
{
25+
define: COMMON_DEFINE,
26+
entryPoints: ["src/js/index.js"],
27+
plugins: [],
28+
format: "esm",
29+
loader: {
30+
".css": "text",
31+
".html": "text",
32+
},
33+
outfile: "dist/cdn/index.js",
34+
},
35+
];
36+
37+
async function compile_css() {
38+
const process_path = (path) => {
39+
const outpath = path.replace("src/less", "dist/css");
40+
fs.mkdirSync(outpath, { recursive: true });
41+
42+
fs.readdirSync(path).forEach((file_or_folder) => {
43+
if (file_or_folder.endsWith(".less")) {
44+
const outfile = file_or_folder.replace(".less", ".css");
45+
const builder = new BuildCss("");
46+
builder.add(
47+
`${path}/${file_or_folder}`,
48+
fs
49+
.readFileSync(path_mod.join(`${path}/${file_or_folder}`))
50+
.toString(),
51+
);
52+
fs.writeFileSync(
53+
`${path.replace("src/less", "dist/css")}/${outfile}`,
54+
builder.compile().get(outfile),
55+
);
56+
} else {
57+
process_path(`${path}/${file_or_folder}`);
58+
}
59+
});
60+
};
61+
// recursively process all less files in src/less
62+
process_path("src/less");
63+
cpy("src/css/*", "dist/css/");
64+
}
65+
66+
async function copy_html() {
67+
fs.mkdirSync("dist/html", { recursive: true });
68+
cpy("src/html/*", "dist/html");
69+
// also copy to top level
70+
cpy("src/html/*", "dist/");
71+
}
72+
73+
async function copy_img() {
74+
fs.mkdirSync("dist/img", { recursive: true });
75+
cpy("src/img/*", "dist/img");
76+
}
77+
78+
async function copy_to_python() {
79+
fs.mkdirSync("../project/extension", { recursive: true });
80+
cpy("dist/**/*", "../project/extension");
81+
}
82+
83+
async function build_all() {
84+
await compile_css();
85+
await copy_html();
86+
await copy_img();
87+
await Promise.all(BUILD.map(build)).catch(() => process.exit(1));
88+
await copy_to_python();
89+
}
90+
91+
build_all();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "project",
3+
"version": "0.1.0",
4+
"description": "",
5+
"repository": "[email protected]:python-project-templates/hatch-js.git",
6+
"author": "hatch-js authors <[email protected]>",
7+
"license": "Apache-2.0",
8+
"private": true,
9+
"type": "module",
10+
"unpkg": "dist/cdn/index.js",
11+
"jsdelivr": "dist/cdn/index.js",
12+
"exports": {
13+
".": {
14+
"types": "./dist/esm/index.d.ts",
15+
"default": "./dist/esm/index.js"
16+
},
17+
"./dist/*": "./dist/*",
18+
"./package.json": "./package.json"
19+
},
20+
"files": [
21+
"dist/**/*",
22+
"index.d.ts"
23+
],
24+
"types": "./dist/esm/index.d.ts",
25+
"publishConfig": {
26+
"access": "public"
27+
},
28+
"scripts": {
29+
"build": "node build.mjs",
30+
"clean": "rm -rf dist playwright-report ../project/extension",
31+
"preinstall": "npx only-allow pnpm",
32+
"prepack": "npm run build"
33+
},
34+
"dependencies": {},
35+
"devDependencies": {
36+
"@finos/perspective-esbuild-plugin": "^3.2.1",
37+
"@prospective.co/procss": "^0.1.17",
38+
"cpy": "^11.1.0",
39+
"esbuild": "^0.25.5",
40+
"esbuild-plugin-less": "^1.3.25",
41+
"npm-run-all": "^4.1.5"
42+
}
43+
}

0 commit comments

Comments
 (0)