@corva/create-app 0.126.1 → 0.128.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/README.md CHANGED
@@ -190,11 +190,47 @@ Options:
190
190
  --silent [boolean] Only log result of the operation (default: false)
191
191
  --remove-on-success App package (.zip) will not be deleted after upload (default: true)
192
192
  --remove-existing [boolean] If package.json version is already taken - remove the previously published package and upload a new one (default: false)
193
+ --zip-file-name <string> Upload an existing ZIP without recreating it or bumping its version
194
+ --prebuilt Skip the Corva build for an existing ZIP (requires --zip-file-name)
193
195
  --author [string] Author name for the audit
194
196
  ```
195
197
 
196
198
  ### Examples
197
199
 
200
+ #### Release a locally built package
201
+
202
+ Build the app and install its dependencies in your own CI, then upload the prepared ZIP:
203
+
204
+ ```sh
205
+ create-corva-app release . --zip-file-name app-prebuilt.zip --prebuilt \
206
+ --env production --api-key "$CORVA_API_KEY" --label BETA --notes "Prebuilt release"
207
+ ```
208
+
209
+ The CLI uploads the ZIP unchanged, waits for deployment, and publishes it with the requested label
210
+ and notes. It does not build, install dependencies, bump versions, or delete the supplied ZIP.
211
+ Without `--prebuilt`, Corva uses its normal build process, including when `--zip-file-name` is supplied.
212
+
213
+ Keep `manifest.json` in the project directory, and `package.json` for JavaScript apps: the CLI uses
214
+ these files to identify the app and, with `--remove-existing`, its version. They must match the ZIP.
215
+ Set the final app key and version **before** compiling an FE app; the bundle embeds this identity.
216
+ With `--prebuilt`, the CLI checks the ZIP structure and relevant metadata before contacting Corva.
217
+ It checks root manifest/app metadata, FE `app.js`, JavaScript package versions, and Python entrypoint and
218
+ installed-dependency layout. Duplicate and unsafe archive paths are rejected. Metadata files are
219
+ limited to 1 MiB each. It does not rewrite the ZIP or verify imports, embedded FE identity, or native
220
+ dependency compatibility; deployment and runtime testing are still needed.
221
+
222
+ ZIP entries must be at the archive root, without an enclosing project folder:
223
+
224
+ - **FE:** `manifest.json`, `package.json`, and the contents of `dist/` (including `app.js`, chunks,
225
+ and any required assets). Do not include `dist/` itself, source code, `node_modules/`, or an offline mirror.
226
+ - **Python BE:** `manifest.json`, application code at the manifest's entrypoint path,
227
+ `requirements.txt`, and installed runtime dependencies under `requirements/`.
228
+ Build dependencies for Linux and the Python version and CPU architecture declared in the manifest;
229
+ macOS or Windows dependencies may not run on Lambda. Include all runtime dependencies because
230
+ Corva skips dependency installation.
231
+
232
+ Prebuilt uploads still use the platform's upload and runtime limits; this option does not increase them.
233
+
198
234
  #### Make a release with ask for version
199
235
 
200
236
  ```sh
@@ -358,7 +358,12 @@ function addPackageJSON(root, manifest, runtime, opts) {
358
358
  },
359
359
  scripts,
360
360
  dependencies,
361
- devDependencies,
361
+ // the app's scripts run the CLI that generated it, from its own node_modules, so a global
362
+ // install of another version cannot change what `yarn zip` or `yarn release` do
363
+ devDependencies: {
364
+ '@corva/create-app': fs.readJSONSync(join(__dirname, '../../package.json')).version,
365
+ ...devDependencies,
366
+ },
362
367
  ...rest,
363
368
  };
364
369
 
@@ -38,8 +38,13 @@ export const releaseCommand = new Command('release')
38
38
  ).default(false),
39
39
  )
40
40
  .addOption(zipFileNameOption)
41
+ .addOption(new Option('--prebuilt', 'Skip the Corva build for an existing ZIP (requires --zip-file-name)'))
41
42
  .addOption(new Option('--author [string]', 'Author name for the audit'))
