webstudio 0.163.0 → 0.168.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/README.md CHANGED
@@ -35,7 +35,7 @@ To get started with the Webstudio CLI:
35
35
  1. Download and install the CLI using the following command:
36
36
 
37
37
  ```bash
38
- npm install -g webstudio
38
+ npm install -g webstudio@latest
39
39
  ```
40
40
 
41
41
  1. Confirm the installation by checking the CLI version:
package/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { main } from "./lib/cli.js";
3
+ import { main } from "#cli";
4
4
 
5
5
  await main();
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
@@ -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 createFileIfNotExists(
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
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,
@@ -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 escape = (value) => JSON.stringify(value);
371
+ var toAttrString = (name, value) => {
372
+ const attName = name.toLowerCase();
373
+ const jsxName = attName === "class" ? "className" : attName;
374
+ if (value === "" && isBooleanAttr(attName)) {
375
+ return `${jsxName}`;
376
+ }
377
+ if (attName === "style") {
378
+ return `${jsxName}={${convertStyleString(value)}}`;
379
+ }
380
+ return `${jsxName}={${escape(value)}}`;
381
+ };
382
+ var attributesToString = (attributes) => attributes.map(([attName, value]) => ` ${toAttrString(attName, value)}`).join("");
383
+ var convertTagName = (tagName) => {
384
+ const tag = tagName.toLowerCase();
385
+ if (tag === "script") {
386
+ return "Script";
387
+ }
388
+ if (tag === "style") {
389
+ return "Style";
390
+ }
391
+ return tag;
392
+ };
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 = escape(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);
@@ -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,22 +506,21 @@ 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 });
388
520
  const routesDir = join4(appRoot, "routes");
389
521
  await rm(routesDir, { recursive: true, force: true });
390
522
  await copyTemplates();
