@appsemble/cli 0.23.4 → 0.23.6

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.
@@ -1,8 +1,108 @@
1
- import { relative } from 'node:path';
2
- import { AppsembleError, logger } from '@appsemble/node-utils';
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { inspect } from 'node:util';
5
+ import { AppsembleError, getWorkspaces, logger, readData } from '@appsemble/node-utils';
6
+ import { prefixBlockURL } from '@appsemble/utils';
7
+ import chalk from 'chalk';
3
8
  import { parse } from 'comment-parser';
9
+ import { cosmiconfig } from 'cosmiconfig';
4
10
  import normalizePath from 'normalize-path';
5
11
  import { createFormatter, createParser, SchemaGenerator, ts } from 'ts-json-schema-generator';
12
+ /**
13
+ * Get the build configuration from a project directory.
14
+ *
15
+ * @param dir The directory in which to search for the configuration file.
16
+ * @returns The configuration.
17
+ */
18
+ export async function getProjectBuildConfig(dir) {
19
+ const explorer = cosmiconfig('appsemble', { stopDir: dir });
20
+ const found = await explorer.search(dir);
21
+ let foundInParent;
22
+ if (!found) {
23
+ foundInParent = await explorer.search(dirname(dir));
24
+ if (!foundInParent) {
25
+ throw new AppsembleError(`No Appsemble configuration file found searching ${dir}`);
26
+ }
27
+ }
28
+ const { config, filepath } = found || foundInParent;
29
+ logger.info(`Found configuration file: ${filepath}`);
30
+ const [pkg] = await readData(join(dir, 'package.json'));
31
+ if (!pkg.private) {
32
+ logger.warn(`It is ${chalk.underline.yellow('highly recommended')} to set “${chalk.green('"private"')}: ${chalk.cyan('true')}” in package.json`);
33
+ }
34
+ let longDescription;
35
+ if (existsSync(join(dir, 'README.md'))) {
36
+ longDescription = await readFile(join(dir, 'README.md'), 'utf8');
37
+ }
38
+ const result = {
39
+ description: pkg.description,
40
+ longDescription,
41
+ name: pkg.name,
42
+ version: pkg.version,
43
+ dir,
44
+ ...config,
45
+ };
46
+ logger.verbose(`Resolved project configuration: ${inspect(result, { colors: true })}`);
47
+ return result;
48
+ }
49
+ /**
50
+ * Discover Appsemble projects based on workspaces in a monorepo.
51
+ *
52
+ * Both Lerna and Yarn workspaces are supported.
53
+ *
54
+ * @param root The project root in which to find workspaces.
55
+ * @returns Discovered Appsemble projects.
56
+ */
57
+ export async function getProjectsBuildConfigs(root) {
58
+ const dirs = await getWorkspaces(root);
59
+ const projectBuildConfigs = await Promise.all(dirs
60
+ .concat(root)
61
+ .map((path) => getProjectBuildConfig(path))
62
+ // Ignore non-project workspaces.
63
+ .map((p) => p.catch(() => null)));
64
+ return projectBuildConfigs.filter(Boolean);
65
+ }
66
+ /**
67
+ * Get the TypeScript program for a given path.
68
+ *
69
+ * @param path The path for which to get the TypeScript program.
70
+ * @returns The TypeScript program.
71
+ */
72
+ function getProgram(path) {
73
+ const diagnosticHost = {
74
+ getNewLine: () => ts.sys.newLine,
75
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
76
+ getCanonicalFileName: (x) => x,
77
+ };
78
+ const tsConfigPath = ts.findConfigFile(path, ts.sys.fileExists);
79
+ const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
80
+ if (error) {
81
+ throw new AppsembleError(ts.formatDiagnostic(error, diagnosticHost));
82
+ }
83
+ if (!config.files || !config.include) {
84
+ config.files = ts.sys.readDirectory(path, ['.ts', '.tsx']).map((f) => relative(path, f));
85
+ }
86
+ const { errors, fileNames, options } = ts.parseJsonConfigFileContent(config, ts.sys, path, undefined, tsConfigPath);
87
+ // Filter: 'rootDir' is expected to contain all source files.
88
+ const diagnostics = errors.filter(({ code }) => code !== 6059);
89
+ if (diagnostics.length) {
90
+ throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost));
91
+ }
92
+ options.noEmit = true;
93
+ delete options.out;
94
+ delete options.outDir;
95
+ delete options.outFile;
96
+ delete options.declaration;
97
+ delete options.declarationDir;
98
+ delete options.declarationMap;
99
+ const program = ts.createProgram(fileNames, options);
100
+ const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
101
+ if (preEmitDiagnostics.length) {
102
+ throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(preEmitDiagnostics, diagnosticHost));
103
+ }
104
+ return program;
105
+ }
6
106
  /**
7
107
  * Get the tsdoc comment for a TypeScript node.
8
108
  *
@@ -104,7 +204,7 @@ function processEvents(eventListenerInterface, eventEmitterInterface, checker) {
104
204
  *
105
205
  * @param program The TypeScript program from which to extract parameters.
106
206
  * @param iface The interface node from which to extract parameters.
107
- * @returns The JSON schema for the block parameters.
207
+ * @returns The JSON schema for the project parameters.
108
208
  */
