Skip to content

Commit 8dd3d88

Browse files
authored
rework plugins (#187)
1 parent 7c00c78 commit 8dd3d88

24 files changed

+400
-1346
lines changed

.changeset/cool-students-look.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@enpage/template-example": patch
3+
"@enpage/editor": patch
4+
"@enpage/sdk": patch
5+
---
6+
7+
Rework plugins

packages/editor/README.md

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1 @@
1-
# React + TypeScript + Vite
2-
3-
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4-
5-
Currently, two official plugins are available:
6-
7-
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9-
10-
## Expanding the ESLint configuration
11-
12-
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13-
14-
- Configure the top-level `parserOptions` property like this:
15-
16-
```js
17-
export default {
18-
// other rules...
19-
parserOptions: {
20-
ecmaVersion: 'latest',
21-
sourceType: 'module',
22-
project: ['./tsconfig.json', './tsconfig.node.json'],
23-
tsconfigRootDir: __dirname,
24-
},
25-
}
26-
```
27-
28-
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29-
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30-
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
1+
# Enpage Editor Component

packages/sdk/INTERNALS.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ Table of differences between the 3 environments:
2626
The HTML document itself is the source of truth for the editor state.
2727
Liquid tags are used to define loops, conditions, and other logic in the HTML document.
2828

29+
## Server-side rendering
30+
31+
### Flow
32+
33+
The SSR flow is composed of 3 main steps:
34+
35+
1. Incoming Request
36+
2. Page Config handler: retrieves the page configuration including context (attributes and data sources)
37+
- In dev mode, the page configuration is loaded from the virtual file `virtual:enpage-page-config.json`
38+
- In local-preview mode, the page configuration is loaded from cache or from enpage.config.js (the local-preview is a node environment)
39+
- In production mode, the page configuration is fetched from the API
40+
3. Render handler: renders the HTML document using the page configuration
41+
- In dev mode, renderer is loaded from the virtual `virtual:vite-entry-server` file
42+
- In production/local-preview mode, the renderer is loaded from the real vite-entry-server file
43+
- The render function returns the HTML document and the state. Those are then merged and returned to the client.
44+
45+
## Vite
46+
47+
### Flow
48+
49+
The `vite-config.ts` is the equivalent of the `vite.config.js` file in a Vite project. It only load the enpage plugin.
50+
51+
### Vite plugins:
52+
53+
- enpage meta: loads all enpage plugins
54+
- enpage: main plugin defining the vite config
55+
- enpage:virtual-files: plugin to handle virtual files. Hooks used: `resolveId`, `load`. It returns the content of the virtual files:
56+
- `virtual:enpage-template:index.html`: the main index.html template file
57+
- `virtual:vite-entry-server`: the entry server file
58+
- `virtual:enpage-page-config.json`: returns a virtual json file containing the GenericPageConfig.
59+
- enpage:context: plugin to handle the PageContext. In dev mode, it generates a fake context. In non-ssr build mode, it fetches the context from the server. Hooks used: `config`. It adds the
60+
`enpageContext` property (of type `PageContext`) to the vite config.
61+
- enpage:render: plugin to handle the rendering of the main index.html file. It uses the transformIndexHtml hook to render the HTML document using the page context. Hooks used: `transformIndexHtml`, `configureServer`.
62+
- enpage:base-url (dev only): plugin to handle the base URL of the website. It uses the transformIndexHtml hook to inject the base URL in the HTML document. Hooks used: `transformIndexHtml`, `configureServer`.
63+
- enpage:manifest: plugin to handle the manifest file. It uses the `generateBundle` hook to generate the enpage.manifest.json file.
2964

3065
## Rendering flow
3166

packages/sdk/env.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type { CloudflareWorkersPlatformInfo } from "@hattip/adapter-cloudflare-w
55
declare global {
66
interface Window {
77
enpage: import("./src/browser/js-api").EnpageJavascriptAPI;
8-
__ENPAGE_STATE__: ConstructorParameters<typeof import("./src/browser/js-api").EnpageJavascriptAPI>;
8+
__ENPAGE_STATE__: import("./src/browser/js-api").State;
9+
__ENPAGE_ROUTING__: import("./src/browser/js-api").Routing;
910
}
1011
}
1112

packages/sdk/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"postcss-preset-env": "10.0.2",
7676
"rimraf": "6.0.1",
7777
"rollup-plugin-strip-banner": "3.1.0",
78+
"serve-favicon": "2.5.0",
7879
"sharp": "0.33.5",
7980
"svgo": "3.3.2",
8081
"tailwindcss": "3.4.10",
@@ -98,6 +99,7 @@
9899
"@types/jsdom": "21.1.7",
99100
"@types/lodash-es": "4.17.12",
100101
"@types/node": "^20.14.10",
102+
"@types/serve-favicon": "2.5.7",
101103
"concurrently": "8.2.2",
102104
"tsup": "8.2.4"
103105
},

