handoff-app 1.1.0 → 1.1.2-0
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/Changelog.md +58 -0
- package/README.md +7 -0
- package/dist/app-builder/build.js +20 -11
- package/dist/app-builder/watchers.js +8 -9
- package/dist/cache/build-cache.js +25 -10
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/transformers/plugins/handlebars-previews.js +17 -5
- package/dist/transformers/preview/component/builder.js +16 -1
- package/dist/transformers/utils/handlebars.d.ts +4 -2
- package/dist/transformers/utils/handlebars.js +10 -5
- package/dist/transformers/utils/html.js +15 -3
- package/dist/types/config.d.ts +25 -0
- package/dist/utils/path.d.ts +15 -0
- package/dist/utils/path.js +31 -0
- package/docs/api.md +65 -71
- package/package.json +1 -1
- package/src/app/components/Component/ComponentLists.tsx +5 -4
- package/src/app/components/Navigation/MainNav.tsx +3 -5
- package/src/app/components/Navigation/SideNav.tsx +4 -7
- package/src/app/components/SideNav/Custom.tsx +5 -3
- package/src/app/components/ui/mobile-nav.tsx +10 -7
- package/src/app/components/util/index.ts +2 -2
- package/src/app/lib/utils.ts +13 -0
- package/src/app/next.config.mjs +21 -13
- package/src/app/pages/system/component/[component]/index.tsx +1 -1
- package/src/app-builder/build.ts +21 -11
- package/src/app-builder/watchers.ts +9 -8
- package/src/cache/build-cache.ts +29 -11
- package/src/cli.ts +1 -1
- package/src/index.ts +1 -1
- package/src/transformers/plugins/handlebars-previews.ts +22 -6
- package/src/transformers/preview/component/builder.ts +17 -1
- package/src/transformers/utils/handlebars.ts +17 -9
- package/src/transformers/utils/html.ts +16 -3
- package/src/types/config.ts +25 -0
- package/src/utils/path.ts +29 -0
- package/templates/default/handoff.config.js.tpl +16 -0
- package/templates/default/handoff.config.with-examples.js.tpl +16 -0
package/docs/api.md
CHANGED
|
@@ -78,70 +78,65 @@ to generate only the integration code.
|
|
|
78
78
|
|
|
79
79
|
## Hooks
|
|
80
80
|
|
|
81
|
-
Hooks
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
For example if you want to alter the integration output, you can do this -
|
|
81
|
+
Hooks let you extend the build and preview pipeline. Configure them in
|
|
82
|
+
`handoff.config.js` (or `.cjs`) under the top-level `hooks` object. Hook names
|
|
83
|
+
use camelCase (for example `validateComponent`, `jsBuildConfig`,
|
|
84
|
+
`registerHandlebarsHelpers`). The Handoff CLI and Node API load this config when
|
|
85
|
+
the `Handoff` instance is created.
|
|
88
86
|
|
|
89
87
|
```js
|
|
90
|
-
|
|
88
|
+
// handoff.config.js
|
|
89
|
+
module.exports = {
|
|
90
|
+
// …
|
|
91
|
+
hooks: {
|
|
92
|
+
registerHandlebarsHelpers: ({ handlebars, componentId }) => {
|
|
93
|
+
handlebars.registerHelper('upperId', () => componentId.toUpperCase());
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
```
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
// You can customize the configuration here
|
|
94
|
-
});
|
|
99
|
+
### registerHandlebarsHelpers
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
value: color.value,
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
data.push({
|
|
104
|
-
filename: 'colors.json',
|
|
105
|
-
data: JSON.stringify(colors, null, 2),
|
|
106
|
-
});
|
|
107
|
-
return data;
|
|
108
|
-
});
|
|
101
|
+
Called immediately after Handoff registers built-in Handlebars helpers (`field`
|
|
102
|
+
and `eq`) while rendering **Handlebars** component previews (static HTML per
|
|
103
|
+
variation). Use it to call `handlebars.registerHelper` for custom helpers used
|
|
104
|
+
in your `.hbs` templates.
|
|
109
105
|
|
|
110
|
-
|
|
111
|
-
```
|
|
106
|
+
**Arguments (single context object)**
|
|
112
107
|
|
|
113
|
-
|
|
108
|
+
* `handlebars` — The Handlebars runtime (same as importing `handlebars`); use
|
|
109
|
+
`registerHelper`, `SafeString`, etc.
|
|
110
|
+
* `componentId` — The current component’s id string.
|
|
111
|
+
* `properties` — The component’s slot metadata map from the schema (same shape
|
|
112
|
+
as used by the `field` helper).
|
|
113
|
+
* `injectFieldWrappers` — `true` when generating inspect-mode previews (field
|
|
114
|
+
inspection markup), `false` for normal previews.
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
after the integration build is complete. The function accepts two arguments -
|
|
116
|
+
**Return**
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
* `void` — register helpers on `handlebars` directly.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
DocumentationObject containing all of the tokens.
|
|
122
|
-
- `data: HookReturn[]` This an array of HookReturns, consisting of `filename`
|
|
123
|
-
and `data`. Each object in this array will be written out to
|
|
124
|
-
`exported/{integration}-tokens`
|
|
120
|
+
**Notes**
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
* This hook runs once per preview render (each variation × normal vs inspect).
|
|
123
|
+
* Registering the same helper name again replaces any previous registration for
|
|
124
|
+
subsequent renders in the same process.
|
|
128
125
|
|
|
129
|
-
|
|
126
|
+
**Example**
|
|
130
127
|
|
|
131
128
|
```js
|
|
132
|
-
handoff.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return data;
|
|
144
|
-
});
|
|
129
|
+
// handoff.config.js
|
|
130
|
+
module.exports = {
|
|
131
|
+
hooks: {
|
|
132
|
+
registerHandlebarsHelpers: ({ handlebars, componentId, injectFieldWrappers }) => {
|
|
133
|
+
handlebars.registerHelper('debugPreview', function () {
|
|
134
|
+
return injectFieldWrappers ? '[inspect]' : '[preview]';
|
|
135
|
+
});
|
|
136
|
+
handlebars.registerHelper('componentLabel', () => componentId);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
145
140
|
```
|
|
146
141
|
|
|
147
142
|
### postBuild
|
|
@@ -150,7 +145,7 @@ This function is called after the app build is complete.
|
|
|
150
145
|
|
|
151
146
|
**arguments**
|
|
152
147
|
|
|
153
|
-
|
|
148
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
154
149
|
DocumentationObject containing all of the tokens.
|
|
155
150
|
|
|
156
151
|
**returns**
|
|
@@ -163,14 +158,14 @@ application to alter the css generation in transit
|
|
|
163
158
|
|
|
164
159
|
**arguments**
|
|
165
160
|
|
|
166
|
-
|
|
161
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
167
162
|
DocumentationObject containing all of the tokens.
|
|
168
|
-
|
|
163
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
169
164
|
tokens for components and foundations, formatted as CSS variables
|
|
170
165
|
|
|
171
166
|
**returns**
|
|
172
167
|
|
|
173
|
-
|
|
168
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
174
169
|
tokens for components and foundations, formatted as CSS variables.
|
|
175
170
|
Any changes you return here will be written to the css files
|
|
176
171
|
|
|
@@ -182,14 +177,14 @@ the transformed output prior to being written to disk.
|
|
|
182
177
|
|
|
183
178
|
**arguments**
|
|
184
179
|
|
|
185
|
-
|
|
180
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
186
181
|
DocumentationObject containing all of the tokens.
|
|
187
|
-
|
|
182
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
188
183
|
tokens for components and foundations, formatted as CSS variables
|
|
189
184
|
|
|
190
185
|
**returns**
|
|
191
186
|
|
|
192
|
-
|
|
187
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
193
188
|
tokens for components and foundations, formatted as CSS variables.
|
|
194
189
|
Any changes you return here will be written to the css files.
|
|
195
190
|
|
|
@@ -201,14 +196,14 @@ the transformed output prior to being written to disk.
|
|
|
201
196
|
|
|
202
197
|
**arguments**
|
|
203
198
|
|
|
204
|
-
|
|
199
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
205
200
|
DocumentationObject containing all of the tokens.
|
|
206
|
-
|
|
201
|
+
* `scss: CssTransformerOutput` This is an object containing all of the
|
|
207
202
|
tokens for components and foundations, formatted as SCSS variables
|
|
208
203
|
|
|
209
204
|
**returns**
|
|
210
205
|
|
|
211
|
-
|
|
206
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
212
207
|
tokens for components and foundations, formatted as SCSS variables.
|
|
213
208
|
Any changes you return here will be written to the scss files.
|
|
214
209
|
|
|
@@ -224,14 +219,14 @@ the transformed output prior to being written to disk.
|
|
|
224
219
|
|
|
225
220
|
**arguments**
|
|
226
221
|
|
|
227
|
-
|
|
222
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
228
223
|
DocumentationObject containing all of the tokens.
|
|
229
|
-
|
|
224
|
+
* `scss: CssTransformerOutput` This is an object containing all of the
|
|
230
225
|
type arrays for components and foundations, formatted as SCSS variables
|
|
231
226
|
|
|
232
227
|
**returns**
|
|
233
228
|
|
|
234
|
-
|
|
229
|
+
* `css: CssTransformerOutput` This is an object containing all of the
|
|
235
230
|
tokens for components and foundations, formatted as type variables.
|
|
236
231
|
Any changes you return here will be written to the scss type files.
|
|
237
232
|
|
|
@@ -244,11 +239,11 @@ and allows you to alter and return that configuration.
|
|
|
244
239
|
|
|
245
240
|
**arguments**
|
|
246
241
|
|
|
247
|
-
|
|
242
|
+
* `webpackConfig: webpack.Configuration` This is a full webpack configuration.
|
|
248
243
|
|
|
249
244
|
**returns**
|
|
250
245
|
|
|
251
|
-
|
|
246
|
+
* `webpackConfig: webpack.Configuration` Return the webpack configuration that
|
|
252
247
|
you have altered to fit your needs.
|
|
253
248
|
|
|
254
249
|
**Example**
|
|
@@ -265,15 +260,14 @@ export const modifyWebpackConfigForTailwind = (webpackConfig: webpack.Configurat
|
|
|
265
260
|
|
|
266
261
|
This hook allows you to alter the exportable list. You could do this by ejecting
|
|
267
262
|
the handoff configuration and modifying the list, but this hook allows you to
|
|
268
|
-
alter the exportable list with just a couple of lines of code rather than
|
|
263
|
+
alter the exportable list with just a couple of lines of code rather than
|
|
269
264
|
exporting the whole list
|
|
270
265
|
|
|
271
266
|
**arguments**
|
|
272
267
|
|
|
273
|
-
|
|
268
|
+
* `exportables: string[]` The current list of exportables
|
|
274
269
|
|
|
275
270
|
**returns**
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
you need for your application.
|
|
279
|
-
|
|
272
|
+
* `exportables: string[]` Return the list with whatever additions and subtractions
|
|
273
|
+
you need for your application.
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import HeadersType from '../Typography/Headers';
|
|
|
8
8
|
import { Button } from '../ui/button';
|
|
9
9
|
import { Input } from '../ui/input';
|
|
10
10
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
|
11
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
|
11
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
12
12
|
import { Metadata } from '../util';
|
|
13
13
|
|
|
14
14
|
interface ComponentMetadata extends Metadata {
|
|
@@ -74,7 +74,8 @@ export const ComponentList = ({
|
|
|
74
74
|
if (!title) title = 'Components';
|
|
75
75
|
if (!description) description = 'Self-contained reusable UI elements that can be used to build larger blocks or design patterns.';
|
|
76
76
|
return (
|
|
77
|
-
<
|
|
77
|
+
<TooltipProvider delayDuration={0}>
|
|
78
|
+
<div className="mx-auto w-full mb-4">
|
|
78
79
|
<div className="flex justify-between bg-accent rounded-xl mb-8">
|
|
79
80
|
<div className="mr-auto flex items-center gap-3 bg-accent p-2.5 rounded-md">
|
|
80
81
|
<div className="relative">
|
|
@@ -183,8 +184,8 @@ export const ComponentList = ({
|
|
|
183
184
|
})}
|
|
184
185
|
</div>
|
|
185
186
|
)}
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
</div>
|
|
188
|
+
</TooltipProvider>
|
|
188
189
|
);
|
|
189
190
|
};
|
|
190
191
|
|
|
@@ -31,13 +31,12 @@ export function MainNav() {
|
|
|
31
31
|
<NavigationMenuLink className={navigationMenuTriggerStyle()} asChild>
|
|
32
32
|
<Link
|
|
33
33
|
href={section.path}
|
|
34
|
-
passHref
|
|
35
34
|
className={cn(
|
|
36
35
|
'block select-none space-y-1 rounded-sm p-3 leading-none no-underline outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground'
|
|
37
36
|
)}
|
|
38
37
|
{...(isActive ? { 'data-active': 'true' } : {})}
|
|
39
38
|
>
|
|
40
|
-
{section.title}
|
|
39
|
+
{section.title}
|
|
41
40
|
</Link>
|
|
42
41
|
</NavigationMenuLink>
|
|
43
42
|
</>
|
|
@@ -51,11 +50,10 @@ export function MainNav() {
|
|
|
51
50
|
<NavigationMenuLink className={navigationMenuTriggerStyle()} asChild>
|
|
52
51
|
<Link
|
|
53
52
|
href={section.path}
|
|
54
|
-
passHref
|
|
55
|
-
legacyBehavior
|
|
56
53
|
className="block select-none space-y-1 rounded-sm p-3 leading-none no-underline outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
54
|
+
{...(isActive ? { 'data-active': 'true' } : {})}
|
|
57
55
|
>
|
|
58
|
-
<
|
|
56
|
+
<span className="text-sm leading-none">{section.title}</span>
|
|
59
57
|
</Link>
|
|
60
58
|
</NavigationMenuLink>
|
|
61
59
|
)}
|
|
@@ -30,19 +30,16 @@ import {
|
|
|
30
30
|
SidebarMenuSub,
|
|
31
31
|
SidebarSeparator,
|
|
32
32
|
} from '../../components/ui/sidebar';
|
|
33
|
+
import { normalizePathForMatch, toAbsolutePath } from '../../lib/utils';
|
|
33
34
|
import { SectionLink } from '../util';
|
|
34
35
|
|
|
35
|
-
const trimSlashes = (input: string): string => {
|
|
36
|
-
return input.replace(/^\/+|\/+$/g, '');
|
|
37
|
-
};
|
|
38
|
-
|
|
39
36
|
const NormalMenuItem = ({ title, icon, path }) => {
|
|
40
37
|
const router = useRouter();
|
|
41
|
-
const isActive =
|
|
38
|
+
const isActive = normalizePathForMatch(path) === normalizePathForMatch(router.asPath);
|
|
42
39
|
return (
|
|
43
40
|
<SidebarMenuItem>
|
|
44
41
|
<SidebarMenuButton asChild isActive={isActive}>
|
|
45
|
-
<a href={
|
|
42
|
+
<a href={toAbsolutePath(path)} className="gap-3">
|
|
46
43
|
<MenuIcon icon={icon} isActive={isActive} />
|
|
47
44
|
<span>{title}</span>
|
|
48
45
|
</a>
|
|
@@ -54,7 +51,7 @@ const NormalMenuItem = ({ title, icon, path }) => {
|
|
|
54
51
|
const CollapsibleMenuItem = ({ title, icon, path, menu }) => {
|
|
55
52
|
const router = useRouter();
|
|
56
53
|
const isActive = menu.some(
|
|
57
|
-
(item) =>
|
|
54
|
+
(item) => normalizePathForMatch(router.asPath).startsWith(normalizePathForMatch(item.path))
|
|
58
55
|
);
|
|
59
56
|
return (
|
|
60
57
|
<Collapsible defaultOpen={isActive} className="group/collapsible">
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { usePathname } from 'next/navigation';
|
|
2
|
+
import { normalizePathForMatch, toAbsolutePath } from '../../lib/utils';
|
|
2
3
|
import NavLink from '../NavLink';
|
|
3
4
|
import { SectionLink } from '../util';
|
|
4
5
|
|
|
5
6
|
const CustomNav = ({ menu }: { menu: SectionLink }) => {
|
|
6
7
|
const pathname = usePathname();
|
|
8
|
+
const normalizedPathname = normalizePathForMatch(pathname ?? '');
|
|
7
9
|
return (
|
|
8
10
|
<nav className="c-sidenav">
|
|
9
11
|
<ul>
|
|
@@ -13,14 +15,14 @@ const CustomNav = ({ menu }: { menu: SectionLink }) => {
|
|
|
13
15
|
<small>{item.title}</small>
|
|
14
16
|
) : (
|
|
15
17
|
<>
|
|
16
|
-
<NavLink href={
|
|
18
|
+
<NavLink href={toAbsolutePath(item.path)}>
|
|
17
19
|
{item.title}
|
|
18
20
|
</NavLink>
|
|
19
21
|
{item.menu && (
|
|
20
|
-
<ul className={`c-sidenav__sub${
|
|
22
|
+
<ul className={`c-sidenav__sub${normalizedPathname.startsWith(normalizePathForMatch(item.path)) ? ' is-active' : ''}`}>
|
|
21
23
|
{item.menu.map((subItem) => (
|
|
22
24
|
<li key={subItem.path}>
|
|
23
|
-
<NavLink key={subItem.path} href={
|
|
25
|
+
<NavLink key={subItem.path} href={toAbsolutePath(subItem.path)} className="c-sidenav__sub-item">
|
|
24
26
|
{subItem.title}
|
|
25
27
|
</NavLink>
|
|
26
28
|
</li>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { usePathname
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
+
import { normalizePathForMatch, toAbsolutePath } from '../../lib/utils';
|
|
6
7
|
import { SectionLink } from '../util';
|
|
7
8
|
|
|
8
9
|
import { Button } from './button';
|
|
@@ -15,7 +16,7 @@ interface MobileNavProps {
|
|
|
15
16
|
export function MobileNav({ menu }: MobileNavProps) {
|
|
16
17
|
const [open, setOpen] = React.useState(false);
|
|
17
18
|
const pathname = usePathname();
|
|
18
|
-
const
|
|
19
|
+
const normalizedPathname = normalizePathForMatch(pathname ?? '');
|
|
19
20
|
|
|
20
21
|
const onOpenChange = React.useCallback((open: boolean) => {
|
|
21
22
|
setOpen(open);
|
|
@@ -52,24 +53,26 @@ export function MobileNav({ menu }: MobileNavProps) {
|
|
|
52
53
|
) : (
|
|
53
54
|
<>
|
|
54
55
|
<Link
|
|
55
|
-
href={
|
|
56
|
+
href={toAbsolutePath(item.path)}
|
|
56
57
|
className="text-[1.15rem]"
|
|
57
58
|
onClick={() => {
|
|
58
|
-
router.push(`/${item.path}`);
|
|
59
59
|
setOpen(false);
|
|
60
60
|
}}
|
|
61
61
|
>
|
|
62
62
|
{item.title}
|
|
63
63
|
</Link>
|
|
64
64
|
{item.menu && (
|
|
65
|
-
<ul
|
|
65
|
+
<ul
|
|
66
|
+
className={`ml-4 mt-2 space-y-2${
|
|
67
|
+
normalizedPathname.startsWith(normalizePathForMatch(item.path)) ? ' is-active' : ''
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
66
70
|
{item.menu.map((subItem) => (
|
|
67
71
|
<li key={subItem.path}>
|
|
68
72
|
<Link
|
|
69
|
-
href={
|
|
73
|
+
href={toAbsolutePath(subItem.path)}
|
|
70
74
|
className="text-[1rem] opacity-80"
|
|
71
75
|
onClick={() => {
|
|
72
|
-
router.push(`/${subItem.path}`);
|
|
73
76
|
setOpen(false);
|
|
74
77
|
}}
|
|
75
78
|
>
|
|
@@ -290,10 +290,10 @@ export const staticBuildMenu = () => {
|
|
|
290
290
|
};
|
|
291
291
|
|
|
292
292
|
const buildBasePath = () => {
|
|
293
|
-
if(!process.env.HANDOFF_APP_BASE_PATH) {
|
|
293
|
+
if (!process.env.HANDOFF_APP_BASE_PATH) {
|
|
294
294
|
return '';
|
|
295
295
|
}
|
|
296
|
-
return (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(
|
|
296
|
+
return (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '') + '/';
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
const staticBuildComponentMenu = (type?: boolean | string) => {
|
package/src/app/lib/utils.ts
CHANGED
|
@@ -5,6 +5,19 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
5
5
|
return twMerge(clsx(inputs));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export const trimSlashes = (input: string): string => {
|
|
9
|
+
return input.replace(/^\/+|\/+$/g, '');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const toAbsolutePath = (input: string): string => {
|
|
13
|
+
return `/${trimSlashes(input)}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const normalizePathForMatch = (input: string): string => {
|
|
17
|
+
const [pathname] = input.split(/[?#]/);
|
|
18
|
+
return trimSlashes(pathname);
|
|
19
|
+
};
|
|
20
|
+
|
|
8
21
|
/**
|
|
9
22
|
* Filters out null values
|
|
10
23
|
* @param value
|
package/src/app/next.config.mjs
CHANGED
|
@@ -2,6 +2,14 @@ import chalk from 'chalk';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
+
const resolveBasePath = (rawBasePath) => {
|
|
6
|
+
if (!rawBasePath || rawBasePath.startsWith('%HANDOFF_')) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
const trimmed = rawBasePath.replace(/^\/+|\/+$/g, '');
|
|
10
|
+
return trimmed ? `/${trimmed}` : '';
|
|
11
|
+
};
|
|
12
|
+
|
|
5
13
|
/** @type {import('next').NextConfig} */
|
|
6
14
|
const nextConfig = {
|
|
7
15
|
output: 'export',
|
|
@@ -19,14 +27,14 @@ const nextConfig = {
|
|
|
19
27
|
tsconfigPath: 'tsconfig.json',
|
|
20
28
|
},
|
|
21
29
|
//distDir: 'out',
|
|
22
|
-
basePath: '',
|
|
30
|
+
basePath: resolveBasePath('%HANDOFF_APP_BASE_PATH%'),
|
|
23
31
|
env: {
|
|
24
|
-
HANDOFF_PROJECT_ID: '',
|
|
25
|
-
HANDOFF_APP_BASE_PATH: '',
|
|
26
|
-
HANDOFF_WORKING_PATH: '',
|
|
27
|
-
HANDOFF_MODULE_PATH: '',
|
|
28
|
-
HANDOFF_EXPORT_PATH: '',
|
|
29
|
-
HANDOFF_WEBSOCKET_PORT: '',
|
|
32
|
+
HANDOFF_PROJECT_ID: '%HANDOFF_PROJECT_ID%',
|
|
33
|
+
HANDOFF_APP_BASE_PATH: '%HANDOFF_APP_BASE_PATH%',
|
|
34
|
+
HANDOFF_WORKING_PATH: '%HANDOFF_WORKING_PATH%',
|
|
35
|
+
HANDOFF_MODULE_PATH: '%HANDOFF_MODULE_PATH%',
|
|
36
|
+
HANDOFF_EXPORT_PATH: '%HANDOFF_EXPORT_PATH%',
|
|
37
|
+
HANDOFF_WEBSOCKET_PORT: '%HANDOFF_WEBSOCKET_PORT%',
|
|
30
38
|
},
|
|
31
39
|
images: {
|
|
32
40
|
unoptimized: true,
|
|
@@ -38,12 +46,12 @@ const nextConfig = {
|
|
|
38
46
|
|
|
39
47
|
// Local environment
|
|
40
48
|
const env = {
|
|
41
|
-
HANDOFF_PROJECT_ID: '',
|
|
42
|
-
HANDOFF_APP_BASE_PATH: '',
|
|
43
|
-
HANDOFF_WORKING_PATH: '',
|
|
44
|
-
HANDOFF_MODULE_PATH: '',
|
|
45
|
-
HANDOFF_EXPORT_PATH: '',
|
|
46
|
-
HANDOFF_WEBSOCKET_PORT: '',
|
|
49
|
+
HANDOFF_PROJECT_ID: '%HANDOFF_PROJECT_ID%',
|
|
50
|
+
HANDOFF_APP_BASE_PATH: '%HANDOFF_APP_BASE_PATH%',
|
|
51
|
+
HANDOFF_WORKING_PATH: '%HANDOFF_WORKING_PATH%',
|
|
52
|
+
HANDOFF_MODULE_PATH: '%HANDOFF_MODULE_PATH%',
|
|
53
|
+
HANDOFF_EXPORT_PATH: '%HANDOFF_EXPORT_PATH%',
|
|
54
|
+
HANDOFF_WEBSOCKET_PORT: '%HANDOFF_WEBSOCKET_PORT%',
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
// Check if client configuration exists
|
|
@@ -163,7 +163,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo
|
|
|
163
163
|
}, [component, id]);
|
|
164
164
|
|
|
165
165
|
if (!component) return <p>Loading...</p>;
|
|
166
|
-
const apiUrl = (window.location.origin && window.location.origin) +
|
|
166
|
+
const apiUrl = (window.location.origin && window.location.origin) + `${normalizedBasePath}/api/component/${id}.json`;
|
|
167
167
|
return (
|
|
168
168
|
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
169
169
|
<div className="flex flex-col gap-3 pb-14">
|
package/src/app-builder/build.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { getAppPath, syncPublicFiles } from './paths';
|
|
|
12
12
|
import { WatcherState, getRuntimeComponentsPathsToWatch, watchAppSource, watchGlobalEntries, watchPages, watchPublicDirectory, watchRuntimeComponents, watchRuntimeConfiguration } from './watchers';
|
|
13
13
|
import { createWebSocketServer } from './websocket';
|
|
14
14
|
|
|
15
|
+
const escapeForSingleQuotedJsString = (value: string): string => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Performs cleanup of the application directory by removing the existing app directory if it exists.
|
|
17
19
|
*/
|
|
@@ -49,8 +51,7 @@ const initializeProjectApp = async (handoff: Handoff): Promise<string> => {
|
|
|
49
51
|
Logger.success(`Custom theme.css loaded`);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
// Prepare project app configuration
|
|
53
|
-
// Warning: Regex replacement is fragile and depends on exact formatting in next.config.mjs
|
|
54
|
+
// Prepare project app configuration using stable placeholder replacement.
|
|
54
55
|
const handoffProjectId = handoff.getProjectId();
|
|
55
56
|
const handoffAppBasePath = handoff.config.app.base_path ?? '';
|
|
56
57
|
const handoffWorkingPath = path.resolve(handoff.workingPath);
|
|
@@ -59,15 +60,24 @@ const initializeProjectApp = async (handoff: Handoff): Promise<string> => {
|
|
|
59
60
|
const nextConfigPath = path.resolve(srcPath, 'next.config.mjs');
|
|
60
61
|
const targetPath = path.resolve(appPath, 'next.config.mjs');
|
|
61
62
|
const handoffWebsocketPort = handoff.config.app.ports?.websocket ?? 3001;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const escapedAppBasePath = escapeForSingleQuotedJsString(handoffAppBasePath);
|
|
64
|
+
const escapedProjectId = escapeForSingleQuotedJsString(handoffProjectId);
|
|
65
|
+
const escapedWorkingPath = escapeForSingleQuotedJsString(handoffWorkingPath);
|
|
66
|
+
const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
|
|
67
|
+
const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
|
|
68
|
+
const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
|
|
69
|
+
const placeholderValues: Record<string, string> = {
|
|
70
|
+
'%HANDOFF_PROJECT_ID%': escapedProjectId,
|
|
71
|
+
'%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
|
|
72
|
+
'%HANDOFF_WORKING_PATH%': escapedWorkingPath,
|
|
73
|
+
'%HANDOFF_MODULE_PATH%': escapedModulePath,
|
|
74
|
+
'%HANDOFF_EXPORT_PATH%': escapedExportPath,
|
|
75
|
+
'%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
|
|
76
|
+
};
|
|
77
|
+
let nextConfigContent = await fs.readFile(nextConfigPath, 'utf-8');
|
|
78
|
+
for (const [placeholder, value] of Object.entries(placeholderValues)) {
|
|
79
|
+
nextConfigContent = nextConfigContent.split(placeholder).join(value);
|
|
80
|
+
}
|
|
71
81
|
await fs.writeFile(targetPath, nextConfigContent);
|
|
72
82
|
return appPath;
|
|
73
83
|
};
|
|
@@ -7,6 +7,7 @@ import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
|
7
7
|
import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
|
|
8
8
|
import { ComponentListObject } from '../transformers/preview/types';
|
|
9
9
|
import { Logger } from '../utils/logger';
|
|
10
|
+
import { isPathInside, normalizePathForCompare } from '../utils/path';
|
|
10
11
|
import { persistClientConfig } from './client-config';
|
|
11
12
|
import { syncPublicFiles } from './paths';
|
|
12
13
|
|
|
@@ -115,12 +116,6 @@ export const watchGlobalEntries = (handoff: Handoff, state: WatcherState, chokid
|
|
|
115
116
|
const pathsToWatch = [...scssPathsToWatch, ...jsPathsToWatch];
|
|
116
117
|
if (pathsToWatch.length === 0) return;
|
|
117
118
|
|
|
118
|
-
const isPathInside = (filePath: string, parentPath: string): boolean => {
|
|
119
|
-
const normalizedFilePath = path.resolve(filePath);
|
|
120
|
-
const normalizedParentPath = path.resolve(parentPath);
|
|
121
|
-
return normalizedFilePath === normalizedParentPath || normalizedFilePath.startsWith(`${normalizedParentPath}${path.sep}`);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
119
|
chokidar.watch(pathsToWatch, chokidarConfig).on('all', async (event, file) => {
|
|
125
120
|
switch (event) {
|
|
126
121
|
case 'add':
|
|
@@ -206,11 +201,17 @@ export const watchRuntimeComponents = (
|
|
|
206
201
|
|
|
207
202
|
if (runtimeComponentPathsToWatch.size > 0) {
|
|
208
203
|
const pathsToWatch = Array.from(runtimeComponentPathsToWatch.keys());
|
|
204
|
+
const runtimeComponentEntryTypeByPath = new Map<string, keyof ComponentListObject['entries']>(
|
|
205
|
+
Array.from(runtimeComponentPathsToWatch.entries()).map(([watchPath, entryType]) => [normalizePathForCompare(watchPath), entryType])
|
|
206
|
+
);
|
|
207
|
+
const configFileSet = new Set(handoff.getConfigFilePaths().map((configPath) => normalizePathForCompare(configPath)));
|
|
208
|
+
|
|
209
209
|
state.runtimeComponentsWatcher = chokidar.watch(pathsToWatch, {
|
|
210
210
|
ignoreInitial: true,
|
|
211
211
|
});
|
|
212
212
|
state.runtimeComponentsWatcher.on('all', async (event, file) => {
|
|
213
|
-
|
|
213
|
+
const normalizedFile = normalizePathForCompare(file);
|
|
214
|
+
if (configFileSet.has(normalizedFile)) {
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
|
|
@@ -221,7 +222,7 @@ export const watchRuntimeComponents = (
|
|
|
221
222
|
if (!state.debounce) {
|
|
222
223
|
state.debounce = true;
|
|
223
224
|
try {
|
|
224
|
-
const entryType =
|
|
225
|
+
const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
|
|
225
226
|
const segmentToUpdate: ComponentSegment = entryType ? mapEntryTypeToSegment(entryType) : undefined;
|
|
226
227
|
|
|
227
228
|
const componentDir = path.basename(path.dirname(file));
|