109
209
  function processParameters(program, iface) {
110
210
  if (!iface) {
@@ -128,7 +228,7 @@ function processParameters(program, iface) {
128
228
  if (schema.definitions && !Object.keys(schema.definitions).length) {
129
229
  delete schema.definitions;
130
230
  }
131
- // This is the tsdoc that has been added to the SDK to aid the block developer.
231
+ // This is the tsdoc that has been added to the SDK to aid the developer.
132
232
  delete schema.description;
133
233
  return schema;
134
234
  }
@@ -143,67 +243,22 @@ function processMessages(iface, checker) {
143
243
  return processInterface(iface, checker, (name, description) => [name, { description }]);
144
244
  }
145
245
  /**
146
- * Get the TypeScript program for a given path.
246
+ * Generate project implementations from the project metadata and TypeScript project.
147
247
  *
148
- * @param blockPath The path for which to get the TypeScript program.
149
- * @returns The TypeScript program.
150
- */
151
- function getProgram(blockPath) {
152
- const diagnosticHost = {
153
- getNewLine: () => ts.sys.newLine,
154
- getCurrentDirectory: ts.sys.getCurrentDirectory,
155
- getCanonicalFileName: (x) => x,
156
- };
157
- const tsConfigPath = ts.findConfigFile(blockPath, ts.sys.fileExists);
158
- const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
159
- if (error) {
160
- throw new AppsembleError(ts.formatDiagnostic(error, diagnosticHost));
161
- }
162
- if (!config.files || !config.include) {
163
- config.files = ts.sys
164
- .readDirectory(blockPath, ['.ts', '.tsx'])
165
- .map((f) => relative(blockPath, f));
166
- }
167
- const { errors, fileNames, options } = ts.parseJsonConfigFileContent(config, ts.sys, blockPath, undefined, tsConfigPath);
168
- // Filter: 'rootDir' is expected to contain all source files.
169
- const diagnostics = errors.filter(({ code }) => code !== 6059);
170
- if (diagnostics.length) {
171
- throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticHost));
172
- }
173
- options.noEmit = true;
174
- delete options.out;
175
- delete options.outDir;
176
- delete options.outFile;
177
- delete options.declaration;
178
- delete options.declarationDir;
179
- delete options.declarationMap;
180
- const program = ts.createProgram(fileNames, options);
181
- const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
182
- if (preEmitDiagnostics.length) {
183
- throw new AppsembleError(ts.formatDiagnosticsWithColorAndContext(preEmitDiagnostics, diagnosticHost));
184
- }
185
- return program;
186
- }
187
- /**
188
- * Generate a block manifest from the block metadata and TypeScript project.
248
+ * Uses the .appsemblerc file and the type definitions of the project.
189
249
  *
190
- * Uses the .appsemblerc file and the type definitions of the block.
191
- *
192
- * @param blockConfig The block configuration
193
- * @returns The block configuration appended from the TypeScript project.
250
+ * @param buildConfig The project build configuration
251
+ * @returns The project implementations from the TypeScript project.
194
252
  */
