webstudio 0.151.0 → 0.167.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/lib/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/cli.ts
2
- import { exit, argv } from "node:process";
2
+ import { exit as exit4, argv } from "node:process";
3
3
  import { hideBin } from "yargs/helpers";
4
4
 
5
5
  // src/config.ts
@@ -68,16 +68,16 @@ var isFileExists = async (filePath) => {
68
68
  return false;
69
69
  }
70
70
  };
71
- var ensureFileInPath = async (filePath, content) => {
71
+ var createFileIfNotExists = async (filePath, content) => {
72
72
  const dir = dirname(filePath);
73
- await ensureFolderExists(dir);
73
+ await createFolderIfNotExists(dir);
74
74
  try {
75
75
  await access(filePath, constants.F_OK);
76
76
  } catch {
77
77
  await writeFile(filePath, content || "", "utf8");
78
78
  }
79
79
  };
80
- var ensureFolderExists = async (folderPath) => {
80
+ var createFolderIfNotExists = async (folderPath) => {
81
81
  try {
82
82
  await access(folderPath, constants.F_OK);
83
83
  } catch {
@@ -94,10 +94,41 @@ var loadJSONFile = async (filePath) => {
94
94
  };
95
95
 
96
96
  // src/commands/link.ts
97
- import { stdin, stdout, cwd } from "node:process";
97
+ import { cwd, exit } from "node:process";
98
98
  import { join as join2 } from "node:path";
99
- import * as readline from "node:readline/promises";
100
99
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
100
+ import { cancel, isCancel, log, text } from "@clack/prompts";
101
+ var parseShareLink = (value) => {
102
+ const url = new URL(value);
103
+ const origin = url.origin;
104
+ const token = url.searchParams.get("authToken");
105
+ const segments = url.pathname.split("/").slice(1);
106
+ if (segments.length !== 2 || segments[0] !== "builder") {
107
+ throw Error("Segments not matching");
108
+ }
109
+ const [_builder, projectId] = segments;
110
+ if (token == null) {
111
+ throw Error("Token is missing");
112
+ }
113
+ return {
114
+ origin,
115
+ projectId,
116
+ token
117
+ };
118
+ };
119
+ var validateShareLink = (value) => {
120
+ if (value.length === 0) {
121
+ return "Share link is required";
122
+ }
123
+ if (URL.canParse(value) === false) {
124
+ return "Share link is invalid";
125
+ }
126
+ try {
127
+ parseShareLink(value);
128
+ } catch {
129
+ return "Share link is invalid";
130
+ }
131
+ };
101
132
  var linkOptions = (yargs) => yargs.option("link", {
102
133
  alias: "l",
103
134
  type: "string",
@@ -108,62 +139,48 @@ var link = async (options) => {
108
139
  if (options.link) {
109
140
  shareLink = options.link;
110
141
  } else {
111
- const rl = readline.createInterface({ input: stdin, output: stdout });
112
- shareLink = await rl.question(
113
- `Please paste a link from the Share Dialog in the builder: `
114
- );
115
- rl.close();
116
- }
117
- const shareLinkUrl = new URL(shareLink);
118
- const origin = shareLinkUrl.origin;
119
- const token = shareLinkUrl.searchParams.get("authToken");
120
- const paths = shareLinkUrl.pathname.split("/").slice(1);
121
- if (paths[0] !== "builder" || paths.length !== 2) {
122
- throw new Error("Invalid share link.");
123
- }
124
- const projectId = paths[1];
125
- if (token == null) {
126
- throw new Error("Invalid share link.");
127
- }
128
- try {
129
- const currentConfig = await readFile2(GLOBAL_CONFIG_FILE, "utf-8");
130
- const currentConfigJson = jsonToGlobalConfig(JSON.parse(currentConfig));
131
- const newConfig = {
132
- ...currentConfigJson,
133
- [projectId]: {
134
- origin,
135
- token
136
- }
137
- };
138
- await writeFile2(GLOBAL_CONFIG_FILE, JSON.stringify(newConfig, null, 2));
139
- console.info(`Saved credentials for project ${projectId}.
140
- You can find your config at ${GLOBAL_CONFIG_FILE}
141
- `);
142
- const localConfig = {
143
- projectId
144
- };
145
- await ensureFileInPath(
146
- join2(cwd(), LOCAL_CONFIG_FILE),
147
- JSON.stringify(localConfig, null, 2)
148
- );
149
- } catch (error) {
150
- if (error instanceof Error && "code" in error && error.code === "ENONET") {
151
- throw new Error(`Global config file is not found`);
142
+ shareLink = await text({
143
+ message: "Please paste a link from the Share Dialog in the builder",
144
+ validate: validateShareLink
145
+ });
146
+ if (isCancel(shareLink)) {
147
+ cancel("Project linking is cancelled");
148
+ exit(1);
152
149
  }
153
- throw error;
154
150
  }
151
+ const { origin, projectId, token } = parseShareLink(shareLink);
152
+ const currentConfig = await readFile2(GLOBAL_CONFIG_FILE, "utf-8");
153
+ const currentConfigJson = jsonToGlobalConfig(JSON.parse(currentConfig));
154
+ const newConfig = {
155
+ ...currentConfigJson,
156
+ [projectId]: {
157
+ origin,
158
+ token
159
+ }
160
+ };
161
+ await writeFile2(GLOBAL_CONFIG_FILE, JSON.stringify(newConfig, null, 2));
162
+ log.info(`Saved credentials for project ${projectId}.
163
+ You can find your config at ${GLOBAL_CONFIG_FILE}`);
164
+ const localConfig = {
165
+ projectId
166
+ };
167
+ await createFileIfNotExists(
168
+ join2(cwd(), LOCAL_CONFIG_FILE),
169
+ JSON.stringify(localConfig, null, 2)
170
+ );
171
+ log.step("The project is linked successfully");
155
172
  };
156
173
 
157
174
  // src/commands/sync.ts
158
175
  import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
159
176
  import { cwd as cwd2 } from "node:process";
160
177
  import { join as join3 } from "node:path";
161
- import ora from "ora";
178
+ import pc from "picocolors";
179
+ import { spinner } from "@clack/prompts";
162
180
  import {
163
181
  loadProjectDataByBuildId,
164
182
  loadProjectDataById
165
183
  } from "@webstudio-is/http-client";
166
- import pc from "picocolors";
167
184
  var syncOptions = (yargs) => yargs.option("buildId", {
168
185
  type: "string",
169
186
  describe: "[Experimental] Project build id to sync"
@@ -175,15 +192,15 @@ var syncOptions = (yargs) => yargs.option("buildId", {
175
192
  describe: "[Experimental] Service token"
176
193
  });
177
194
  var sync = async (options) => {
178
- const spinner = ora("Syncing project data").start();
179
- spinner.text = "Loading project data from config file";
195
+ const syncing = spinner();
196
+ syncing.start("Synchronizing project data");
180
197
  const definedOptionValues = [
181
198
  options.buildId,
182
199
  options.origin,
183
200
  options.authToken
184
201
  ].filter(Boolean);
185
202
  if (definedOptionValues.length > 0 && definedOptionValues.length < 3) {
186
- spinner.fail(`Please provide buildId, origin and authToken`);
203
+ syncing.stop(`Please provide buildId, origin and authToken`, 2);
187
204
  return;
188
205
  }
189
206
  let project;
@@ -194,17 +211,12 @@ var sync = async (options) => {
194
211
  origin: options.origin
195
212
  });
196
213
  } else {
197
- if (await isFileExists(GLOBAL_CONFIG_FILE) === false) {
198
- spinner.fail(
199
- `Global config file at path ${GLOBAL_CONFIG_FILE} is not found. Please link your project using webstudio link command`
200
- );
201
- return;
202
- }
203
214
  const globalConfigText = await readFile3(GLOBAL_CONFIG_FILE, "utf-8");
204
215
  const globalConfig = jsonToGlobalConfig(JSON.parse(globalConfigText));
205
216
  if (await isFileExists(LOCAL_CONFIG_FILE) === false) {
206
- spinner.fail(
207
- `Local config file is not found. Please make sure current directory is a webstudio project`
217
+ syncing.stop(
218
+ `Local config file is not found. Please make sure current directory is a webstudio project`,
219
+ 2
208
220
  );
209
221
  return;
210
222
  }
@@ -215,29 +227,35 @@ var sync = async (options) => {
215
227
  const localConfig = jsonToLocalConfig(JSON.parse(localConfigText));
216
228
  const projectConfig = globalConfig[localConfig.projectId];
217
229
  if (projectConfig === void 0) {
218
- spinner.fail(
219
- `Project config is not found, please run ${pc.dim("webstudio link")}`
230
+ syncing.stop(
231
+ `Project config is not found, please run ${pc.dim("webstudio link")}`,
232
+ 2
220
233
  );
221
234
  return;
222
235
  }
223
236
  const { origin, token } = projectConfig;
224
- spinner.text = "Loading project data from webstudio\n";
225
- project = await loadProjectDataById({
226
- projectId: localConfig.projectId,
227
- authToken: token,
228
- origin
229
- });
237
+ try {
238
+ project = await loadProjectDataById({
239
+ projectId: localConfig.projectId,
240
+ authToken: token,
241
+ origin
242
+ });
243
+ } catch (error) {
244
+ syncing.stop(error.message, 2);
245
+ return;
246
+ }
230
247
  }
231
248
  project;
232
- spinner.text = "Saving project data to config file";
233
249
  const localBuildFilePath = join3(cwd2(), LOCAL_DATA_FILE);
234
- await ensureFileInPath(localBuildFilePath);
250
+ await createFileIfNotExists(localBuildFilePath);
235
251
  await writeFile3(localBuildFilePath, JSON.stringify(project, null, 2), "utf8");
236
- spinner.succeed("Project data synced successfully");
252
+ syncing.stop("Project data synchronized successfully");
237
253
  };
238
254
 
239
255
  // src/commands/build.ts
240
256
  import { access as access3 } from "node:fs/promises";
257
+ import { exit as exit3 } from "node:process";
258
+ import { log as log3 } from "@clack/prompts";
241
259
 
242
260
  // src/prebuild.ts
243
261
  import { basename, dirname as dirname2, join as join4, normalize } from "node:path";
@@ -252,10 +270,10 @@ import {
252
270
  readdir
253
271
  } from "node:fs/promises";
254
272
  import { pipeline } from "node:stream/promises";
255
- import { cwd as cwd3 } from "node:process";
273
+ import { cwd as cwd3, exit as exit2 } from "node:process";
256
274
  import { fileURLToPath, pathToFileURL } from "node:url";
257
275
  import pLimit from "p-limit";
258
- import ora2 from "ora";
276
+ import { log as log2, spinner as spinner2 } from "@clack/prompts";
259
277
  import merge from "deepmerge";
260
278
  import {
261
279
  generateCss,
@@ -265,7 +283,7 @@ import {
265
283
  normalizeProps,
266
284
  generateRemixRoute,
267
285
  generateRemixParams,
268
- collectionComponent
286
+ isCoreComponent
269
287
  } from "@webstudio-is/react-sdk";
270
288
  import {
271
289
  createScope,
@@ -281,6 +299,122 @@ import { createImageLoader } from "@webstudio-is/image";
281
299
  import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
282
300
  import * as remixComponentMetas from "@webstudio-is/sdk-components-react-remix/metas";
283
301
  import * as radixComponentMetas from "@webstudio-is/sdk-components-react-radix/metas";
302
+
303
+ // src/html-to-jsx.ts
304
+ import {
305
+ parseFragment,
306
+ defaultTreeAdapter
307
+ } from "parse5";
308
+ import { camelCase } from "change-case";
309
+ var BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
310
+ "async",
311
+ "autofocus",
312
+ "autoplay",
313
+ "checked",
314
+ "contenteditable",
315
+ "controls",
316
+ "default",
317
+ "defer",
318
+ "disabled",
319
+ "formnovalidate",
320
+ "hidden",
321
+ "ismap",
322
+ "itemscope",
323
+ "loop",
324
+ "multiple",
325
+ "muted",
326
+ "nomodule",
327
+ "novalidate",
328
+ "open",
329
+ "playsinline",
330
+ "readonly",
331
+ "required",
332
+ "reversed",
333
+ "scoped",
334
+ "selected",
335
+ "truespeed"
336
+ ]);
337
+ var isBooleanAttr = (name) => BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
338
+ function* walkChildNodes(node) {
339
+ if (defaultTreeAdapter.isCommentNode(node) || defaultTreeAdapter.isTextNode(node) || defaultTreeAdapter.isDocumentTypeNode(node)) {
340
+ throw new Error("Unsupported node type");
341
+ }
342
+ for (const childNode of node.childNodes) {
343
+ if (defaultTreeAdapter.isCommentNode(childNode)) {
344
+ continue;
345
+ }
346
+ if (defaultTreeAdapter.isTextNode(childNode)) {
347
+ yield { type: "text", value: childNode.value };
348
+ continue;
349
+ }
350
+ if (false === defaultTreeAdapter.isElementNode(childNode)) {
351
+ continue;
352
+ }
353
+ const attributes = childNode.attrs.map((attr) => [
354
+ attr.name,
355
+ attr.value
356
+ ]);
357
+ yield { type: "element-start", tagName: childNode.tagName, attributes };
358
+ yield* walkChildNodes(childNode);
359
+ yield { type: "element-end", tagName: childNode.tagName };
360
+ }
361
+ }
362
+ var convertStyleString = (style) => {
363
+ const styles = style.split(";").map((style2) => style2.trim()).map((style2) => style2.split(":").map((part) => part.trim()));
364
+ const res = {};
365
+ for (const [name, value] of styles) {
366
+ res[camelCase(name)] = value;
367
+ }
368
+ return JSON.stringify(res);
369
+ };
370
+ var toAttrString = (name, value) => {
371
+ const attName = name.toLowerCase();
372
+ const jsxName = attName === "class" ? "className" : attName;
373
+ if (value === "" && isBooleanAttr(attName)) {
374
+ return `${jsxName}`;
375
+ }
376
+ if (attName === "style") {
377
+ return `${jsxName}={${convertStyleString(value)}}`;
378
+ }
379
+ return `${jsxName}="${value}"`;
380
+ };
381
+ var attributesToString = (attributes) => attributes.map(([attName, value]) => ` ${toAttrString(attName, value)}`).join("");
382
+ var convertTagName = (tagName) => {
383
+ const tag = tagName.toLowerCase();
384
+ if (tag === "script") {
385
+ return "Script";
386
+ }
387
+ if (tag === "style") {
388
+ return "Style";
389
+ }
390
+ return tag;
391
+ };
392
+ var escapeValue = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
393
+ var htmlToJsx = (html) => {
394
+ const parsedHtml = parseFragment(html, { scriptingEnabled: false });
395
+ let result = "";
396
+ for (const walkNode of walkChildNodes(parsedHtml)) {
397
+ switch (walkNode.type) {
398
+ case "text": {
399
+ const escapedValue = escapeValue(walkNode.value);
400
+ result += escapedValue ? "{`" + escapedValue + "`}" : "";
401
+ break;
402
+ }
403
+ case "element-start": {
404
+ const tag = convertTagName(walkNode.tagName);
405
+ result += `<${tag}${attributesToString(walkNode.attributes)}>`;
406
+ break;
407
+ }
408
+ case "element-end": {
409
+ result += `</${convertTagName(walkNode.tagName)}>`;
410
+ break;
411
+ }
412
+ }
413
+ }
414
+ return result;
415
+ };
416
+
417
+ // src/prebuild.ts
284
418
  var limit = pLimit(10);
285
419
  var downloadAsset = async (url, name, assetBaseUrl) => {
286
420
  const assetPath = join4("public", assetBaseUrl, name);
@@ -288,7 +422,7 @@ var downloadAsset = async (url, name, assetBaseUrl) => {
288
422
  try {
289
423
  await access2(assetPath);
290
424
  } catch {
291
- await ensureFolderExists(dirname2(assetPath));
425
+ await createFolderIfNotExists(dirname2(assetPath));
292
426
  try {
293
427
  const response = await fetch(url);
294
428
  if (!response.ok) {
@@ -357,12 +491,12 @@ var copyTemplates = async (template = "defaults") => {
357
491
  }
358
492
  };
359
493
  var prebuild = async (options) => {
360
- if (options.template === void 0) {
361
- throw new Error(
362
- `
363
- Template is not provided
364
- Please check webstudio --help for more details`
494
+ if (options.template.length === 0) {
495
+ log2.error(
496
+ `Template is not provided
497
+ Please check webstudio --help for more details`
365
498
  );
499
+ exit2(1);
366
500
  }
367
501
  for (const template of options.template) {
368
502
  if (template === "vanilla") {
@@ -372,16 +506,14 @@ var prebuild = async (options) => {
372
506
  continue;
373
507
  }
374
508
  if (await isCliTemplate(template) === false) {
375
- throw Error(
376
- `
377
- Template ${options.template} is not available
378
- Please check webstudio --help for more details`
509
+ log2.error(
510
+ `Template ${options.template} is not available
511
+ Please check webstudio --help for more details`
379
512
  );
513
+ exit2(1);
380
514
  }
381
515
  }
382
- const spinner = ora2("Scaffolding the project files");
383
- spinner.start();
384
- spinner.text = "Generating files";
516
+ log2.step("Scaffolding the project files");
385
517
  const appRoot = "app";
386
518
  const generatedDir = join4(appRoot, "__generated__");
387
519
  await rm(generatedDir, { recursive: true, force: true });
@@ -480,7 +612,7 @@ var prebuild = async (options) => {
480
612
  };
481
613
  componentsByPage[page.id] = /* @__PURE__ */ new Set();
482
614
  for (const [_instanceId, instance] of instances) {
483
- if (instance.component === collectionComponent) {
615
+ if (isCoreComponent(instance.component)) {
484
616
  continue;
485
617
  }
486
618
  componentsByPage[page.id].add(instance.component);
@@ -544,8 +676,9 @@ var prebuild = async (options) => {
544
676
  }
545
677
  }
546
678
  const assets = new Map(siteData.assets.map((asset) => [asset.id, asset]));
547
- spinner.text = "Generating css file";
548
679
  const { cssText, classesMap } = generateCss({
680
+ instances: new Map(siteData.build.instances),
681
+ props: new Map(siteData.build.props),
549
682
  assets,
550
683
  breakpoints: new Map(siteData.build?.breakpoints),
551
684
  styles: new Map(siteData.build?.styles),
@@ -555,11 +688,17 @@ var prebuild = async (options) => {
555
688
  assetBaseUrl,
556
689
  atomic: siteData.build.pages.compiler?.atomicStyles ?? true
557
690
  });
558
- await ensureFileInPath(join4(generatedDir, "index.css"), cssText);
559
- spinner.text = "Generating routes and pages";
560
- const routeTemplatePath = normalize(join4(cwd3(), "app/routes/template.tsx"));
691
+ await createFileIfNotExists(join4(generatedDir, "index.css"), cssText);
692
+ const routeTemplatesDir = join4(cwd3(), "app/route-templates");
693
+ const routeTemplatePath = normalize(join4(routeTemplatesDir, "html.tsx"));
694
+ const routeXmlTemplatePath = normalize(join4(routeTemplatesDir, "xml.tsx"));
695
+ const defaultSiteMapXmlPath = normalize(
696
+ join4(routeTemplatesDir, "default-sitemap.tsx")
697
+ );
561
698
  const routeFileTemplate = await readFile4(routeTemplatePath, "utf8");
562
- await rm(routeTemplatePath);
699
+ const routeXmlFileTemplate = await readFile4(routeXmlTemplatePath, "utf8");
700
+ const defaultSiteMapTemplate = await readFile4(defaultSiteMapXmlPath, "utf8");
701
+ await rm(routeTemplatesDir, { recursive: true, force: true });
563
702
  for (const [pageId, pageComponents] of Object.entries(componentsByPage)) {
564
703
  const scope = createScope([
565
704
  // manually maintained list of occupied identifiers
@@ -592,14 +731,36 @@ var prebuild = async (options) => {
592
731
  namespaces.get(namespace)?.add([shortName, component]);
593
732
  }
594
733
  let componentImports = "";
734
+ let xmlPresentationComponents = "";
735
+ const pageData = siteDataByPage[pageId];
736
+ const documentType = pageData.page.meta.documentType ?? "html";
595
737
  for (const [namespace, componentsSet] of namespaces.entries()) {
596
- const specifiers = Array.from(componentsSet).map(
597
- ([shortName, component]) => `${shortName} as ${scope.getName(component, shortName)}`
598
- ).join(", ");
599
- componentImports += `import { ${specifiers} } from "${namespace}";
738
+ switch (documentType) {
739
+ case "html":
740
+ {
741
+ const specifiers = Array.from(componentsSet).map(
742
+ ([shortName, component]) => `${shortName} as ${scope.getName(component, shortName)}`
743
+ ).join(", ");
744
+ componentImports += `import { ${specifiers} } from "${namespace}";
600
745
  `;
746
+ }
747
+ break;
748
+ case "xml":
749
+ {
750
+ componentImports = `import { XmlNode } from "@webstudio-is/sdk-components-react";
751
+ `;
752
+ xmlPresentationComponents += Array.from(componentsSet).map(
753
+ ([shortName, component]) => scope.getName(component, shortName)
754
+ ).filter((scopedName) => scopedName !== "XmlNode").map(
755
+ (scopedName) => scopedName === "Body" ? `const ${scopedName} = (props: any) => props.children;` : `const ${scopedName} = () => null;`
756
+ ).join("\n");
757
+ }
758
+ break;
759
+ default: {
760
+ documentType;
761
+ }
762
+ }
601
763
  }
602
- const pageData = siteDataByPage[pageId];
603
764
  const pageFontAssets = fontAssetsByPage[pageId];
604
765
  const pageBackgroundImageAssets = backgroundImageAssetsByPage[pageId];
605
766
  const rootInstanceId = pageData.page.rootInstanceId;
@@ -638,83 +799,108 @@ var prebuild = async (options) => {
638
799
  const pageMeta = pageData.page.meta;
639
800
  const favIconAsset = assets.get(projectMeta?.faviconAssetId ?? "");
640
801
  const socialImageAsset = assets.get(pageMeta.socialImageAssetId ?? "");
802
+ const pagePath = getPagePath(pageData.page.id, siteData.build.pages);
803
+ const remixRoute = generateRemixRoute(pagePath);
804
+ const fileName = `${remixRoute}.tsx`;
641
805
  const pageExports = `/* eslint-disable */
642
- /* This is a auto generated file for building the project */
806
+ /* This is a auto generated file for building the project */
643
807
 
644
808
 
645
- import { Fragment, useState } from "react";
646
- import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
647
- import { useResource } from "@webstudio-is/react-sdk";
648
- ${componentImports}
809
+ import { Fragment, useState } from "react";
810
+ import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
811
+ import { useResource } from "@webstudio-is/react-sdk";
812
+ ${componentImports}
649
813
 
650
- export const siteName = ${JSON.stringify(projectMeta?.siteName)};
814
+ export const siteName = ${JSON.stringify(projectMeta?.siteName)};
651
815
 
652
- export const favIconAsset: ImageAsset | undefined =
653
- ${JSON.stringify(favIconAsset)};
816
+ export const favIconAsset: ImageAsset | undefined =
817
+ ${JSON.stringify(favIconAsset)};
654
818
 
655
- export const socialImageAsset: ImageAsset | undefined =
656
- ${JSON.stringify(socialImageAsset)};
819
+ export const socialImageAsset: ImageAsset | undefined =
820
+ ${JSON.stringify(socialImageAsset)};
657
821
 
658
- // Font assets on current page (can be preloaded)
659
- export const pageFontAssets: FontAsset[] =
660
- ${JSON.stringify(pageFontAssets)}
822
+ // Font assets on current page (can be preloaded)
823
+ export const pageFontAssets: FontAsset[] =
824
+ ${JSON.stringify(pageFontAssets)}
661
825
 
662
- export const pageBackgroundImageAssets: ImageAsset[] =
663
- ${JSON.stringify(pageBackgroundImageAssets)}
826
+ export const pageBackgroundImageAssets: ImageAsset[] =
827
+ ${JSON.stringify(pageBackgroundImageAssets)}
664
828
 
829
+ ${remixRoute === "_index" ? `
830
+ ${projectMeta?.code ? `
831
+ const Script = ({children, ...props}: Record<string, string | boolean>) => {
832
+ if (children == null) {
833
+ return <script {...props} />;
834
+ }
665
835
 
836
+ return <script {...props} dangerouslySetInnerHTML={{__html: children}} />;
837
+ };
838
+ const Style = ({children, ...props}: Record<string, string | boolean>) => {
839
+ if (children == null) {
840
+ return <style {...props} />;
841
+ }
666
842
 
667
- ${pageComponent}
843
+ return <style {...props} dangerouslySetInnerHTML={{__html: children}} />;
844
+ };
845
+ ` : ""}
668
846
 
669
- export { Page }
670
- `;
847
+ export const CustomCode = () => {
848
+ return (<>${projectMeta?.code ? htmlToJsx(projectMeta.code) : ""}</>);
849
+ }
850
+ ` : ""}
851
+
852
+ ${xmlPresentationComponents}
853
+
854
+ ${pageComponent}
855
+
856
+ export { Page }
857
+ `;
671
858
  const serverExports = `/* eslint-disable */
672
- /* This is a auto generated file for building the project */
859
+ /* This is a auto generated file for building the project */
673
860
 
674
861
 
675
- import type { PageMeta } from "@webstudio-is/sdk";
676
- ${generateResourcesLoader({
862
+ import type { PageMeta } from "@webstudio-is/sdk";
863
+ ${generateResourcesLoader({
677
864
  scope,
678
865
  page: pageData.page,
679
866
  dataSources,
680
867
  resources
681
868
  })}
682
869
 
683
- ${generatePageMeta({
870
+ ${generatePageMeta({
684
871
  globalScope: scope,
685
872
  page: pageData.page,
686
873
  dataSources
687
874
  })}
688
875
 
689
- ${generateFormsProperties(props)}
876
+ ${generateFormsProperties(props)}
690
877
 
691
- ${generateRemixParams(pageData.page.path)}
878
+ ${generateRemixParams(pageData.page.path)}
692
879
 
693
- export const projectId = "${siteData.build.projectId}";
880
+ export const projectId = "${siteData.build.projectId}";
694
881
 
695
- export const contactEmail = ${JSON.stringify(contactEmail)};
696
-
697
- export const customCode = ${JSON.stringify(projectMeta?.code?.trim() ?? "")};
698
- `;
699
- const pagePath = getPagePath(pageData.page.id, siteData.build.pages);
700
- const remixRoute = generateRemixRoute(pagePath);
701
- const fileName = `${remixRoute}.tsx`;
702
- const routeFileContent = routeFileTemplate.replace(
882
+ export const contactEmail = ${JSON.stringify(contactEmail)};
883
+ `;
884
+ const routeFileContent = (documentType === "html" ? routeFileTemplate : routeXmlFileTemplate).replace(
703
885
  /".*\/__generated__\/_index"/,
704
886
  `"../__generated__/${remixRoute}"`
705
887
  ).replace(
706
888
  /".*\/__generated__\/_index.server"/,
707
889
  `"../__generated__/${remixRoute}.server"`
708
890
  );
709
- await ensureFileInPath(join4(routesDir, fileName), routeFileContent);
710
- await ensureFileInPath(join4(generatedDir, fileName), pageExports);
711
- await ensureFileInPath(
891
+ await createFileIfNotExists(join4(routesDir, fileName), routeFileContent);
892
+ await createFileIfNotExists(join4(generatedDir, fileName), pageExports);
893
+ await createFileIfNotExists(
712
894
  join4(generatedDir, `${remixRoute}.server.tsx`),
713
895
  serverExports
714
896
  );
715
897
  }
716
- await writeFile4(
717
- join4(generatedDir, "[sitemap.xml].ts"),
898
+ await createFileIfNotExists(
899
+ join4(routesDir, "[sitemap.xml]._index.tsx"),
900
+ defaultSiteMapTemplate.replace(/".*\/__generated__\//, `"../__generated__/`)
901
+ );
902
+ await createFileIfNotExists(
903
+ join4(generatedDir, "$resources.sitemap.xml.ts"),
718
904
  `
719
905
  export const sitemap = ${JSON.stringify(
720
906
  getStaticSiteMapXml(siteData.build.pages, siteData.build.updatedAt),
@@ -725,22 +911,25 @@ export const customCode = ${JSON.stringify(projectMeta?.code?.trim() ?? "")};
725
911
  );
726
912
  const redirects = siteData.build.pages?.redirects;
727
913
  if (redirects !== void 0 && redirects.length > 0) {
728
- spinner.text = "Generating redirects";
729
914
  for (const redirect of redirects) {
730
915
  const redirectPagePath = generateRemixRoute(redirect.old);
731
916
  const redirectFileName = `${redirectPagePath}.ts`;
732
917
  const content = `import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
733
918
 
734
- export const loader = (arg: LoaderFunctionArgs) => {
735
- return redirect("${redirect.new}", ${redirect.status ?? 301});
736
- };
737
- `;
738
- await ensureFileInPath(join4(routesDir, redirectFileName), content);
919
+ export const loader = (arg: LoaderFunctionArgs) => {
920
+ return redirect("${redirect.new}", ${redirect.status ?? 301});
921
+ };
922
+ `;
923
+ await createFileIfNotExists(join4(routesDir, redirectFileName), content);
739
924
  }
740
925
  }
741
- spinner.text = "Downloading fonts and images";
742
- await Promise.all(assetsToDownload);
743
- spinner.succeed("Build finished");
926
+ if (assetsToDownload.length > 0) {
927
+ const downloading = spinner2();
928
+ downloading.start("Downloading fonts and images");
929
+ await Promise.all(assetsToDownload);
930
+ downloading.stop("Downloaded fonts and images");
931
+ }
932
+ log2.step("Build finished");
744
933
  };
745
934
 
746
935
  // src/commands/build.ts
@@ -755,6 +944,7 @@ var buildOptions = (yargs) => yargs.option("assets", {
755
944
  }).option("template", {
756
945
  type: "array",
757
946
  string: true,
947
+ default: [],
758
948
  describe: `Template to use for the build [choices: ${PROJECT_TEMPALTES.join(
759
949
  ", "
760
950
  )}]`
@@ -764,9 +954,10 @@ var build = async (options) => {
764
954
  await access3(LOCAL_DATA_FILE);
765
955
  } catch (error) {
766
956
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
767
- throw new Error(
957
+ log3.error(
768
958
  `You need to link a webstudio project before building it. Run \`webstudio link\` to link a project.`
769
959
  );
960
+ exit3(1);
770
961
  }
771
962
  throw error;
772
963
  }
@@ -776,110 +967,99 @@ var build = async (options) => {
776
967
  // src/commands/init-flow.ts
777
968
  import { chdir, cwd as cwd4 } from "node:process";
778
969
  import { join as join5 } from "node:path";
779
- import ora3 from "ora";
780
-
781
- // src/prompts.ts
782
- import prompts from "prompts";
783
- var prompt = (prompt2) => {
784
- return prompts([
785
- {
786
- ...prompt2,
787
- onState: (state) => {
788
- if (state.aborted) {
789
- process.nextTick(() => {
790
- process.exit(0);
791
- });
792
- }
793
- }
794
- }
795
- ]);
796
- };
797
-
798
- // src/commands/init-flow.ts
799
970
  import pc2 from "picocolors";
971
+ import {
972
+ cancel as cancel2,
973
+ confirm,
974
+ isCancel as isCancel2,
975
+ log as log4,
976
+ select,
977
+ spinner as spinner3,
978
+ text as text2
979
+ } from "@clack/prompts";
800
980
  import { $ } from "execa";
801
981
  import { titleCase } from "title-case";
982
+ var exitIfCancelled = (value) => {
983
+ if (isCancel2(value)) {
984
+ cancel2("Project initialization is cancelled");
985
+ process.exit(0);
986
+ }
987
+ return value;
988
+ };
802
989
  var initFlow = async (options) => {
803
990
  const isProjectConfigured = await isFileExists(".webstudio/config.json");
804
991
  let shouldInstallDeps = false;
805
992
  let folderName;
806
993
  let projectTemplate = void 0;
807
994
  if (isProjectConfigured === false) {
808
- const { shouldCreateFolder } = await prompt({
809
- type: "confirm",
810
- name: "shouldCreateFolder",
811
- message: "Would you like to create a project folder? (no to use current folder)",
812
- initial: true
813
- });
995
+ const shouldCreateFolder = exitIfCancelled(
996
+ await confirm({
997
+ message: "Would you like to create a project folder? (no to use current folder)",
998
+ initialValue: true
999
+ })
1000
+ );
814
1001
  if (shouldCreateFolder === true) {
815
- folderName = (await prompt({
816
- type: "text",
817
- name: "folderName",
818
- message: "Please enter a project name"
819
- })).folderName;
820
- if (folderName === void 0) {
821
- throw new Error("Folder name is required");
822
- }
823
- await ensureFolderExists(join5(cwd4(), folderName));
1002
+ folderName = exitIfCancelled(
1003
+ await text2({
1004
+ message: "Please enter a project name",
1005
+ validate(value) {
1006
+ if (value.length === 0) {
1007
+ return "Folder name is required";
1008
+ }
1009
+ }
1010
+ })
1011
+ );
1012
+ await createFolderIfNotExists(join5(cwd4(), folderName));
824
1013
  chdir(join5(cwd4(), folderName));
825
1014
  }
826
- const { projectLink } = await prompt({
827
- type: "text",
828
- name: "projectLink",
829
- message: "Please paste a link from the Share dialog in the builder"
830
- });
831
- if (projectLink === void 0) {
832
- throw new Error(`Project Link is required`);
833
- }
834
- await link({ link: projectLink });
835
- const { deployTarget } = await prompt({
836
- type: "select",
837
- name: "deployTarget",
838
- message: "Where would you like to deploy your project?",
839
- choices: PROJECT_TEMPALTES.map((template) => {
840
- return {
841
- title: titleCase(template),
842
- value: template
843
- };
1015
+ const shareLink = exitIfCancelled(
1016
+ await text2({
1017
+ message: "Please paste a link from the Share Dialog in the builder",
1018
+ validate: validateShareLink
844
1019
  })
845
- });
846
- projectTemplate = deployTarget;
847
- const { installDeps } = await prompt({
848
- type: "confirm",
849
- name: "installDeps",
850
- message: "Would you like to install dependencies? (recommended)",
851
- initial: true
852
- });
853
- shouldInstallDeps = installDeps;
1020
+ );
1021
+ await link({ link: shareLink });
1022
+ projectTemplate = exitIfCancelled(
1023
+ await select({
1024
+ message: "Where would you like to deploy your project?",
1025
+ options: PROJECT_TEMPALTES.map((template) => ({
1026
+ value: template,
1027
+ label: titleCase(template)
1028
+ }))
1029
+ })
1030
+ );
1031
+ shouldInstallDeps = exitIfCancelled(
1032
+ await confirm({
1033
+ message: "Would you like to install dependencies? (recommended)",
1034
+ initialValue: true
1035
+ })
1036
+ );
854
1037
  }
855
- await sync({ buildId: void 0, origin: void 0, authToken: void 0 });
856
1038
  if (projectTemplate === void 0) {
857
- const { deployTarget } = await prompt({
858
- type: "select",
859
- name: "deployTarget",
860
- message: "Where would you like to deploy your project?",
861
- choices: PROJECT_TEMPALTES.map((template) => {
862
- return {
863
- title: titleCase(template),
864
- value: template
865
- };
1039
+ projectTemplate = exitIfCancelled(
1040
+ await select({
1041
+ message: "Where would you like to deploy your project?",
1042
+ options: PROJECT_TEMPALTES.map((template) => ({
1043
+ value: template,
1044
+ label: titleCase(template)
1045
+ }))
866
1046
  })
867
- });
868
- projectTemplate = deployTarget;
1047
+ );
869
1048
  }
1049
+ await sync({ buildId: void 0, origin: void 0, authToken: void 0 });
870
1050
  await build({
871
1051
  ...options,
872
1052
  ...projectTemplate && { template: [projectTemplate] }
873
1053
  });
874
1054
  if (shouldInstallDeps === true) {
875
- const spinner = ora3().start();
876
- spinner.text = "Installing dependencies";
1055
+ const install = spinner3();
1056
+ install.start("Installing dependencies");
877
1057
  await $`npm install`;
878
- spinner.succeed("Installed dependencies");
1058
+ install.stop("Installed dependencies");
879
1059
  }
880
- console.info(pc2.bold(pc2.green(`
881
- Your project was successfully synced \u{1F389}`)));
882
- console.info(
1060
+ log4.message();
1061
+ log4.message(pc2.green(pc2.bold(`Your project was successfully synced \u{1F389}`)));
1062
+ log4.message(
883
1063
  [
884
1064
  "Now you can:",
885
1065
  folderName && `Go to your project: ${pc2.dim(`cd ${folderName}`)}`,
@@ -912,7 +1092,7 @@ import makeCLI from "yargs";
912
1092
  // package.json
913
1093
  var package_default = {
914
1094
  name: "webstudio",
915
- version: "0.151.0",
1095
+ version: "0.167.0",
916
1096
  description: "Webstudio CLI",
917
1097
  author: "Webstudio <github@webstudio.is>",
918
1098
  homepage: "https://webstudio.is",
@@ -932,10 +1112,12 @@ var package_default = {
932
1112
  checks: "pnpm typecheck",
933
1113
  build: "rm -rf lib && esbuild src/cli.ts --outdir=lib --bundle --format=esm --packages=external",
934
1114
  "local-run": "tsx --no-warnings ./src/bin.ts",
935
- dev: "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib"
1115
+ dev: "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib",
1116
+ test: "NODE_OPTIONS=--experimental-vm-modules jest"
936
1117
  },
937
1118
  license: "AGPL-3.0-or-later",
938
1119
  dependencies: {
1120
+ "@clack/prompts": "^0.7.0",
939
1121
  "@webstudio-is/http-client": "workspace:*",
940
1122
  "@webstudio-is/image": "workspace:*",
941
1123
  "@webstudio-is/react-sdk": "workspace:*",
@@ -943,39 +1125,39 @@ var package_default = {
943
1125
  "@webstudio-is/sdk-components-react": "workspace:*",
944
1126
  "@webstudio-is/sdk-components-react-radix": "workspace:*",
945
1127
  "@webstudio-is/sdk-components-react-remix": "workspace:*",
1128
+ "change-case": "^5.0.2",
946
1129
  deepmerge: "^4.3.1",
947
1130
  "env-paths": "^3.0.0",
948
1131
  execa: "^7.2.0",
949
- ora: "^7.0.1",
1132
+ parse5: "7.1.2",
950
1133
  "p-limit": "^4.0.0",
951
- picocolors: "^1.0.0",
952
- prompts: "^2.4.2",
1134
+ picocolors: "^1.0.1",
953
1135
  "strip-indent": "^4.0.0",
954
1136
  "title-case": "^4.1.0",
955
1137
  yargs: "^17.7.2",
956
1138
  zod: "^3.22.4"
957
1139
  },
958
1140
  devDependencies: {
959
- "@netlify/remix-adapter": "^2.3.1",
960
- "@netlify/remix-edge-adapter": "3.2.2",
961
- "@remix-run/cloudflare": "^2.9.1",
962
- "@remix-run/cloudflare-pages": "^2.9.1",
963
- "@remix-run/dev": "^2.9.1",
964
- "@remix-run/node": "^2.9.1",
965
- "@remix-run/react": "^2.9.1",
966
- "@remix-run/server-runtime": "^2.9.1",
1141
+ "@jest/globals": "^29.7.0",
1142
+ "@netlify/remix-adapter": "^2.4.0",
1143
+ "@netlify/remix-edge-adapter": "3.3.0",
1144
+ "@remix-run/cloudflare": "^2.9.2",
1145
+ "@remix-run/cloudflare-pages": "^2.9.2",
1146
+ "@remix-run/dev": "^2.9.2",
1147
+ "@remix-run/node": "^2.9.2",
1148
+ "@remix-run/react": "^2.9.2",
1149
+ "@remix-run/server-runtime": "^2.9.2",
967
1150
  "@types/node": "^20.12.7",
968
- "@types/prompts": "^2.4.5",
969
1151
  "@types/react": "^18.2.70",
970
1152
  "@types/react-dom": "^18.2.25",
971
1153
  "@types/yargs": "^17.0.32",
972
1154
  "@webstudio-is/form-handlers": "workspace:*",
1155
+ "@webstudio-is/jest-config": "workspace:*",
973
1156
  "@webstudio-is/tsconfig": "workspace:*",
974
1157
  react: "18.3.0-canary-14898b6a9-20240318",
975
1158
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
976
- tsx: "^4.7.2",
977
1159
  typescript: "5.4.5",
978
- vite: "^5.2.11",
1160
+ vite: "^5.2.12",
979
1161
  wrangler: "^3.48.0"
980
1162
  }
981
1163
  };
@@ -983,7 +1165,7 @@ var package_default = {
983
1165
  // src/cli.ts
984
1166
  var main = async () => {
985
1167
  try {
986
- await ensureFileInPath(GLOBAL_CONFIG_FILE, "{}");
1168
+ await createFileIfNotExists(GLOBAL_CONFIG_FILE, "{}");
987
1169
  const cmd = makeCLI(hideBin(argv)).strict().fail(function(msg, err, yargs) {
988
1170
  if (err) {
989
1171
  throw err;
@@ -1019,7 +1201,7 @@ var main = async () => {
1019
1201
  await cmd.parse();
1020
1202
  } catch (error) {
1021
1203
  console.error(error);
1022
- exit(1);
1204
+ exit4(1);
1023
1205
  }
1024
1206
  };
1025
1207
  export {