Skip to content

Commit 4719fac

Browse files
author
Anatoly Ostrovsky
committed
Map, Set, Date support. Closure integration
1 parent 201f0f0 commit 4719fac

54 files changed

Lines changed: 11840 additions & 168 deletions

Some content is hidden

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

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build build-ts check test test-types types docs-examples-check coverage coverage-check coverage-update-baseline coverage-open setup ensure-deps lint lint-check lint-fix
1+
.PHONY: build build-ts check test test-integrations test-types types public-namespace-api update-public-namespace-api docs-examples-check coverage coverage-check coverage-update-baseline coverage-open setup ensure-deps lint lint-check lint-fix
22

33
BUILD_DIR = ./dist
44
TS_BUILD_DIR = ./.build
@@ -87,6 +87,11 @@ types: ensure-deps
8787
@./node_modules/.bin/tsc --project tsconfig.types.json
8888
@npx prettier ./@types --write --cache --log-level=silent
8989

90+
public-namespace-api: types
91+
@$(MAKE) -f integrations/closure/Makefile closure-generate
92+
93+
update-public-namespace-api: public-namespace-api
94+
9095
TYPEDOC_DIR = docs/static/typedoc
9196
doc: ensure-deps
9297
@rm -rf $(TYPEDOC_DIR)
@@ -106,6 +111,11 @@ PLAYWRIGHT_TEST := npx playwright test
106111
test: ensure-deps
107112
@echo $(INFO) "Playwright test JS"
108113
@$(PLAYWRIGHT_TEST)
114+
@$(MAKE) test-integrations
115+
116+
test-integrations: ensure-deps
117+
@echo $(INFO) "Playwright integration tests"
118+
@$(MAKE) -f integrations/closure/Makefile closure-test
109119

110120
test-ui: ensure-deps
111121
@echo $(INFO) "Playwright test JS with ui"

integrations/closure/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/dist/
2+
/clojurescript/.shadow-cljs/
3+
/clojurescript/demo/dist/
4+
/clojurescript/node_modules/
5+
/clojurescript/target/

