oc-template-preact-compiler 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scaffold/src/.cursor/rules/oc-project.mdc +1 -1
- package/scaffold/src/package.json +8 -9
- package/scaffold/src/src/App.test.tsx +2 -22
- package/scaffold/src/src/App.tsx +36 -24
- package/scaffold/src/src/server.ts +25 -60
- package/scaffold/src/src/styles.css +96 -16
- package/scaffold/src/vite.config.ts +1 -1
- package/scaffold/src/src/types.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-template-preact-compiler",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Compiler for the Preact OC template",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@preact/preset-vite": "^2.7.0",
|
|
32
32
|
"oc-template-preact": "^0.1.1",
|
|
33
|
-
"oc-vite-compiler": "^6.0.
|
|
33
|
+
"oc-vite-compiler": "^6.0.2",
|
|
34
34
|
"preact": "^10"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
@@ -56,7 +56,7 @@ OpenComponents is a microfrontend technology that resembles a Vite project but w
|
|
|
56
56
|
|
|
57
57
|
### Parameters vs Actions
|
|
58
58
|
|
|
59
|
-
- **Parameters**: Initial props passed via URL query string (like `?
|
|
59
|
+
- **Parameters**: Initial props passed via URL query string (like `?name=Ada&year=1986`)
|
|
60
60
|
- **Actions**: Server endpoints that the client can call after initial render
|
|
61
61
|
|
|
62
62
|
### Streaming Mode Benefits
|
|
@@ -23,15 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"@testing-library/
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"oc-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"vitest": "^0.29.7",
|
|
26
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
27
|
+
"@testing-library/preact": "^3.2.4",
|
|
28
|
+
"jsdom": "^30.1.2",
|
|
29
|
+
"oc-server": "^4.0.3",
|
|
30
|
+
"oc-template-preact-compiler": "^4.0.2",
|
|
31
|
+
"preact": "^11.0.1",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"vitest": "^5.0.3",
|
|
35
34
|
"vite": "^7.1.12"
|
|
36
35
|
}
|
|
37
36
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { describe, it, afterEach, beforeEach, expect, vi } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/
|
|
3
|
-
import { DataProvider } from 'oc-template-preact-compiler/dist/utils/useData';
|
|
2
|
+
import { render, screen } from '@testing-library/preact';
|
|
4
3
|
|
|
5
4
|
import App from './App';
|
|
6
5
|
|
|
7
|
-
const getData = vi.fn();
|
|
8
|
-
|
|
9
6
|
describe('App - Page', () => {
|
|
10
7
|
beforeEach(() => {
|
|
11
8
|
window.oc = { events: { on: vi.fn(), fire: vi.fn() } } as any;
|
|
@@ -15,24 +12,7 @@ describe('App - Page', () => {
|
|
|
15
12
|
});
|
|
16
13
|
|
|
17
14
|
it('Gets more data when clicking the button', () => {
|
|
18
|
-
|
|
19
|
-
render(
|
|
20
|
-
<DataProvider
|
|
21
|
-
value={{
|
|
22
|
-
firstName: 'firstName',
|
|
23
|
-
lastName: 'lastName',
|
|
24
|
-
userId: 0,
|
|
25
|
-
getData,
|
|
26
|
-
}}
|
|
27
|
-
>
|
|
28
|
-
<App
|
|
29
|
-
firstName="firstName"
|
|
30
|
-
lastName="lastName"
|
|
31
|
-
born={1991}
|
|
32
|
-
hobbies={[]}
|
|
33
|
-
/>
|
|
34
|
-
</DataProvider>
|
|
35
|
-
);
|
|
15
|
+
render(<App name="firstName" year={1991} />);
|
|
36
16
|
|
|
37
17
|
const extraInfoButton = screen.getByText(/Fun year fact/i);
|
|
38
18
|
expect(extraInfoButton).toBeTruthy();
|
package/scaffold/src/src/App.tsx
CHANGED
|
@@ -7,46 +7,58 @@ import {
|
|
|
7
7
|
import styles from './styles.css';
|
|
8
8
|
import logo from '../public/logo.png';
|
|
9
9
|
|
|
10
|
-
type
|
|
10
|
+
type FunFact = ActionOutput<'funFact'>;
|
|
11
11
|
|
|
12
12
|
const App = (props: InitialData) => {
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
);
|
|
13
|
+
// These come from the server.ts handler return value
|
|
14
|
+
const { name, year } = props;
|
|
15
|
+
const [fact, setFact] = useState<FunFact | null>(null);
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
17
|
const [error, setError] = useState('');
|
|
18
18
|
|
|
19
19
|
const getFunFact = async () => {
|
|
20
|
+
setLoading(true);
|
|
20
21
|
setError('');
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// Call the server action with type safety
|
|
24
|
+
setFact(await serverClient.funFact({ year }));
|
|
24
25
|
} catch (err) {
|
|
25
26
|
setError(String(err));
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false);
|
|
26
29
|
}
|
|
27
30
|
};
|
|
28
31
|
|
|
29
|
-
if (error) {
|
|
30
|
-
return <div>Something wrong happened!</div>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
32
|
return (
|
|
34
33
|
<div className={styles.container}>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
<div className={styles.card}>
|
|
35
|
+
<div className={styles.brand}>
|
|
36
|
+
{/* Static assets are imported from ../public/ */}
|
|
37
|
+
<img
|
|
38
|
+
className={styles.logo}
|
|
39
|
+
width="40"
|
|
40
|
+
height="40"
|
|
41
|
+
src={logo}
|
|
42
|
+
alt="Logo"
|
|
43
|
+
/>
|
|
44
|
+
<p className={styles.eyebrow}>OpenComponents · Vite</p>
|
|
44
45
|
</div>
|
|
46
|
+
<h1 className={styles.title}>
|
|
47
|
+
Hello, <span className={styles.name}>{name}</span>
|
|
48
|
+
</h1>
|
|
49
|
+
<p className={styles.subtitle}>
|
|
50
|
+
Born in <span className={styles.pill}>{year}</span>
|
|
51
|
+
</p>
|
|
52
|
+
{fact && <div className={styles.fact}>{fact.funFact}</div>}
|
|
53
|
+
{error && <div className={styles.error}>{error}</div>}
|
|
54
|
+
<button
|
|
55
|
+
className={styles.button}
|
|
56
|
+
onClick={getFunFact}
|
|
57
|
+
disabled={loading}
|
|
58
|
+
>
|
|
59
|
+
{loading ? 'Loading…' : 'Fun year fact'}
|
|
60
|
+
</button>
|
|
45
61
|
</div>
|
|
46
|
-
{additionalData && <div>{additionalData.funFact}</div>}
|
|
47
|
-
<button className={styles.button} onClick={getFunFact}>
|
|
48
|
-
Fun year fact
|
|
49
|
-
</button>
|
|
50
62
|
</div>
|
|
51
63
|
);
|
|
52
64
|
};
|
|
@@ -1,82 +1,47 @@
|
|
|
1
1
|
import { Server } from 'oc-server';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const yearsFunFactDatabase: Record<number, string> = {
|
|
9
|
-
1986: "Halley's Comet made its closest approach to Earth in 1986, visible to the naked eye.",
|
|
10
|
-
1987: 'The first Final Fantasy game was released in 1987.',
|
|
3
|
+
// Small lookup for the action below. Kept tiny on purpose: it only shows
|
|
4
|
+
// that an action receives input and returns data.
|
|
5
|
+
const funFacts: Record<number, string> = {
|
|
6
|
+
1986: "Halley's Comet made its closest approach to Earth in 1986.",
|
|
11
7
|
1991: 'The first web page was created in 1991 by Tim Berners-Lee.',
|
|
12
8
|
};
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(`User with ID ${userId} not found`);
|
|
17
|
-
}
|
|
18
|
-
return userDatabase[userId];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function getFunFact(year: number) {
|
|
22
|
-
return yearsFunFactDatabase[year] || `No fun fact available for year ${year}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Export the server instance - this is required for OC
|
|
10
|
+
// Tip: `new Server({ stream: true })` lets handlers and actions return
|
|
11
|
+
// Promises that resolve on the client instead of blocking the render.
|
|
26
12
|
export const server = new Server({
|
|
27
13
|
development: {
|
|
28
|
-
//
|
|
14
|
+
// Stream browser console logs to the terminal during `npm start`
|
|
29
15
|
console: true,
|
|
30
16
|
},
|
|
31
17
|
})
|
|
32
|
-
//
|
|
18
|
+
// Parameters become URL query strings, e.g. `?name=Ada&year=1986`
|
|
33
19
|
.withParameters({
|
|
34
|
-
|
|
35
|
-
default:
|
|
36
|
-
description: '
|
|
37
|
-
example:
|
|
38
|
-
|
|
20
|
+
name: {
|
|
21
|
+
default: 'Jane Doe',
|
|
22
|
+
description: 'Who to greet',
|
|
23
|
+
example: 'Jane Doe',
|
|
24
|
+
type: 'string',
|
|
25
|
+
},
|
|
26
|
+
year: {
|
|
27
|
+
default: 1991,
|
|
28
|
+
description: 'Birth year used for the fun fact',
|
|
29
|
+
example: 1991,
|
|
39
30
|
type: 'number',
|
|
40
31
|
},
|
|
41
32
|
})
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const { userId } = params;
|
|
46
|
-
const user = await getUser(userId);
|
|
47
|
-
const [firstName, lastName] = user.name.split(/\s+/);
|
|
48
|
-
|
|
49
|
-
if (firstName === 'Invalid') {
|
|
50
|
-
// Return undefined to prevent rendering
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Set cache headers when same params produce same result
|
|
55
|
-
// This improves performance by reducing server load
|
|
56
|
-
// Don't cache in local environment because we might get render mismatches
|
|
57
|
-
if (ctx.env.name !== 'local') {
|
|
58
|
-
ctx.setHeader('Cache-Control', 'max-age=300'); // Cache for 5 minutes
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Return data that will be passed as props to React component
|
|
62
|
-
return {
|
|
63
|
-
firstName,
|
|
64
|
-
lastName,
|
|
65
|
-
born: user.born,
|
|
66
|
-
hobbies: user.hobbies,
|
|
67
|
-
};
|
|
33
|
+
// Runs once per render. Its return value becomes the UI props.
|
|
34
|
+
.handler(async ({ name = 'Jane Doe', year = 1991 }) => {
|
|
35
|
+
return { name: name.trim() || 'Jane Doe', year };
|
|
68
36
|
})
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
.action('funFact', async (params: { year: number }) => {
|
|
72
|
-
const { year } = params;
|
|
73
|
-
const funFact = await getFunFact(year);
|
|
37
|
+
// Runs on demand from the browser via `serverClient.funFact({ year })`
|
|
38
|
+
.action('funFact', async ({ year }: { year: number }) => {
|
|
74
39
|
return {
|
|
75
|
-
funFact
|
|
40
|
+
funFact: funFacts[year] ?? `No fun fact available for year ${year}`,
|
|
76
41
|
};
|
|
77
42
|
});
|
|
78
43
|
|
|
79
|
-
//
|
|
44
|
+
// Enables type-safe props and actions on the client
|
|
80
45
|
declare module 'oc-server' {
|
|
81
46
|
interface Register {
|
|
82
47
|
server: typeof server;
|
|
@@ -1,30 +1,110 @@
|
|
|
1
1
|
.container {
|
|
2
|
-
background-color: #
|
|
2
|
+
background-color: #0f172a;
|
|
3
3
|
color: #fff;
|
|
4
|
-
font-family:
|
|
5
|
-
|
|
4
|
+
font-family: 'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', Roboto,
|
|
5
|
+
sans-serif;
|
|
6
|
+
padding: 56px 24px;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
.
|
|
9
|
-
background-color: #
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
.card {
|
|
10
|
+
background-color: #ffffff;
|
|
11
|
+
color: #0f172a;
|
|
12
|
+
border-radius: 20px;
|
|
13
|
+
padding: 32px;
|
|
14
|
+
max-width: 440px;
|
|
15
|
+
margin: 0 auto;
|
|
16
|
+
box-shadow: 0 12px 32px rgba(2, 6, 23, 0.45);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.brand {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
margin-bottom: 20px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.logo {
|
|
27
|
+
width: 40px;
|
|
28
|
+
height: 40px;
|
|
29
|
+
border-radius: 12px;
|
|
30
|
+
display: block;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.eyebrow {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
letter-spacing: 0.14em;
|
|
38
|
+
text-transform: uppercase;
|
|
39
|
+
color: #4f46e5;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.title {
|
|
43
|
+
margin: 0 0 8px 0;
|
|
44
|
+
font-size: 30px;
|
|
45
|
+
line-height: 1.15;
|
|
46
|
+
letter-spacing: -0.02em;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.name {
|
|
50
|
+
color: #4f46e5;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.subtitle {
|
|
54
|
+
margin: 0 0 4px 0;
|
|
13
55
|
font-size: 16px;
|
|
14
|
-
|
|
56
|
+
color: #475569;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pill {
|
|
15
60
|
display: inline-block;
|
|
16
|
-
color:
|
|
61
|
+
background-color: #eef2ff;
|
|
62
|
+
color: #4338ca;
|
|
63
|
+
border-radius: 999px;
|
|
64
|
+
padding: 2px 12px;
|
|
65
|
+
font-weight: 700;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.button {
|
|
69
|
+
margin-top: 20px;
|
|
70
|
+
width: 100%;
|
|
71
|
+
background-color: #4f46e5;
|
|
72
|
+
color: #fff;
|
|
73
|
+
border: none;
|
|
74
|
+
border-radius: 12px;
|
|
75
|
+
padding: 12px 24px;
|
|
76
|
+
font-size: 15px;
|
|
77
|
+
font-weight: 600;
|
|
17
78
|
cursor: pointer;
|
|
18
79
|
}
|
|
19
80
|
|
|
20
|
-
.
|
|
21
|
-
|
|
81
|
+
.button:hover {
|
|
82
|
+
background-color: #4338ca;
|
|
22
83
|
}
|
|
23
84
|
|
|
24
|
-
.
|
|
25
|
-
|
|
85
|
+
.button:disabled {
|
|
86
|
+
opacity: 0.6;
|
|
87
|
+
cursor: default;
|
|
26
88
|
}
|
|
27
89
|
|
|
28
|
-
.
|
|
29
|
-
|
|
90
|
+
.fact {
|
|
91
|
+
margin-top: 20px;
|
|
92
|
+
background-color: #eef2ff;
|
|
93
|
+
border-left: 4px solid #4f46e5;
|
|
94
|
+
border-radius: 0 12px 12px 0;
|
|
95
|
+
padding: 14px 16px;
|
|
96
|
+
font-size: 15px;
|
|
97
|
+
line-height: 1.5;
|
|
98
|
+
color: #1e1b4b;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.error {
|
|
102
|
+
margin-top: 20px;
|
|
103
|
+
background-color: #fef2f2;
|
|
104
|
+
border-left: 4px solid #ef4444;
|
|
105
|
+
border-radius: 0 12px 12px 0;
|
|
106
|
+
padding: 14px 16px;
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
line-height: 1.5;
|
|
109
|
+
color: #991b1b;
|
|
30
110
|
}
|
|
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite';
|
|
|
2
2
|
|
|
3
3
|
// https://vitejs.dev/config/
|
|
4
4
|
export default defineConfig({
|
|
5
|
-
// Add your own plugins here for
|
|
5
|
+
// Add your own plugins here for things like eslint or typecheckers
|
|
6
6
|
plugins: [],
|
|
7
7
|
// @ts-ignore Missing test property
|
|
8
8
|
test: {
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface OcParameters {
|
|
2
|
-
userId: number;
|
|
3
|
-
getMoreData?: boolean;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface AdditionalData {
|
|
7
|
-
hobbies: string[];
|
|
8
|
-
age: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ClientProps extends Partial<AdditionalData> {
|
|
12
|
-
userId: number;
|
|
13
|
-
firstName: string;
|
|
14
|
-
lastName: string;
|
|
15
|
-
}
|