Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/playwright/scripts/run-ancestry-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ cleanup() {
fi
done
# Kill any remaining processes on our ports
for port in 3343 3344 3345 3346 3347 3350; do
for port in 3343 3344 3345 3346 3347 3350 3353; do
lsof -ti:$port | xargs kill -9 2>/dev/null || true
done
echo -e "${GREEN}Cleanup complete${NC}"
Expand Down Expand Up @@ -120,6 +120,7 @@ start_app "vite-solid-project" "Solid" 3345
start_app "vite-preact-project" "Preact" 3346
start_app "vite-svelte-project" "Svelte" 3347
start_app "vite-vue-project" "Vue" 3350
start_app "vite-rescript" "ReScript" 3353

echo ""
echo -e "${YELLOW}Waiting for apps to be ready...${NC}"
Expand All @@ -133,6 +134,7 @@ wait_for_url "http://localhost:3345" "Solid"
wait_for_url "http://localhost:3346" "Preact"
wait_for_url "http://localhost:3347" "Svelte"
wait_for_url "http://localhost:3350" "Vue"
wait_for_url "http://localhost:3353" "ReScript"

echo ""
echo -e "${GREEN}All apps are ready!${NC}"
Expand Down
78 changes: 78 additions & 0 deletions apps/playwright/tests/ancestry/rescript.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { test, expect, Page } from "@playwright/test";
import { projects } from "../consts";

async function getAncestryPath(page: Page, selector: string): Promise<string | null> {
return await page.evaluate((sel) => {
const api = (window as any).__treelocator__;
if (!api) return null;
return api.getPath(sel);
}, selector);
}

async function getAncestryData(page: Page, selector: string): Promise<any[] | null> {
return await page.evaluate((sel) => {
const api = (window as any).__treelocator__;
if (!api) return null;
return api.getAncestry(sel);
}, selector);
}

async function waitForLocator(page: Page): Promise<void> {
await page.waitForFunction(
() => typeof (window as any).__treelocator__ !== "undefined",
{ timeout: 10000 }
);
}

test.describe("ReScript ancestry chain", () => {
test.beforeEach(async ({ page }) => {
await page.goto(projects.rescript);
await waitForLocator(page);
});

test("reports the module name (Button), not the literal `make`", async ({ page }) => {
const path = await getAncestryPath(page, ".submit-button");
expect(path).not.toBeNull();
expect(path).toContain("Button");
expect(path).not.toMatch(/\bmake\b/);
});

test("reports the wrapping Card module", async ({ page }) => {
const path = await getAncestryPath(page, ".submit-button");
expect(path).not.toBeNull();
expect(path).toContain("Card");
});

test("file paths point at .res files, not .res.js", async ({ page }) => {
const data = await getAncestryData(page, ".submit-button");
expect(data).not.toBeNull();

const buttonItem = data!.find(
(item) =>
item.componentName === "Button" ||
item.ownerComponents?.some((c: any) => c.name === "Button")
);
expect(buttonItem).toBeDefined();

const filePath: string | undefined = buttonItem?.filePath;
expect(filePath).toBeTruthy();
// Must end in .res, not .res.js
expect(filePath).toMatch(/Button\.res$/);
});

test("line numbers are inside the .res file (not the JS output)", async ({ page }) => {
const data = await getAncestryData(page, ".submit-button");
expect(data).not.toBeNull();

const buttonEntry = data!.find((item) =>
item.filePath?.endsWith("Button.res")
);
expect(buttonEntry).toBeDefined();
// The fixture's only mapping for the JSX is original line 3 (the <button>).
// The exact line is allowed to drift, but it must be a positive number
// within the .res file (which is only 6 lines long).
expect(typeof buttonEntry?.line).toBe("number");
expect(buttonEntry!.line).toBeGreaterThan(0);
expect(buttonEntry!.line).toBeLessThanOrEqual(6);
});
});
1 change: 1 addition & 0 deletions apps/playwright/tests/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const projects = {
reactServerComponents: "http://localhost:3351/",
phoenix: "http://localhost:3344/",
nextjs: "http://localhost:3352/",
rescript: "http://localhost:3353/",
};
12 changes: 12 additions & 0 deletions apps/vite-rescript/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + ReScript + TreeLocatorJS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions apps/vite-rescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "vite-rescript-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3353",
"build": "vite build",
"preview": "vite preview --port 3353",
"fixtures": "node scripts/build-fixtures.mjs"
},
"dependencies": {
"@treelocator/runtime": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@treelocator/vite-plugin-rescript": "workspace:*",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^4.0.0",
"source-map": "^0.7.4",
"vite": "^5.0.0"
}
}
20 changes: 20 additions & 0 deletions apps/vite-rescript/rescript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "vite-rescript-demo",
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"package-specs": {
"module": "esmodule",
"in-source": true
},
"suffix": ".res.js",
"bs-dependencies": ["@rescript/react"],
"jsx": {
"version": 4,
"mode": "classic",
"preserve": true
}
}
101 changes: 101 additions & 0 deletions apps/vite-rescript/scripts/build-fixtures.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Generates .res.js + .res.js.map fixtures that mimic ReScript's output
// when configured with `"jsx": { "version": 4, "preserve": true }`.
//
// The real toolchain is `rescript build` against rescript.json. Committing
// pre-built fixtures lets the demo app and Playwright tests run without a
// rescript binary on the developer's PATH. Run `pnpm fixtures` to regenerate
// them after editing the .res files.

