Skip to content

Commit 82a8bd6

Browse files
authored
rewrite the client in typescript (#951)
* initial work to rewrite the client in typescript * minor fixes * fix js tests * misc fixes * rewrite event-to-object * rewrite event-to-object tests * finish tests * improve typescript configuration * show npm version * workspace order * minor fixes * rework docs extension + misc changes * build before check types * fix typos * move client under namespace dir * fix types in noxfile
1 parent 6a4c947 commit 82a8bd6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2947
-3712
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ repos:
99
- id: isort
1010
name: isort
1111
- repo: https://github.com/pre-commit/mirrors-prettier
12-
rev: v3.0.0-alpha.4
12+
rev: v3.0.0-alpha.6
1313
hooks:
1414
- id: prettier

docs/source/_custom_js/package-lock.json

+29-26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/source/_custom_js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
"rollup": "^2.35.1"
1616
},
1717
"dependencies": {
18-
"@reactpy/client": "file:../../../src/client/packages/client"
18+
"@reactpy/client": "file:../../../src/client/packages/@reactpy/client"
1919
}
2020
}

docs/source/_custom_js/src/index.js

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { mountWithLayoutServer, LayoutServerInfo } from "@reactpy/client";
1+
import { SimpleReactPyClient, mount } from "@reactpy/client";
22

33
let didMountDebug = false;
44

55
export function mountWidgetExample(
66
mountID,
77
viewID,
88
reactpyServerHost,
9-
useActivateButton
9+
useActivateButton,
1010
) {
1111
let reactpyHost, reactpyPort;
1212
if (reactpyServerHost) {
@@ -16,27 +16,27 @@ export function mountWidgetExample(
1616
reactpyPort = window.location.port;
1717
}
1818

19-
const serverInfo = new LayoutServerInfo({
20-
host: reactpyHost,
21-
port: reactpyPort,
22-
path: "/_reactpy/",
23-
query: `view_id=${viewID}`,
24-
secure: window.location.protocol == "https:",
19+
const client = new SimpleReactPyClient({
20+
serverLocation: {
21+
url: `${window.location.protocol}//${reactpyHost}:${reactpyPort}`,
22+
route: "/",
23+
query: `?view_id=${viewID}`,
24+
},
2525
});
2626

2727
const mountEl = document.getElementById(mountID);
2828
let isMounted = false;
2929
triggerIfInViewport(mountEl, () => {
3030
if (!isMounted) {
31-
activateView(mountEl, serverInfo, useActivateButton);
31+
activateView(mountEl, client, useActivateButton);
3232
isMounted = true;
3333
}
3434
});
3535
}
3636

37-
function activateView(mountEl, serverInfo, useActivateButton) {
37+
function activateView(mountEl, client, useActivateButton) {
3838
if (!useActivateButton) {
39-
mountWithLayoutServer(mountEl, serverInfo);
39+
mount(mountEl, client);
4040
return;
4141
}
4242

@@ -51,7 +51,7 @@ function activateView(mountEl, serverInfo, useActivateButton) {
5151
mountEl.setAttribute("class", "interactive widget-container");
5252
mountWithLayoutServer(mountEl, serverInfo);
5353
}
54-
})
54+
}),
5555
);
5656

5757
function fadeOutElementThenCallback(element, callback) {
@@ -87,7 +87,7 @@ function triggerIfInViewport(element, callback) {
8787
{
8888
root: null,
8989
threshold: 0.1, // set offset 0.1 means trigger if atleast 10% of element in viewport
90-
}
90+
},
9191
);
9292

9393
observer.observe(element);

noxfile.py

+68-42
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def __call__(self, session: Session) -> Callable[[bool], None]:
3535
SRC_DIR = ROOT_DIR / "src"
3636
CLIENT_DIR = SRC_DIR / "client"
3737
REACTPY_DIR = SRC_DIR / "reactpy"
38-
LANGUAGE_TYPES: list[LanguageName] = ["py", "js"]
3938
TAG_PATTERN = re.compile(
4039
# start
4140
r"^"
@@ -46,7 +45,6 @@ def __call__(self, session: Session) -> Callable[[bool], None]:
4645
# end
4746
r"$"
4847
)
49-
print(TAG_PATTERN.pattern)
5048
REMAINING_ARGS = Option(nargs=REMAINDER, type=str)
5149

