@merkur/cli 0.35.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/src/logger.mjs ADDED
@@ -0,0 +1,77 @@
1
+ import chalk from 'chalk';
2
+
3
+ export class Logger {
4
+ #identifier = null;
5
+ #cliConfig = null;
6
+
7
+ constructor(identifier, cliConfig) {
8
+ this.#identifier = identifier;
9
+ this.#cliConfig = cliConfig;
10
+ }
11
+
12
+ #log(prefix, color, message) {
13
+ if (prefix) {
14
+ process.stdout.write(
15
+ color(`${prefix}: ${this.#identifier ? `(${this.#identifier}) ` : ''}`),
16
+ );
17
+ }
18
+
19
+ try {
20
+ message = message ?? typeof message;
21
+ process.stdout.write(message);
22
+ } catch (error) {
23
+ console.error(error);
24
+ }
25
+
26
+ process.stdout.write('\n');
27
+ }
28
+
29
+ info(message, options) {
30
+ // if (!this.#cliConfig?.verbose) {
31
+ // return;
32
+ // }
33
+
34
+ this.#log('info', chalk.bold.cyan, message, options);
35
+ }
36
+
37
+ log(message, options) {
38
+ this.#log(null, chalk.bold.cyan, message, options);
39
+ }
40
+
41
+ debug(message, options) {
42
+ if (!this.#cliConfig?.verbose) {
43
+ return;
44
+ }
45
+
46
+ this.#log('debug', chalk.bold.cyan, message, options);
47
+ }
48
+
49
+ warn(message, options) {
50
+ this.#log('warn', chalk.bold.yellow, message, options);
51
+ }
52
+
53
+ error(message, options) {
54
+ if (message instanceof Error) {
55
+ const [_, ...stackLines] = message.stack?.split('\n') ?? ''; //eslint-disable-line
56
+
57
+ this.#log(
58
+ 'error',
59
+ chalk.bold.red,
60
+ `${chalk.underline(message.name)}: ${message.message.trim()}`,
61
+ options,
62
+ );
63
+
64
+ process.stdout.write(`\n${chalk.gray(stackLines.join('\n'))}\n`);
65
+
66
+ if (message?.cause instanceof Error) {
67
+ this.error(message.cause, options);
68
+ }
69
+ } else {
70
+ this.#log('error', chalk.bold.red, message, options);
71
+ }
72
+ }
73
+ }
74
+
75
+ export function createLogger(name, cliConfig = {}) {
76
+ return new Logger(name, cliConfig);
77
+ }
@@ -0,0 +1,252 @@
1
+ import path from 'node:path';
2
+
3
+ import { EMITTER_EVENTS, emitter, RESULT_KEY } from './emitter.mjs';
4
+ import { createLogger } from './logger.mjs';
5
+ import { devPlugin } from './plugins/devPlugin.mjs';
6
+ import { deepMerge } from './utils.mjs';
7
+ import { COMMAND_NAME } from './commands/constant.mjs';
8
+
9
+ const MERKUR_CONFIG_FILE = 'merkur.config.mjs';
10
+
11
+ export async function createMerkurConfig({ cliConfig, context } = {}) {
12
+ const logger = createLogger('merkurConfig', cliConfig);
13
+ const { projectFolder } = cliConfig;
14
+ let merkurConfig;
15
+
16
+ try {
17
+ logger.debug(
18
+ `Load merkur config on path ${projectFolder}/${MERKUR_CONFIG_FILE}`,
19
+ );
20
+
21
+ const file = await import(`${projectFolder}/${MERKUR_CONFIG_FILE}`);
22
+ merkurConfig = await file.default({
23
+ cliConfig,
24
+ context,
25
+ emitter,
26
+ EMITTER_EVENTS,
27
+ });
28
+ } catch (error) {
29
+ logger.error(error);
30
+ }
31
+
32
+ cliConfig = { ...(merkurConfig?.cliConfig ?? {}), ...cliConfig };
33
+
34
+ await loadExtender({ merkurConfig, cliConfig, logger, context });
35
+
36
+ await registerHooks({ merkurConfig });
37
+
38
+ let event = {
39
+ merkurConfig: {
40
+ ...merkurConfig,
41
+ },
42
+ cliConfig,
43
+ context,
44
+ [RESULT_KEY]: 'merkurConfig',
45
+ };
46
+
47
+ event = await emitter.emit(EMITTER_EVENTS.MERKUR_CONFIG, event);
48
+
49
+ return event;
50
+ }
51
+
52
+ async function loadExtender({ merkurConfig, cliConfig, logger, context }) {
53
+ await Promise.all(
54
+ merkurConfig?.extends?.map(async (modulePath) => {
55
+ try {
56
+ const file = await import(`${modulePath}`);
57
+ await file.default({
58
+ cliConfig,
59
+ merkurConfig,
60
+ context,
61
+ emitter,
62
+ EMITTER_EVENTS,
63
+ });
64
+ } catch (error) {
65
+ logger.error(error);
66
+ }
67
+ }) ?? [],
68
+ );
69
+ }
70
+
71
+ async function registerHooks({ merkurConfig }) {
72
+ Object.values(EMITTER_EVENTS)
73
+ .filter((eventName) => eventName in merkurConfig)
74
+ .forEach((eventName) => {
75
+ emitter.on(eventName, function autoRegister(event) {
76
+ return merkurConfig[eventName](event);
77
+ });
78
+ });
79
+ }
80
+
81
+ emitter.on(
82
+ EMITTER_EVENTS.MERKUR_CONFIG,
83
+ function defaultTask({ merkurConfig, cliConfig }) {
84
+ const { staticFolder, runTask } = cliConfig;
85
+
86
+ merkurConfig.task = merkurConfig.task ?? {};
87
+
88
+ const defaultTaskDefinition = {
89
+ node: {
90
+ name: 'node',
91
+ build: {
92
+ platform: 'node',
93
+ write: true,
94
+ },
95
+ },
96
+ es13: {
97
+ name: 'es13',
98
+ build: {
99
+ platform: 'browser',
100
+ outdir: `${staticFolder}/es13`,
101
+ plugins: [devPlugin],
102
+ },
103
+ },
104
+ es9: {
105
+ name: 'es9',
106
+ build: {
107
+ platform: 'browser',
108
+ target: 'es2018',
109
+ outdir: `${staticFolder}/es9`,
110
+ },
111
+ },
112
+ };
113
+
114
+ const tasks = [
115
+ ...new Set([
116
+ ...Object.keys(merkurConfig.task ?? {}),
117
+ ...Object.keys(defaultTaskDefinition),
118
+ ]),
119
+ ];
120
+
121
+ tasks.forEach((key) => {
122
+ merkurConfig.task[key] = deepMerge(
123
+ defaultTaskDefinition[key] ?? {},
124
+ merkurConfig.task[key] ?? {},
125
+ );
126
+ });
127
+
128
+ if (runTask.length !== 0) {
129
+ Object.keys(merkurConfig.task)
130
+ .filter((taskName) => !runTask.includes(taskName))
131
+ .forEach((taskKey) => {
132
+ delete merkurConfig.task[taskKey];
133
+ });
134
+ }
135
+
136
+ return merkurConfig;
137
+ },
138
+ );
139
+
140
+ emitter.on(
141
+ EMITTER_EVENTS.MERKUR_CONFIG,
142
+ function devServer({ merkurConfig, cliConfig }) {
143
+ merkurConfig.devServer = {
144
+ ...merkurConfig.devServer,
145
+ ...{
146
+ protocol: 'http:',
147
+ host: 'localhost:4445',
148
+ port: 4445,
149
+ staticPath: cliConfig.staticPath,
150
+ staticFolder: path.resolve(
151
+ cliConfig.projectFolder,
152
+ cliConfig.staticFolder,
153
+ ),
154
+ },
155
+ };
156
+
157
+ const { origin, host, protocol } = merkurConfig.devServer;
158
+
159
+ merkurConfig.devServer.origin =
160
+ origin ?? new URL(`${protocol}//${host}`).origin;
161
+
162
+ return merkurConfig;
163
+ },
164
+ );
165
+
166
+ emitter.on(
167
+ EMITTER_EVENTS.MERKUR_CONFIG,
168
+ function defaultEntries({ merkurConfig, cliConfig }) {
169
+ merkurConfig.defaultEntries = {
170
+ client: [`${cliConfig.projectFolder}/src/entries/client.js`],
171
+ server: [`${cliConfig.projectFolder}/src/entries/server.js`],
172
+ ...merkurConfig.defaultEntries,
173
+ };
174
+
175
+ return merkurConfig;
176
+ },
177
+ );
178
+
179
+ emitter.on(
180
+ EMITTER_EVENTS.MERKUR_CONFIG,
181
+ function playground({ merkurConfig, cliConfig }) {
182
+ merkurConfig.playground = {
183
+ template: `${cliConfig.cliFolder}/templates/playground.ejs`,
184
+ templateFolder: `${cliConfig.cliFolder}/templates`,
185
+ path: '/',
186
+ widgetHandler: async (req) => {
187
+ const { protocol, host } = merkurConfig.widgetServer;
188
+ let widgetProperties = null;
189
+ const response = await fetch(
190
+ `${protocol}//${host}/widget?${new URLSearchParams(req.params)}`,
191
+ );
192
+
193
+ widgetProperties = await response.json();
194
+ if (!response.ok) {
195
+ const error = new Error(widgetProperties?.error?.message);
196
+ error.stack = widgetProperties?.error?.stack;
197
+ throw error;
198
+ }
199
+
200
+ return widgetProperties;
201
+ },
202
+ ...merkurConfig.playground,
203
+ };
204
+
205
+ return merkurConfig;
206
+ },
207
+ );
208
+
209
+ emitter.on(
210
+ EMITTER_EVENTS.MERKUR_CONFIG,
211
+ function socketServer({ merkurConfig }) {
212
+ merkurConfig.socketServer = {
213
+ protocol: 'ws:',
214
+ host: 'localhost:4321',
215
+ port: 4321,
216
+ ...merkurConfig.socketServer,
217
+ };
218
+
219
+ return merkurConfig;
220
+ },
221
+ );
222
+
223
+ emitter.on(
224
+ EMITTER_EVENTS.MERKUR_CONFIG,
225
+ function widgetServer({ merkurConfig, cliConfig }) {
226
+ merkurConfig.widgetServer = {
227
+ protocol: 'http:',
228
+ host: 'localhost:4444',
229
+ port: 4444,
230
+ staticPath: cliConfig.staticPath,
231
+ staticFolder: path.resolve(
232
+ cliConfig.projectFolder,
233
+ cliConfig.staticFolder,
234
+ ),
235
+ buildFolder: path.resolve(cliConfig.projectFolder, cliConfig.buildFolder),
236
+ clusters: cliConfig.command === COMMAND_NAME.DEV ? 0 : 3,
237
+ ...merkurConfig.widgetServer,
238
+ };
239
+
240
+ const { origin, host, protocol } = merkurConfig.widgetServer;
241
+
242
+ merkurConfig.widgetServer.origin =
243
+ origin ?? new URL(`${protocol}//${host}`).origin;
244
+
245
+ return merkurConfig;
246
+ },
247
+ );
248
+ emitter.on(EMITTER_EVENTS.MERKUR_CONFIG, function devServer({ merkurConfig }) {
249
+ merkurConfig.HMR = merkurConfig?.HMR ?? true;
250
+
251
+ return merkurConfig;
252
+ });
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs';
2
+
3
+ import chalk from 'chalk';
4
+
5
+ import { createClient } from '../websocket.mjs';
6
+
7
+ import { createLogger } from '../logger.mjs';
8
+
9
+ export function devPlugin({ definition, merkurConfig, cliConfig }) {
10
+ const logger = createLogger('devPlugin', cliConfig);
11
+ const { projectFolder, buildDir } = cliConfig;
12
+ let memory = {};
13
+ let changed = [];
14
+ let errors = [];
15
+
16
+ return {
17
+ name: 'devPlugin',
18
+ setup(build) {
19
+ logger.debug(`Setup plugin for "${chalk.cyan(definition.name)}" task.`);
20
+
21
+ if (cliConfig.isProduction) {
22
+ return;
23
+ }
24
+
25
+ build.onEnd((result) => {
26
+ errors = result.errors.map((error) => {
27
+ const source = fs.readFileSync(
28
+ `${projectFolder}/${error.location.file}`,
29
+ 'utf-8',
30
+ );
31
+
32
+ return {
33
+ ...error,
34
+ message: error.text,
35
+ source,
36
+ columnNumber: error.location.column,
37
+ fileName: error.location.file,
38
+ lineNumber: error.location.line,
39
+ };
40
+ });
41
+
42
+ changed =
43
+ result?.outputFiles?.reduce((changed, file) => {
44
+ const name = file.path
45
+ .replace(projectFolder, '')
46
+ .replace(buildDir, '')
47
+ .split('/')
48
+ .pop();
49
+
50
+ const isMapFile = name.endsWith('.map');
51
+
52
+ if (
53
+ (!memory[name] || memory[name].hash !== file.hash) &&
54
+ !isMapFile
55
+ ) {
56
+ memory[name] = file;
57
+ changed.push({ name });
58
+ }
59
+
60
+ return changed;
61
+ }, []) ?? [];
62
+
63
+ const client = createClient({ merkurConfig });
64
+ client.on('error', (error) => {
65
+ logger.error(error);
66
+ client.terminate();
67
+ });
68
+
69
+ client.on('open', function open() {
70
+ if (merkurConfig.HMR) {
71
+ client.send(
72
+ JSON.stringify({
73
+ to: 'browser',
74
+ command: 'refresh',
75
+ changed,
76
+ errors,
77
+ }),
78
+ );
79
+ client.terminate();
80
+ } else {
81
+ setTimeout(() => {
82
+ client.send(
83
+ JSON.stringify({
84
+ to: 'browser',
85
+ command: 'reload',
86
+ changed,
87
+ errors,
88
+ }),
89
+ );
90
+ client.terminate();
91
+ }, 50);
92
+ }
93
+ });
94
+ });
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import chalk from 'chalk';
5
+
6
+ import { createLogger } from '../logger.mjs';
7
+
8
+ export function memoryStaticPlugin({ definition, cliConfig, context }) {
9
+ const logger = createLogger('memoryStaticPlugin', cliConfig);
10
+ let manifest = {};
11
+ let manifestHash = '';
12
+
13
+ return {
14
+ name: 'memoryStaticPlugin',
15
+ setup(build) {
16
+ const { projectFolder, buildFolder } = cliConfig;
17
+ logger.debug(`Setup plugin for "${chalk.cyan(definition.name)}" task.`);
18
+
19
+ build.onEnd(async (result) => {
20
+ if (result.errors.length) {
21
+ return;
22
+ }
23
+
24
+ await generateManifest(result);
25
+ await saveGeneratedFileToMemory(result);
26
+ });
27
+
28
+ async function generateManifest(result) {
29
+ const pathToOutDir = path.resolve(
30
+ projectFolder,
31
+ definition?.build?.outdir,
32
+ );
33
+ result.outputFiles.map((file) => {
34
+ const key = file.path.replace(pathToOutDir, '').replace('/', '');
35
+
36
+ manifest[key] = key;
37
+ });
38
+ let newManifestHash = JSON.stringify(manifest);
39
+
40
+ if (newManifestHash !== manifestHash) {
41
+ try {
42
+ await createFolderStructure(pathToOutDir);
43
+
44
+ await fs.writeFile(
45
+ `${pathToOutDir}/manifest.json`,
46
+ JSON.stringify(manifest),
47
+ );
48
+ manifestHash = newManifestHash;
49
+
50
+ logger.debug(
51
+ `Create manifest for ${definition.name} to ${definition?.build?.outdir} folder.`,
52
+ );
53
+ } catch (error) {
54
+ logger.error(error);
55
+ }
56
+ }
57
+ }
58
+
59
+ async function saveGeneratedFileToMemory(result) {
60
+ result.outputFiles.map((file) => {
61
+ const key = file.path.replace(
62
+ path.resolve(projectFolder, buildFolder),
63
+ '',
64
+ );
65
+ const size = Math.round(
66
+ (result?.metafile?.outputs?.[`${path.normalize(buildFolder)}${key}`]
67
+ ?.bytes ?? 0) / 1024,
68
+ );
69
+
70
+ logger.debug(`Save file "${key}", size ${size} [kB] to memory.`);
71
+ context.memory[key] = file;
72
+ });
73
+ }
74
+ },
75
+ };
76
+ }
77
+
78
+ async function createFolderStructure(dir) {
79
+ try {
80
+ await fs.access(dir, fs.constants.R_OK);
81
+ } catch {
82
+ await fs.mkdir(dir, { recursive: true });
83
+ }
84
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import { createLogger } from '../logger.mjs';
4
+ import { time } from '../utils.mjs';
5
+
6
+ import chalk from 'chalk';
7
+
8
+ export function metaPlugin({ definition, config, cliConfig }) {
9
+ const { projectFolder } = cliConfig;
10
+ const logger = createLogger('metaPlugin', cliConfig);
11
+ return {
12
+ name: 'metaPlugin',
13
+ setup(build) {
14
+ let timer = null;
15
+ logger.debug(`Setup plugin for "${chalk.cyan(definition.name)}" task.`);
16
+
17
+ build.onStart(() => {
18
+ timer = time();
19
+ });
20
+
21
+ build.onEnd(async (result) => {
22
+ if (result.errors.length) {
23
+ return;
24
+ }
25
+ let metaInformation = [];
26
+
27
+ if (config.writeToDisk) {
28
+ const generatedFiles = Object.keys(
29
+ result?.metafile?.outputs ?? {},
30
+ ).filter((file) => !file.endsWith('.map'));
31
+
32
+ metaInformation = await Promise.all(
33
+ generatedFiles.map(async (file) => {
34
+ const stat = await fs.stat(`${projectFolder}/${file}`);
35
+
36
+ return { stat, file };
37
+ }),
38
+ );
39
+ }
40
+
41
+ logger.log(
42
+ `Task ${chalk.bold.green(definition.name)} complete for ${chalk.bold.green(timer())} [ms]`,
43
+ );
44
+
45
+ metaInformation.map(({ file, stat }) => {
46
+ logger.log(
47
+ ` -> ${chalk.bold(file)}, ${Math.round(stat.size / 1024)} kB`,
48
+ );
49
+ });
50
+ });
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,21 @@
1
+ import esbuild from 'esbuild';
2
+
3
+ export async function runTask({ cliConfig, build }) {
4
+ const { watch } = cliConfig;
5
+
6
+ //es6 === es2015, es9 === es2018, es11 === es2020 es13 ===es2022
7
+ try {
8
+ const result = await (watch
9
+ ? esbuild.context(build)
10
+ : esbuild.build(build));
11
+
12
+ if (watch) {
13
+ await result.watch();
14
+ }
15
+
16
+ return result;
17
+ } catch (error) {
18
+ console.error(error);
19
+ process.exit(1);
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ import { EMITTER_EVENTS, emitter, RESULT_KEY } from './emitter.mjs';
2
+
3
+ export async function createTaskConfig({
4
+ cliConfig,
5
+ definition,
6
+ context,
7
+ } = {}) {
8
+ let event = {
9
+ config: {
10
+ isServer: definition?.build?.platform === 'node',
11
+ writeToDisk: definition?.build?.write ?? cliConfig.writeToDisk,
12
+ ...definition.config,
13
+ },
14
+ definition,
15
+ cliConfig,
16
+ context,
17
+ [RESULT_KEY]: 'config',
18
+ };
19
+
20
+ event = await emitter.emit(EMITTER_EVENTS.TASK_CONFIG, event);
21
+
22
+ return event.config;
23
+ }
File without changes
@@ -0,0 +1,20 @@
1
+ <script>
2
+ window.__merkur_dev__ = window.__merkur_dev__ || {};
3
+ window.__merkur_dev__.merkurConfig = <%- JSON.stringify(merkurConfig) %>;
4
+ window.__merkur_dev__.assets = <%- JSON.stringify(assets) %>;
5
+ window.__merkur_dev__.widgetProperties = <%- JSON.stringify(widgetProperties) %>;
6
+ <%- devClient %>
7
+ </script>
8
+ <% assets.forEach((asset)=> { %>
9
+ <%if (asset.type==='stylesheet' ) { %>
10
+ <link rel='stylesheet' href='<%= asset.source %>' data-name="<%= asset.name %>" />
11
+ <% } %>
12
+ <%if (asset.type==='script' ) { %>
13
+ <%if (typeof asset.source==='string' ) { %>
14
+ <script src='<%= asset.source %>' defer='true' data-name="<%= asset.name %>"></script>
15
+ <% } %>
16
+ <%if (typeof asset.source==='object' ) { %>
17
+ <script src='<%= asset.source.es13 %>' defer='true' data-name="<%= asset.name %>"></script>
18
+ <% } %>
19
+ <% } %>
20
+ <% }); %>
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <%- include('./head.ejs', {assets, merkurConfig, devClient, widgetProperties}) %>
8
+ <title>MERKUR - widget</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div class="headline-view"><%- widgetProperties && widgetProperties.slot && widgetProperties.slot.headline.html %>
13
+ </div>
14
+ <div class="merkur-view"><%- html %></div>
15
+ <script>
16
+ window.addEventListener('load', function () {
17
+ __merkur__.create(<%- JSON.stringify(widgetProperties) %>)
18
+ .then(function (widget) {
19
+ widget.containerSelector = '.merkur-view';
20
+ widget.slot.headline.containerSelector = '.headline-view';
21
+
22
+ widget.mount();
23
+ });
24
+ });
25
+ </script>
26
+ <%- include('./footer.ejs', {assets, merkurConfig, devClient, widgetProperties}) %>
27
+ </body>
28
+
29
+ </html>
package/src/utils.mjs ADDED
@@ -0,0 +1,33 @@
1
+ export function time() {
2
+ const start = process.hrtime.bigint();
3
+
4
+ return () => Number((process.hrtime.bigint() - start) / BigInt(1e6));
5
+ }
6
+
7
+ const PROTECTED_FIELDS = ['__proto__', 'prototype', 'constructor'];
8
+ export function deepMerge(target, source) {
9
+ const isObject = (obj) => !!obj && obj.constructor === Object;
10
+
11
+ if (!isObject(target) || !isObject(source)) {
12
+ return source;
13
+ }
14
+
15
+ Object.keys(source).forEach((key) => {
16
+ if (PROTECTED_FIELDS.includes(key)) {
17
+ return;
18
+ }
19
+
20
+ const targetValue = target[key];
21
+ const sourceValue = source[key];
22
+
23
+ if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
24
+ target[key] = targetValue.concat(sourceValue);
25
+ } else if (isObject(targetValue) && isObject(sourceValue)) {
26
+ target[key] = deepMerge(Object.assign({}, targetValue), sourceValue);
27
+ } else {
28
+ target[key] = sourceValue;
29
+ }
30
+ });
31
+
32
+ return target;
33
+ }