195
- export function getBlockConfigFromTypeScript(blockConfig) {
196
- if ('actions' in blockConfig && 'events' in blockConfig && 'parameters' in blockConfig) {
197
- return blockConfig;
198
- }
199
- logger.info(`Extracting data from TypeScript project ${blockConfig.dir}`);
200
- const program = getProgram(blockConfig.dir);
253
+ export function getProjectImplementations(buildConfig) {
254
+ logger.info(`Extracting data from TypeScript project ${buildConfig.dir}`);
255
+ const program = getProgram(buildConfig.dir);
201
256
  const checker = program.getTypeChecker();
202
257
  let actionInterface;
203
258
  let eventEmitterInterface;
204
259
  let eventListenerInterface;
205
260
  let messagesInterface;
206
- let patametersInterface;
261
+ let parametersInterface;
207
262
  for (const sourceFile of program.getSourceFiles()) {
208
263
  const fileName = relative(process.cwd(), sourceFile.fileName);
209
264
  // Filter TypeScript default libs
@@ -254,10 +309,10 @@ export function getBlockConfigFromTypeScript(blockConfig) {
254
309
  break;
255
310
  case 'Parameters':
256
311
  logger.info(`Found augmented interface 'Parameters' in '${loc}'`);
257
- if (patametersInterface) {
312
+ if (parametersInterface) {
258
313
  throw new AppsembleError(`Found duplicate interface 'Parameters' in '${loc}'`);
259
314
  }
260
- patametersInterface = iface;
315
+ parametersInterface = iface;
261
316
  break;
262
317
  case 'Messages':
263
318
  logger.info(`Found augmented interface 'Messages' in '${loc}'`);
@@ -270,16 +325,57 @@ export function getBlockConfigFromTypeScript(blockConfig) {
270
325
  });
271
326
  }
272
327
  return {
273
- actions: 'actions' in blockConfig ? blockConfig.actions : processActions(actionInterface, checker),
274
- events: 'events' in blockConfig
275
- ? blockConfig.events
328
+ actions: 'actions' in buildConfig ? buildConfig.actions : processActions(actionInterface, checker),
329
+ events: 'events' in buildConfig
330
+ ? buildConfig.events
276
331
  : processEvents(eventListenerInterface, eventEmitterInterface, checker),
277
- parameters: 'parameters' in blockConfig
278
- ? blockConfig.parameters
279
- : processParameters(program, patametersInterface),
280
- messages: 'messages' in blockConfig
281
- ? blockConfig.messages
332
+ parameters: 'parameters' in buildConfig
333
+ ? buildConfig.parameters
334
+ : processParameters(program, parametersInterface),
335
+ messages: 'messages' in buildConfig
336
+ ? buildConfig.messages
282
337
  : processMessages(messagesInterface, checker),
283
338
  };
284
339
  }
285
- //# sourceMappingURL=getBlockConfigFromTypeScript.js.map
340
+ /**
341
+ * Load a webpack configuration file.
342
+ *
343
+ * A webpack configuration file may export either an webpack configuration object, or a synchronous
344
+ * or asynchronous function which returns a webpack configuration object. This function supports
345
+ * all 3 use cases.
346
+ *
347
+ * @param buildConfig The project build config.
348
+ * @param mode The env that would be passed to webpack by invoking `webpack --env $env`.
349
+ * @param outputPath The path where the build will be output on disk.
350
+ * @returns The webpack configuration as exposed by the webpack configuration file.
351
+ */
352
+ export async function getProjectWebpackConfig(buildConfig, mode, outputPath) {
353
+ let configPath;
354
+ if (buildConfig.webpack) {
355
+ configPath = join(buildConfig.dir, buildConfig.webpack);
356
+ }
357
+ else {
358
+ configPath = join(buildConfig.dir, 'webpack.config.js');
359
+ try {
360
+ await stat(configPath);
361
+ }
362
+ catch {
363
+ configPath = '@appsemble/webpack-config';
364
+ }
365
+ }
366
+ logger.info(`Using webpack config from ${configPath}`);
367
+ const publicPath = prefixBlockURL({ type: buildConfig.name, version: buildConfig.version }, '');
368
+ let config = await import(String(configPath));
369
+ config = await (config.default || config);
370
+ config = config instanceof Function ? await config(buildConfig, { mode, publicPath }) : config;
371
+ // Koa-webpack serves assets on the `output.path` path. Normally this field describes where to
372
+ // output the files on the file system. This is monkey patched to support usage with our dev
373
+ // server.
374
+ config.output = config.output || {};
375
+ config.output.path = outputPath || publicPath;
376
+ logger.verbose(`Patched webpack config output.path to ${config.output.path}`);
377
+ config.output.publicPath = publicPath;
378
+ logger.verbose(`Patched webpack config output.publicPath to ${config.output.publicPath}`);
379
+ return config;
380
+ }
381
+ //# sourceMappingURL=config.js.map
@@ -4,19 +4,19 @@ import { fileURLToPath } from 'node:url';
4
4
  import { AppsembleError, resolveFixture } from '@appsemble/node-utils';
5
5
  import { ts } from 'ts-json-schema-generator';
6
6
  import { beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { getBlockConfigFromTypeScript } from './getBlockConfigFromTypeScript.js';
7
+ import { getProjectImplementations } from './config.js';
8
8
  beforeEach(() => {
9
9
  vi.spyOn(process, 'cwd').mockReturnValue(resolveFixture('.'));
10
10
  });
11
- describe('getBlockConfigFromTypeScript', () => {
11
+ describe('getProjectImplementations', () => {
12
12
  it('should extract configuration from a TypeScript project', () => {
13
- const result = getBlockConfigFromTypeScript({
13
+ const result = getProjectImplementations({
14
14
  name: '',
15
15
  layout: 'float',
16
16
  version: '1.33.7',
17
17
  webpack: '',
18
18
  output: '',
19
- dir: resolveFixture('getBlockConfigFromTypeScript/valid'),
19
+ dir: resolveFixture('getProjectImplementations/valid'),
20
20
  });
21
21
  expect(result).toStrictEqual({
22
22
  actions: { testAction: { description: undefined } },
@@ -62,87 +62,87 @@ describe('getBlockConfigFromTypeScript', () => {
62
62
  version: '1.33.7',
63
63
  webpack: '',
64
64
  output: '',
65
- dir: resolveFixture('getBlockConfigFromTypeScript/valid'),
65
+ dir: resolveFixture('getProjectImplementations/valid'),
66
66
  name: '',
67
67
  };
68
- const result = getBlockConfigFromTypeScript(input);
68
+ const result = getProjectImplementations(input);
69
69
  expect(result).toBe(input);
70
70
  expect(ts.createProgram).not.toHaveBeenCalled();
71
71
  });
72
72
  it('should prefer actions overrides over TypeScript actions', () => {
73
- const result = getBlockConfigFromTypeScript({
73
+ const result = getProjectImplementations({
74
74
  actions: {},
75
75
  version: '1.33.7',
76
76
  webpack: '',
77
77
  output: '',
78
- dir: resolveFixture('getBlockConfigFromTypeScript/valid'),
78
+ dir: resolveFixture('getProjectImplementations/valid'),
79
79
  name: '',
80
80
  });
81
81
  expect(result.actions).toStrictEqual({});
82
82
  });
83
83
  it('should prefer events overrides over TypeScript events', () => {
84
- const result = getBlockConfigFromTypeScript({
84
+ const result = getProjectImplementations({
85
85
  events: { emit: { onSuccess: {} } },
86
86
  version: '1.33.7',
87
87
  webpack: '',
88
88
  output: '',
89
- dir: resolveFixture('getBlockConfigFromTypeScript/valid'),
89
+ dir: resolveFixture('getProjectImplementations/valid'),
90
90
  name: '',
91
91
  });
92
92
  expect(result.events).toStrictEqual({ emit: { onSuccess: {} } });
93
93
  });
94
94
  it('should prefer parameters overrides over TypeScript parameters', () => {
95
- const result = getBlockConfigFromTypeScript({
95
+ const result = getProjectImplementations({
96
96
  parameters: { type: 'object' },
97
97
  version: '1.33.7',
98
98
  webpack: '',
99
99
  output: '',
100
- dir: resolveFixture('getBlockConfigFromTypeScript/valid'),
100
+ dir: resolveFixture('getProjectImplementations/valid'),
101
101
  name: '',
102
102
  });
103
103
  expect(result.parameters).toStrictEqual({ type: 'object' });
104
104
  });
105
105
  it('should throw if duplicate Actions are found', () => {
106
- expect(() => getBlockConfigFromTypeScript({
106
+ expect(() => getProjectImplementations({
107
107
  webpack: '',
108
108
  output: '',
109
- dir: resolveFixture('getBlockConfigFromTypeScript/duplicateActions'),
109
+ dir: resolveFixture('getProjectImplementations/duplicateActions'),
110
110
  name: '',
111
111
  version: '1.33.7',
112
- })).toThrow(new AppsembleError("Found duplicate interface 'Actions' in 'getBlockConfigFromTypeScript/duplicateActions/index.ts:31'"));
112
+ })).toThrow(new AppsembleError("Found duplicate interface 'Actions' in 'getProjectImplementations/duplicateActions/index.ts:31'"));
113
113
  });
114
114
  it('should throw if duplicate EventEmitters are found', () => {
115
- expect(() => getBlockConfigFromTypeScript({
115
+ expect(() => getProjectImplementations({
116
116
  webpack: '',
117
117
  output: '',
118
- dir: resolveFixture('getBlockConfigFromTypeScript/duplicateEventEmitters'),
118
+ dir: resolveFixture('getProjectImplementations/duplicateEventEmitters'),
119
119
  name: '',
120
120
  version: '1.33.7',
121
- })).toThrow(new AppsembleError("Found duplicate interface 'EventEmitters' in 'getBlockConfigFromTypeScript/duplicateEventEmitters/index.ts:31'"));
121
+ })).toThrow(new AppsembleError("Found duplicate interface 'EventEmitters' in 'getProjectImplementations/duplicateEventEmitters/index.ts:31'"));
122
122
  });
123
123
  it('should throw if duplicate EventListeners are found', () => {
124
- expect(() => getBlockConfigFromTypeScript({
124
+ expect(() => getProjectImplementations({
125
125
  webpack: '',
126
126
  output: '',
127
- dir: resolveFixture('getBlockConfigFromTypeScript/duplicateEventListeners'),
127
+ dir: resolveFixture('getProjectImplementations/duplicateEventListeners'),
128
128
  name: '',
129
129
  version: '1.33.7',
130
- })).toThrow(new AppsembleError("Found duplicate interface 'EventListeners' in 'getBlockConfigFromTypeScript/duplicateEventListeners/index.ts:31'"));
130
+ })).toThrow(new AppsembleError("Found duplicate interface 'EventListeners' in 'getProjectImplementations/duplicateEventListeners/index.ts:31'"));
131
131
  });
132
132
  it('should throw if duplicate Parameters are found', () => {
133
- expect(() => getBlockConfigFromTypeScript({
133
+ expect(() => getProjectImplementations({
134
134
  webpack: '',
135
135
  output: '',
136
- dir: resolveFixture('getBlockConfigFromTypeScript/duplicateParameters'),
136
+ dir: resolveFixture('getProjectImplementations/duplicateParameters'),
137
137
  name: '',
138
138
  version: '1.33.7',
139
- })).toThrow(new AppsembleError("Found duplicate interface 'Parameters' in 'getBlockConfigFromTypeScript/duplicateParameters/index.ts:31'"));
139
+ })).toThrow(new AppsembleError("Found duplicate interface 'Parameters' in 'getProjectImplementations/duplicateParameters/index.ts:31'"));
140
140
  });
141
141
  it('should handle fontawesome icons', () => {
142
- const result = getBlockConfigFromTypeScript({
142
+ const result = getProjectImplementations({
143
143
  webpack: '',
144
144
  output: '',
145
- dir: resolveFixture('getBlockConfigFromTypeScript/fontawesomeParameters'),
145
+ dir: resolveFixture('getProjectImplementations/fontawesomeParameters'),
146
146
  name: '',
147
147
  version: '1.33.7',
148
148
  });
@@ -162,26 +162,26 @@ describe('getBlockConfigFromTypeScript', () => {
162
162
  });
163
163
  it('should handle TypeScript pre emit diagnostics', () => {
164
164
  function fn() {
165
- getBlockConfigFromTypeScript({
165
+ getProjectImplementations({
166
166
  name: '',
167
167
  layout: 'float',
168
168
  version: '1.33.7',
169
169
  webpack: '',
170
170
  output: '',
171
- dir: resolveFixture('getBlockConfigFromTypeScript/tsError'),
171
+ dir: resolveFixture('getProjectImplementations/tsError'),
172
172
  });
173
173
  }
174
174
  expect(fn).toThrow(AppsembleError);
175
175
  expect(fn).toThrow(/'unused' is declared but its value is never read/);
176
176
  });
177
177
  it('should extract comments', () => {
178
- const result = getBlockConfigFromTypeScript({
178
+ const result = getProjectImplementations({
179
179
  name: '',
180
180
  layout: 'float',
181
181
  version: '1.33.7',
182
182
  webpack: '',
183
183
  output: '',
184
- dir: resolveFixture('getBlockConfigFromTypeScript/comments'),
184
+ dir: resolveFixture('getProjectImplementations/comments'),
185
185
  });
186
186
  expect(result).toStrictEqual({
187
187
  actions: {
@@ -223,7 +223,7 @@ describe('getBlockConfigFromTypeScript', () => {
223
223
  const blocksDir = new URL('../../../blocks/', import.meta.url);
224
224
  it.each(readdirSync(blocksDir))('%s', (name) => {
225
225
  const dir = fileURLToPath(new URL(name, blocksDir));
226
- const result = getBlockConfigFromTypeScript({
226
+ const result = getProjectImplementations({
227
227
  name,
228
228
  version: '',
229
229
  output: '',
@@ -234,4 +234,4 @@ describe('getBlockConfigFromTypeScript', () => {
234
234
  });
235
235
  });
236
236
  });
237
- //# sourceMappingURL=getBlockConfigFromTypeScript.test.js.map
237
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1,12 @@
1
+ import { type ProjectBuildConfig, type ProjectImplementations } from '@appsemble/types';
2
+ import { type BuildResult } from 'esbuild';
3
+ import FormData from 'form-data';
4
+ export declare function buildProject(config: ProjectBuildConfig): Promise<BuildResult>;
5
+ /**
6
+ * Configure the payload for a new project upload.
7
+ *
8
+ * @param buildConfig The project configuration
9
+ * @returns The payload that should be sent to the endpoint.
10
+ */
11
+ export declare function makeProjectPayload(buildConfig: ProjectBuildConfig): Promise<[FormData, ProjectImplementations]>;
12
+ export declare function publishProject(buildConfig: ProjectBuildConfig, url: string, ignoreConflict?: boolean): Promise<void>;
package/lib/project.js ADDED
@@ -0,0 +1,145 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { basename, extname, join, relative, resolve as resolvePath } from 'node:path';
4
+ import { inspect } from 'node:util';
5
+ import { AppsembleError, logger, opendirSafe, readData } from '@appsemble/node-utils';
6
+ import { compareStrings } from '@appsemble/utils';
7
+ import axios from 'axios';
8
+ import { build } from 'esbuild';
9
+ import FormData from 'form-data';
10
+ import { getProjectImplementations } from './config.js';
11
+ export async function buildProject(config) {
12
+ const buildOptions = {
13
+ entryPoints: ['index.ts'],
14
+ bundle: true,
15
+ write: false,
16
+ absWorkingDir: join(config.dir),
17
+ loader: {
18
+ '.png': 'dataurl',
19
+ '.svg': 'dataurl',
20
+ },
21
+ logLevel: 'info',
22
+ minify: true,
23
+ };
24
+ logger.info(`Building ${config.name}@${config.version} 🔨`);
25
+ const buildResult = await build(buildOptions);
26
+ if (buildResult.errors.length > 0) {
27
+ throw new AppsembleError(String(buildResult.errors));
28
+ }
29
+ return buildResult;
30
+ }
31
+ /**
32
+ * Configure the payload for a new project upload.
33
+ *
34
+ * @param buildConfig The project configuration
35
+ * @returns The payload that should be sent to the endpoint.
36
+ */
37
+ export async function makeProjectPayload(buildConfig) {
38
+ const { dir, output } = buildConfig;
39
+ const distPath = output ? resolvePath(dir, output) : undefined;
40
+ const form = new FormData();
41
+ const gatheredData = {};
42
+ const { description, layout, longDescription, name, version, visibility } = buildConfig;
43
+ const { actions, events, messages, parameters } = getProjectImplementations(buildConfig);
44
+ const files = await readdir(dir);
45
+ const icon = files.find((entry) => entry.match(/^icon\.(png|svg)$/));
46
+ function append(field, value) {
47
+ if (value) {
48
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
49
+ logger.verbose(`Using ${field}: ${inspect(value, { colors: true, depth: 20 })}`);
50
+ form.append(field, serialized);
51
+ gatheredData[field] = value;
52
+ }
53
+ else {
54
+ logger.silly(`Skipping parameter ${field}`);
55
+ }
56
+ }
57
+ if (visibility) {
58
+ append('visibility', visibility);
59
+ }
60
+ append('actions', actions);
61
+ append('description', description);
62
+ append('longDescription', longDescription);
63
+ append('events', events);
64
+ append('layout', layout);
65
+ append('name', name);
66
+ append('parameters', parameters);
67
+ append('version', version);
68
+ if (icon) {
69
+ const iconPath = join(dir, icon);
70
+ logger.info(`Using icon: ${iconPath}`);
71
+ form.append('icon', createReadStream(iconPath));
72
+ gatheredData.iconUrl = basename(iconPath, extname(iconPath));
73
+ }
74
+ if (messages) {
75
+ if (!files.includes('i18n')) {
76
+ throw new AppsembleError('This block has messages defined, but the message files could not be found. Try running extract-messages');
77
+ }
78
+ const messageKeys = Object.keys(messages).sort(compareStrings);
79
+ const messagesResult = {};
80
+ const messagesPath = join(dir, 'i18n');
81
+ const translations = (await readdir(messagesPath)).map((language) => language.toLowerCase());
82
+ if (!translations.includes('en.json')) {
83
+ throw new AppsembleError('Could not find ‘en.json’. Try running extract-messages');
84
+ }
85
+ const duplicates = translations.filter((language, index) => translations.indexOf(language) !== index);
86
+ if (duplicates.length) {
87
+ throw new AppsembleError(`Found duplicate language codes: ‘${duplicates.join('’, ')}`);
88
+ }
89
+ for (const languageFile of translations.filter((t) => t.endsWith('.json'))) {
90
+ const language = basename(languageFile, '.json');
91
+ const languagePath = join(messagesPath, languageFile);
92
+ const [m] = await readData(languagePath);
93
+ const languageKeys = Object.keys(m).sort(compareStrings);
94
+ if (languageKeys.length !== messageKeys.length ||
95
+ languageKeys.some((key) => !messageKeys.includes(key))) {
96
+ throw new AppsembleError(`‘${languagePath}’ contains mismatching message keys. Try running extract-messages`);
97
+ }
98
+ logger.info(`Including ${language} translations from ‘${languagePath}’`);
99
+ messagesResult[language] = m;
100
+ }
101
+ form.append('messages', JSON.stringify(messagesResult));
102
+ gatheredData.messages = messagesResult;
103
+ }
104
+ if (files.includes('examples')) {
105
+ await opendirSafe(join(dir, 'examples'), async (file, stat) => {
106
+ if (!stat.isFile() || !file.endsWith('.yaml')) {
107
+ throw new AppsembleError(`Expected ${file} to be a YAML file`);
108
+ }
109
+ logger.info(`Adding example file ${file}`);
110
+ form.append('examples', await readFile(file, 'utf8'));
111
+ gatheredData.examples = [...(gatheredData.examples || []), basename(file, extname(file))];
112
+ }, { allowMissing: true });
113
+ }
114
+ if (distPath) {
115
+ await opendirSafe(distPath, (fullpath, stat) => {
116
+ if (!stat.isFile()) {
117
+ return;
118
+ }
119
+ const relativePath = relative(distPath, fullpath);
120
+ const realPath = relative(process.cwd(), fullpath);
121
+ logger.info(`Adding file: “${realPath}” as “${relativePath}”`);
122
+ form.append('files', createReadStream(fullpath), {
123
+ filename: encodeURIComponent(relativePath),
124
+ });
125
+ gatheredData.files = [...(gatheredData.files || []), relativePath];
126
+ }, { recursive: true });
127
+ }
128
+ return [form, gatheredData];
129
+ }
130
+ export async function publishProject(buildConfig, url, ignoreConflict) {
131
+ logger.info(`Publishing ${buildConfig.name}@${buildConfig.version}…`);
132
+ const [form] = await makeProjectPayload(buildConfig);
133
+ try {
134
+ await axios.post(url, form);
135
+ logger.info(`Successfully published ${buildConfig.name}@${buildConfig.version} 🎉`);
136
+ }
137
+ catch (err) {
138
+ if (ignoreConflict && axios.isAxiosError(err) && err.response.status === 409) {
139
+ logger.warn(`${buildConfig.name}@${buildConfig.version} was already published.`);
140
+ return;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ //# sourceMappingURL=project.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/cli",
3
- "version": "0.23.4",
3
+ "version": "0.23.6",
4
4
  "description": "The CLI for developing with Appsemble apps and blocks",
5
5
  "keywords": [
6
6
  "app",
@@ -42,9 +42,9 @@
42
42
  "test": "vitest"
43
43
  },
44
44
  "dependencies": {
45
- "@appsemble/node-utils": "0.23.4",
46
- "@appsemble/types": "0.23.4",
47
- "@appsemble/utils": "0.23.4",
45
+ "@appsemble/node-utils": "0.23.6",
46
+ "@appsemble/types": "0.23.6",
47
+ "@appsemble/utils": "0.23.6",
48
48
  "@fortawesome/fontawesome-common-types": "^6.0.0",
49
49
  "@koa/cors": "^4.0.0",
50
50
  "@odata/parser": "^0.2.0",
@@ -53,6 +53,7 @@
53
53
  "comment-parser": "^1.0.0",
54
54
  "cosmiconfig": "^7.0.0",
55
55
  "csvtojson": "^2.0.0",
56
+ "esbuild": "^0.19.0",
56
57
  "fast-glob": "^3.0.0",
57
58
  "form-data": "^4.0.0",
58
59
  "global-cache-dir": "^6.0.0",
@@ -85,7 +86,7 @@
85
86
  "yargs": "^17.0.0"
86
87
  },
87
88
  "devDependencies": {
88
- "@appsemble/types": "0.23.4",
89
+ "@appsemble/types": "0.23.6",
89
90
  "@types/concat-stream": "^2.0.0",
90
91
  "@types/inquirer": "^9.0.0",
91
92
  "@types/normalize-path": "^3.0.0",