import { SourceMapGenerator } from 'source-map'
import { writeFileSync } from 'node:fs'
import { dirname, join, basename } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const SRC = join(__dirname, '..', 'src')

/**
* @typedef Fixture
* @property {string} name Module name, e.g. "Button"
* @property {string} jsCode JS body (preserved JSX) to write
* @property {Array<{
* generated: { line: number, column: number },
* original: { line: number, column: number }
* }>} mappings
*/

/** @type {Fixture[]} */
const fixtures = [
{
name: 'Button',
jsCode: `import * as React from "react";

function Button(props) {
return <button className="submit-button">{props.label}</button>;
}

let make = Button;

export { make };
`,
mappings: [
// <button> in compiled JS → line 3 col 9 in Button.res
{ generated: { line: 4, column: 9 }, original: { line: 3, column: 2 } },
{ generated: { line: 4, column: 41 }, original: { line: 4, column: 4 } },
],
},
{
name: 'Card',
jsCode: `import * as React from "react";

function Card(props) {
return (
<section className="card">
<header className="card-header">{props.title}</header>
<div className="card-body">{props.children}</div>
</section>
);
}

let make = Card;

export { make };
`,
mappings: [
// <section>
{ generated: { line: 5, column: 4 }, original: { line: 3, column: 2 } },
// <header>
{ generated: { line: 6, column: 6 }, original: { line: 4, column: 4 } },
// <div>
{ generated: { line: 7, column: 6 }, original: { line: 5, column: 4 } },
],
},
]

for (const fx of fixtures) {
const jsName = `${fx.name}.res.js`
const jsPath = join(SRC, jsName)
const mapPath = `${jsPath}.map`

writeFileSync(jsPath, fx.jsCode)

const gen = new SourceMapGenerator({ file: jsName })
for (const m of fx.mappings) {
gen.addMapping({
source: `${fx.name}.res`,
generated: m.generated,
original: m.original,
})
}
// Append the sourceMappingURL pragma so dev tools find it.
const map = JSON.parse(gen.toString())
writeFileSync(mapPath, JSON.stringify(map, null, 2))

// Append sourceMappingURL pragma to the JS (matches what rescript emits).
writeFileSync(
jsPath,
`${fx.jsCode}\n//# sourceMappingURL=${basename(mapPath)}\n`
)

console.log(`wrote ${jsName} + ${basename(mapPath)}`)
}
17 changes: 17 additions & 0 deletions apps/vite-rescript/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { make as Button } from './Button.res.js'
import { make as Card } from './Card.res.js'

export default function App() {
return (
<div id="app-root" style={{ fontFamily: 'sans-serif', padding: '2rem' }}>
<h1>ReScript demo</h1>
<p>Alt+click any element below to copy its component ancestry.</p>
<div id="card-container">
<Card title="Hello">
<Button label="Submit" />
<Button label="Cancel" />
</Card>
</div>
</div>
)
}
6 changes: 6 additions & 0 deletions apps/vite-rescript/src/Button.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@react.component
let make = (~label) => {
<button className="submit-button">
{React.string(label)}
</button>
}
11 changes: 11 additions & 0 deletions apps/vite-rescript/src/Button.res.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions apps/vite-rescript/src/Button.res.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/vite-rescript/src/Card.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@react.component
let make = (~title, ~children) => {
<section className="card">
<header className="card-header">{React.string(title)}</header>
<div className="card-body">{children}</div>
</section>
}
16 changes: 16 additions & 0 deletions apps/vite-rescript/src/Card.res.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions apps/vite-rescript/src/Card.res.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions apps/vite-rescript/src/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { setup } from '@treelocator/runtime'

setup()

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
Loading
Loading