Skip to content

Commit 937961b

Browse files
committed
[TASK] add buildModule and uploadModule commands
1 parent 020ef28 commit 937961b

10 files changed

+516
-34
lines changed

lib/cmd/buildModule.exec.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { buildModule } from './buildModule.js'
2+
buildModule()

lib/cmd/buildModule.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import '../utils/check.js'
2+
import * as ui from '../utils/ui.js'
3+
import { compileModule } from '../compile/module.js'
4+
5+
/**
6+
* #### Build/compile standalone hubspot module
7+
* @async
8+
* @memberof Commands
9+
* @example
10+
* // run with node
11+
* echo "import '@resultify/hubspot-cms-lib/buildModule'" > buildModule.js
12+
* node buildModule.js
13+
*
14+
*
15+
* // run with npm by adding to package.json scripts:
16+
* "scripts": {
17+
* "buildModule": "cmslib --buildModule",
18+
* }
19+
* or
20+
* "scripts": {
21+
* "buildModule": "node -e 'import(`@resultify/hubspot-cms-lib/buildModule`)'"
22+
* }
23+
* npm run buildModule
24+
*/
25+
async function buildModule () {
26+
const timeStart = ui.startTaskGroup('Build standalone module')
27+
await compileModule()
28+
ui.endTaskGroup({ taskName: 'Build standalone module', timeStart })
29+
}
30+
31+
export { buildModule }

lib/cmd/uploadModule.exec.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { uploadModule } from './uploadModule.js'
2+
uploadModule()

lib/cmd/uploadModule.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import '../utils/check.js'
2+
import { loadAuthConfig } from '../hubspot/auth/auth.js'
3+
import { uploadSelectedModules } from '../hubspot/upload.js'
4+
import * as ui from '../utils/ui.js'
5+
6+
/**
7+
* #### Upload standalone modules to the root of HUBSPOT cms portall
8+
* @async
9+
* @memberof Commands
10+
* @example
11+
* // run with node
12+
* echo "import '@resultify/hubspot-cms-lib/uploadModule'" > uploadModule.js
13+
* node uploadModule.js
14+
*
15+
*
16+
* // run with npm by adding to package.json scripts:
17+
* "scripts": {
18+
* "uploadModule": "cmslib --uploadModule",
19+
* }
20+
* or
21+
* "scripts": {
22+
* "uploadModule": "node -e 'import(`@resultify/hubspot-cms-lib/uploadModule`)'"
23+
* }
24+
* npm run uploadModule
25+
*/
26+
async function uploadModule () {
27+
const timeStart = ui.startTaskGroup('Upload modules to Design Manager root')
28+
const hubAuthData = await loadAuthConfig()
29+
await uploadSelectedModules(hubAuthData)
30+
ui.endTaskGroup({ taskName: 'Upload modules to Design Manager root', timeStart })
31+
}
32+
33+
export { uploadModule }