42
43
  .action(async (dirName, patterns, options) => {
44
+ if (options.prebuilt && !options.zipFileName) {
45
+ releaseCommand.error('--prebuilt requires --zip-file-name');
46
+ }
47
+
43
48
  // if author is present in CLI, save it to process.env
44
49
  if (options.author) {
45
50
  process.env.githubUsername = options.author;
@@ -0,0 +1,139 @@
1
+ import { posix } from 'node:path';
2
+ import yauzl from 'yauzl';
3
+ import { StepError } from './step-error.js';
4
+
5
+ export async function validatePrebuiltZip(zipPath, { manifest, pkg, options }) {
6
+ let zip;
7
+
8
+ try {
9
+ zip = await yauzl.openPromise(zipPath, { autoClose: false, strictFileNames: true });
10
+
11
+ const entries = new Map();
12
+
13
+ for await (const entry of zip.eachEntry()) {
14
+ const name = posix.normalize(entry.fileName);
15
+
16
+ if (entries.has(name)) {
17
+ throw new Error(`Duplicate ZIP entry: ${name}`);
18
+ }
19
+
20
+ entries.set(name, entry);
21
+ }
22
+
23
+ const requireFile = (name) => {
24
+ const entry = entries.get(name);
25
+
26
+ if (!entry || entry.fileName.endsWith('/')) {
27
+ const nested = [...entries.keys()].find((path) => path.endsWith(`/${name}`));
28
+
29
+ throw new Error(
30
+ `${name} is missing at the ZIP root.${
31
+ nested ? ` Found ${nested}; ZIP the contents of the folder, not the folder itself.` : ''
32
+ }`,
33
+ );
34
+ }
35
+
36
+ return entry;
37
+ };
38
+
39
+ const readText = async (name) => {
40
+ const entry = requireFile(name);
41
+
42
+ if (entry.uncompressedSize > 1024 * 1024) {
43
+ throw new Error(`${name} exceeds the 1 MiB metadata limit`);
44
+ }
45
+
46
+ const stream = await zip.openReadStreamPromise(entry);
47
+ const chunks = [];
48
+
49
+ for await (const chunk of stream) {
50
+ chunks.push(chunk);
51
+ }
52
+
53
+ return Buffer.concat(chunks).toString('utf8');
54
+ };
55
+
56
+ const readJson = async (name) => {
57
+ try {
58
+ const value = JSON.parse(await readText(name));
59
+
60
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
61
+ throw new Error('expected a JSON object');
62
+ }
63
+
64
+ return value;
65
+ } catch (error) {
66
+ throw new Error(`${name}: ${error.message}`);
67
+ }
68
+ };
69
+
70
+ const archivedManifest = await readJson('manifest.json');
71
+ const expected = manifest.manifest;
72
+
73
+ for (const [field, actual, required] of [
74
+ ['application.key', archivedManifest.application?.key, expected.application.key],
75
+ ['application.type', archivedManifest.application?.type, expected.application.type],
76
+ ['settings.runtime', archivedManifest.settings?.runtime, expected.settings.runtime],
77
+ ]) {
78
+ if (!required || actual !== required) {
79
+ throw new Error(`manifest.json ${field} must match the release project (${required})`);
80
+ }
81
+ }
82
+
83
+ if (options.appKey && options.appKey !== archivedManifest.application.key) {
84
+ throw new Error('manifest.json application.key must match --app-key');
85
+ }
86
+
87
+ if (manifest.isUi()) {
88
+ if (!requireFile('app.js').uncompressedSize) {
89
+ throw new Error('app.js must not be empty');
90
+ }
91
+ }
92
+
93
+ if (manifest.isJs()) {
94
+ const archivedPackage = await readJson('package.json');
95
+
96
+ if (!pkg.version || archivedPackage.version !== pkg.version) {
97
+ throw new Error(`package.json version must match the release project (${pkg.version})`);
98
+ }
99
+ } else if (manifest.isPython()) {
100
+ const settings = archivedManifest.settings;
101
+
102
+ for (const field of ['file', 'function']) {
103
+ if (
104
+ typeof settings.entrypoint?.[field] !== 'string' ||
105
+ !settings.entrypoint[field] ||
106
+ settings.entrypoint[field] !== expected.settings.entrypoint?.[field]
107
+ ) {
108
+ throw new Error(`manifest.json settings.entrypoint.${field} must match the release project`);
109
+ }
110
+ }
111
+
112
+ if (expected.settings.cpu_architecture && settings.cpu_architecture !== expected.settings.cpu_architecture) {
113
+ throw new Error('manifest.json settings.cpu_architecture must match the release project');
114
+ }
115
+
116
+ const modulePath = settings.entrypoint.file.replaceAll('.', '/');
117
+
118
+ if (!entries.has(`${modulePath}.py`) && !entries.has(`${modulePath}/__init__.py`)) {
119
+ throw new Error(`Python entrypoint is missing: expected ${modulePath}.py or ${modulePath}/__init__.py`);
120
+ }
121
+
122
+ const requirements = await readText('requirements.txt');
123
+ const hasDependencies = requirements.split(/\r?\n/).some((line) => line.trim() && !line.trim().startsWith('#'));
124
+
125
+ if (
126
+ hasDependencies &&
127
+ ![...entries].some(
128
+ ([name, entry]) => name.startsWith('requirements/') && !name.endsWith('/') && entry.uncompressedSize,
129
+ )
130
+ ) {
131
+ throw new Error('requirements.txt declares dependencies, but requirements/ contains no installed dependencies');
132
+ }
133
+ }
134
+ } catch (error) {
135
+ throw new StepError(`Invalid prebuilt archive: ${error.message}`, { cause: error });
136
+ } finally {
137
+ zip?.close();
138
+ }
139
+ }
@@ -14,11 +14,11 @@ export const RELEASE_FLOW = {
14
14
  name: 'release',
15
15
  steps: [
16
16
  PREPARE_FLOW,
17
- SETUP_API_CLIENT_STEP,
18
- RELEASE_PREPARE_DATA_STEP,
19
17
  // With --zip-file-name the archive already exists, so the zip flow is skipped entirely. That
20
18
  // also lets runtimes the zip flow has no file list for (e.g. `provided.*`) be released.
21
19
  RESOLVE_PREBUILT_ZIP_STEP,
20
+ SETUP_API_CLIENT_STEP,
21
+ RELEASE_PREPARE_DATA_STEP,
22
22
  ZIP_SIMPLE_FLOW,
23
23
  UPLOAD_ZIP_TO_CORVA_STEP,
24
24
  WAIT_FOR_BUILD_FINISH_STEP,
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  import { StepError } from '../../lib/step-error.js';
5
+ import { validatePrebuiltZip } from '../../lib/validate-prebuilt-zip.js';
5
6
 
6
7
  /**
7
8
  * Handles `--zip-file-name`: the caller has already built the package archive, so this records it
@@ -9,7 +10,7 @@ import { StepError } from '../../lib/step-error.js';
9
10
  */
10
11
  export const RESOLVE_PREBUILT_ZIP_STEP = {
11
12
  message: 'Resolving prebuilt archive...',
12
- fn: async ({ dirName, options }) => {
13
+ fn: async ({ dirName, options, manifest, pkg }) => {
13
14
  const { zipFileName } = options;
14
15
 
15
16
  if (!zipFileName) {
@@ -24,6 +25,10 @@ export const RESOLVE_PREBUILT_ZIP_STEP = {
24
25
  throw new StepError(`Prebuilt archive ${zipFileName} not found in ${dirName}`);
25
26
  }
26
27
 
28
+ if (options.prebuilt) {
29
+ await validatePrebuiltZip(zipPath, { manifest, pkg, options });
30
+ }
31
+
27
32
  return {
28
33
  zipFileName,
29
34
  skipZip: true,
@@ -102,6 +102,10 @@ export const UPLOAD_ZIP_TO_CORVA_STEP = {
102
102
 
103
103
  form.append('package', createReadStream(resolve(dirName, zipFileName)), 'package.zip');
104
104
 
105
+ if (options.prebuilt) {
106
+ form.append('prebuilt', 'true');
107
+ }
108
+
105
109
  let githubUsername = getGithubUsernameActor();
106
110
 
107
111
  if (!githubUsername) {
@@ -1,17 +1,14 @@
1
1
  import chalk from 'chalk';
2
- import Glob from 'glob';
2
+ import { glob } from 'glob';
3
3
  import isGlob from 'is-glob';
4
4
  import _ from 'lodash/fp.js';
5
5
  import { promises as fs } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
- import { promisify } from 'node:util';
8
7
  import { getIncreasedVersion } from '../../helpers/cli-version.js';
9
8
  import { generateRandomString } from '../../helpers/utils.js';
10
9
  import { loadJson } from '../lib/json.js';
11
10
  import { StepError } from '../lib/step-error.js';
12
11
 
13
- const glob = promisify(Glob);
14
-
15
12
  import debugFn from 'debug';
16
13
 
17
14
  const debug = debugFn('cca:flow:zip:resolve');
@@ -1,4 +1,3 @@
1
- import NpmApi from 'npm-api';
2
1
  import chalk from 'chalk';
3
2
  import semver from 'semver';
4
3
  import inquirer from 'inquirer';
@@ -9,8 +8,6 @@ import fs from 'fs-extra';
9
8
  import { logger } from './logger.js';
10
9
  import { StepError } from '../flows/lib/step-error.js';
11
10
 
12
- const npm = new NpmApi();
13
-
14
11
  const error = chalk.bold.red;
15
12
  const warning = chalk.hex('#FFA500'); // Orange
16
13
  const code = chalk.cyan;
@@ -19,20 +16,32 @@ const asterisks = '*************************************************************
19
16
  const getCurrentVersion = async () =>
20
17
  (await fs.readJSON(join(url.fileURLToPath(new URL('.', import.meta.url)), '../../package.json'))).version;
21
18
 
19
+ // An app that lists @corva/create-app owns the version its scripts run, so there is nothing for the
20
+ // user to update globally; the nag belongs to a global install only.
21
+ const isCliPinnedByApp = async (appDirectory) => {
22
+ const packageJsonPath = join(appDirectory, 'package.json');
23
+
24
+ if (!(await fs.pathExists(packageJsonPath))) {
25
+ return false;
26
+ }
27
+
28
+ const { dependencies = {}, devDependencies = {} } = await fs.readJSON(packageJsonPath);
29
+
30
+ return Boolean(dependencies['@corva/create-app'] || devDependencies['@corva/create-app']);
31
+ };
32
+
22
33
  const getLatestVersion = async () => {
23
34
  try {
24
- return await npm.repo('@corva/create-app').prop('version');
35
+ const response = await fetch('https://registry.npmjs.org/@corva/create-app/latest');
36
+
37
+ return (await response.json()).version;
25
38
  } catch (caughtError) {
26
- if (caughtError.name === 'FetchError') {
27
- throw new StepError(`The request to check the latest available version has failed due to the network error. Details:
39
+ throw new StepError(`The request to check the latest available version has failed due to the network error. Details:
28
40
 
29
41
  ${caughtError}
30
42
 
31
43
  Please check your internet connection and that the above request is not blocked in your network
32
44
  `);
33
- }
34
-
35
- throw caughtError;
36
45
  }
37
46
  };
38
47
 
@@ -71,7 +80,11 @@ export async function ensureLatestVersion() {
71
80
  }
72
81
 
73
82
  // NOTE: Show user-friendly warning if version is outdated
74
- export async function warnIfOutdated() {
83
+ export async function warnIfOutdated(appDirectory = process.cwd()) {
84
+ if (await isCliPinnedByApp(appDirectory)) {
85
+ return;
86
+ }
87
+
75
88
  logger.write('Checking for updates...\n');
76
89
 
77
90
  const currentVersion = await getCurrentVersion();
package/lib/main.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolve } from 'node:path';
2
+
1
3
  import chalk from 'chalk';
2
4
  import { Command } from 'commander';
3
5
  import semver from 'semver';
@@ -35,10 +37,13 @@ function checkNodeVersion() {
35
37
 
36
38
  export async function run() {
37
39
  const program = new Command('create-corva-app')
38
- .hook('preAction', async () => {
40
+ .hook('preAction', async (_, actionCommand) => {
39
41
  checkNodeVersion();
40
42
 
41
- await warnIfOutdated();
43
+ const [projectDirectory = '.'] = actionCommand.args;
44
+ const { originalCwd = process.cwd() } = actionCommand.opts();
45
+
46
+ await warnIfOutdated(resolve(originalCwd, projectDirectory));
42
47
  })
43
48
  .addCommand(createCommand, { isDefault: true })
44
49
  .addCommand(zipCommand)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corva/create-app",
3
- "version": "0.126.1",
3
+ "version": "0.128.0",
4
4
  "private": false,
5
5
  "description": "Create an app to use it in CORVA.AI",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "lodash": "4.18.1"
46
46
  },
47
47
  "dependencies": {
48
- "archiver": "^5.3.0",
48
+ "archiver": "^7.0.1",
49
49
  "chalk": "^4.1.0",
50
50
  "commander": "^9.1.0",
51
51
  "cross-spawn": "^7.0.3",
@@ -54,14 +54,14 @@
54
54
  "figlet": "^1.5.0",
55
55
  "form-data": "^4.0.4",
56
56
  "fs-extra": "^9.0.1",
57
- "glob": "^8.0.1",
57
+ "glob": "^13.0.6",
58
58
  "got": "^12.5.1",
59
59
  "inquirer": "^8.2.3",
60
60
  "is-glob": "^4.0.3",
61
61
  "lodash": "^4.18.1",
62
- "npm-api": "^1.0.0",
63
62
  "semver": "^7.3.2",
64
- "terminal-link": "^3.0.0"
63
+ "terminal-link": "^3.0.0",
64
+ "yauzl": "^3.4.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@babel/core": "^7.11.0",