packages/sdk/src/browser/js-api.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import type { PageContext } from "../shared/page-context";
1+
import type { PageContext } from "../shared/page-config";
22
import type { NavigateEvent } from "./events";
33

4+
export type State = {
5+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
6+
ctx: PageContext<any, any>;
7+
pageIndex: number;
8+
};
9+
410
export class EnpageJavascriptAPI extends EventTarget {
11+
private _totalPages: number;
12+
513
constructor(
6-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
7-
private ctx: PageContext<any, any>,
8-
private pageIndex = 0,
9-
private pagesCount = 1,
10-
private pagesSlugs: string[] = [],
14+
private state: State,
15+
private _slugs: string[] = [],
1116
) {
1217
super();
18+
this._totalPages = this.slugs.length
19+
? this.slugs.length + 1
20+
: Array.from(document.querySelectorAll("body > section")).length;
1321
this.setupListeners();
1422
}
1523

@@ -18,10 +26,14 @@ export class EnpageJavascriptAPI extends EventTarget {
1826
const evt = e as NavigateEvent;
1927
const oldIndex = evt.detail.from;
2028
const newIndex = evt.detail.to;
21-
const slug = this.pagesSlugs.at(newIndex - 1);
29+
const slug = this.slugs.at(newIndex - 1);
2230

23-
this.pageIndex = newIndex;
24-
history.pushState({ page: newIndex }, "", newIndex === 0 ? "/" : slug ? `/${slug}` : undefined);
31+
this.state.pageIndex = newIndex;
32+
history.pushState(
33+
{ page: newIndex },
34+
"",
35+
newIndex === 0 ? "/" : slug ? `/${slug}` : `/page-${newIndex}`,
36+
);
2537

2638
this.dispatchEvent(
2739
new CustomEvent("afternavigate", {
@@ -33,15 +45,15 @@ export class EnpageJavascriptAPI extends EventTarget {
3345
}
3446

3547
get currentPage() {
36-
return this.pageIndex;
48+
return this.state.pageIndex;
3749
}
3850

3951
get totalPages() {
40-
return this.pagesCount;
52+
return this._totalPages;
4153
}
4254

4355
get context() {
44-
return this.ctx;
56+
return this.state.ctx;
4557
}
4658

4759
nextPage() {
@@ -69,7 +81,7 @@ export class EnpageJavascriptAPI extends EventTarget {
6981
}
7082

7183
goToPage(index: number) {
72-
if (index < 0 || index >= this.pagesCount) {
84+
if (index < 0 || index >= this.totalPages) {
7385
throw new RangeError(`Invalid page index: ${index}`);
7486
}
7587
this.dispatchEvent(
@@ -94,23 +106,23 @@ export class EnpageJavascriptAPI extends EventTarget {
94106
lastPage() {
95107
this.dispatchEvent(
96108
new CustomEvent("beforenavigate", {
97-
detail: { from: this.currentPage, to: this.pagesCount - 1 },
109+
detail: { from: this.currentPage, to: this.totalPages - 1 },
98110
bubbles: true,
99111
cancelable: true,
100112
}),
101113
);
102114
}
103115

104116
get canGoBack() {
105-
return this.pageIndex > 0;
117+
return this.state.pageIndex > 0;
106118
}
107119

108120
get canGoForward() {
109-
return this.pageIndex < this.pagesCount - 1;
121+
return this.state.pageIndex < this.totalPages - 1;
110122
}
111123

112124
get slugs() {
113-
return this.pagesSlugs;
125+
return this._slugs;
114126
}
115127

116128
async saveDataRecord(dataRecordId: string, record: Record<string, unknown>) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Retrieves all page sections from the document.
3+
* @param {Document} doc - The document to search for sections.
4+
*/
5+
export function getPageSections(doc: Document) {
6+
const sections = doc.querySelectorAll("body > section[ep-type='page']");
7+
return sections;
8+
}
9+
/**
10+
* Processes page sections, setting attributes and generating slugs.
11+
* @param {NodeListOf<Element>} sections - The list of section elements to process.
12+
*/
13+
export function processPageSections(sections: NodeListOf<Element>) {
14+
const slugs: string[] = [];
15+
sections.forEach((section, index) => {
16+
section.setAttribute("ep-label", `Page ${index + 1}`);
17+
if (!section.getAttribute("id")) {
18+
section.setAttribute("id", `page-${index + 1}`);
19+
}
20+
if (!section.getAttribute("ep-animate-appear")) {
21+
section.setAttribute("ep-animate-appear", "fadeIn");
22+
}
23+
if (!section.getAttribute("ep-animate-disappear")) {
24+
section.setAttribute("ep-animate-disappear", "fadeOut");
25+
}
26+
// add [ep-editable] if not present
27+
if (!section.getAttribute("ep-editable")) {
28+
section.setAttribute("ep-editable", "");
29+
}
30+
// hide all sections except the first one
31+
if (index > 0) {
32+
section.setAttribute("hidden", "");
33+
let slug = section.getAttribute("ep-slug");
34+
if (!slug) {
35+
slug = `page-${index + 1}`;
36+
section.setAttribute("ep-slug", slug);
37+
}
38+
slugs.push(slug);
39+
} else {
40+
// make sure the first section has role="main"
41+
section.setAttribute("role", "main");
42+
// make sure the first section has no slug
43+
section.removeAttribute("ep-slug");
44+
// hide it by default if it as an animation
45+
if (section.hasAttribute("ep-animate-appear")) {
46+
section.setAttribute("hidden", "");
47+
}
48+
}
49+
});
50+
return { slugs, pageCount: sections.length };
51+
}
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EnpageJavascriptAPI } from "./js-api";
2-
if (window.__ENPAGE_STATE__) {
3-
console.debug("Loading Enpage Javascript API...");
4-
window.enpage = new EnpageJavascriptAPI(...window.__ENPAGE_STATE__);
5-
}
2+
import { getPageSections, processPageSections } from "./page-sections";
3+
const sections = getPageSections(document);
4+
const { slugs } = processPageSections(sections);
5+
console.debug("Loading Enpage Javascript API...");
6+
window.enpage = new EnpageJavascriptAPI(window.__ENPAGE_STATE__, slugs);

packages/sdk/src/node/builder/context.ts renamed to packages/sdk/src/node/builder/page-context.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { EnpageTemplateConfig } from "~/shared/template-config";
2-
import type { PageContext } from "~/shared/page-context";
2+
import type { PageContext } from "~/shared/page-config";
33
import { samples } from "~/shared/datasources/samples";
44
import type { AttributesResolved } from "~/shared/attributes";
55
import invariant from "~/shared/utils/invariant";
@@ -22,8 +22,9 @@ export function createFakeContext<Config extends EnpageTemplateConfig>(cfg: Conf
2222

2323
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
2424
const attributes: AttributesResolved<any> = {};
25-
for (const key in cfg.attributes) {
26-
attributes[key] = cfg.attributes[key].defaultValue;
25+
26+
for (const key in cfg.attributes.properties) {
27+
attributes[key] = cfg.attributes.properties[key].default;
2728
}
2829

2930
return { data, attr: attributes } as PageContext<typeof cfg.datasources, typeof cfg.attributes>;

packages/sdk/src/node/builder/plugin-context.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { EnpageTemplateConfig } from "~/shared/template-config";
22
import type { ConfigEnv, Plugin } from "vite";
3-
import type { PageContext } from "~/shared/page-context";
4-
import { createFakeContext, fetchContext } from "./context";
3+
import type { PageContext } from "~/shared/page-config";
4+
import { createFakeContext, fetchContext } from "./page-context";
55
import type { EnpageEnv } from "~/shared/env";
6-
import type { Logger } from "../shared/logger";
76

87
export const contextPlugin = (cfg: EnpageTemplateConfig, viteEnv: ConfigEnv, env: EnpageEnv): Plugin => {
98
const isBuildMode = viteEnv.command === "build";
@@ -23,7 +22,7 @@ export const contextPlugin = (cfg: EnpageTemplateConfig, viteEnv: ConfigEnv, env
2322

2423
// If in dev mode, use fake context
2524
if (!isBuildMode) {
26-
// console.warn("Using fake context.");
25+
console.warn("Using fake context.");
2726
context = createFakeContext(cfg);
2827
// If in build mode, fetch context from API if not SSR build
2928
} else if (!isSsrBuild) {

0 commit comments

Comments
 (0)