@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.
@@ -0,0 +1,61 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { createBuildConfig } from '../buildConfig.mjs';
4
+ import { createCLIConfig } from '../CLIConfig.mjs';
5
+ import { createContext } from '../context.mjs';
6
+ import { createTaskConfig } from '../taskConfig.mjs';
7
+ import { runTask } from '../runTask.mjs';
8
+ import { createMerkurConfig } from '../merkurConfig.mjs';
9
+ import { createLogger } from '../logger.mjs';
10
+ import { time } from '../utils.mjs';
11
+ import { handleExit } from '../handleExit.mjs';
12
+
13
+ export async function build({ args, command }) {
14
+ const buildTime = time();
15
+
16
+ const context = await createContext();
17
+ const baseCliConfig = await createCLIConfig({ args, context, command });
18
+
19
+ const { merkurConfig, cliConfig } = await createMerkurConfig({
20
+ cliConfig: baseCliConfig,
21
+ context,
22
+ });
23
+ const logger = createLogger(undefined, cliConfig);
24
+
25
+ await handleExit({ context });
26
+
27
+ const task = await Object.keys(merkurConfig.task).reduce(
28
+ async (result, key) => {
29
+ const definition = merkurConfig.task[key];
30
+
31
+ const config = await createTaskConfig({
32
+ definition,
33
+ merkurConfig,
34
+ cliConfig,
35
+ context,
36
+ });
37
+ const build = await createBuildConfig({
38
+ definition,
39
+ config,
40
+ merkurConfig,
41
+ cliConfig,
42
+ context,
43
+ });
44
+ result[definition.name] = await runTask({
45
+ definition,
46
+ build,
47
+ merkurConfig,
48
+ cliConfig,
49
+ config,
50
+ context,
51
+ });
52
+
53
+ return result;
54
+ },
55
+ context.task,
56
+ );
57
+
58
+ await Promise.all(Object.values(task));
59
+
60
+ logger.log(`Build success ${chalk.green(buildTime())} [ms]`);
61
+ }
@@ -0,0 +1,6 @@
1
+ export const COMMAND_NAME = {
2
+ START: 'start',
3
+ DEV: 'dev',
4
+ BUILD: 'build',
5
+ TEST: 'test',
6
+ };
@@ -0,0 +1,59 @@
1
+ import { createBuildConfig } from '../buildConfig.mjs';
2
+ import { createCLIConfig } from '../CLIConfig.mjs';
3
+ import { createContext } from '../context.mjs';
4
+ import { runDevServer } from '../devServer.mjs';
5
+ import { createTaskConfig } from '../taskConfig.mjs';
6
+ import { runTask } from '../runTask.mjs';
7
+ import { createMerkurConfig } from '../merkurConfig.mjs';
8
+ import { runSocketServer } from '../websocket.mjs';
9
+ import { runWidgetServer } from '../widgetServer.mjs';
10
+ import { handleExit } from '../handleExit.mjs';
11
+
12
+ export async function dev({ args, command }) {
13
+ const context = await createContext();
14
+ const baseCliConfig = await createCLIConfig({ args, context, command });
15
+
16
+ const { merkurConfig, cliConfig } = await createMerkurConfig({
17
+ cliConfig: baseCliConfig,
18
+ context,
19
+ });
20
+
21
+ await handleExit({ context });
22
+
23
+ const task = await Object.keys(merkurConfig.task).reduce(
24
+ async (result, key) => {
25
+ const definition = merkurConfig.task[key];
26
+
27
+ const config = await createTaskConfig({
28
+ definition,
29
+ merkurConfig,
30
+ cliConfig,
31
+ context,
32
+ });
33
+ const build = await createBuildConfig({
34
+ definition,
35
+ config,
36
+ merkurConfig,
37
+ cliConfig,
38
+ context,
39
+ });
40
+ result[definition.name] = await runTask({
41
+ definition,
42
+ build,
43
+ merkurConfig,
44
+ cliConfig,
45
+ config,
46
+ context,
47
+ });
48
+
49
+ return result;
50
+ },
51
+ context.task,
52
+ );
53
+
54
+ await Promise.all(Object.values(task));
55
+
56
+ await runDevServer({ merkurConfig, cliConfig, context });
57
+ await runSocketServer({ merkurConfig, cliConfig, context });
58
+ await runWidgetServer({ merkurConfig, cliConfig, context });
59
+ }
@@ -0,0 +1,21 @@
1
+ import { createCLIConfig } from '../CLIConfig.mjs';
2
+ import { createContext } from '../context.mjs';
3
+ import { runDevServer } from '../devServer.mjs';
4
+ import { createMerkurConfig } from '../merkurConfig.mjs';
5
+ import { runWidgetServer } from '../widgetServer.mjs';
6
+ import { handleExit } from '../handleExit.mjs';
7
+
8
+ export async function start({ args, command }) {
9
+ const context = await createContext();
10
+ let baseCliConfig = await createCLIConfig({ args, context, command });
11
+
12
+ const { merkurConfig, cliConfig } = await createMerkurConfig({
13
+ cliConfig: baseCliConfig,
14
+ context,
15
+ });
16
+
17
+ await handleExit({ context });
18
+
19
+ await runDevServer({ merkurConfig, cliConfig, context });
20
+ await runWidgetServer({ merkurConfig, cliConfig, context });
21
+ }
@@ -0,0 +1,42 @@
1
+ import { spawn } from 'node:child_process';
2
+ import process from 'node:process';
3
+
4
+ import { createCLIConfig } from '../CLIConfig.mjs';
5
+ import { createContext } from '../context.mjs';
6
+ import { createLogger } from '../logger.mjs';
7
+ import { createMerkurConfig } from '../merkurConfig.mjs';
8
+ import { handleExit } from '../handleExit.mjs';
9
+
10
+ export async function test({ args, command }) {
11
+ const context = await createContext();
12
+ let baseCliConfig = await createCLIConfig({ args, context, command });
13
+
14
+ const { merkurConfig, cliConfig } = await createMerkurConfig({
15
+ cliConfig: baseCliConfig,
16
+ context,
17
+ });
18
+ const logger = createLogger('testRunner', cliConfig);
19
+
20
+ await handleExit({ context });
21
+
22
+ args.unshift('./jest.config.js');
23
+ args.unshift('-c');
24
+
25
+ const runner = spawn('./node_modules/.bin/jest', args, {
26
+ env: {
27
+ ...process.env,
28
+ NODE_CONFIG_DIR: './server/config',
29
+ MERKUR_CONFIG: JSON.stringify(merkurConfig),
30
+ CLI_CONFIG: JSON.stringify(cliConfig),
31
+ },
32
+ stdio: 'inherit',
33
+ });
34
+
35
+ runner.on('spawn', () => {
36
+ logger.debug(`Run test runner ${args.join(', ')}`);
37
+ });
38
+ runner.on('exit', (code, signal) => {
39
+ logger.info(`child process exited with code ${code} and signal ${signal}`);
40
+ process.exit(code);
41
+ });
42
+ }
@@ -0,0 +1,17 @@
1
+ import { emitter, EMITTER_EVENTS, RESULT_KEY } from './emitter.mjs';
2
+
3
+ export async function createContext() {
4
+ let event = {
5
+ context: {
6
+ task: {},
7
+ memory: {},
8
+ process: {},
9
+ server: {},
10
+ },
11
+ [RESULT_KEY]: 'context',
12
+ };
13
+
14
+ event = await emitter.emit(EMITTER_EVENTS.CONTEXT, event);
15
+
16
+ return event.context;
17
+ }
@@ -0,0 +1,79 @@
1
+ import { Observable } from '@esmj/observable';
2
+
3
+ const MAX_RECONNECTION = 5;
4
+
5
+ export class WebSocketClient extends Observable {
6
+ /**
7
+ * @type {?WebSocket}
8
+ */
9
+ #socket = null;
10
+ #reconnectionAttempt = 0;
11
+ #options = {};
12
+
13
+ constructor(options) {
14
+ super();
15
+ /**
16
+ * @type {Object}
17
+ */
18
+ this.#options = options;
19
+ }
20
+
21
+ init() {
22
+ this.#connect();
23
+
24
+ return this;
25
+ }
26
+
27
+ send(data) {
28
+ if (this.#socket) {
29
+ this.#socket.send(data);
30
+ }
31
+
32
+ return this;
33
+ }
34
+
35
+ destroy() {
36
+ if (this.#socket) {
37
+ this.#socket.close();
38
+ this.#socket = null;
39
+ }
40
+
41
+ return this;
42
+ }
43
+
44
+ #connect() {
45
+ this.#socket = Reflect.construct(WebSocket, [
46
+ `${this.#options.protocol}//${this.#options.host}`,
47
+ ]);
48
+
49
+ this.#socket.onopen = () => {
50
+ this.#reconnectionAttempt = 0;
51
+ };
52
+
53
+ this.#socket.onmessage = (event) => {
54
+ try {
55
+ let data = JSON.parse(event.data);
56
+
57
+ this.next(data);
58
+ } catch (error) {
59
+ console.error(error); // eslint-disable-line no-console
60
+ }
61
+ };
62
+
63
+ this.#socket.onerror = (error) => {
64
+ console.error(error); // eslint-disable-line no-console
65
+ };
66
+
67
+ this.#socket.onclose = () => {
68
+ if (this.#reconnectionAttempt >= MAX_RECONNECTION) {
69
+ return;
70
+ }
71
+
72
+ this.#reconnectionAttempt += 1;
73
+
74
+ setTimeout(() => {
75
+ this.#connect();
76
+ }, this.#reconnectionAttempt * 500);
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,92 @@
1
+ export async function hmr({ to, command, changed }) {
2
+ if (to === 'browser' && command === 'refresh') {
3
+ await Promise.all(
4
+ changed.map(async (asset) => {
5
+ return new Promise((resolve) => {
6
+ const element = document.querySelector(`[data-name="${asset.name}"]`);
7
+
8
+ if (!element) {
9
+ location.reload();
10
+ return;
11
+ }
12
+
13
+ let newElement = null;
14
+ const searchParams = new URLSearchParams({ version: Math.random() });
15
+
16
+ if (element.nodeName === 'LINK') {
17
+ const url = new URL(element.href);
18
+ newElement = element.cloneNode();
19
+
20
+ newElement.onload = () => {
21
+ element.remove();
22
+ resolve();
23
+ };
24
+ newElement.onerror = () => {
25
+ element.remove();
26
+ resolve();
27
+ };
28
+ newElement.href = new URL(
29
+ `${url.origin}${url.pathname}?${searchParams.toString()}`,
30
+ );
31
+ }
32
+
33
+ if (element.nodeName === 'SCRIPT') {
34
+ const url = new URL(element.src);
35
+ newElement = document.createElement('script');
36
+ newElement.setAttribute('data-name', asset.name);
37
+ newElement.onload = () => {
38
+ element.remove();
39
+ resolve();
40
+ };
41
+ // TODO check bad scenario
42
+ newElement.onerror = () => {
43
+ element.remove();
44
+ resolve();
45
+ };
46
+
47
+ newElement.src = new URL(
48
+ `${url.origin}${url.pathname}?${searchParams.toString()}`,
49
+ );
50
+ }
51
+
52
+ element.parentNode.insertBefore(newElement, element.nextSibling);
53
+ });
54
+ }),
55
+ );
56
+
57
+ if (!changed.some((asset) => asset.name.endsWith('.js'))) {
58
+ return;
59
+ }
60
+
61
+ // HMR JS
62
+ const widgets = window.__merkur_dev__.widgets;
63
+ window.__merkur_dev__.widgets = [];
64
+
65
+ widgets.forEach(async (widget) => {
66
+ const {
67
+ props,
68
+ state,
69
+ $external,
70
+ name,
71
+ version,
72
+ containerSelector,
73
+ container,
74
+ slot,
75
+ } = widget;
76
+ const widgetProperties = {
77
+ props,
78
+ state,
79
+ $external,
80
+ name,
81
+ version,
82
+ containerSelector,
83
+ container,
84
+ slot,
85
+ };
86
+ const newWidget = await window.__merkur__.create(widgetProperties);
87
+
88
+ await widget.unmount();
89
+ await newWidget.mount();
90
+ });
91
+ }
92
+ }
@@ -0,0 +1,26 @@
1
+ import { WebSocketClient } from './WebSocketClient.mjs';
2
+
3
+ import { reload } from './reload.mjs';
4
+ import { hmr } from './hmr.mjs';
5
+
6
+ const { __merkur_dev__ } = window;
7
+
8
+ const webSocket = new WebSocketClient(__merkur_dev__.merkurConfig.socketServer);
9
+ __merkur_dev__.webSocket = webSocket;
10
+ __merkur_dev__.widgets = [];
11
+
12
+ webSocket.init();
13
+
14
+ webSocket.subscribe(reload);
15
+ webSocket.subscribe(hmr);
16
+
17
+ addEventListener('load', function hookMerkurCreate() {
18
+ const originalMerkurCreate = window.__merkur__.create;
19
+ window.__merkur__.create = async function devClientHook(...rest) {
20
+ const widget = await originalMerkurCreate(...rest);
21
+
22
+ __merkur_dev__.widgets.push(widget);
23
+
24
+ return widget;
25
+ };
26
+ });
@@ -0,0 +1,5 @@
1
+ export async function reload({ to, command }) {
2
+ if (to === 'browser' && command === 'reload') {
3
+ location.reload();
4
+ }
5
+ }
@@ -0,0 +1,142 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import ejs from 'ejs';
4
+ import chalk from 'chalk';
5
+
6
+ import compression from 'compression';
7
+ import express from 'express';
8
+
9
+ import { createLogger } from './logger.mjs';
10
+ import { COMMAND_NAME } from './commands/constant.mjs';
11
+
12
+ function asyncMiddleware(fn) {
13
+ return (req, res, next) => {
14
+ Promise.resolve(fn(req, res, next)).catch(next);
15
+ };
16
+ }
17
+
18
+ export async function runDevServer({ context, merkurConfig, cliConfig }) {
19
+ const logger = createLogger('devServer', cliConfig);
20
+ const { protocol, host, port, staticPath, staticFolder, origin } =
21
+ merkurConfig.devServer;
22
+ const {
23
+ template,
24
+ templateFolder,
25
+ path: playgroundPath,
26
+ widgetHandler,
27
+ } = merkurConfig.playground;
28
+ const { cliFolder, command } = cliConfig;
29
+
30
+ return new Promise((resolve, reject) => {
31
+ const app = express();
32
+
33
+ const server = app
34
+ .use((req, res, next) => {
35
+ const headerOrigin = req.get('Origin');
36
+ // Allow cors
37
+ if (headerOrigin) {
38
+ res.header('Access-Control-Allow-Origin', `${origin}`);
39
+ res.header('Access-Control-Allow-Headers', '*');
40
+ }
41
+
42
+ next();
43
+ })
44
+ .use(compression())
45
+ .get(
46
+ playgroundPath,
47
+ asyncMiddleware(async (req, res) => {
48
+ const isDevCommand = command === COMMAND_NAME.DEV;
49
+
50
+ const widgetProperties = await widgetHandler(req);
51
+
52
+ // TODO refactor
53
+ if (isDevCommand) {
54
+ const { widgetServer } = merkurConfig;
55
+ widgetProperties.assets = widgetProperties?.assets?.map((asset) => {
56
+ if (typeof asset.source === 'string') {
57
+ asset.source = asset.source.replace(
58
+ widgetServer.origin,
59
+ origin,
60
+ );
61
+
62
+ return asset;
63
+ }
64
+
65
+ if (Object.keys(asset.source) !== 0) {
66
+ Object.keys(asset.source).map((assetVersion) => {
67
+ if (typeof asset.source[assetVersion] === 'string') {
68
+ asset.source[assetVersion] = asset.source[
69
+ assetVersion
70
+ ].replace(widgetServer.origin, origin);
71
+ }
72
+ });
73
+ }
74
+
75
+ return asset;
76
+ });
77
+ }
78
+
79
+ const devClient = isDevCommand
80
+ ? fs.readFileSync(`${cliFolder}/../lib/devClient.mjs`, 'utf8')
81
+ : '';
82
+
83
+ const playgroundTemplate = ejs.compile(
84
+ fs.readFileSync(template, 'utf8'),
85
+ {
86
+ views: [path.dirname(template), templateFolder],
87
+ },
88
+ );
89
+
90
+ const { html, assets, ...restProperties } = widgetProperties;
91
+
92
+ res.status(200).send(
93
+ playgroundTemplate({
94
+ widgetProperties: restProperties,
95
+ assets,
96
+ merkurConfig,
97
+ devClient,
98
+ html,
99
+ }),
100
+ );
101
+ }),
102
+ )
103
+ .use(staticPath, express.static(staticFolder))
104
+ .use((req, res) => {
105
+ const key = req.path;
106
+ const record = context.memory[key];
107
+
108
+ if (key.endsWith('.js')) {
109
+ res.type('js');
110
+ }
111
+
112
+ if (key.endsWith('.css')) {
113
+ res.type('css');
114
+ }
115
+
116
+ res.status(record ? 200 : 404).send(record?.text);
117
+ })
118
+ .use((error, req, res, next) => {
119
+ if (res.headersSent) {
120
+ return next(error);
121
+ }
122
+
123
+ logger.error(error);
124
+ res.status(500).json({
125
+ error: {
126
+ message: `Something is wrong with the @merkur/cli/devServer: ${error.message}`,
127
+ stack: error.stack,
128
+ },
129
+ });
130
+ })
131
+ .listen(port, () => {
132
+ logger.info(`Playground: ${chalk.green(`${protocol}//${host}`)}`);
133
+ resolve(app);
134
+ });
135
+
136
+ context.server.devServer = server;
137
+
138
+ server.on('error', (error) => {
139
+ reject(error);
140
+ });
141
+ });
142
+ }
@@ -0,0 +1,13 @@
1
+ import { Emitter, RESULT_KEY } from '@esmj/emitter';
2
+
3
+ const emitter = new Emitter();
4
+
5
+ const EMITTER_EVENTS = {
6
+ MERKUR_CONFIG: 'onMerkurConfig',
7
+ CLI_CONFIG: 'onCliConfig',
8
+ CLI_CONTEXT: 'onCliContext',
9
+ TASK_CONFIG: 'onTaskConfig',
10
+ TASK_BUILD: 'onTaskBuild',
11
+ };
12
+
13
+ export { emitter, EMITTER_EVENTS, RESULT_KEY };
@@ -0,0 +1,33 @@
1
+ import process from 'node:process';
2
+
3
+ export async function handleExit({ context }) {
4
+ const handleExit = async () => {
5
+ Object.values(context.process).forEach((childProcess) => {
6
+ childProcess.kill('SIGTERM');
7
+ });
8
+
9
+ Object.values(context.task).forEach((task) => {
10
+ task.dispose();
11
+ });
12
+
13
+ await Promise.all(
14
+ Object.values(context.server).map((server) => {
15
+ return new Promise((resolve) => {
16
+ const timer = setTimeout(() => {
17
+ resolve();
18
+ }, 100);
19
+ server.close(() => {
20
+ clearTimeout(timer);
21
+ resolve();
22
+ });
23
+ });
24
+ }),
25
+ );
26
+
27
+ process.exit(0);
28
+ };
29
+
30
+ process.on('SIGINT', handleExit);
31
+ process.on('SIGQUIT', handleExit);
32
+ process.on('SIGTERM', handleExit);
33
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,62 @@
1
+ import process from 'node:process';
2
+
3
+ import { createClient } from './websocket.mjs';
4
+
5
+ let cliConfig, merkurConfig;
6
+
7
+ export function resolveConfig() {
8
+ if (process.env.CLI_CONFIG && !cliConfig) {
9
+ cliConfig = JSON.parse(process.env.CLI_CONFIG);
10
+ }
11
+
12
+ if (process.env.MERKUR_CONFIG && !merkurConfig) {
13
+ merkurConfig = JSON.parse(process.env.MERKUR_CONFIG);
14
+ }
15
+
16
+ return {
17
+ merkurConfig,
18
+ cliConfig,
19
+ };
20
+ }
21
+
22
+ export function autoReload({ merkurConfig, cliConfig }) {
23
+ function reload() {
24
+ const client = createClient({ merkurConfig });
25
+ client.on('error', (error) => {
26
+ console.error(error);
27
+ client.terminate();
28
+ });
29
+
30
+ client.on('open', function open() {
31
+ setTimeout(() => {
32
+ client.send(
33
+ JSON.stringify({
34
+ to: 'browser',
35
+ command: 'reload',
36
+ changed: [],
37
+ errors: [],
38
+ }),
39
+ );
40
+ client.terminate();
41
+ }, 50);
42
+ });
43
+ }
44
+
45
+ if (cliConfig.watch) {
46
+ reload();
47
+
48
+ const handleExit = () => process.exit(0);
49
+ process.on('SIGINT', handleExit);
50
+ process.on('SIGQUIT', handleExit);
51
+ process.on('SIGTERM', handleExit);
52
+
53
+ // TODO improve for error-overlay
54
+ process.on('uncaughtException', () => {
55
+ reload();
56
+ });
57
+
58
+ process.on('unhandledRejection', () => {
59
+ reload();
60
+ });
61
+ }
62
+ }