lib/compile/module.js

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/** @module compile/module */
2+
/// <reference path="../types/types.js" />
3+
import fsPromises from 'fs/promises'
4+
import { getFileList, isFileDir } from '../utils/fs.js'
5+
import * as ui from '../utils/ui.js'
6+
import fs from 'fs'
7+
import path from 'path'
8+
import { pipeline } from 'stream/promises'
9+
import readline from 'readline'
10+
import { copy } from 'fs-extra/esm'
11+
import { deleteAsync } from 'del'
12+
13+
/**
14+
* #### Read from a file and return a readable stream
15+
* @private
16+
* @param {string} filePath - env variables
17+
* @returns {fs.ReadStream} readable stream
18+
*/
19+
function createReadStream (filePath) {
20+
return fs.createReadStream(filePath, 'utf8')
21+
}
22+
23+
/**
24+
25+
* #### insert content into a file using streams
26+
* @async
27+
* @private
28+
* @param {string} destinationFile - destination file
29+
* @param {string} insertContent - content to insert
30+
* @param {string} initialContent - initial content
31+
* @param {number} index - index
32+
* @returns undefined
33+
*/
34+
async function insertContentIntoFile (destinationFile, insertContent, initialContent, index) {
35+
// Create a temp file to write the content to
36+
const tempFilePath = `standalone-modules/temp${index}`
37+
// Write the content to the temp file
38+
const writeStream = fs.createWriteStream(tempFilePath, 'utf8')
39+
40+
writeStream.write(insertContent)
41+
await pipeline(initialContent, writeStream)
42+
writeStream.end()
43+
44+
// Replace the original file with the temp file
45+
await fsPromises.rename(tempFilePath, destinationFile)
46+
}
47+
48+
/**
49+
* #### concatenate content from multiple streams
50+
* @async
51+
* @private
52+
* @param {fs.ReadStream[]} streams - list of streams
53+
* @returns {Promise<string>} concatenated content
54+
*/
55+
async function concatenateStreams (streams) {
56+
let concatenatedContent = ''
57+
// add patterns to remove blocks
58+
const rmblockPattern = /{#.*\[rmstart\] #}[\s\S]*?{#.*\[rmend\] #}/g
59+
const rmTmplBlockPattern = /<!--[\s\S]*?-->/g
60+
const importblockPattern = /{%\s*import.*%}/g
61+
62+
for (const stream of streams) {
63+
const rl = readline.createInterface({
64+
input: stream,
65+
crlfDelay: Infinity
66+
})
67+
68+
for await (const line of rl) {
69+
concatenatedContent += line + '\n'
70+
}
71+
concatenatedContent = concatenatedContent.replace(rmblockPattern, '')
72+
concatenatedContent = concatenatedContent.replace(importblockPattern, '')
73+
concatenatedContent = concatenatedContent.replace(rmTmplBlockPattern, '')
74+
}
75+
return concatenatedContent
76+
}
77+
78+
/**
79+
* #### files to include during standalone module compilation
80+
* @typedef {Object} FileIncludes
81+
* @property {Array<string>} files - list of files to include
82+
* @property {Array<string>} dirs - list of directories to include
83+
*/
84+
85+
/**
86+
* #### module initial configuration to be used during standalone module compilation
87+
* @typedef {Object} InitModuleConfig
88+
* @property {JSON} meta - module meta data
89+
* @property {FileIncludes} template - module template data
90+
* @property {FileIncludes} js - module js data
91+
* @property {FileIncludes} css - module css data
92+
*/
93+
94+
/**
95+
* #### Standalone Modules Config
96+
* @typedef {Object} StandaloneModulesConfig
97+
* @property {string} moduleSrcConfigFile - path to the config file defining the module source
98+
* @property {string} moduleSrcName - module source name
99+
* @property {string} moduleSrcPath - path to the module source
100+
* @property {string} newModuleName - new module name
101+
* @property {string} newModulePath - path to the new module
102+
* @property {JSON} newModuleMeta - module meta data
103+
* @property {(FILE_LIST | { path: string; name: string; })[]} newModuleTemplateIncludes - path to the module template includes
104+
* @property {(FILE_LIST | { path: string; name: string; })[]} newModuleJsIncludes - path to the module js includes
105+
* @property {(FILE_LIST | { path: string; name: string; })[]} newModuleCssIncludes - path to the module css includes
106+
*/
107+
108+
/**
109+
* #### Collect File Includes
110+
* @async
111+
* @private
112+
* @param {FileIncludes} fileIncludes - files to include
113+
* @param {string} moduleSrcName - module source name
114+
* @returns {Promise<(FILE_LIST|{ path: string; name: string;})[]>} list of files to include
115+
*/
116+
async function collectFileIncludes (fileIncludes, moduleSrcName) {
117+
const files = []
118+
if (fileIncludes !== undefined) {
119+
for await (const dir of fileIncludes.dirs) {
120+
const dirFiles = await getFileList(`${dir}`, { objectMode: true })
121+
files.push(...dirFiles)
122+
}
123+
for await (let file of fileIncludes.files) {
124+
if (file.includes('module.html')) {
125+
file = `theme/modules/${moduleSrcName}/${file}`
126+
} else if (file.includes('module.css')) {
127+
file = `theme/modules/${moduleSrcName}/${file}`
128+
} else if (file.includes('module.js')) {
129+
file = `theme/modules/${moduleSrcName}/${file}`
130+
}
131+
132+
if (await isFileDir(`${file}`)) {
133+
files.push({ path: `${file}`, name: path.basename(file) })
134+
}
135+
}
136+
}
137+
return files
138+
}
139+
140+
/**
141+
* #### Parse Standalone Modules Config
142+
* @async
143+
* @private
144+
* @param {FILE_LIST[]} configFiles - env variables
145+
* @returns {Promise<StandaloneModulesConfig[]>} standalone modules config
146+
*/
147+
async function parseStandaloneModulesConfig (configFiles) {
148+
/**
149+
* @type {Array<StandaloneModulesConfig>}
150+
*/
151+
const config = []
152+
if (configFiles !== undefined) {
153+
for await (const file of configFiles) {
154+
if (await isFileDir(file.path)) {
155+
const data = await import(file.path)
156+
const moduleSrcName = file.name.replace('.js', '')
157+
const moduleSrcPath = `theme/modules/${moduleSrcName}`
158+
let newModuleName = moduleSrcName
159+
if (data.default && data.default.name) {
160+
const newModuleNameVal = data.default.name
161+
newModuleName = `${newModuleNameVal.toLowerCase()
162+
.replace(/\s+/g, '-') // Replace spaces with dashes
163+
.replace(/[^a-z0-9-]/g, '')}.module` // Remove non-alphanumeric characters except dashes
164+
}
165+
const newModulePath = `standalone-modules/${newModuleName}`
166+
if (await isFileDir(moduleSrcPath)) {
167+
/**
168+
* @type {StandaloneModulesConfig}
169+
*/
170+
const moduleConfig = {
171+
moduleSrcConfigFile: file.path,
172+
moduleSrcName,
173+
moduleSrcPath,
174+
newModuleName,
175+
newModulePath,
176+
newModuleMeta: data.default.meta,
177+
newModuleTemplateIncludes: await collectFileIncludes(data.default.template, moduleSrcName),
178+
newModuleJsIncludes: await collectFileIncludes(data.default.js, moduleSrcName),
179+
newModuleCssIncludes: await collectFileIncludes(data.default.css, moduleSrcName)
180+
}
181+
config.push(moduleConfig)
182+
}
183+
}
184+
}
185+
}
186+
return config
187+
}
188+
189+
/**
190+
* #### update module meta data
191+
* @async
192+
* @private
193+
* @param {StandaloneModulesConfig} config - module config
194+
* @returns undefined
195+
*/
196+
async function updateModuleMeta (config) {
197+
const existingMeta = await fsPromises.readFile(`${config.newModulePath}/meta.json`, 'utf8')
198+
if (existingMeta !== undefined) {
199+
const existingMetaObj = JSON.parse(existingMeta || '{}')
200+
const updatedMeta = JSON.stringify({ ...existingMetaObj, ...config.newModuleMeta }, null, 2)
201+
await fsPromises.writeFile(`${config.newModulePath}/meta.json`, updatedMeta, 'utf8')
202+
}
203+
}
204+
205+
/**
206+
* #### Compile Module
207+
* @async
208+
* @param {boolean} [hideStatus] - hide status messages
209+
* @returns undefined
210+
*/
211+
async function compileModule (hideStatus) {
212+
try {
213+
const timeStart = ui.startTask('compileModule')
214+
215+
const moduleSrcConfigFiles = await getFileList(`${process.cwd()}/standalone-modules/*module.js`, { objectMode: true })
216+
const config = await parseStandaloneModulesConfig(moduleSrcConfigFiles)
217+
const fileList = []
218+
for await (const [index, module] of config.entries()) {
219+
// create destination directory
220+
await fsPromises.mkdir(module.newModulePath, { recursive: true })
221+
// delete all files in the destination directory
222+
await deleteAsync([`${module.newModulePath}/*`], { force: true })
223+
// copy source files to destination directory
224+
await copy(`${module.moduleSrcPath}`, `${module.newModulePath}`)
225+
// update module meta data
226+
await updateModuleMeta(module)
227+
228+
// create readable streams for all source files
229+
const templateSrcStream = module.newModuleTemplateIncludes.map(filePath => createReadStream(filePath.path))
230+
const templateDistStream = createReadStream(`${module.newModulePath}/module.html`)
231+
const jsDistStream = createReadStream(`${module.moduleSrcPath}/module.js`)
232+
const jsSrcStream = module.newModuleJsIncludes.map(filePath => createReadStream(filePath.path))
233+
const cssDistStream = createReadStream(`${module.moduleSrcPath}/module.css`)
234+
const cssSrcStream = module.newModuleCssIncludes.map(filePath => createReadStream(filePath.path))
235+
236+
// concatenate content from all source streams
237+
const concatTemplateSrc = await concatenateStreams(templateSrcStream)
238+
const concatTemplateDist = await concatenateStreams([templateDistStream])
239+
const concatJsDist = await concatenateStreams([jsDistStream])
240+
const concatJsSrc = await concatenateStreams(jsSrcStream)
241+
const concatCssDist = await concatenateStreams([cssDistStream])
242+
const concatCssSrc = await concatenateStreams(cssSrcStream)
243+
244+
// insert the concatenated content into the destination file
245+
await insertContentIntoFile(`${module.newModulePath}/module.html`, concatTemplateSrc, concatTemplateDist, index)
246+
await insertContentIntoFile(`${module.newModulePath}/module.js`, concatJsSrc, concatJsDist, index)
247+
await insertContentIntoFile(`${module.newModulePath}/module.css`, concatCssSrc, concatCssDist, index)
248+
// remove unnecessary files
249+
await deleteAsync([`${module.newModulePath}/fields.js`], { force: true })
250+
251+
const file = {
252+
name: `${module.newModulePath}.js`,
253+
size: '',
254+
dist: `${module.moduleSrcPath} --> ${module.newModulePath}`
255+
}
256+
fileList.push(file)
257+
}
258+
259+
hideStatus || ui.endTask({ files: fileList, taskName: 'compileModule', timeStart })
260+
} catch (error) {
261+
console.error(error)
262+
}
263+
}
264+
265+
export { compileModule }

0 commit comments

Comments
 (0)