5250

@@ -60,6 +58,7 @@ def __call__(self, session: Session) -> Callable[[bool], None]:
6058
def setup_checks(session: Session) -> None:
6159
session.install("--upgrade", "pip")
6260
session.run("pip", "--version")
61+
session.run("npm", "--version", external=True)
6362

6463

6564
@group.setup("check-javascript")
@@ -88,6 +87,12 @@ def format(session: Session) -> None:
8887
session.run("npm", "run", "format", external=True)
8988

9089

90+
@group.session
91+
def tsc(session: Session) -> None:
92+
session.chdir(CLIENT_DIR)
93+
session.run("npx", "tsc", "-b", "-w", "packages/app", external=True)
94+
95+
9196
@group.session
9297
def example(session: Session) -> None:
9398
"""Run an example"""
@@ -232,21 +237,24 @@ def check_docs(session: Session) -> None:
232237

233238

234239
@group.session
235-
def check_javascript_suite(session: Session) -> None:
236-
"""Run the Javascript-based test suite and ensure it bundles succesfully"""
237-
session.run("npm", "run", "test", external=True)
240+
def check_javascript_tests(session: Session) -> None:
241+
session.run("npm", "run", "check:tests", external=True)
238242

239243

240244
@group.session
241-
def check_javascript_build(session: Session) -> None:
242-
"""Run the Javascript-based test suite and ensure it bundles succesfully"""
243-
session.run("npm", "run", "test", external=True)
245+
def check_javascript_format(session: Session) -> None:
246+
session.run("npm", "run", "check:format", external=True)
244247

245248

246249
@group.session
247-
def check_javascript_format(session: Session) -> None:
248-
"""Check that Javascript style guidelines are being followed"""
249-
session.run("npm", "run", "check-format", external=True)
250+
def check_javascript_types(session: Session) -> None:
251+
session.run("npm", "run", "build", external=True)
252+
session.run("npm", "run", "check:types", external=True)
253+
254+
255+
@group.session
256+
def check_javascript_build(session: Session) -> None:
257+
session.run("npm", "run", "build", external=True)
250258

251259

252260
@group.session
@@ -266,16 +274,38 @@ def build_python(session: Session) -> None:
266274

267275

268276
@group.session
269-
def publish(session: Session, dry_run: bool = False) -> None:
277+
def publish(
278+
session: Session,
279+
publish_dry_run: Annotated[
280+
bool,
281+
Option(help="whether to test the release process"),
282+
] = False,
283+
publish_fake_tags: Annotated[
284+
Sequence[str],
285+
Option(nargs="*", type=str, help="fake tags to use for a dry run release"),
286+
] = (),
287+
) -> None:
270288
packages = get_packages(session)
271289

272290
release_prep: dict[LanguageName, ReleasePrepFunc] = {
273291
"js": prepare_javascript_release,
274292
"py": prepare_python_release,
275293
}
276294

295+
if publish_fake_tags and not publish_dry_run:
296+
session.error("Cannot specify --publish-fake-tags without --publish-dry-run")
297+
298+
parsed_tags: list[TagInfo] = []
299+
for tag in publish_fake_tags or get_current_tags(session):
300+
tag_info = parse_tag(tag)
301+
if tag_info is None:
302+
session.error(
303+
f"Invalid tag {tag} - must be of the form <package>-<language>-<version>"
304+
)
305+
parsed_tags.append(tag_info) # type: ignore
306+
277307
publishers: list[tuple[Path, Callable[[bool], None]]] = []
278-
for tag, tag_pkg, tag_ver in get_current_tags(session):
308+
for tag, tag_pkg, tag_ver in parsed_tags:
279309
if tag_pkg not in packages:
280310
session.error(f"Tag {tag} references package {tag_pkg} that does not exist")
281311