523
+ await writeFile4(join4(cwd3(), ".npmrc"), "force=true");
391
524
  for (const template of options.template) {
392
525
  if (template === "vanilla") {
393
526
  continue;
@@ -544,7 +677,6 @@ var prebuild = async (options) => {
544
677
  }
545
678
  }
546
679
  const assets = new Map(siteData.assets.map((asset) => [asset.id, asset]));
547
- spinner.text = "Generating css file";
548
680
  const { cssText, classesMap } = generateCss({
549
681
  instances: new Map(siteData.build.instances),
550
682
  props: new Map(siteData.build.props),
@@ -558,7 +690,6 @@ var prebuild = async (options) => {
558
690
  atomic: siteData.build.pages.compiler?.atomicStyles ?? true
559
691
  });
560
692
  await createFileIfNotExists(join4(generatedDir, "index.css"), cssText);
561
- spinner.text = "Generating routes and pages";
562
693
  const routeTemplatesDir = join4(cwd3(), "app/route-templates");
563
694
  const routeTemplatePath = normalize(join4(routeTemplatesDir, "html.tsx"));
564
695
  const routeXmlTemplatePath = normalize(join4(routeTemplatesDir, "xml.tsx"));
@@ -669,6 +800,9 @@ var prebuild = async (options) => {
669
800
  const pageMeta = pageData.page.meta;
670
801
  const favIconAsset = assets.get(projectMeta?.faviconAssetId ?? "");
671
802
  const socialImageAsset = assets.get(pageMeta.socialImageAssetId ?? "");
803
+ const pagePath = getPagePath(pageData.page.id, siteData.build.pages);
804
+ const remixRoute = generateRemixRoute(pagePath);
805
+ const fileName = `${remixRoute}.tsx`;
672
806
  const pageExports = `/* eslint-disable */
673
807
  /* This is a auto generated file for building the project */
674
808
 
@@ -693,6 +827,29 @@ var prebuild = async (options) => {
693
827
  export const pageBackgroundImageAssets: ImageAsset[] =
694
828
  ${JSON.stringify(pageBackgroundImageAssets)}
695
829
 
830
+ ${remixRoute === "_index" ? `
831
+ ${projectMeta?.code ? `
832
+ const Script = ({children, ...props}: Record<string, string | boolean>) => {
833
+ if (children == null) {
834
+ return <script {...props} />;
835
+ }
836
+
837
+ return <script {...props} dangerouslySetInnerHTML={{__html: children}} />;
838
+ };
839
+ const Style = ({children, ...props}: Record<string, string | boolean>) => {
840
+ if (children == null) {
841
+ return <style {...props} />;
842
+ }
843
+
844
+ return <style {...props} dangerouslySetInnerHTML={{__html: children}} />;
845
+ };
846
+ ` : ""}
847
+
848
+ export const CustomCode = () => {
849
+ return (<>${projectMeta?.code ? htmlToJsx(projectMeta.code) : ""}</>);
850
+ }
851
+ ` : ""}
852
+
696
853
  ${xmlPresentationComponents}
697
854
 
698
855
  ${pageComponent}
@@ -724,14 +881,7 @@ var prebuild = async (options) => {
724
881
  export const projectId = "${siteData.build.projectId}";
725
882
 
726
883
  export const contactEmail = ${JSON.stringify(contactEmail)};
727
-
728
- export const customCode = ${JSON.stringify(
729
- projectMeta?.code?.trim() ?? ""
730
- )};
731
884
  `;
732
- const pagePath = getPagePath(pageData.page.id, siteData.build.pages);
733
- const remixRoute = generateRemixRoute(pagePath);
734
- const fileName = `${remixRoute}.tsx`;
735
885
  const routeFileContent = (documentType === "html" ? routeFileTemplate : routeXmlFileTemplate).replace(
736
886
  /".*\/__generated__\/_index"/,
737
887
  `"../__generated__/${remixRoute}"`
@@ -762,7 +912,6 @@ var prebuild = async (options) => {
762
912
  );
763
913
  const redirects = siteData.build.pages?.redirects;
764
914
  if (redirects !== void 0 && redirects.length > 0) {
765
- spinner.text = "Generating redirects";
766
915
  for (const redirect of redirects) {
767
916
  const redirectPagePath = generateRemixRoute(redirect.old);
768
917
  const redirectFileName = `${redirectPagePath}.ts`;
@@ -775,9 +924,13 @@ var prebuild = async (options) => {
775
924
  await createFileIfNotExists(join4(routesDir, redirectFileName), content);
776
925
  }
777
926
  }
778
- spinner.text = "Downloading fonts and images";
779
- await Promise.all(assetsToDownload);
780
- spinner.succeed("Build finished");
927
+ if (assetsToDownload.length > 0) {
928
+ const downloading = spinner2();
929
+ downloading.start("Downloading fonts and images");
930
+ await Promise.all(assetsToDownload);
931
+ downloading.stop("Downloaded fonts and images");
932
+ }
933
+ log2.step("Build finished");
781
934
  };
782
935
 
783
936
  // src/commands/build.ts
@@ -792,6 +945,7 @@ var buildOptions = (yargs) => yargs.option("assets", {
792
945
  }).option("template", {
793
946
  type: "array",
794
947
  string: true,
948
+ default: [],
795
949
  describe: `Template to use for the build [choices: ${PROJECT_TEMPALTES.join(
796
950
  ", "
797
951
  )}]`
@@ -801,9 +955,10 @@ var build = async (options) => {
801
955
  await access3(LOCAL_DATA_FILE);
802
956
  } catch (error) {
803
957
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
804
- throw new Error(
958
+ log3.error(
805
959
  `You need to link a webstudio project before building it. Run \`webstudio link\` to link a project.`
806
960
  );
961
+ exit3(1);
807
962
  }
808
963
  throw error;
809
964
  }
@@ -813,110 +968,99 @@ var build = async (options) => {
813
968
  // src/commands/init-flow.ts
814
969
  import { chdir, cwd as cwd4 } from "node:process";
815
970
  import { join as join5 } from "node:path";
816
- import ora3 from "ora";
817
-
818
- // src/prompts.ts
819
- import prompts from "prompts";
820
- var prompt = (prompt2) => {
821
- return prompts([
822
- {
823
- ...prompt2,
824
- onState: (state) => {
825
- if (state.aborted) {
826
- process.nextTick(() => {
827
- process.exit(0);
828
- });
829
- }
830
- }
831
- }
832
- ]);
833
- };
834
-
835
- // src/commands/init-flow.ts
836
971
  import pc2 from "picocolors";
972
+ import {
973
+ cancel as cancel2,
974
+ confirm,
975
+ isCancel as isCancel2,
976
+ log as log4,
977
+ select,
978
+ spinner as spinner3,
979
+ text as text2
980
+ } from "@clack/prompts";
837
981
  import { $ } from "execa";
838
982
  import { titleCase } from "title-case";
983
+ var exitIfCancelled = (value) => {
984
+ if (isCancel2(value)) {
985
+ cancel2("Project initialization is cancelled");
986
+ process.exit(0);
987
+ }
988
+ return value;
989
+ };
839
990
  var initFlow = async (options) => {
840
991
  const isProjectConfigured = await isFileExists(".webstudio/config.json");
841
992
  let shouldInstallDeps = false;
842
993
  let folderName;
843
994
  let projectTemplate = void 0;
844
995
  if (isProjectConfigured === false) {
845
- const { shouldCreateFolder } = await prompt({
846
- type: "confirm",
847
- name: "shouldCreateFolder",
848
- message: "Would you like to create a project folder? (no to use current folder)",
849
- initial: true
850
- });
996
+ const shouldCreateFolder = exitIfCancelled(
997
+ await confirm({
998
+ message: "Would you like to create a project folder? (no to use current folder)",
999
+ initialValue: true
1000
+ })
1001
+ );
851
1002
  if (shouldCreateFolder === true) {
852
- folderName = (await prompt({
853
- type: "text",
854
- name: "folderName",
855
- message: "Please enter a project name"
856
- })).folderName;
857
- if (folderName === void 0) {
858
- throw new Error("Folder name is required");
859
- }
1003
+ folderName = exitIfCancelled(
1004
+ await text2({
1005
+ message: "Please enter a project name",
1006
+ validate(value) {
1007
+ if (value.length === 0) {
1008
+ return "Folder name is required";
1009
+ }
1010
+ }
1011
+ })
1012
+ );
860
1013
  await createFolderIfNotExists(join5(cwd4(), folderName));
861
1014
  chdir(join5(cwd4(), folderName));
862
1015
  }
863
- const { projectLink } = await prompt({
864
- type: "text",
865
- name: "projectLink",
866
- message: "Please paste a link from the Share dialog in the builder"
867
- });
868
- if (projectLink === void 0) {
869
- throw new Error(`Project Link is required`);
870
- }
871
- await link({ link: projectLink });
872
- const { deployTarget } = await prompt({
873
- type: "select",
874
- name: "deployTarget",
875
- message: "Where would you like to deploy your project?",
876
- choices: PROJECT_TEMPALTES.map((template) => {
877
- return {
878
- title: titleCase(template),
879
- value: template
880
- };
1016
+ const shareLink = exitIfCancelled(
1017
+ await text2({
1018
+ message: "Please paste a link from the Share Dialog in the builder",
1019
+ validate: validateShareLink
881
1020
  })
882
- });
883
- projectTemplate = deployTarget;
884
- const { installDeps } = await prompt({
885
- type: "confirm",
886
- name: "installDeps",
887
- message: "Would you like to install dependencies? (recommended)",
888
- initial: true
889
- });
890
- shouldInstallDeps = installDeps;
1021
+ );
1022
+ await link({ link: shareLink });
1023
+ projectTemplate = exitIfCancelled(
1024
+ await select({
1025
+ message: "Where would you like to deploy your project?",
1026
+ options: PROJECT_TEMPALTES.map((template) => ({
1027
+ value: template,
1028
+ label: titleCase(template)
1029
+ }))
1030
+ })
1031
+ );
1032
+ shouldInstallDeps = exitIfCancelled(
1033
+ await confirm({
1034
+ message: "Would you like to install dependencies? (recommended)",
1035
+ initialValue: true
1036
+ })
1037
+ );
891
1038
  }
892
- await sync({ buildId: void 0, origin: void 0, authToken: void 0 });
893
1039
  if (projectTemplate === void 0) {
894
- const { deployTarget } = await prompt({
895
- type: "select",
896
- name: "deployTarget",
897
- message: "Where would you like to deploy your project?",
898
- choices: PROJECT_TEMPALTES.map((template) => {
899
- return {
900
- title: titleCase(template),
901
- value: template
902
- };
1040
+ projectTemplate = exitIfCancelled(
1041
+ await select({
1042
+ message: "Where would you like to deploy your project?",
1043
+ options: PROJECT_TEMPALTES.map((template) => ({
1044
+ value: template,
1045
+ label: titleCase(template)
1046
+ }))
903
1047
  })
904
- });
905
- projectTemplate = deployTarget;
1048
+ );
906
1049
  }
1050
+ await sync({ buildId: void 0, origin: void 0, authToken: void 0 });
907
1051
  await build({
908
1052
  ...options,
909
1053
  ...projectTemplate && { template: [projectTemplate] }
910
1054
  });
911
1055
  if (shouldInstallDeps === true) {
912
- const spinner = ora3().start();
913
- spinner.text = "Installing dependencies";
1056
+ const install = spinner3();
1057
+ install.start("Installing dependencies");
914
1058
  await $`npm install`;
915
- spinner.succeed("Installed dependencies");
1059
+ install.stop("Installed dependencies");
916
1060
  }
917
- console.info(pc2.bold(pc2.green(`
918
- Your project was successfully synced \u{1F389}`)));
919
- console.info(
1061
+ log4.message();
1062
+ log4.message(pc2.green(pc2.bold(`Your project was successfully synced \u{1F389}`)));
1063
+ log4.message(
920
1064
  [
921
1065
  "Now you can:",
922
1066
  folderName && `Go to your project: ${pc2.dim(`cd ${folderName}`)}`,
@@ -949,7 +1093,7 @@ import makeCLI from "yargs";
949
1093
  // package.json
950
1094
  var package_default = {
951
1095
  name: "webstudio",
952
- version: "0.163.0",
1096
+ version: "0.168.0",
953
1097
  description: "Webstudio CLI",
954
1098
  author: "Webstudio <github@webstudio.is>",
955
1099
  homepage: "https://webstudio.is",
@@ -958,6 +1102,12 @@ var package_default = {
958
1102
  "webstudio-cli": "./bin.js",
959
1103
  webstudio: "./bin.js"
960
1104
  },
1105
+ imports: {
1106
+ "#cli": {
1107
+ webstudio: "./src/cli.ts",
1108
+ default: "./lib/cli.js"
1109
+ }
1110
+ },
961
1111
  files: [
962
1112
  "lib/*",
963
1113
  "templates/*",
@@ -968,11 +1118,15 @@ var package_default = {
968
1118
  typecheck: "tsc",
969
1119
  checks: "pnpm typecheck",
970
1120
  build: "rm -rf lib && esbuild src/cli.ts --outdir=lib --bundle --format=esm --packages=external",
971
- "local-run": "tsx --no-warnings ./src/bin.ts",
972
- dev: "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib"
1121
+ dev: "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib",
1122
+ test: "NODE_OPTIONS=--experimental-vm-modules jest"
973
1123
  },
974
1124
  license: "AGPL-3.0-or-later",
1125
+ engines: {
1126
+ node: ">=20.12"
1127
+ },
975
1128
  dependencies: {
1129
+ "@clack/prompts": "^0.7.0",
976
1130
  "@webstudio-is/http-client": "workspace:*",
977
1131
  "@webstudio-is/image": "workspace:*",
978
1132
  "@webstudio-is/react-sdk": "workspace:*",
@@ -980,39 +1134,39 @@ var package_default = {
980
1134
  "@webstudio-is/sdk-components-react": "workspace:*",
981
1135
  "@webstudio-is/sdk-components-react-radix": "workspace:*",
982
1136
  "@webstudio-is/sdk-components-react-remix": "workspace:*",
1137
+ "change-case": "^5.4.4",
983
1138
  deepmerge: "^4.3.1",
984
1139
  "env-paths": "^3.0.0",
985
1140
  execa: "^7.2.0",
986
- ora: "^7.0.1",
987
1141
  "p-limit": "^4.0.0",
988
- picocolors: "^1.0.0",
989
- prompts: "^2.4.2",
1142
+ parse5: "7.1.2",
1143
+ picocolors: "^1.0.1",
990
1144
  "strip-indent": "^4.0.0",
991
1145
  "title-case": "^4.1.0",
992
1146
  yargs: "^17.7.2",
993
1147
  zod: "^3.22.4"
994
1148
  },
995
1149
  devDependencies: {
996
- "@netlify/remix-adapter": "^2.3.1",
997
- "@netlify/remix-edge-adapter": "3.2.2",
998
- "@remix-run/cloudflare": "^2.9.1",
999
- "@remix-run/cloudflare-pages": "^2.9.1",
1000
- "@remix-run/dev": "^2.9.1",
1001
- "@remix-run/node": "^2.9.1",
1002
- "@remix-run/react": "^2.9.1",
1003
- "@remix-run/server-runtime": "^2.9.1",
1150
+ "@jest/globals": "^29.7.0",
1151
+ "@netlify/remix-adapter": "^2.4.0",
1152
+ "@netlify/remix-edge-adapter": "3.3.0",
1153
+ "@remix-run/cloudflare": "^2.9.2",
1154
+ "@remix-run/cloudflare-pages": "^2.9.2",
1155
+ "@remix-run/dev": "^2.9.2",
1156
+ "@remix-run/node": "^2.9.2",
1157
+ "@remix-run/react": "^2.9.2",
1158
+ "@remix-run/server-runtime": "^2.9.2",
1004
1159
  "@types/node": "^20.12.7",
1005
- "@types/prompts": "^2.4.5",
1006
1160
  "@types/react": "^18.2.70",
1007
1161
  "@types/react-dom": "^18.2.25",
1008
1162
  "@types/yargs": "^17.0.32",
1009
1163
  "@webstudio-is/form-handlers": "workspace:*",
1164
+ "@webstudio-is/jest-config": "workspace:*",
1010
1165
  "@webstudio-is/tsconfig": "workspace:*",
1011
1166
  react: "18.3.0-canary-14898b6a9-20240318",
1012
1167
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
1013
- tsx: "^4.7.2",
1014
1168
  typescript: "5.4.5",
1015
- vite: "^5.2.11",
1169
+ vite: "^5.2.13",
1016
1170
  wrangler: "^3.48.0"
1017
1171
  }
1018
1172
  };
@@ -1056,7 +1210,7 @@ var main = async () => {
1056
1210
  await cmd.parse();
1057
1211
  } catch (error) {
1058
1212
  console.error(error);
1059
- exit(1);
1213
+ exit4(1);
1060
1214
  }
1061
1215
  };
1062
1216
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.163.0",
3
+ "version": "0.168.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -9,6 +9,12 @@
9
9
  "webstudio-cli": "./bin.js",
10
10
  "webstudio": "./bin.js"
11
11
  },
12
+ "imports": {
13
+ "#cli": {
14
+ "webstudio": "./src/cli.ts",
15
+ "default": "./lib/cli.js"
16
+ }
17
+ },
12
18
  "files": [
13
19
  "lib/*",
14
20
  "templates/*",
@@ -16,54 +22,58 @@
16
22
  "!*.{test,stories}.*"
17
23
  ],
18
24
  "license": "AGPL-3.0-or-later",
25
+ "engines": {
26
+ "node": ">=20.12"
27
+ },
19
28
  "dependencies": {
29
+ "@clack/prompts": "^0.7.0",
30
+ "change-case": "^5.4.4",
20
31
  "deepmerge": "^4.3.1",
21
32
  "env-paths": "^3.0.0",
22
33
  "execa": "^7.2.0",
23
- "ora": "^7.0.1",
24
34
  "p-limit": "^4.0.0",
25
- "picocolors": "^1.0.0",
26
- "prompts": "^2.4.2",
35
+ "parse5": "7.1.2",
36
+ "picocolors": "^1.0.1",
27
37
  "strip-indent": "^4.0.0",
28
38
  "title-case": "^4.1.0",
29
39
  "yargs": "^17.7.2",
30
40
  "zod": "^3.22.4",
31
- "@webstudio-is/http-client": "0.163.0",
32
- "@webstudio-is/react-sdk": "0.163.0",
33
- "@webstudio-is/image": "0.163.0",
34
- "@webstudio-is/sdk": "0.163.0",
35
- "@webstudio-is/sdk-components-react-radix": "0.163.0",
36
- "@webstudio-is/sdk-components-react": "0.163.0",
37
- "@webstudio-is/sdk-components-react-remix": "0.163.0"
41
+ "@webstudio-is/image": "0.168.0",
42
+ "@webstudio-is/react-sdk": "0.168.0",
43
+ "@webstudio-is/sdk": "0.168.0",
44
+ "@webstudio-is/http-client": "0.168.0",
45
+ "@webstudio-is/sdk-components-react": "0.168.0",
46
+ "@webstudio-is/sdk-components-react-radix": "0.168.0",
47
+ "@webstudio-is/sdk-components-react-remix": "0.168.0"
38
48
  },
39
49
  "devDependencies": {
40
- "@netlify/remix-adapter": "^2.3.1",
41
- "@netlify/remix-edge-adapter": "3.2.2",
42
- "@remix-run/cloudflare": "^2.9.1",
43
- "@remix-run/cloudflare-pages": "^2.9.1",
44
- "@remix-run/dev": "^2.9.1",
45
- "@remix-run/node": "^2.9.1",
46
- "@remix-run/react": "^2.9.1",
47
- "@remix-run/server-runtime": "^2.9.1",
50
+ "@jest/globals": "^29.7.0",
51
+ "@netlify/remix-adapter": "^2.4.0",
52
+ "@netlify/remix-edge-adapter": "3.3.0",
53
+ "@remix-run/cloudflare": "^2.9.2",
54
+ "@remix-run/cloudflare-pages": "^2.9.2",
55
+ "@remix-run/dev": "^2.9.2",
56
+ "@remix-run/node": "^2.9.2",
57
+ "@remix-run/react": "^2.9.2",
58
+ "@remix-run/server-runtime": "^2.9.2",
48
59
  "@types/node": "^20.12.7",
49
- "@types/prompts": "^2.4.5",
50
60
  "@types/react": "^18.2.70",
51
61
  "@types/react-dom": "^18.2.25",
52
62
  "@types/yargs": "^17.0.32",
53
63
  "react": "18.3.0-canary-14898b6a9-20240318",
54
64
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
55
- "tsx": "^4.7.2",
56
65
  "typescript": "5.4.5",
57
- "vite": "^5.2.11",
66
+ "vite": "^5.2.13",
58
67
  "wrangler": "^3.48.0",
59
- "@webstudio-is/form-handlers": "0.163.0",
68
+ "@webstudio-is/jest-config": "1.0.7",
69
+ "@webstudio-is/form-handlers": "0.168.0",
60
70
  "@webstudio-is/tsconfig": "1.0.7"
61
71
  },
62
72
  "scripts": {
63
73
  "typecheck": "tsc",
64
74
  "checks": "pnpm typecheck",
65
75
  "build": "rm -rf lib && esbuild src/cli.ts --outdir=lib --bundle --format=esm --packages=external",
66
- "local-run": "tsx --no-warnings ./src/bin.ts",
67
- "dev": "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib"
76
+ "dev": "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib",
77
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest"
68
78
  }
69
79
  }
@@ -12,9 +12,9 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
- "@remix-run/cloudflare": "2.9.1",
16
- "@remix-run/cloudflare-pages": "2.9.1",
17
- "isbot": "^4.1.0"
15
+ "@remix-run/cloudflare": "2.9.2",
16
+ "@remix-run/cloudflare-pages": "2.9.2",
17
+ "isbot": "^5.1.8"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@cloudflare/workers-types": "^4.20240405.0",
@@ -8,7 +8,7 @@
8
8
  "**/.client/**/*.tsx"
9
9
  ],
10
10
  "compilerOptions": {
11
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
11
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
12
12
  "types": [
13
13
  "@remix-run/cloudflare",
14
14
  "vite/client",
@@ -1,13 +1,31 @@
1
- import { Links, Meta, Outlet } from "@remix-run/react";
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+
3
+ import { Links, Meta, Outlet, useMatches } from "@remix-run/react";
4
+ // @todo think about how to make __generated__ typeable
5
+ // @ts-ignore
6
+ import { CustomCode } from "./__generated__/_index";
2
7
 
3
8
  const Root = () => {
9
+ // Get language from matches
10
+ const matches = useMatches();
11
+
12
+ const lastMatchWithLanguage = matches.findLast((match) => {
13
+ // @ts-ignore
14
+ const language = match?.data?.pageMeta?.language;
15
+ return language != null;
16
+ });
17
+
18
+ // @ts-ignore
19
+ const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? "en";
20
+
4
21
  return (
5
- <html lang="en">
22
+ <html lang={lang}>
6
23
  <head>
7
24
  <meta charSet="utf-8" />
8
25
  <meta name="viewport" content="width=device-width,initial-scale=1" />
9
26
  <Meta />
10
27
  <Links />
28
+ <CustomCode />
11
29
  </head>
12
30
  <Outlet />
13
31
  </html>
@@ -81,7 +81,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
81
81
  status: pageMeta.status,
82
82
  headers: {
83
83
  "Cache-Control": "public, max-age=600",
84
- "x-ws-language": pageMeta.language ?? "en",
85
84
  },
86
85
  }
87
86
  );
@@ -90,7 +89,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
90
89
  export const headers: HeadersFunction = ({ loaderHeaders }) => {
91
90
  return {
92
91
  "Cache-Control": "public, max-age=0, must-revalidate",
93
- "x-ws-language": loaderHeaders.get("x-ws-language") ?? "",
94
92
  };
95
93
  };
96
94
 
@@ -188,24 +186,15 @@ export const links: LinksFunction = () => {
188
186
  rel: "icon",
189
187
  href: imageLoader({
190
188
  src: favIconAsset.name,
191
- width: 128,
189
+ // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search
190
+ width: 144,
191
+ height: 144,
192
+ fit: "pad",
192
193
  quality: 100,
193
194
  format: "auto",
194
195
  }),
195
196
  type: undefined,
196
197
  });
197
- } else {
198
- result.push({
199
- rel: "icon",
200
- href: "/favicon.ico",
201
- type: "image/x-icon",
202
- });
203
-
204
- result.push({
205
- rel: "shortcut icon",
206
- href: "/favicon.ico",
207
- type: "image/x-icon",
208
- });
209
198
  }
210
199
 
211
200
  for (const asset of pageFontAssets) {
@@ -8,26 +8,26 @@
8
8
  "typecheck": "tsc"
9
9
  },
10
10
  "dependencies": {
11
- "@remix-run/node": "2.9.1",
12
- "@remix-run/react": "2.9.1",
13
- "@remix-run/server-runtime": "2.9.1",
14
- "@webstudio-is/react-sdk": "0.163.0",
15
- "@webstudio-is/sdk-components-react-radix": "0.163.0",
16
- "@webstudio-is/sdk-components-react-remix": "0.163.0",
17
- "@webstudio-is/sdk-components-react": "0.163.0",
18
- "@webstudio-is/form-handlers": "0.163.0",
19
- "@webstudio-is/image": "0.163.0",
20
- "@webstudio-is/sdk": "0.163.0",
21
- "isbot": "^3.6.8",
11
+ "@remix-run/node": "2.9.2",
12
+ "@remix-run/react": "2.9.2",
13
+ "@remix-run/server-runtime": "2.9.2",
14
+ "@webstudio-is/react-sdk": "0.168.0",
15
+ "@webstudio-is/sdk-components-react-radix": "0.168.0",
16
+ "@webstudio-is/sdk-components-react-remix": "0.168.0",
17
+ "@webstudio-is/sdk-components-react": "0.168.0",
18
+ "@webstudio-is/form-handlers": "0.168.0",
19
+ "@webstudio-is/image": "0.168.0",
20
+ "@webstudio-is/sdk": "0.168.0",
21
+ "isbot": "^5.1.8",
22
22
  "react": "18.3.0-canary-14898b6a9-20240318",
23
23
  "react-dom": "18.3.0-canary-14898b6a9-20240318"
24
24
  },
25
25
  "devDependencies": {
26
- "@remix-run/dev": "2.9.1",
26
+ "@remix-run/dev": "2.9.2",
27
27
  "@types/react": "^18.2.70",
28
28
  "@types/react-dom": "^18.2.25",
29
29
  "typescript": "5.4.5",
30
- "vite": "^5.2.11"
30
+ "vite": "^5.2.13"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=20.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
3
3
  "compilerOptions": {
4
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
4
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
5
  "types": ["@remix-run/node", "vite/client"],
6
6
  "isolatedModules": true,
7
7
  "esModuleInterop": true,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
3
3
  "compilerOptions": {
4
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
4
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
5
5
  "types": ["@remix-run/node", "vite/client"],
6
6
  "isolatedModules": true,
7
7
  "esModuleInterop": true,
@@ -3,7 +3,7 @@
3
3
  "start": "netlify serve"
4
4
  },
5
5
  "dependencies": {
6
- "@netlify/edge-functions": "^2.6.0",
7
- "@netlify/remix-edge-adapter": "^3.2.2"
6
+ "@netlify/edge-functions": "^2.8.1",
7
+ "@netlify/remix-edge-adapter": "^3.3.0"
8
8
  }
9
9
  }
@@ -3,7 +3,7 @@
3
3
  "start": "netlify serve"
4
4
  },
5
5
  "dependencies": {
6
- "@netlify/functions": "^2.6.0",
7
- "@netlify/remix-adapter": "^2.3.1"
6
+ "@netlify/functions": "^2.7.0",
7
+ "@netlify/remix-adapter": "^2.4.0"
8
8
  }
9
9
  }
@@ -8,7 +8,7 @@
8
8
  "**/.client/**/*.tsx"
9
9
  ],
10
10
  "compilerOptions": {
11
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
11
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
12
12
  "types": [
13
13
  "@remix-run/cloudflare",
14
14
  "vite/client",