create-qwik 1.1.4 → 1.1.5

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.
Files changed (58) hide show
  1. package/index.cjs +36 -36
  2. package/package.json +1 -1
  3. package/starters/apps/base/.vscode/launch.json +17 -0
  4. package/starters/apps/base/package.json +9 -9
  5. package/starters/apps/basic/package.json +1 -1
  6. package/starters/apps/basic/src/components/starter/next-steps/next-steps.tsx +1 -1
  7. package/starters/apps/basic/src/routes/layout.tsx +12 -0
  8. package/starters/apps/empty/package.json +1 -1
  9. package/starters/apps/empty/src/global.css +0 -7
  10. package/starters/apps/empty/src/routes/layout.tsx +12 -0
  11. package/starters/apps/library/package.json +7 -7
  12. package/starters/apps/site-with-visual-cms/.env +2 -0
  13. package/starters/apps/site-with-visual-cms/builder-integration.ts +853 -0
  14. package/starters/apps/site-with-visual-cms/package.json +17 -0
  15. package/starters/apps/site-with-visual-cms/public/favicon.svg +1 -0
  16. package/starters/apps/site-with-visual-cms/public/manifest.json +9 -0
  17. package/starters/apps/site-with-visual-cms/public/robots.txt +0 -0
  18. package/starters/apps/site-with-visual-cms/src/components/builder-registry.ts +15 -0
  19. package/starters/apps/site-with-visual-cms/src/components/counter/counter.module.css +24 -0
  20. package/starters/apps/site-with-visual-cms/src/components/counter/counter.tsx +81 -0
  21. package/starters/apps/site-with-visual-cms/src/components/footer/footer.module.css +17 -0
  22. package/starters/apps/site-with-visual-cms/src/components/footer/footer.tsx +14 -0
  23. package/starters/apps/site-with-visual-cms/src/components/gauge/gauge.module.css +22 -0
  24. package/starters/apps/site-with-visual-cms/src/components/gauge/index.tsx +30 -0
  25. package/starters/apps/site-with-visual-cms/src/components/header/header.module.css +50 -0
  26. package/starters/apps/site-with-visual-cms/src/components/header/header.tsx +34 -0
  27. package/starters/apps/site-with-visual-cms/src/components/icons/qwik.tsx +38 -0
  28. package/starters/apps/{documentation-site → site-with-visual-cms}/src/components/router-head/router-head.tsx +3 -3
  29. package/starters/apps/site-with-visual-cms/src/entry.dev.tsx +17 -0
  30. package/starters/apps/site-with-visual-cms/src/entry.preview.tsx +20 -0
  31. package/starters/apps/site-with-visual-cms/src/entry.ssr.tsx +27 -0
  32. package/starters/apps/site-with-visual-cms/src/global.css +116 -0
  33. package/starters/apps/{documentation-site → site-with-visual-cms}/src/root.tsx +3 -1
  34. package/starters/apps/site-with-visual-cms/src/routes/[...index]/index.tsx +57 -0
  35. package/starters/apps/{documentation-site → site-with-visual-cms}/src/routes/layout.tsx +2 -2
  36. package/starters/apps/site-with-visual-cms/src/routes/service-worker.ts +18 -0
  37. package/starters/apps/site-with-visual-cms/vite.config.ts +10 -0
  38. package/starters/apps/documentation-site/package.json +0 -13
  39. package/starters/apps/documentation-site/src/components/breadcrumbs/breadcrumbs.css +0 -25
  40. package/starters/apps/documentation-site/src/components/breadcrumbs/breadcrumbs.tsx +0 -74
  41. package/starters/apps/documentation-site/src/components/footer/footer.css +0 -22
  42. package/starters/apps/documentation-site/src/components/footer/footer.tsx +0 -36
  43. package/starters/apps/documentation-site/src/components/header/header.css +0 -34
  44. package/starters/apps/documentation-site/src/components/header/header.tsx +0 -26
  45. package/starters/apps/documentation-site/src/components/icons/qwik.tsx +0 -20
  46. package/starters/apps/documentation-site/src/components/menu/menu.css +0 -13
  47. package/starters/apps/documentation-site/src/components/menu/menu.tsx +0 -36
  48. package/starters/apps/documentation-site/src/components/on-this-page/on-this-page.css +0 -33
  49. package/starters/apps/documentation-site/src/components/on-this-page/on-this-page.tsx +0 -62
  50. package/starters/apps/documentation-site/src/global.css +0 -66
  51. package/starters/apps/documentation-site/src/routes/about-us/index.md +0 -15
  52. package/starters/apps/documentation-site/src/routes/docs/advanced/index.md +0 -11
  53. package/starters/apps/documentation-site/src/routes/docs/docs.css +0 -22
  54. package/starters/apps/documentation-site/src/routes/docs/getting-started/index.md +0 -13
  55. package/starters/apps/documentation-site/src/routes/docs/index.md +0 -22
  56. package/starters/apps/documentation-site/src/routes/docs/layout.tsx +0 -25
  57. package/starters/apps/documentation-site/src/routes/docs/menu.md +0 -21
  58. package/starters/apps/documentation-site/src/routes/index.tsx +0 -167