@@ -293,7 +323,7 @@ def publish(session: Session, dry_run: bool = False) -> None:
293323
for pkg_path, publish in publishers:
294324
session.log(f"Publishing {pkg_path}...")
295325
session.chdir(pkg_path)
296-
publish(dry_run)
326+
publish(publish_dry_run)
297327

298328

299329
# --- Utilities ------------------------------------------------------------------------
@@ -386,23 +416,28 @@ def get_packages(session: Session) -> dict[str, PackageInfo]:
386416
}
387417

388418
# collect javascript packages
389-
for pkg in (CLIENT_DIR / "packages").glob("*"):
390-
pkg_json_file = pkg / "package.json"
391-
if not pkg_json_file.exists():
392-
session.error(f"package.json not found in {pkg}")
419+
js_package_paths: list[Path] = []
420+
for maybed_pkg in (CLIENT_DIR / "packages").glob("*"):
421+
if not (maybed_pkg / "package.json").exists():
422+
for nmaybe_namespaced_pkg in maybed_pkg.glob("*"):
423+
if (nmaybe_namespaced_pkg / "package.json").exists():
424+
js_package_paths.append(nmaybe_namespaced_pkg)
425+
else:
426+
js_package_paths.append(maybed_pkg)
427+
428+
# get javascript package info
429+
for pkg in js_package_paths:
430+
pkg_json_file = pkg / "package.json" # we already know this exists
393431

394432
pkg_json = json.loads(pkg_json_file.read_text())
395433

396434
pkg_name = pkg_json.get("name")
397435
pkg_version = pkg_json.get("version")
398436

399-
if pkg_version is None:
400-
session.log(f"Skipping - {pkg_name} has no name or version in package.json")
437+
if pkg_version is None or pkg_name is None:
438+
session.log(f"Skipping - {pkg_name} has no name/version in package.json")
401439
continue
402440

403-
if pkg_name is None:
404-
session.error(f"Package {pkg} has no name in package.json")
405-
406441
if pkg_name in packages:
407442
session.error(f"Duplicate package name {pkg_name}")
408443

@@ -417,7 +452,7 @@ class PackageInfo(NamedTuple):
417452
version: str
418453

419454

420-
def get_current_tags(session: Session) -> list[TagInfo]:
455+
def get_current_tags(session: Session) -> list[str]:
421456
"""Get tags for the current commit"""
422457
# check if unstaged changes
423458
try:
@@ -465,24 +500,16 @@ def get_current_tags(session: Session) -> list[TagInfo]:
465500
if not tags:
466501
session.error("No tags found for current commit")
467502

468-
parsed_tags: list[TagInfo] = []
469-
for tag in tags:
470-
match = TAG_PATTERN.match(tag)
471-
if not match:
472-
session.error(
473-
f"Invalid tag {tag} - must be of the form <package>-<language>-<version>"
474-
)
475-
parsed_tags.append(
476-
TagInfo(
477-
tag,
478-
match["name"], # type: ignore[index]
479-
match["version"], # type: ignore[index]
480-
)
481-
)
503+
session.log(f"Found tags: {tags}")
504+
505+
return tags
482506

483-
session.log(f"Found tags: {[info.tag for info in parsed_tags]}")
484507

485-
return parsed_tags
508+
def parse_tag(tag: str) -> TagInfo | None:
509+
match = TAG_PATTERN.match(tag)
510+
if not match:
511+
return None
512+
return TagInfo(tag, match["name"], match["version"])
486513

487514

488515
class TagInfo(NamedTuple):
@@ -506,5 +533,4 @@ def get_reactpy_package_version(session: Session) -> str: # type: ignore[return
506533
# remove the quotes
507534
[1:-1]
508535
)
509-
else:
510-
session.error(f"No version found in {pkg_root_init_file}")
536+
session.error(f"No version found in {pkg_root_init_file}")

src/client/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tsconfig.tsbuildinfo
2+
packages/**/package-lock.json

0 commit comments

Comments
 (0)