integrations/closure/Makefile

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
.PHONY: closure-generate closure-externs closure-validate closure-build closure-test closure-demo clojurescript-generate clojurescript-validate clojurescript-build clojurescript-test clojurescript-package clojurescript-deploy java-generate java-check
2+
3+
CLOSURE_INTEGRATION_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
4+
CLOSURE_REPO_ROOT := $(abspath $(CLOSURE_INTEGRATION_DIR)/../..)
5+
CLOSURE_NODE ?= $(shell command -v node)
6+
SHADOW_CLJS_VERSION ?= 3.4.10
7+
CLOJURESCRIPT_JAVA_HOME ?= $(firstword $(wildcard $(HOME)/.sdkman/candidates/java/21*))
8+
CLOSURE_JAVA_21_HOME ?= $(firstword $(wildcard $(HOME)/.sdkman/candidates/java/21*))
9+
JSINTEROP_GENERATOR_JAVA ?= $(if $(CLOSURE_JAVA_21_HOME),$(CLOSURE_JAVA_21_HOME)/bin/java,java)
10+
CLOJARS_REPOSITORY_ID ?= clojars
11+
CLOJARS_URL ?= https://repo.clojars.org/
12+
CLOJURESCRIPT_GROUP_ID ?= org.angular.ts
13+
CLOJURESCRIPT_ARTIFACT_ID ?= angular-ts-cljs
14+
CLOJURESCRIPT_PACKAGE_VERSION ?= $(shell cd $(CLOSURE_REPO_ROOT) && node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")
15+
16+
closure-generate: closure-externs clojurescript-generate java-generate
17+
18+
closure-externs:
19+
@"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/scripts/generate-externs.mjs
20+
21+
closure-validate:
22+
@"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/scripts/validate-externs.mjs
23+
24+
closure-build: closure-validate
25+
@$(MAKE) -C $(CLOSURE_REPO_ROOT) build
26+
@"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/scripts/build.mjs
27+
28+
closure-test: closure-build clojurescript-build
29+
@cd $(CLOSURE_REPO_ROOT) && npx playwright test --config integrations/closure/playwright.config.ts
30+
31+
closure-demo: closure-build
32+
@echo "Open http://localhost:4000/integrations/closure/demo/index.html after running make serve"
33+
34+
clojurescript-generate: closure-externs
35+
@"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/clojurescript/scripts/generate-cljs-types.mjs
36+
37+
clojurescript-validate: closure-validate
38+
@"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/clojurescript/scripts/generate-cljs-types.mjs --check
39+
40+
clojurescript-build: clojurescript-validate
41+
@cd $(CLOSURE_INTEGRATION_DIR)/clojurescript && \
42+
if [ -n "$(CLOJURESCRIPT_JAVA_HOME)" ]; then \
43+
PATH="$(CLOJURESCRIPT_JAVA_HOME)/bin:$$PATH" npx --yes shadow-cljs@$(SHADOW_CLJS_VERSION) release todo; \
44+
else \
45+
npx --yes shadow-cljs@$(SHADOW_CLJS_VERSION) release todo; \
46+
fi
47+
48+
clojurescript-test: clojurescript-build
49+
@cd $(CLOSURE_REPO_ROOT) && npx playwright test --config integrations/closure/playwright.config.ts integrations/closure/clojurescript/clojurescript.test.ts
50+
51+
clojurescript-package: clojurescript-generate
52+
@CLOJURESCRIPT_GROUP_ID="$(CLOJURESCRIPT_GROUP_ID)" \
53+
CLOJURESCRIPT_ARTIFACT_ID="$(CLOJURESCRIPT_ARTIFACT_ID)" \
54+
CLOJURESCRIPT_PACKAGE_VERSION="$(CLOJURESCRIPT_PACKAGE_VERSION)" \
55+
"$(CLOSURE_NODE)" $(CLOSURE_INTEGRATION_DIR)/clojurescript/scripts/package-cljs.mjs
56+
57+
clojurescript-deploy: clojurescript-package
58+
@cd $(CLOSURE_REPO_ROOT) && \
59+
mvn deploy:deploy-file \
60+
-Dfile=integrations/closure/clojurescript/target/$(CLOJURESCRIPT_ARTIFACT_ID)-$(CLOJURESCRIPT_PACKAGE_VERSION).jar \
61+
-DpomFile=integrations/closure/clojurescript/target/$(CLOJURESCRIPT_ARTIFACT_ID)-$(CLOJURESCRIPT_PACKAGE_VERSION).pom \
62+
-DrepositoryId=$(CLOJARS_REPOSITORY_ID) \
63+
-Durl=$(CLOJARS_URL)
64+
65+
java-generate: closure-externs
66+
@cd $(CLOSURE_REPO_ROOT) && \
67+
JSINTEROP_GENERATOR_JAVA="$(JSINTEROP_GENERATOR_JAVA)" \
68+
mvn -f integrations/closure/java/pom.xml process-sources
69+
70+
java-check:
71+
@cd $(CLOSURE_REPO_ROOT) && \
72+
JSINTEROP_GENERATOR_JAVA="$(JSINTEROP_GENERATOR_JAVA)" \
73+
mvn -f integrations/closure/java/pom.xml clean verify
74+
@cd $(CLOSURE_REPO_ROOT) && \
75+
mvn -f integrations/closure/java/demo/pom.xml clean j2cl:clean verify

integrations/closure/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# AngularTS Closure Compiler Integration
2+
3+
This integration treats AngularTS as an external browser runtime and compiles
4+
application code with Google Closure Compiler in `ADVANCED` mode with verbose
5+
warnings promoted to errors.
6+
7+
The integration is intentionally isolated from the core build:
8+
9+
- `externs/angular-ts.externs.js` describes the public AngularTS API and mirrors
10+
the public `ng` namespace with documented structural Closure externs.
11+
- `demo/app.js` registers the Closure-compiled todo app with AngularTS.
12+
- `demo/todo.js` and `demo/todo-controller.js` are separate `goog.module`
13+
files for the todo model and controller.
14+
- `demo/goog-base.js` is a compiler-only Closure primitive shim so the demo can
15+
use `goog.module` / `goog.require` without shipping Closure Library runtime code.
16+
- `demo/index.html` loads the Vite-served AngularTS source runtime and the
17+
checked-in Closure-compiled todo app output at `demo/compiled.js`.
18+
- `scripts/build.mjs` runs Closure Compiler.
19+
- `scripts/generate-externs.mjs` regenerates documented structural externs from
20+
the public `ng` namespace and reuses Closure browser externs for DOM/browser
21+
aliases such as `WindowService`, `DocumentService`, and `RootElementService`.
22+
- `scripts/validate-externs.mjs` fails when a public `ng` namespace type is
23+
missing from the externs file, lacks a JSDoc extern declaration, lacks a
24+
structural contract, or the generated externs are out of date.
25+
- `clojurescript/` contains a Shadow CLJS todo demo that generates a typed
26+
ClojureScript facade from the AngularTS Closure externs and compiles with
27+
Closure `ADVANCED` optimizations.
28+
- `Makefile` can be included from the repo root later.
29+
30+
Supported direction:
31+
32+
- Closure compiles user application code.
33+
- AngularTS is loaded as a prebuilt external global.
34+
35+
## Commands
36+
37+
From the repository root:
38+
39+
```bash
40+
node integrations/closure/scripts/generate-externs.mjs
41+
make -f integrations/closure/Makefile closure-validate
42+
make -f integrations/closure/Makefile closure-build
43+
make -f integrations/closure/Makefile clojurescript-build
44+
make -f integrations/closure/Makefile closure-test
45+
node integrations/closure/scripts/validate-externs.mjs
46+
```
47+
48+
The demo page is available at:
49+
50+
```text
51+
http://localhost:4000/integrations/closure/demo/index.html
52+
http://localhost:4000/integrations/closure/clojurescript/demo/index.html
53+
```
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# AngularTS ClojureScript Closure Integration
2+
3+
This folder contains the publishable ClojureScript facade for the Closure
4+
integration plus a Shadow CLJS todo demo. The demo compiles with Closure
5+
`ADVANCED` optimizations while treating AngularTS as an external browser
6+
runtime.
7+
8+
The generated facade in `src/angular_ts/generated.cljs` is produced from
9+
`../externs/angular-ts.externs.js`. It gives ClojureScript code concrete
10+
AngularTS type hints such as `^js/ng.NgModule` and `^js/ng.Scope`, and
11+
fails generation if the required AngularTS extern contracts disappear. Extern
12+
JSDoc is preserved where ClojureScript can use it: public type descriptions are
13+
kept in a source-only `public-type-docs` comment block, and generated strict
14+
wrapper docstrings use the original extern descriptions, parameter docs, and
15+
return docs. Generation fails if a public extern type is missing documentation.
16+
17+
The facade is deliberately strict:
18+
19+
- `*warn-on-infer*` is enabled.
20+
- AngularTS dependency annotations and module requirements use JavaScript
21+
arrays at the boundary.
22+
- Generated method wrappers are emitted only when every receiver, parameter, and
23+
non-void return type has a concrete Closure extern tag that ClojureScript can
24+
represent. Wildcards, unions, function-typed parameters, and variadic externs
25+
are skipped unless there is a hand-written strict wrapper.
26+
- The generator pins the reviewed public type-tag count and strict wrapper set,
27+
so extern-surface changes require an intentional update.
28+
- The build runs Shadow CLJS with Closure `ADVANCED` optimizations and treats
29+
compiler inference warnings as warnings.
30+
31+
## Commands
32+
33+
From the repository root:
34+
35+
```bash
36+
make -f integrations/closure/Makefile clojurescript-generate
37+
make -f integrations/closure/Makefile clojurescript-build
38+
make -f integrations/closure/Makefile clojurescript-test
39+
make -f integrations/closure/Makefile clojurescript-package
40+
```
41+
42+
The package target writes a Maven/Clojars-ready jar and POM to:
43+
44+
```text
45+
integrations/closure/clojurescript/target/angular-ts-cljs-<version>.jar
46+
integrations/closure/clojurescript/target/angular-ts-cljs-<version>.pom
47+
```
48+
49+
The jar contains `angular-ts.generated` plus
50+
`angular_ts/externs/angular-ts.externs.js`; it does not include the todo demo.
51+
The version defaults to the root `package.json` version. Override it with
52+
`CLOJURESCRIPT_PACKAGE_VERSION=...` when needed.
53+
54+
To publish to Clojars after verifying the release:
55+
56+
```bash
57+
make public-namespace-api
58+
make test-integrations
59+
make -f integrations/closure/Makefile clojurescript-deploy
60+
```
61+
62+
Consumers can depend on the published artifact:
63+
64+
```clojure
65+
{:deps {org.angular.ts/angular-ts-cljs {:mvn/version "0.27.0"}}}
66+
```
67+
68+
And pass the packaged extern resource to Shadow CLJS:
69+
70+
```clojure
71+
{:compiler-options
72+
{:externs ["angular_ts/externs/angular-ts.externs.js"]}}
73+
```
74+
75+
Shadow CLJS 3.x uses a Closure Compiler build that requires Java 21. The
76+
Makefile automatically uses an SDKMAN Java 21 installation when present. Set
77+
`CLOJURESCRIPT_JAVA_HOME=/path/to/jdk-21` if your default `java` is older.
78+
79+
The demo page is available at:
80+
81+
```text
82+
http://localhost:4000/integrations/closure/clojurescript/demo/index.html
83+
```
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("ClojureScript todo demo runs against generated AngularTS Closure types", async ({
4+
page,
5+
}) => {
6+
await page.goto("/integrations/closure/clojurescript/demo/index.html");
7+
8+
await expect(
9+
page.getByRole("heading", { name: "ClojureScript Todo App" }),
10+
).toBeVisible();
11+
await expect(
12+
page.getByText(
13+
"ClojureScript compiled with Closure ADVANCED using generated AngularTS type hints",
14+
),
15+
).toBeVisible();
16+
await expect(page.locator(".todo-row")).toHaveCount(2);
17+
await expect(page.locator("#cljs-remaining")).toContainText(
18+
"2 of 2 remaining",
19+
);
20+
21+
await page
22+
.getByLabel("New ClojureScript todo")
23+
.fill("Ship ClojureScript integration");
24+
await page.getByLabel("New ClojureScript todo").press("Enter");
25+
await expect(page.locator(".todo-row")).toHaveCount(3);
26+
await expect(page.getByText("Ship ClojureScript integration")).toBeVisible();
27+
await expect(page.locator("#cljs-remaining")).toContainText(
28+
"3 of 3 remaining",
29+
);
30+
31+
const newTodo = page
32+
.locator(".todo-row")
33+
.filter({ hasText: "Ship ClojureScript integration" });
34+
35+
await newTodo.getByRole("checkbox").click();
36+
await expect(newTodo.getByRole("checkbox")).toBeChecked();
37+
await expect(newTodo).toHaveClass(/is-done/);
38+
await expect(page.locator("#cljs-remaining")).toContainText(
39+
"2 of 3 remaining",
40+
);
41+
await expect(
42+
page.evaluate(() => {
43+
const scope = (window as any).angular.getScope(
44+
document.querySelector("main"),
45+
);
46+
47+
return scope.$ctrl.tasks.find(
48+
(todo: Record<string, unknown>) =>
49+
todo.task === "Ship ClojureScript integration",
50+
)?.done;
51+
}),
52+
).resolves.toBe(true);
53+
54+
await page.getByRole("button", { name: "Archive completed" }).click();
55+
await expect(page.locator(".todo-row")).toHaveCount(2);
56+
await expect(page.getByText("Ship ClojureScript integration")).toHaveCount(0);
57+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>AngularTS ClojureScript Todo Demo</title>
6+
<link rel="stylesheet" href="/src/directive/http/demo.css" />
7+
<style>
8+
.todo-row {
9+
align-items: center;
10+
display: flex;
11+
gap: 10px;
12+
justify-content: space-between;
13+
}
14+
15+
.todo-row.is-done strong {
16+
color: var(--muted);
17+
text-decoration: line-through;
18+
}
19+
</style>
20+
<script type="module" src="/src/index.ts"></script>
21+
<script
22+
type="module"
23+
src="/integrations/closure/clojurescript/demo/dist/compiled.js?v=todo-submit-done-archive"
24+
></script>
25+
</head>
26+
<body ng-app="cljsTodo">
27+
<main class="demo-shell" ng-controller="TodoCtrl as $ctrl">
28+
<section class="demo-hero">
29+
<h1>{{ $ctrl.greeting }}</h1>
30+
<p cljs-badge></p>
31+
</section>
32+
33+
<section class="demo-content">
34+
<form ng-submit="$ctrl.submit($event)">
35+
<input
36+
aria-label="New ClojureScript todo"
37+
id="cljs-new-todo"
38+
name="todo"
39+
type="text"
40+
ng-input="$ctrl.setNewTodo($event.target.value)"
41+
ng-required="true"
42+
placeholder="What needs doing?"
43+
/>
44+
<button type="submit">Add</button>
45+
</form>
46+
47+
<p id="cljs-remaining" class="demo-note">
48+
{{ $ctrl.remainingCount }} of {{ $ctrl.tasks.length }} remaining
49+
</p>
50+
51+
<div class="demo-actions">
52+
<button type="button" ng-click="$ctrl.archive()">
53+
Archive completed
54+
</button>
55+
</div>
56+
57+
<div class="demo-stack">
58+
<article
59+
class="event-card todo-row"
60+
ng-class="{ 'is-done': todo.done }"
61+
ng-repeat="todo in $ctrl.tasks"
62+
data-index="id"
63+
>
64+
<strong>{{ todo.task }}</strong>
65+
<label>
66+
Done
67+
<input
68+
type="checkbox"
69+
ng-checked="todo.done"
70+
ng-change="$ctrl.setDone(todo, $event.target.checked)"
71+
/>
72+
</label>
73+
</article>
74+
</div>
75+
</section>
76+
</main>
77+
</body>
78+
</html>

0 commit comments

Comments
 (0)