@lightspeed/crane 0.1.0 → 0.1.1
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 +113 -0
- package/dist/cli.mjs +1 -1
- package/package.json +1 -1
- package/template/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Crane
|
|
2
|
+
|
|
3
|
+
### Preface
|
|
4
|
+
|
|
5
|
+
Crane is a command line interface that aids the creation, build and deployment of fully customisable website components (so-called sections)
|
|
6
|
+
within the Lightspeed E-Commerce ecosystem.
|
|
7
|
+
|
|
8
|
+
### Glossary
|
|
9
|
+
|
|
10
|
+
- Custom application: a small project that defines the boundaries of custom building-blocks that belong to one particular topic.
|
|
11
|
+
- Custom section: smallest building block to define a particular custom functionality.
|
|
12
|
+
- Default section: existing building block within the Lightspeed E-Commerce product.
|
|
13
|
+
- Settings
|
|
14
|
+
- Content: configuration regarding the content of a custom section, e.g.: titles, buttons and texts.
|
|
15
|
+
- Design: similarly, the design aspects of a custom section configuration, e.g.: colours and font size.
|
|
16
|
+
- Showcase: the highlight of a custom section with very specific configuration values.
|
|
17
|
+
|
|
18
|
+
### Prerequisites
|
|
19
|
+
|
|
20
|
+
In order to use this utility, first a Lightspeed specific application needs to be created containing the necessary
|
|
21
|
+
permissions granted, as well as an optional test site in order to verify the changes made in a custom application.
|
|
22
|
+
|
|
23
|
+
```For both the test site and the permission, please contact you Partner Manager!```
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
26
|
+
|
|
27
|
+
The following commands need to be performed to install the required dependencies and Crane utility, along with our linter:
|
|
28
|
+
```
|
|
29
|
+
npm install vite
|
|
30
|
+
npm install vue
|
|
31
|
+
npm install @lightspeed/crane@latest
|
|
32
|
+
npm install @lightspeed/eslint-config-crane@latest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Commands
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npx @lightspeed/crane@latest init --app <name>
|
|
39
|
+
```
|
|
40
|
+
Creates an application folder inside the current directory with default resources (configuration, assets and JS files).
|
|
41
|
+
By default, there is one custom section located in the folder "sections" named "example-section".
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
npx @lightspeed/crane@latest init --section <name>
|
|
45
|
+
```
|
|
46
|
+
Creates an additional custom section. The command will create files necessary for this section and locate them in
|
|
47
|
+
folder "sections" under the directory with the given name.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
npx @lightspeed/crane@latest build
|
|
51
|
+
```
|
|
52
|
+
Builds the application. Upon a successful build, the following directories can be located inside the application folder: ```dist``` and ```node_modules```.
|
|
53
|
+
A prompt describing the next steps will be shown.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
npx @lightspeed/crane@latest deploy
|
|
57
|
+
```
|
|
58
|
+
Deploys the application to the Lightspeed E-Commerce platform.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
npx @lightspeed/crane@latest --help
|
|
62
|
+
```
|
|
63
|
+
Displays list of available commands, resource descriptions and actions.
|
|
64
|
+
|
|
65
|
+
### Example
|
|
66
|
+
|
|
67
|
+
The following example will demonstrate how to create a custom application with 2 sections and to deploy it to
|
|
68
|
+
Lightspeed E-Series, provided the necessary prerequisites are fulfilled.
|
|
69
|
+
|
|
70
|
+
1. Initialization of the application with the name: 'my-app'
|
|
71
|
+
```
|
|
72
|
+
npx @lightspeed/crane@latest init --app my-app
|
|
73
|
+
```
|
|
74
|
+
2. Switching the directory
|
|
75
|
+
```
|
|
76
|
+
cd my-app
|
|
77
|
+
```
|
|
78
|
+
3. Creation of additional custom sections, in case it is necessary. **Optional**
|
|
79
|
+
```
|
|
80
|
+
npx @lightspeed/crane@latest init --section my-section
|
|
81
|
+
```
|
|
82
|
+
4. Editing ```crane.config.json```
|
|
83
|
+
|
|
84
|
+
In order to successfully deploy a custom application, its credentials are need to be specified.
|
|
85
|
+
This can be done by specifying the proper ```client_id``` and ```client_secret``` in ```crane.config.json```, located in the root of the custom application.
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"app_client_id": "{client_id}",
|
|
89
|
+
"app_secret_key": "{client_secret}"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
5. Building the application
|
|
93
|
+
```
|
|
94
|
+
npx @lightspeed/crane@latest build
|
|
95
|
+
```
|
|
96
|
+
6. Deploying the application
|
|
97
|
+
```
|
|
98
|
+
npx @lightspeed/crane@latest deploy
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Common issues and remedies
|
|
102
|
+
|
|
103
|
+
1. In case of a failed build command execution, the following action needs to be
|
|
104
|
+
performed, followed by a repeat execution of said command:
|
|
105
|
+
```
|
|
106
|
+
npm install
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
2. HTTP 401 during deployment:
|
|
110
|
+
Invalid credentials provided, please verify that the ```crane.config.json``` file is correct.
|
|
111
|
+
|
|
112
|
+
3. HTTP 403 during deployment:
|
|
113
|
+
Insufficient permissions, please contact your Partner Manager regarding the necessary permissions.
|
package/dist/cli.mjs
CHANGED
|
@@ -4,4 +4,4 @@ Note: Errors regarding failingKeyword: "then" can sometimes be false negatives.
|
|
|
4
4
|
Errors: ${JSON.stringify([...e.errors??[],...s??[]],null,2)}`)}async function ct(e){const t=[];for(const o of e.sections){const s=e.sections.indexOf(o);if(s===0&&o.id!=="header"&&t.push({instancePath:`/sections/${s}/id`,message:"The first section must be a default `header`"}),s===e.sections.length-1&&o.id!=="footer"&&t.push({instancePath:`/sections/${s}/id`,message:"The last section must be a default `footer`"}),o.type==="custom")if(!A(`dist/sections/${o.id}`))t.push({instancePath:`/sections/${s}/id`,message:"Custom section must have a corresponding block defined"});else{if(o.content!==void 0)if(!A(`dist/sections/${o.id}/js/settings/content.mjs`))t.push({instancePath:`/sections/${s}/content`,message:"Content descriptor is overridden, please provide a default descriptor in the block's settings folder"});else{const n=await I(`dist/sections/${o.id}/js/settings/content`,!1),i=new Set(Object.keys(n));Object.keys(o.content).every(a=>i.has(a))||t.push({instancePath:`/sections/${s}/content`,message:"Content descriptor must be a subset of the default content descriptor"})}if(o.design!==void 0)if(!A(`dist/sections/${o.id}/js/settings/design.mjs`))t.push({instancePath:`/sections/${s}/design`,message:"Design descriptor is overridden, please provide a default descriptor in the block's settings folder"});else{const n=await I(`dist/sections/${o.id}/js/settings/design`,!1),i=new Set(Object.keys(n));Object.keys(o.design).every(a=>i.has(a))||t.push({instancePath:`/sections/${s}/design`,message:"Design descriptor must be a subset of the default design descriptor"})}}}return t.length===0?void 0:t}async function pt(){try{const e=await nt();for(const s of e){const{name:n,serverEntrypoint:i,clientEntrypoint:a,contentSettingsEntrypoint:u,designSettingsEntrypoint:y,settingsTranslationsEntrypoint:g,showcasesEntrypoints:R,showcaseTranslationsEntrypoint:k,assetsEntrypoints:B}=s;if(await h({configFile:!1,...$(Ke(n,i))}),a!==void 0&&await h({configFile:!1,...$(ze(n,a))}),u!==void 0&&await h({configFile:!1,...$(S(n,u))}),y!==void 0&&await h({configFile:!1,...$(S(n,y))}),g!==void 0&&await h({configFile:!1,...$(S(n,g))}),R!==void 0)for(const L of R)await h({configFile:!1,...$(z(n,L))});if(k!==void 0&&await h({configFile:!1,...$(z(n,k))}),B!==void 0)for(const L of B)await h({configFile:!1,...$(Ve(n,L))})}const t=await rt(),o=await it();for(const s of o.entryPoints){await h({configFile:!1,...$(Je(s,T))});const n=`dist/template/js/${v(s).name}`,i=await I(n,!0);await at(t,i,n,await ct(i))}m.info("Build successful. For deploy run: npx @lightspeed/crane@latest deploy")}catch(e){m.error(`Error while building: ${e.message}`)}}function je(e){if(e!==void 0)return e.startsWith("global.")?{type:"GLOBAL_FONT",font:e}:{type:"PRESET_FONT",font:e}}function E(e){if(e===void 0)return;if(e.startsWith("global."))return{type:"GLOBAL_COLOR",raw:e};const t=qe(e);return{type:"STRUCTURED_COLOR",raw:e,hex:t.toHex8String(),hsl:t.toHsl(),rgba:t.toRgb(),auto:!1}}function lt(e){if(e!==void 0)return typeof e=="string"&&e.startsWith("global.")?{type:"GLOBAL_TEXT_SIZE",size:e}:{type:"NUMERIC_TEXT_SIZE",size:e}}const dt={COLOR:"COLOR",GRADIENT:"GRADIENT"};function ut(e){switch(e){case"COLOR":return"solid";case"GRADIENT":return"gradient";default:throw new Error(`Unknown background type: ${e}. Right options: ${Object.keys(dt)}`)}}function ft(e){const t=e.style,o=e.color,s=Array.isArray(o)?o:[o,o];return e.background={type:ut(t),solid:{color:E(s.at(0))},gradient:{fromColor:E(s.at(0)),toColor:E(s.at(1))}},e.style=void 0,e.color=void 0,e}const mt={SOLID:"SOLID",OUTLINE:"OUTLINE",TEXT:"TEXT"};function yt(e){switch(e){case"SOLID":return"solid-button";case"OUTLINE":return"outline-button";case"TEXT":return"text-link";default:throw new Error(`Unknown button appearance: ${e}. Right options: ${Object.keys(mt)}`)}}const gt={SMALL:"SMALL",MEDIUM:"MEDIUM",LARGE:"LARGE"};function ht(e){switch(e){case"SMALL":return"small";case"MEDIUM":return"medium";case"LARGE":return"large";default:throw new Error(`Unknown button size: ${e}. Right options: ${Object.keys(gt)}`)}}const $t={ROUND_CORNER:"ROUND_CORNER",RECTANGLE:"RECTANGLE",PILL:"PILL"};function Et(e){switch(e){case"ROUND_CORNER":return"round-corner";case"RECTANGLE":return"rectangle";case"PILL":return"pill";default:throw new Error(`Unknown button shape: ${e}. Right options: ${Object.keys($t)}`)}}function bt(e){const t=e.appearance;t!==void 0&&(e.appearance=yt(t));const o=e.size;o!==void 0&&(e.size=ht(o));const s=e.shape;s!==void 0&&(e.style=Et(s),e.shape=void 0);const n=e.font;e.font=je(n);const i=e.color;return e.color=E(i),e}const Tt={COLOR:"COLOR",GRADIENT:"GRADIENT",NONE:"NONE"};function wt(e){switch(e){case"COLOR":return"solid";case"GRADIENT":return"gradient";case"NONE":return"none";default:throw new Error(`Unknown image overlay type: ${e}. Right options: ${Object.keys(Tt)}`)}}function Ot(e){const t=e.overlay,o=e.color,s=Array.isArray(o)?o:[o,o];return e.overlay={type:wt(t),solid:{color:E(s.at(0))},gradient:{fromColor:E(s.at(0)),toColor:E(s.at(1))}},e.color=void 0,e}function jt(e){const t=e.font;e.font=je(t);const o=e.color;e.color=E(o);const s=e.size;return e.size=lt(s),e}function vt(e){const t=e.color;return e.color=E(t),e}function ve(e){Object.keys(e).forEach(t=>{const o=e[t],s=o.type;Ce(s,o.defaults)})}function Ct(e){Object.keys(e).forEach(t=>{const o=e[t],s=o.type;Ce(s,o)})}function Ce(e,t){switch(e){case"TEXT":{jt(t);break}case"BUTTON":{bt(t);break}case"IMAGE":{Ot(t);break}case"BACKGROUND":{ft(t);break}case"COLOR_PICKER":{vt(t);break}case"TOGGLE":case"SELECTBOX":break;default:throw new Error(`Unknown design editor type: ${e}`)}return t}const Lt="https://blockbuster.ecwid.com";async function Dt(){const e=await x(r(c.cwd(),"crane.config.json")),t=JSON.parse(e.toString());return{appClientId:t.app_client_id,appSecretKey:btoa(t.app_secret_key)}}async function At(){const e=await x(r(c.cwd(),"package.json")),t=JSON.parse(e.toString()),o=Fe(t.version,"patch");return t.version=o,ke(r(c.cwd(),"package.json"),`${JSON.stringify(t,null,2)}
|
|
5
5
|
`),t.version}async function Nt(e,t){try{const o=(await import(r(c.cwd(),`dist/sections/${e}/js/settings/content.mjs`))).default;return w(o,t),o}catch{throw new Error(`Content descriptor for section [${e}] is either invalid or undefined`)}}async function St(e,t){try{const o=(await import(r(c.cwd(),`dist/sections/${e}/js/settings/design.mjs`))).default;return ve(o),w(o,t),o}catch(o){const s=o;throw new Error(`Design settings is invalid or undefined. Error ${s.stack}`)}}async function It(e,t){try{const o=await l("*.mjs",{cwd:r(c.cwd(),`dist/sections/${e}/js/showcases/`),ignore:"**/translations.mjs"});return Promise.all(o.map(async s=>{const n=(await import(r(c.cwd(),`dist/sections/${e}/js/showcases/${s}`))).default;return Ct(n.design),w(n,t),n}))}catch(o){throw new Error(`Showcases is invalid or undefined. Error ${o}`)}}async function Le(e){return await De(`dist/sections/${e}/js/settings/translations.mjs`)}async function Gt(e){return await De(`dist/sections/${e}/js/showcases/translations.mjs`)}async function De(e){const t=(await import(r(c.cwd(),e))).default;return Rt(t)}function w(e,t){if(e&&typeof e=="object"){const o=e;for(let s in o){const n=o[s];typeof n=="string"&&n.startsWith("$")&&(o[s]=_t(t,n)),typeof n=="object"&&w(n,t)}}}function _t(e,t){if(!t)return;const o=e[t];return o===void 0?{en:t}:o}function Rt(e){const t={};for(let o in e){const s=e[o];for(let n in s){const i=t[n],a=s[n];if(i===void 0){const u={};u[o]=a,t[n]=u}else i[o]=a}}return t}async function kt(){const e=await l("*/",{cwd:r(c.cwd(),"dist/sections/")});return Promise.all(e.map(async t=>{const o=await Le(t),s=await Gt(t),n=await Nt(t,o),i=await St(t,o),a=await It(t,s);return{id:t,name:{en:t},contentEditors:n,designEditors:i,showcases:a}}))}async function Bt(e){try{return(await import(r(c.cwd(),e))).default}catch{throw new Error(`Template descriptor [${e}] is either invalid or undefined`)}}async function Pt(){const e=await l("dist/template/js/**.mjs",{ignore:[`dist/template/js/**${T}.mjs`]});return Promise.all(e.map(async t=>{const o=await Bt(t),s=o.sections.filter(n=>n.type==="custom");for(const n of s){const i=await Le(n.id);n.content!==void 0&&w(n.content,i),n.design!==void 0&&(ve(n.design),w(n.design,i))}return{id:`${v(c.cwd()).name.replace(/[^a-zA-Z0-9]/g,"_")}_${v(t).name}`,descriptor:o}}))}async function Ut(e,t,o,s){const n=(await l("server.js",{cwd:r(c.cwd(),`dist/sections/${s}/js/`)})).at(0);n!==void 0&&await e.post("/api/v1/app/resource/upload",{file:C(r(c.cwd(),`dist/sections/${s}/js/server.js`))},{params:{appClientId:t.appClientId,type:"server_js",version:o,block:s,fileName:n},headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${t.appSecretKey}`}})}async function xt(e,t,o,s){(await l("*.js",{cwd:r(c.cwd(),`dist/sections/${s}/js/`),ignore:"**/server.js"})).forEach(async n=>{await e.post("/api/v1/app/resource/upload",{file:C(r(c.cwd(),`dist/sections/${s}/js/${n}`))},{params:{appClientId:t.appClientId,type:"client_js",version:o,block:s,fileName:n},headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${t.appSecretKey}`}})})}async function Xt(e,t,o,s){(await l("*",{cwd:r(c.cwd(),`dist/sections/${s}/assets/`)})).forEach(async n=>{await e.post("/api/v1/app/resource/upload",{file:C(r(c.cwd(),`dist/sections/${s}/assets/${n}`))},{params:{appClientId:t.appClientId,type:"assets",version:o,block:s,fileName:n},headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${t.appSecretKey}`}})})}async function Ft(e,t,o){(await l("package.json")).at(0)!==void 0&&await e.post("/api/v1/app/resource/upload",{file:C(r(c.cwd(),"package.json"))},{params:{appClientId:t.appClientId,type:"dependencies",version:o},headers:{"Content-Type":"multipart/form-data",Authorization:`Bearer ${t.appSecretKey}`}})}async function qt(e,t,o,s){for(const n of s)await Ut(e,t,o,n.id),await xt(e,t,o,n.id),await Xt(e,t,o,n.id),await Ft(e,t,o)}async function Mt(e,t,o,s,n){await e.post("/api/v1/app/manifest",{version:o,name:"Custom Block App",blocks:s,templates:n},{params:{appClientId:t.appClientId},headers:{Authorization:`Bearer ${t.appSecretKey}`}})}async function zt(e){try{const t=e??Lt,o=Xe.create({baseURL:t}),s=await Dt(),n=await At(),i=await kt(),a=await Pt();await qt(o,s,n,i),await Mt(o,s,n,i,a);const u=i.map(y=>y.id);m.info(`Deploy to ${t} successful
|
|
6
6
|
Current app version: ${n}
|
|
7
|
-
Deployed blocks: ${u.join(", ")}`),a.length>0&&m.info(`Deployed templates: ${a.map(y=>y.id).join(", ")}`)}catch(t){m.error(`Error while deploying: ${t.message}`)}}const Kt="0.1.
|
|
7
|
+
Deployed blocks: ${u.join(", ")}`),a.length>0&&m.info(`Deployed templates: ${a.map(y=>y.id).join(", ")}`)}catch(t){m.error(`Error while deploying: ${t.message}`)}}const Kt="0.1.1",p=Ae("crane");function Jt(){p.command("init","Initialize a new resource in the form of a directory.").option("--app <name>","Creates an app folder inside your current directory.").option("--section <name>","Creates the files necessary for one custom section with the given name, this can be repeated for each section.").option("--template <name>","Creates the directory and files necessary to build a custom template inside your app folder.").action(e=>{if(e.app)return F(e.app);if(e.section)return q(e.section);if(e.template)return M(e.template);p.outputHelp()}),p.command("build","Builds your resource code").action(pt),p.command("deploy","Deploys your resource code into Ecwid"),p.on("command:*",()=>{console.error("Invalid command: %s",p.args.join(" ")),p.outputHelp(),process.exit(1)}),p.on("command:init",()=>{const e=p.options.section,t=p.options.app,o=p.options.template;_(t)?G("app",j("init --app <name>"),F):_(e)?G("section",j("init --section <name>"),q):_(o)&&G("template",j("init --template <name>"),M)}),p.on("command:deploy",()=>zt(p.options.url)),p.help(),p.usage("<action> <resource>"),p.version(Kt),p.parse()}try{Jt()}catch{}async function G(e,t,o){const s=`You can use ${t} to directly specify the name of the ${e}.`;console.log(s);const n=await Me({type:"text",name:"name",message:`Please specify a name for your ${e}:`});if(n.name)return o(n.name);console.log("Please provide a name for the template."),p.outputHelp()}function _(e){return e&&(typeof e!="string"||e.trim().length==0)}
|
package/package.json
CHANGED