@@ -0,0 +1,853 @@
1
+ import type { Logger, Plugin } from 'vite';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { homedir, hostname } from 'node:os';
5
+ import { request } from 'node:https';
6
+ import { IncomingMessage } from 'node:http';
7
+
8
+ function html(content: string) {
9
+ return `
10
+ <!DOCTYPE html>
11
+ <html>
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <title>Visual CMS Site Integration With Builder.io</title>
15
+ <style>
16
+ html {
17
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
18
+ 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
19
+ 'Segoe UI Symbol', 'Noto Color Emoji';
20
+ }
21
+ body {
22
+ padding: 80px 0;
23
+ line-height: 1.8;
24
+ }
25
+ main {
26
+ display: grid;
27
+ grid-template-columns: 200px 1fr;
28
+ gap: 40px;
29
+ width: 100%;
30
+ max-width: 800px;
31
+ margin: 0 auto;
32
+ }
33
+ h1 {
34
+ margin-top: 0;
35
+ }
36
+ button {
37
+ cursor: pointer;
38
+ }
39
+ aside ul {
40
+ margin: 0;
41
+ padding: 0;
42
+ list-style: none;
43
+ }
44
+ aside li {
45
+ margin: 0;
46
+ padding: 20px 10px;
47
+ }
48
+ aside li a {
49
+ text-decoration: none;
50
+ color: inherit;
51
+ }
52
+ aside li.active {
53
+ font-weight: bold;
54
+ }
55
+ aside li.completed {
56
+ color: gray;
57
+ }
58
+ </style>
59
+ <link rel="icon shortcut" href="https://cdn.builder.io/favicon.ico">
60
+ </head>
61
+ <body>
62
+ <main>${content}</main>
63
+ </body>
64
+ </html>
65
+ `;
66
+ }
67
+
68
+ /**
69
+ * First step in the setup process. Show all the steps and explain what's going on.
70
+ */
71
+ function setupOverviewStep(ctx: BuilderSetupContext, url: URL) {
72
+ debug(`show overview step`);
73
+ const nextStepUrl = getAuthConnectUrl(ctx, url);
74
+
75
+ return html(`
76
+ <aside>
77
+ <ul>
78
+ <li class="active">
79
+ Overview
80
+ </li>
81
+ <li>
82
+ Connect Builder.io
83
+ </li>
84
+ <li>
85
+ Setup Content Page
86
+ </li>
87
+ </ul>
88
+ </aside>
89
+ <section>
90
+ <h1>
91
+ Integrate Builder.io Visual CMS with ${ctx.framework}
92
+ </h1>
93
+ <p>
94
+ Success! Your ${ctx.framework} app has been created!
95
+ </p>
96
+ <p>
97
+ Next let's connect Builder.io so you can start editing and publishing content.
98
+ </p>
99
+ <nav>
100
+ <p>
101
+ <a href="${nextStepUrl}">Next</a>
102
+ </p>
103
+ </nav>
104
+ </section>
105
+ `);
106
+ }
107
+
108
+ /**
109
+ * Returning from the builder.io auth flow, show the next step in the setup process.
110
+ */
111
+ function connectedToBuilderStep(ctx: BuilderSetupContext, url: URL, backgroundUpdate: boolean) {
112
+ debug(`show connected to builder step (background update: ${backgroundUpdate})`);
113
+ const appBaseUrl = getAppBaseUrl(ctx, url).pathname;
114
+ const connectedStepUrl = getConnectedStepUrl(ctx, url);
115
+ const backgroundUpdateUrl = getBackgroundUpdateUrl(ctx, url);
116
+
117
+ return html(`
118
+ <aside>
119
+ <ul>
120
+ <li class="completed">
121
+ Overview
122
+ </li>
123
+ <li class="active">
124
+ Connect Builder.io
125
+ </li>
126
+ <li>
127
+ Setup Content Page
128
+ </li>
129
+ </ul>
130
+ </aside>
131
+ <section>
132
+ <h1>
133
+ Visual CMS Connected
134
+ </h1>
135
+ <p>
136
+ Great! Your ${ctx.framework} app has been connected to the Builder.io Visual CMS.
137
+ </p>
138
+ <p>
139
+ Next let's connect Builder.io so you can start editing and publishing content.
140
+ </p>
141
+ <nav>
142
+ <p>
143
+ <a id="next" disabled href="${appBaseUrl}">Next</a>
144
+ </p>
145
+ </nav>
146
+ </section>
147
+ ${
148
+ backgroundUpdate
149
+ ? `
150
+ <script>
151
+ history.replaceState({}, "", "${connectedStepUrl}");
152
+ fetch("${backgroundUpdateUrl}").then((rsp) => {
153
+ if (rsp.ok) {
154
+ document.getElementById("next").removeAttribute("disabled");
155
+ } else {
156
+ console.error("Failed to update Builder.io page", rsp.status);
157
+ rsp.text().then((text) => {
158
+ console.error(text);
159
+ });
160
+ }
161
+ }).catch((err) => {
162
+ console.error(err);
163
+ });
164
+ </script>
165
+ `
166
+ : ``
167
+ }
168
+ `);
169
+ }
170
+
171
+ function getDefaultHomepage(ctx: BuilderSetupContext) {
172
+ return JSON.stringify({
173
+ '@version': 4,
174
+ name: DEFAULT_HOMEPAGE_PAGE_NAME,
175
+ ownerId: ctx.credentials.publicApiKey,
176
+ published: 'published',
177
+ query: [
178
+ {
179
+ '@type': '@builder.io/core:Query',
180
+ property: 'urlPath',
181
+ value: ctx.appBasePathname,
182
+ operator: 'is',
183
+ },
184
+ ],
185
+ data: {
186
+ blocksString: DEFAULT_HOMEPAGE_BLOCK,
187
+ title: DEFAULT_HOMEPAGE_PAGE_NAME,
188
+ url: ctx.appBasePathname,
189
+ },
190
+ });
191
+ }
192
+
193
+ const DEFAULT_HOMEPAGE_PAGE_NAME = `Homepage`;
194
+ const DEFAULT_HOMEPAGE_BLOCK = `[{"@type":"@builder.io/sdk:Element","@version":2,"id":"builder-b3e7bacb8fc740109a8154507ad3f39b","component":{"name":"Text","options":{"text":"<h1>Hello Qwik!</h1>"}},"responsiveStyles":{"large":{"display":"flex","flexDirection":"column","position":"relative","flexShrink":"0","boxSizing":"border-box","marginTop":"20px","lineHeight":"normal","height":"auto","marginLeft":"auto","marginRight":"auto"}}},{"@type":"@builder.io/sdk:Element","@version":2,"id":"builder-15627179727f4fcba1246a6da5e8ae67","component":{"name":"Image","options":{"image":"https://cdn.builder.io/api/v1/image/assets%2F4025e37ed968472fac153d65c579ca46%2Fb2a221368b714e8991e94790bb3a0068","backgroundSize":"cover","backgroundPosition":"center","lazy":false,"fitContent":true,"aspectRatio":1.333,"lockAspectRatio":false,"height":1300,"width":975}},"responsiveStyles":{"large":{"display":"flex","flexDirection":"column","position":"relative","flexShrink":"0","boxSizing":"border-box","marginTop":"20px","width":"100%","minHeight":"20px","minWidth":"20px","overflow":"hidden"}}}]`;
195
+
196
+ async function validateBuilderIntegration(
197
+ ctx: BuilderSetupContext,
198
+ url: URL
199
+ ): Promise<BuilderIntegrationResult> {
200
+ const result: BuilderIntegrationResult = {};
201
+
202
+ try {
203
+ if (ctx.isValid || !isPageRequest(url)) {
204
+ // all good, already validated
205
+ // or this is not a page request so don't bother
206
+ return result;
207
+ }
208
+
209
+ // get the keys from the querystring (if they exist)
210
+ const qsPublicApiKey = url.searchParams.get(BUILDER_PUBLIC_API_KEY_QS);
211
+ const qsPrivateAuthKey = url.searchParams.get(BUILDER_PRIVATE_AUTH_KEY_QS);
212
+
213
+ // check if we're returning from the builder.io auth flow
214
+ if (qsPublicApiKey && qsPrivateAuthKey) {
215
+ debug(`url has auth keys`);
216
+ // we've returned from the builder.io auth flow
217
+ // and have the auth keys in the querystring
218
+ ctx.credentials = {
219
+ publicApiKey: qsPublicApiKey,
220
+ privateAuthKey: qsPrivateAuthKey,
221
+ };
222
+
223
+ if (url.searchParams.get(BUILDER_SETUP_STEP_QS) === BUILDER_UPDATE_STEP) {
224
+ debug(`step: ${BUILDER_UPDATE_STEP}`);
225
+ // handle the background fetch() request that should:
226
+ // - create the homepage
227
+ // - write the private auth key to the user's home directory
228
+ // - update the .env file with the public api key
229
+
230
+ // see if this builder account already has a homepage created
231
+ const hasHomepage = await hasBuilderHomepage(ctx);
232
+ if (!hasHomepage) {
233
+ // there is no homepage content created yet
234
+ // create the default homepage for them
235
+ await createBuilderHomepage(ctx);
236
+ }
237
+
238
+ // write the app credientials to the user's home directory builder config
239
+ setBuilderAppCredentials(ctx);
240
+
241
+ // write the api key it to the app's .env file
242
+ // writing to the .env file will trigger a server restart
243
+ setBuilderPublicApiKey(ctx);
244
+
245
+ // set the result to show the connected to builder step
246
+ result.html = `set public api key: ${ctx.credentials.publicApiKey}`;
247
+ return result;
248
+ }
249
+
250
+ // returning from auth flow then redirected to this page
251
+ // show that we're connected to builder
252
+ // In the background we'll fire off a fetch() that will:
253
+ // - create the homepage
254
+ // - write the private auth key to the user's home directory
255
+ // - update the .env file with the public api key
256
+ // - updating the .env file will trigger a server restart
257
+ result.html = connectedToBuilderStep(ctx, url, true);
258
+ return result;
259
+ }
260
+
261
+ // get the builder public api key from the .env file
262
+ const envApiKey = getBuilderApiKey(ctx);
263
+ if (!envApiKey) {
264
+ debug(`invalid api key in .env file`);
265
+ // we don't have a valid api key saved in the .env file
266
+ // respond with the first step of setup UI
267
+ result.html = setupOverviewStep(ctx, url);
268
+ return result;
269
+ }
270
+
271
+ debug(`public api key from the .env file: ${envApiKey}`);
272
+
273
+ // set the public api key in the process.env
274
+ // dotenv will normally do this on a fresh server start
275
+ process.env[BUILDER_API_KEY_ENV] = envApiKey;
276
+
277
+ // get the private api key from the user home dir builder config file
278
+ const appCredentials = getBuilderAppCredentials(ctx, envApiKey);
279
+ if (!appCredentials) {
280
+ debug(`invalid app credentials in user home dir`);
281
+ // we don't have a valid private key saved in the user home dir config
282
+ // respond with the first step of setup UI
283
+ result.html = setupOverviewStep(ctx, url);
284
+ return result;
285
+ }
286
+
287
+ // remember the valid app credentials
288
+ ctx.credentials = appCredentials;
289
+
290
+ if (url.searchParams.get(BUILDER_SETUP_STEP_QS) === BUILDER_CONNECTED_STEP) {
291
+ debug(`step: ${BUILDER_CONNECTED_STEP}`);
292
+ // continue showing the connected step
293
+ // we may have reloaded the page and forgetten the context
294
+ // at this point don't do a background fetch() update
295
+ result.html = connectedToBuilderStep(ctx, url, false);
296
+ return result;
297
+ }
298
+
299
+ // it's possible that the builder auth is setup, but they still don't have a homepage somehow
300
+ // double check if this builder account already has a homepage created
301
+ const hasHomepage = await hasBuilderHomepage(ctx);
302
+ if (!hasHomepage) {
303
+ debug(`no homepage created yet`);
304
+ // there is no homepage content created yet for the valid public api key
305
+ // we don't have their private key, so let's redirect to the auth flow
306
+ // so we can get their private key and create the homepage for them
307
+ // respond with the first step of setup UI
308
+ result.html = setupOverviewStep(ctx, url);
309
+ return result;
310
+ }
311
+
312
+ // awesome, we're all set
313
+ // the public key is saved correctly in the .env file
314
+ // and they have a homepage created
315
+ // no need to respond with the setup UI
316
+ // set isValid to true so we don't have to validate again
317
+ debug(`set is valid`);
318
+ ctx.isValid = true;
319
+ } catch (e: any) {
320
+ // collect the error and let the build decide how to handle it
321
+ result.errors = [e.message];
322
+ }
323
+
324
+ return result;
325
+ }
326
+
327
+ async function hasBuilderHomepage(ctx: BuilderSetupContext) {
328
+ try {
329
+ const url = new URL(`https://cdn.builder.io/api/v3/content/page`);
330
+ url.searchParams.set(`apiKey`, ctx.credentials.publicApiKey);
331
+ url.searchParams.set(`url`, ctx.appBasePathname);
332
+ url.searchParams.set(`cachebust`, Math.random().toString());
333
+
334
+ const res = await requestJSON<{ results: any[] }>({
335
+ url,
336
+ method: 'GET',
337
+ });
338
+
339
+ const hasHomepage = res.results.length > 0;
340
+ debug(`has homepage: ${hasHomepage}`);
341
+ return hasHomepage;
342
+ } catch (e) {
343
+ console.error(e);
344
+ return false;
345
+ }
346
+ }
347
+
348
+ async function createBuilderHomepage(ctx: BuilderSetupContext) {
349
+ debug(`create homepage`);
350
+ const url = new URL(`https://cdn.builder.io/api/v1/write/page`);
351
+
352
+ const body = getDefaultHomepage(ctx);
353
+
354
+ await requestJSON({
355
+ url,
356
+ method: 'POST',
357
+ headers: {
358
+ Authorization: `Bearer ${ctx.credentials.privateAuthKey}`,
359
+ },
360
+ body,
361
+ });
362
+ }
363
+
364
+ function setBuilderPublicApiKey(ctx: BuilderSetupContext) {
365
+ const comment = `# https://www.builder.io/c/docs/using-your-api-key`;
366
+
367
+ // check if we already have an .env file
368
+ if (existsSync(ctx.envFilePath)) {
369
+ // read the existing .env file
370
+ let envContent = readFileSync(ctx.envFilePath, 'utf-8');
371
+ if (envContent.includes(BUILDER_API_KEY_ENV)) {
372
+ // existing .env has a builder api key already, update its value
373
+ if (!envContent.includes(ctx.credentials.publicApiKey)) {
374
+ // existing .env has a builder api key, but it's not the same as the one we have
375
+ debug(`update public api key in existing .env file`);
376
+ envContent = envContent.replace(
377
+ new RegExp(`${BUILDER_API_KEY_ENV}=.*`),
378
+ `${BUILDER_API_KEY_ENV}=${ctx.credentials.publicApiKey}`
379
+ );
380
+ writeFileSync(ctx.envFilePath, envContent);
381
+ } else {
382
+ debug(`public api key already in existing .env file`);
383
+ }
384
+ } else {
385
+ // existing .env does not have a builder api key, append the key/value
386
+ debug(`append public api key to existing .env file`);
387
+ envContent += `\n\n${comment}\n${BUILDER_API_KEY_ENV}=${ctx.credentials.publicApiKey}\n\n`;
388
+ writeFileSync(ctx.envFilePath, envContent);
389
+ }
390
+ } else {
391
+ // create a new .env file since it doesn't exist yet
392
+ debug(`create .env file with public api key`);
393
+ const newEnv = [comment, `${BUILDER_API_KEY_ENV}=${ctx.credentials.publicApiKey}`, ``];
394
+ writeFileSync(ctx.envFilePath, newEnv.join('\n'));
395
+ }
396
+ }
397
+
398
+ function getBuilderApiKey(ctx: BuilderSetupContext) {
399
+ if (existsSync(ctx.envFilePath)) {
400
+ const envContent = readFileSync(ctx.envFilePath, 'utf-8');
401
+
402
+ const envs = envContent
403
+ .split('\n')
404
+ .map((l) => l.trim())
405
+ .filter((l) => l.length > 0)
406
+ .filter((l) => !l.startsWith('#'))
407
+ .filter((l) => l.includes('='))
408
+ .map((l) => {
409
+ const [key, value] = l.split('=');
410
+ return { key, value };
411
+ });
412
+
413
+ const builderApiKey = envs.find((e) => e.key === BUILDER_API_KEY_ENV);
414
+ if (typeof builderApiKey?.value === 'string' && builderApiKey.value.length > 0) {
415
+ if (builderApiKey.value !== 'YOUR_API_KEY') {
416
+ return builderApiKey.value;
417
+ }
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+
423
+ function getBuilderAppCredentials(ctx: BuilderSetupContext, publicApiKey: string) {
424
+ try {
425
+ const credintialsFilePath = getCredentialsFilePath(ctx, publicApiKey);
426
+ const config = readFileSync(credintialsFilePath, 'utf-8');
427
+ return JSON.parse(config) as BuilderAppCredentials;
428
+ } catch (e: any) {
429
+ if (e.code === 'ENOENT') {
430
+ return null;
431
+ }
432
+ throw e;
433
+ }
434
+ }
435
+
436
+ function setBuilderAppCredentials(ctx: BuilderSetupContext) {
437
+ debug(`set credentials`);
438
+ const credintialsFilePath = getCredentialsFilePath(ctx, ctx.credentials.publicApiKey);
439
+ mkdirSync(dirname(credintialsFilePath), { recursive: true });
440
+ writeFileSync(credintialsFilePath, JSON.stringify(ctx.credentials, null, 2));
441
+ }
442
+
443
+ function getCredentialsFilePath(ctx: BuilderSetupContext, publicApiKey: string) {
444
+ return join(ctx.credentialsDirPath, `${publicApiKey}.json`);
445
+ }
446
+
447
+ function isPageRequest(url: URL) {
448
+ if (url.pathname.endsWith('/')) {
449
+ return true;
450
+ }
451
+
452
+ const filename = url.pathname.split('/').pop();
453
+ if (filename) {
454
+ if (!filename.includes('.')) {
455
+ return true;
456
+ }
457
+
458
+ const ext = filename.split('.').pop();
459
+ if (ext === 'html') {
460
+ return true;
461
+ }
462
+ }
463
+ return false;
464
+ }
465
+
466
+ function interceptPageRequest(ctx: BuilderSetupContext) {
467
+ const result: BuilderIntegrationResult = {};
468
+
469
+ if (ctx.isValid) {
470
+ try {
471
+ result.html = `<script id="builder-dev-tools">(function(){\n${getBuilderDevToolsRuntime()}\n})();</script>
472
+ `;
473
+ } catch (e: any) {
474
+ result.errors = [e.message];
475
+ }
476
+ }
477
+
478
+ return result;
479
+ }
480
+
481
+ function getBuilderDevToolsRuntime() {
482
+ return `
483
+ try {
484
+ const editButton = document.createElement('builder-dev-tools-edit-button');
485
+ editButton.style.display = 'none';
486
+ editButton.setAttribute('aria-hidden', 'true');
487
+
488
+ function onPointerOver(ev) {
489
+ const hoverElm = ev.target;
490
+ if (!hoverElm) {
491
+ hideEditButton();
492
+ return;
493
+ }
494
+
495
+ if (hoverElm.closest('builder-dev-tools-edit-button')) {
496
+ return;
497
+ }
498
+
499
+ const contentElm = hoverElm.closest('[builder-content-id]');
500
+ const builderElm = hoverElm.closest('[builder-id]');
501
+ if (!contentElm || !builderElm) {
502
+ hideEditButton();
503
+ return;
504
+ }
505
+
506
+ const contentId = contentElm.getAttribute('builder-content-id');
507
+ const builderId = builderElm.getAttribute('builder-id');
508
+ if (!contentId || !builderId) {
509
+ hideEditButton();
510
+ return;
511
+ }
512
+
513
+ const rect = builderElm.getBoundingClientRect();
514
+ editButton.style.display = 'block';
515
+ editButton.style.top = (builderElm.offsetTop - 1) + 'px';
516
+ editButton.style.left = (builderElm.offsetLeft) + 'px';
517
+ editButton.style.width = (rect.width - 2) + 'px';
518
+ editButton.style.height = (rect.height - 2) + 'px';
519
+ editButton.setEditUrl(contentId, builderId);
520
+ }
521
+
522
+ function hideEditButton() {
523
+ editButton.style.display = 'none';
524
+ }
525
+
526
+ class BuilderDevToolsEditButton extends HTMLElement {
527
+ constructor() {
528
+ super();
529
+ this.shadow = this.attachShadow({ mode: 'open' });
530
+ }
531
+
532
+ connectedCallback() {
533
+ this.shadow.innerHTML = \`
534
+ <style>
535
+ :host {
536
+ --builder-blue: rgb(26, 115, 232);
537
+ box-sizing: border-box;
538
+ position: absolute;
539
+ z-index: 100;
540
+ user-select: none;
541
+ }
542
+ a {
543
+ display: inline-block;
544
+ box-sizing: border-box;
545
+ padding: 4px;
546
+ }
547
+ a span {
548
+ display: inline-block;
549
+ box-sizing: border-box;
550
+ padding: 4px 8px;
551
+ font-size: 12px;
552
+ font-weight: 500;
553
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
554
+ color: white;
555
+ background-color: var(--builder-blue);
556
+ border: 1px solid transparent;
557
+ border-radius: 3px;
558
+ text-align: center;
559
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 3px 0px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 2px 1px -1px;
560
+ text-decoration: none;
561
+ pointer-events: none;
562
+ }
563
+ a:hover span {
564
+ border-color: var(--builder-blue);
565
+ color: var(--builder-blue);
566
+ background: white;
567
+ }
568
+ .outline {
569
+ position: absolute;
570
+ top: 0;
571
+ left: 0;
572
+ width: 100%;
573
+ height: 100%;
574
+ pointer-events: none;
575
+ border: 1px solid var(--builder-blue);
576
+ }
577
+ </style>
578
+ <a id="edit" target="_blank"><span>Edit</span></a>
579
+ <div class="outline"></div>
580
+ \`;
581
+
582
+ this.editButton = this.shadow.getElementById('edit');
583
+ }
584
+
585
+ setEditUrl(contentId, builderId) {
586
+ const pathname = '/content/' + contentId + '/edit';
587
+ const url = new URL(pathname, 'https://builder.io');
588
+ url.searchParams.set('selectedBlock', builderId);
589
+ this.editButton.href = url.href;
590
+ }
591
+ }
592
+
593
+ customElements.define('builder-dev-tools-edit-button', BuilderDevToolsEditButton);
594
+ document.body.appendChild(editButton);
595
+
596
+ document.addEventListener('pointerover', onPointerOver, { passive: true });
597
+ document.addEventListener('pointerleave', hideEditButton, { passive: true });
598
+ document.addEventListener('pointercancel', hideEditButton, { passive: true });
599
+ document.addEventListener('visibilitychange', hideEditButton, { passive: true });
600
+
601
+ window.addEventListener('popstate', hideEditButton, { passive: true });
602
+
603
+ const originalPushState = history.pushState;
604
+ history.pushState = function () {
605
+ hideEditButton();
606
+ originalPushState.apply(this, arguments);
607
+ };
608
+
609
+ const originalReplaceState = history.replaceState;
610
+ history.replaceState = function () {
611
+ hideEditButton();
612
+ originalReplaceState.apply(this, arguments);
613
+ };
614
+ } catch (e) {
615
+ console.error(e);
616
+ }
617
+ `;
618
+ }
619
+
620
+ function createBuilderSetup(opts: CreateSetupOptions) {
621
+ const ctx: BuilderSetupContext = {
622
+ ...opts,
623
+ credentials: {
624
+ publicApiKey: '',
625
+ privateAuthKey: '',
626
+ },
627
+ isValid: false,
628
+ };
629
+
630
+ const builder: BuilderIntegration = {
631
+ intercept: () => interceptPageRequest(ctx),
632
+ validate: (url) => validateBuilderIntegration(ctx, url),
633
+ };
634
+ return builder;
635
+ }
636
+
637
+ /**
638
+ * Get the auth url to connect to builder, and the url to redirect to after connecting
639
+ */
640
+ function getAuthConnectUrl(ctx: BuilderSetupContext, url: URL) {
641
+ const authUrl = new URL(`/cli-auth`, `https://builder.io`);
642
+ authUrl.searchParams.set('response_type', 'code');
643
+ authUrl.searchParams.set('cli', 'true');
644
+ authUrl.searchParams.set('client_id', ctx.clientId);
645
+ authUrl.searchParams.set('host', ctx.clientHostname);
646
+
647
+ const returnUrl = getAppBaseUrl(ctx, url).href;
648
+ authUrl.searchParams.set('redirect_url', returnUrl);
649
+
650
+ return authUrl.href;
651
+ }
652
+
653
+ function getAppBaseUrl(ctx: BuilderSetupContext, url: URL) {
654
+ return new URL(ctx.appBasePathname, url.origin);
655
+ }
656
+
657
+ function getConnectedStepUrl(ctx: BuilderSetupContext, url: URL) {
658
+ const appBaseUrl = getAppBaseUrl(ctx, url);
659
+ appBaseUrl.searchParams.set(BUILDER_SETUP_STEP_QS, BUILDER_CONNECTED_STEP);
660
+ return appBaseUrl.pathname + appBaseUrl.search;
661
+ }
662
+
663
+ function getBackgroundUpdateUrl(ctx: BuilderSetupContext, url: URL) {
664
+ const appBaseUrl = getAppBaseUrl(ctx, url);
665
+ appBaseUrl.searchParams.set(BUILDER_SETUP_STEP_QS, BUILDER_UPDATE_STEP);
666
+ appBaseUrl.searchParams.set(BUILDER_PUBLIC_API_KEY_QS, ctx.credentials.publicApiKey);
667
+ appBaseUrl.searchParams.set(BUILDER_PRIVATE_AUTH_KEY_QS, ctx.credentials.privateAuthKey);
668
+ return appBaseUrl.pathname + appBaseUrl.search;
669
+ }
670
+
671
+ function requestJSON<T>(opts: RequestOptions) {
672
+ return new Promise<T>((resolve, reject) => {
673
+ const req = request(
674
+ {
675
+ protocol: opts.url.protocol,
676
+ host: opts.url.host,
677
+ port: opts.url.port,
678
+ path: opts.url.pathname + opts.url.search,
679
+ method: opts.method,
680
+ headers: opts.headers,
681
+ },
682
+ (res) => {
683
+ let data = '';
684
+ res.on('data', (chunk) => {
685
+ data += chunk;
686
+ });
687
+
688
+ res.on('end', () => {
689
+ if (!res.statusCode || res.statusCode > 299) {
690
+ reject(`Request to ${res.url} failed with status ${res.statusCode}`);
691
+ } else {
692
+ if (
693
+ typeof res.headers['content-type'] !== 'string' ||
694
+ !res.headers['content-type'].includes('application/json')
695
+ ) {
696
+ reject(`Response from ${res.url} content-type is ${res.headers['content-type']}`);
697
+ } else {
698
+ try {
699
+ resolve(JSON.parse(data));
700
+ } catch (err) {
701
+ reject(`Response from ${res.url} is not valid JSON: ${data}\n${err}`);
702
+ }
703
+ }
704
+ }
705
+ });
706
+ }
707
+ ).on('error', reject);
708
+
709
+ if (opts.body) {
710
+ req.setHeader('Content-Type', 'application/json');
711
+ req.write(opts.body);
712
+ }
713
+
714
+ req.end();
715
+ });
716
+ }
717
+
718
+ const BUILDER_API_KEY_ENV = `PUBLIC_BUILDER_API_KEY`;
719
+ const BUILDER_PUBLIC_API_KEY_QS = `api-key`;
720
+ const BUILDER_PRIVATE_AUTH_KEY_QS = `p-key`;
721
+ const BUILDER_SETUP_STEP_QS = `builder-connect`;
722
+ const BUILDER_UPDATE_STEP = `update`;
723
+ const BUILDER_CONNECTED_STEP = `connected`;
724
+
725
+ /**
726
+ * Vite plugin that adds builder.io setup UI
727
+ */
728
+ export function builderDevTools(opts: BuilderioOptions = {}): Plugin {
729
+ let builder: BuilderIntegration | undefined;
730
+ let logger: Logger | undefined;
731
+ let port: number | undefined;
732
+
733
+ return {
734
+ name: 'builderDevTools',
735
+
736
+ configResolved(config) {
737
+ logger = config.logger;
738
+ port = config.server.port;
739
+ builder = createBuilderSetup({
740
+ appBasePathname: config.base,
741
+ clientHostname: hostname(),
742
+ clientId: 'create-qwik-app',
743
+ credentialsDirPath: join(homedir(), `.config`, `builder`),
744
+ envFilePath: opts.envFilePath || join(config.root, `.env`),
745
+ framework: 'Qwik',
746
+ });
747
+ },
748
+
749
+ configureServer(server) {
750
+ server.middlewares.use(async (req, res, next) => {
751
+ req.socket.address();
752
+
753
+ const orgResponseEnd = res.end;
754
+ res.end = function (...args: any[]) {
755
+ if (builder) {
756
+ const contentType = (res.getHeader('Content-Type') || '').toString();
757
+
758
+ if (contentType.includes('text/html')) {
759
+ const result = builder.intercept();
760
+ if (result.errors && logger) {
761
+ result.errors.map((e) => logger!.error(e));
762
+ }
763
+ if (result.html) {
764
+ res.write(result.html);
765
+ }
766
+ }
767
+ }
768
+
769
+ return orgResponseEnd.apply(this, args);
770
+ };
771
+
772
+ // add Vite dev server middleware that
773
+ // shows builder setup UI if needed
774
+ if (builder) {
775
+ const url = getNodeHttpUrl(port, req);
776
+
777
+ const result = await builder.validate(url);
778
+
779
+ if (result.errors && logger) {
780
+ result.errors.map((e) => logger!.error(e));
781
+ }
782
+
783
+ if (result.html) {
784
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
785
+ res.setHeader('Cache-Control', 'max-age=0, no-cache, no-store');
786
+ res.setHeader('X-Builderio-Vite-Dev-Server', 'true');
787
+ res.end(result.html);
788
+ return;
789
+ }
790
+ }
791
+ next();
792
+ });
793
+ },
794
+ };
795
+ }
796
+
797
+ function getNodeHttpUrl(port: number | undefined, req: IncomingMessage) {
798
+ const a = req.socket.address();
799
+ const address = 'address' in a && typeof a.address === 'string' ? a.address : 'localhost';
800
+ port = 'port' in a && typeof a.port === 'number' ? a.port : port;
801
+
802
+ return new URL(req.url || '/', `http://${address}:${port}`);
803
+ }
804
+
805
+ function debug(...args: any[]) {
806
+ if (process.env.DEBUG) {
807
+ // eslint-disable-next-line no-console
808
+ console.debug('[builder.io]', ...args);
809
+ }
810
+ }
811
+
812
+ export interface BuilderioOptions {
813
+ /**
814
+ * Absolute path to the project's `.env` file.
815
+ */
816
+ envFilePath?: string;
817
+ }
818
+
819
+ interface CreateSetupOptions {
820
+ appBasePathname: string;
821
+ clientHostname: string;
822
+ clientId: string;
823
+ credentialsDirPath: string;
824
+ envFilePath: string;
825
+ framework: string;
826
+ }
827
+
828
+ interface BuilderSetupContext extends CreateSetupOptions {
829
+ isValid: boolean;
830
+ credentials: BuilderAppCredentials;
831
+ }
832
+
833
+ interface BuilderIntegration {
834
+ intercept: () => BuilderIntegrationResult;
835
+ validate: (url: URL) => Promise<BuilderIntegrationResult>;
836
+ }
837
+
838
+ interface BuilderIntegrationResult {
839
+ html?: string;
840
+ errors?: string[];
841
+ }
842
+
843
+ interface BuilderAppCredentials {
844
+ publicApiKey: string;
845
+ privateAuthKey: string;
846
+ }
847
+
848
+ interface RequestOptions {
849
+ url: URL;
850
+ headers?: Record<string, string>;
851
+ method?: string;
852
+ body?: any;
853
+ }