Skip to content

Commit 73702c3

Browse files
committed
New flow for static builds
- Instead of serving the static build and capturing DOM, compressed static build would be sent to service, served and directly capture screenshots
1 parent f250243 commit 73702c3

File tree

3 files changed

+210
-71
lines changed

3 files changed

+210
-71
lines changed

commands/storybook.js

Lines changed: 131 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,150 @@
11
const { default: axios } = require('axios')
2-
const fs = require('fs');
2+
const fs = require('fs')
33
const { sendDoM } = require('./utils/dom')
44
const { validateStorybookUrl, validateStorybookDir } = require('./utils/validate')
55
const { defaultSmartUIConfig } = require('./utils/config')
66
const { skipStory } = require('./utils/story')
7-
const { createStaticServer } = require('./utils/server')
7+
const { getLastCommit } = require('./utils/git')
8+
const static = require('./utils/static')
89

910
async function storybook(serve, options) {
10-
let server, url;
11-
let type = /^https?:\/\//.test(serve) ? 'url' : 'dir'
11+
let type = /^https?:\/\//.test(serve) ? 'url' : 'dir';
12+
let storybookConfig = options.config ? options.config : defaultSmartUIConfig.storybook;
13+
1214
if (type === 'url') {
1315
await validateStorybookUrl(serve);
14-
url = serve;
15-
} else {
16-
await validateStorybookDir(serve);
17-
server = await createStaticServer(serve);
18-
url = `http://localhost:${server.address().port}`;
19-
}
16+
let url = serve;
2017

21-
// TODO: modularize this and separate check for file exists
22-
let storybookConfig = defaultSmartUIConfig.storybook;
23-
if (options.config) {
24-
try {
25-
storybookConfig = JSON.parse(fs.readFileSync(options.config)).storybook;
26-
} catch (error) {
27-
console.log('[smartui] Error: ', error.message);
28-
process.exit(1);
29-
}
30-
31-
storybookConfig.browsers.forEach(element => {
32-
if (!(['chrome', 'safari', 'firefox'].includes(element))) {
33-
console.log('[smartui] Error: Invalid value for browser. Accepted browsers are chrome, safari and firefox');
34-
process.exit(0);
35-
}
18+
// Convert browsers and resolutions arrays to string
19+
let resolutions = [];
20+
storybookConfig.resolutions.forEach(element => {
21+
resolutions.push(element.join('x'));
3622
});
37-
// TODO: Sanity check resolutions
38-
}
23+
storybookConfig.resolutions = (!resolutions.length) ? 'all' : resolutions.toString();
24+
storybookConfig.browsers = (!storybookConfig.browsers.length) ? 'all' : storybookConfig.browsers.toString();
25+
26+
// Get stories object from stories.json and add url corresponding to every story ID
27+
await axios.get(new URL('stories.json', url).href)
28+
.then(async function (response) {
29+
let stories = {}
30+
for (const [storyId, storyInfo] of Object.entries(response.data.stories)) {
31+
if (!skipStory(storyInfo.name, storybookConfig)) {
32+
stories[storyId] = {
33+
name: storyInfo.name,
34+
kind: storyInfo.kind,
35+
url: new URL('/iframe.html?id=' + storyId + '&viewMode=story', url).href
36+
}
37+
}
38+
}
39+
40+
if (Object.keys(stories).length === 0) {
41+
console.log('[smartui] Error: No stories found');
42+
process.exit(0);
43+
} else {
44+
for (const [storyId, storyInfo] of Object.entries(stories)) {
45+
console.log('[smartui] Story found: ' + storyInfo.name);
46+
}
47+
}
48+
// Capture DoM of every story and send it to renderer API
49+
await sendDoM(url, stories, storybookConfig, options);
50+
})
51+
.catch(function (error) {
52+
if (error.response) {
53+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
54+
} else if (error.request) {
55+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
56+
} else {
57+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
58+
}
59+
});
60+
} else {
61+
let dirPath = serve;
62+
await validateStorybookDir(dirPath);
63+
64+
// Get storyIds to be rendered
65+
let storyIds = static.filterStories(dirPath, storybookConfig)
3966

40-
// Convert browsers and resolutions arrays to string
41-
let resolutions = [];
42-
storybookConfig.resolutions.forEach(element => {
43-
resolutions.push(element.join('x'));
44-
});
45-
storybookConfig.resolutions = (!resolutions.length) ? 'all' : resolutions.toString();
46-
storybookConfig.browsers = (!storybookConfig.browsers.length) ? 'all' : storybookConfig.browsers.toString();
67+
// Upload Storybook static
68+
await static.getSignedUrl(options)
69+
.then(async function (response) {
70+
let { url, uploadId } = response.data.data;
4771

48-
// Get stories object from stories.json and add url corresponding to every story ID
49-
await axios.get(new URL('stories.json', url).href)
50-
.then(async function (response) {
51-
let stories = {}
52-
for (const [storyId, storyInfo] of Object.entries(response.data.stories)) {
53-
if (!skipStory(storyInfo.name, storybookConfig)) {
54-
stories[storyId] = {
55-
name: storyInfo.name,
56-
kind: storyInfo.kind,
57-
url: new URL('/iframe.html?id=' + storyId + '&viewMode=story', url).href
72+
// Compress static build
73+
await static.compress(dirPath, uploadId)
74+
.then(function () {
75+
console.log('[smartui] Successfully compressed static build.')
76+
})
77+
.catch(function (err) {
78+
console.log('[smartui] Cannot compress static build. Error: ', err.message);
79+
process.exit(0);
80+
});
81+
82+
// Upload to S3
83+
const zipData = fs.readFileSync('storybook-static.zip');
84+
await axios.put(url, zipData, {
85+
headers: {
86+
'Content-Type': 'application/zip',
87+
'Content-Length': zipData.length
88+
}})
89+
.then(function (response) {
90+
console.log('[smartui] Static build successfully uploaded');
91+
fs.rmSync('storybook-static.zip');
92+
})
93+
.catch(function (error) {
94+
console.log('[smartui] Cannot upload static build. Error: ', error.message);
95+
fs.rmSync('storybook-static.zip');
96+
process.exit(0);
97+
});
98+
99+
// Prepare payload data
100+
let resolutions = []
101+
storybookConfig.resolutions.forEach(element => {
102+
resolutions.push({width: element[0], height: element[1]});
103+
});
104+
let commit = await getLastCommit();
105+
let payload = {
106+
downloadURL: url.substring(0, url.search(/.zip/)+4),
107+
uploadId: uploadId,
108+
projectToken: process.env.PROJECT_TOKEN,
109+
storybookConfig: {
110+
browsers: storybookConfig.browsers,
111+
resolutions: resolutions,
112+
storyIds: storyIds
113+
},
114+
git: {
115+
branch: commit.branch,
116+
commitId: commit.shortHash,
117+
commitAuthor: commit.author.name,
118+
commitMessage: commit.subject,
119+
githubURL: process.env.GITHUB_URL || '',
58120
}
59121
}
60-
}
61-
62-
if (Object.keys(stories).length === 0) {
63-
console.log('[smartui] Error: No stories found');
64-
process.exit(0);
65-
} else {
66-
for (const [storyId, storyInfo] of Object.entries(stories)) {
67-
console.log('[smartui] Story found: ' + storyInfo.name);
122+
console.log(payload);
123+
124+
// Call static render API
125+
// await axios.post(new URL(constants[options.env].STATIC_BASE_URL, constants[options.env].STATIC_RENDER_PATH).href, payload)
126+
// .then(async function (response) {
127+
// console.log('[smartui] Build in progress...');
128+
// await static.shortPolling(response.data.buildId, 0, options);
129+
// })
130+
// .catch(function (error) {
131+
// if (error.response) {
132+
// console.log('[smartui] Build failed: Error: ', error.response.data.message);
133+
// } else {
134+
// console.log('[smartui] Build failed: Error: ', error.message);
135+
// }
136+
// });
137+
})
138+
.catch(function (error) {
139+
if (error.response) {
140+
console.log('[smartui] Error: ', error.response.data.error?.message);
141+
} else {
142+
console.log('[smartui] Error: ', error.message);
68143
}
69-
}
70-
// Capture DoM of every story and send it to renderer API
71-
await sendDoM(url, stories, storybookConfig, options);
72-
})
73-
.catch(function (error) {
74-
if (error.response) {
75-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
76-
} else if (error.request) {
77-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
78-
} else {
79-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
80-
}
81-
});
82-
83-
// Close static server
84-
if (server) server?.close();
85-
144+
process.exit(0);
145+
});
146+
147+
}
86148
};
87149

88150
module.exports = { storybook };

commands/utils/constants.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ constants.stage = {
66
BUILD_STATUS_URL: "https://stage-api.lambdatestinternal.com/storybook/status",
77
BASE_URL: "https://stage-api.lambdatestinternal.com",
88
SB_BUILD_VALIDATE_PATH: "/storybook/validate",
9-
CHECK_UPDATE_PATH: "storybook/packageinfo"
9+
CHECK_UPDATE_PATH: "storybook/packageinfo",
10+
GET_SIGNED_URL_PATH: "/storybook/url",
11+
STATIC_RENDER_PATH: "/storybook/static-render"
1012
};
1113
constants.prod = {
1214
AUTH_URL: "https://api.lambdatest.com/storybook/auth",
1315
RENDER_API_URL: "https://api.lambdatest.com/storybook/render",
1416
BUILD_STATUS_URL: "https://api.lambdatest.com/storybook/status",
1517
BASE_URL: "https://api.lambdatest.com",
1618
SB_BUILD_VALIDATE_PATH: "/storybook/validate",
17-
CHECK_UPDATE_PATH: "storybook/packageinfo"
19+
CHECK_UPDATE_PATH: "storybook/packageinfo",
20+
GET_SIGNED_URL_PATH: "/storybook/url",
21+
STATIC_RENDER_PATH: "/storybook/static-render"
1822
};
1923

2024
module.exports = { constants };

commands/utils/static.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const fs = require('fs');
2+
const axios = require('axios');
3+
const archiver = require('archiver');
4+
var { constants } = require('./constants');
5+
const { skipStory } = require('./story');
6+
const { shortPolling } = require('./polling');
7+
8+
var INTERVAL = 2000
9+
const MAX_INTERVAL = 512000
10+
11+
function getSignedUrl(options) {
12+
return axios.get(new URL(constants[options.env].GET_SIGNED_URL_PATH, constants[options.env].BASE_URL).href, {
13+
headers: {
14+
projectToken: process.env.PROJECT_TOKEN
15+
}});
16+
}
17+
18+
async function compress(dirPath, uploadId) {
19+
return new Promise(function (resolve, reject) {
20+
// create a file to stream archive data to.
21+
const output = fs.createWriteStream('storybook-static.zip', {autoClose: true, emitClose: false});
22+
const archive = archiver('zip', {
23+
zlib: { level: 9 } // Sets the compression level.
24+
});
25+
26+
output.on('end', function() {
27+
console.log('Data has been drained');
28+
});
29+
30+
output.on('finish', function() {
31+
resolve();
32+
});
33+
34+
// Catch warnings (ie stat failures and other non-blocking errors)
35+
archive.on('warning', function(err) {
36+
if (err.code === 'ENOENT') {
37+
console.log('Warning: ', err)
38+
} else {
39+
reject(err)
40+
}
41+
});
42+
43+
// Catch errors
44+
archive.on('error', function(err) {
45+
reject(err);
46+
});
47+
48+
// pipe archive data to the file
49+
archive.pipe(output);
50+
// append files from a sub-directory and naming it `new-subdir` within the archive
51+
archive.directory(dirPath, uploadId);
52+
archive.finalize();
53+
});
54+
}
55+
56+
function filterStories(dirPath, storybookConfig) {
57+
let storyIds = [];
58+
stories = JSON.parse(fs.readFileSync(`${dirPath}/stories.json`)).stories;
59+
60+
for (const [storyId, storyInfo] of Object.entries(stories)) {
61+
if (!skipStory(storyInfo.name, storybookConfig)) {
62+
storyIds.push(storyId);
63+
}
64+
}
65+
if (storyIds.length === 0) {
66+
console.log('[smartui] Error: No stories found');
67+
process.exit(0);
68+
}
69+
70+
return storyIds
71+
}
72+
73+
module.exports = { getSignedUrl, compress, filterStories };

0 commit comments

Comments
 (0)