@mouryaamit/nx-polystack 1.1.0 → 1.2.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 +1 -0
- package/USER-MANUAL.md +58 -4
- package/dist/src/cli/commands/app-from.d.ts +4 -0
- package/dist/src/cli/commands/app-from.js +248 -0
- package/dist/src/cli/commands/app-from.js.map +1 -0
- package/dist/src/cli/commands/app.d.ts +8 -1
- package/dist/src/cli/commands/app.js +5 -1
- package/dist/src/cli/commands/app.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +5 -0
- package/dist/src/cli/commands/update.js +21 -2
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/help.js +12 -2
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/launcher.js +7 -1
- package/dist/src/cli/launcher.js.map +1 -1
- package/dist/src/cli/prompt.d.ts +17 -0
- package/dist/src/cli/prompt.js +58 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/generators/app/from.d.ts +86 -0
- package/dist/src/generators/app/from.js +757 -0
- package/dist/src/generators/app/from.js.map +1 -0
- package/dist/src/generators/app/generator.js +21 -8
- package/dist/src/generators/app/generator.js.map +1 -1
- package/dist/src/generators/app/schema.d.ts +6 -0
- package/dist/src/generators/app/schema.json +19 -2
- package/dist/src/lib/env/empty.d.ts +16 -0
- package/dist/src/lib/env/empty.js +81 -0
- package/dist/src/lib/env/empty.js.map +1 -0
- package/dist/src/lib/env-encryption/env-file.d.ts +4 -0
- package/dist/src/lib/env-encryption/env-file.js +8 -2
- package/dist/src/lib/env-encryption/env-file.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appFromReport = exports.appFrom = exports.nameForms = exports.describeBaseApp = exports.APP_ID_RE = exports.defaultAppId = exports.APP_FROM_SKIP = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const devkit_1 = require("@nx/devkit");
|
|
8
|
+
const allowlist_1 = require("../../lib/csp/allowlist");
|
|
9
|
+
const render_1 = require("../../lib/csp/render");
|
|
10
|
+
const empty_1 = require("../../lib/env/empty");
|
|
11
|
+
const plan_1 = require("../../lib/release/plan");
|
|
12
|
+
const angular_project_1 = require("../../lib/utils/angular-project");
|
|
13
|
+
const app_dir_1 = require("../../lib/utils/app-dir");
|
|
14
|
+
const base_href_1 = require("../../lib/utils/base-href");
|
|
15
|
+
const devkit_deps_1 = require("../../lib/utils/devkit-deps");
|
|
16
|
+
const llm_block_1 = require("../../lib/utils/llm-block");
|
|
17
|
+
const npm_scripts_1 = require("../../lib/utils/npm-scripts");
|
|
18
|
+
const project_json_files_1 = require("../../lib/utils/project-json-files");
|
|
19
|
+
const serve_port_1 = require("../../lib/utils/serve-port");
|
|
20
|
+
const copy_1 = require("../migrate/copy");
|
|
21
|
+
/** Not copied from the base app (on top of what git ignores). */
|
|
22
|
+
exports.APP_FROM_SKIP = {
|
|
23
|
+
anyDepth: ['node_modules', '.git', '.dart_tool', '.angular', '.nx', '.gradle'],
|
|
24
|
+
topLevel: ['dist', 'www', 'build', 'coverage', 'out-tsc', 'tmp', '.idea', '.vscode', '.packages'],
|
|
25
|
+
topLevelPatterns: [/^\.flutter-plugins/],
|
|
26
|
+
paths: [
|
|
27
|
+
'ios/Pods',
|
|
28
|
+
'ios/.symlinks',
|
|
29
|
+
'ios/build',
|
|
30
|
+
'ios/Flutter/Generated.xcconfig',
|
|
31
|
+
'ios/Flutter/flutter_export_environment.sh',
|
|
32
|
+
'macos/Pods',
|
|
33
|
+
'android/build',
|
|
34
|
+
'android/app/build',
|
|
35
|
+
'android/local.properties',
|
|
36
|
+
'docs/migrations',
|
|
37
|
+
],
|
|
38
|
+
pathPatterns: [/(^|\/)flutter\/ephemeral$/i],
|
|
39
|
+
};
|
|
40
|
+
const FLUTTER_EXECUTOR = plan_1.FLUTTER_BUILD_EXECUTOR;
|
|
41
|
+
const DEV_NAMES = ['dev', 'development'];
|
|
42
|
+
/** com.example.<name, alphanumerics only>; prefixed with "app" when it would start with a digit. */
|
|
43
|
+
function defaultAppId(name) {
|
|
44
|
+
let segment = name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
45
|
+
if (!/^[a-z]/.test(segment))
|
|
46
|
+
segment = `app${segment}`;
|
|
47
|
+
return `com.example.${segment}`;
|
|
48
|
+
}
|
|
49
|
+
exports.defaultAppId = defaultAppId;
|
|
50
|
+
exports.APP_ID_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
|
|
51
|
+
const CAPACITOR_APP_ID = /(\bappId\s*:\s*)(['"])([^'"\n]*)\2/;
|
|
52
|
+
/** The app named `from`, if pstack builds it (Angular/Ionic release target, Flutter flutter-build). */
|
|
53
|
+
function describeBaseApp(tree, from) {
|
|
54
|
+
const project = (0, project_json_files_1.scanProjects)(tree).find((p) => p.name === from);
|
|
55
|
+
if (!project)
|
|
56
|
+
throw new Error(`no project named "${from}" in this workspace.`);
|
|
57
|
+
const { root, json } = project;
|
|
58
|
+
const targets = (json['targets'] ?? {});
|
|
59
|
+
const tags = Array.isArray(json['tags']) ? json['tags'] : [];
|
|
60
|
+
const under = root.startsWith('apps/') ? root.slice('apps/'.length).split('/') : undefined;
|
|
61
|
+
const dir = under && under.length > 1 ? under.slice(0, -1).join('/') : undefined;
|
|
62
|
+
const found = (0, base_href_1.baseHrefTarget)(json);
|
|
63
|
+
const baseHref = Object.values(found?.target.configurations ?? {}).some((c) => c?.['baseHref'] !== undefined);
|
|
64
|
+
const release = Object.values(targets).find((t) => t.executor === angular_project_1.POLYSTACK_BUILD_EXECUTOR);
|
|
65
|
+
if (release) {
|
|
66
|
+
const type = release.options?.['type'] === 'ionic' ? 'ionic' : 'angular';
|
|
67
|
+
const configs = release.configurations ?? {};
|
|
68
|
+
const envs = Object.keys(configs);
|
|
69
|
+
const devEnv = envs.find((e) => String(configs[e]?.['env'] ?? '').toUpperCase() === 'DEV') ??
|
|
70
|
+
envs.find((e) => DEV_NAMES.includes(e)) ??
|
|
71
|
+
'dev';
|
|
72
|
+
let appId;
|
|
73
|
+
if (type === 'ionic' && tree.exists(`${root}/capacitor.config.ts`)) {
|
|
74
|
+
appId = CAPACITOR_APP_ID.exec(tree.read(`${root}/capacitor.config.ts`, 'utf-8') ?? '')?.[3];
|
|
75
|
+
}
|
|
76
|
+
const envProject = typeof release.options?.['envProject'] === 'string'
|
|
77
|
+
? release.options['envProject']
|
|
78
|
+
: from;
|
|
79
|
+
// The release executor reads environment.<env>.ts (DEV: environment.ts or environment.dev.ts).
|
|
80
|
+
const envFiles = {};
|
|
81
|
+
for (const e of envs) {
|
|
82
|
+
const env = String(configs[e]?.['env'] ?? e).toLowerCase();
|
|
83
|
+
envFiles[e] =
|
|
84
|
+
e === devEnv || DEV_NAMES.includes(env)
|
|
85
|
+
? ['environment.ts', 'environment.dev.ts']
|
|
86
|
+
: [`environment.${env}.ts`];
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
name: from,
|
|
90
|
+
type,
|
|
91
|
+
root,
|
|
92
|
+
dir,
|
|
93
|
+
envs: devFirst(envs.includes(devEnv) ? envs : [devEnv, ...envs], devEnv),
|
|
94
|
+
devEnv,
|
|
95
|
+
baseHref,
|
|
96
|
+
tags,
|
|
97
|
+
appId,
|
|
98
|
+
envProject,
|
|
99
|
+
envFiles,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const flutter = Object.values(targets).some((t) => t.executor === FLUTTER_EXECUTOR);
|
|
103
|
+
if (flutter && tree.exists(`${root}/env`)) {
|
|
104
|
+
const envs = tree
|
|
105
|
+
.children(`${root}/env`)
|
|
106
|
+
.filter((f) => f.endsWith('.json'))
|
|
107
|
+
.map((f) => f.slice(0, -'.json'.length));
|
|
108
|
+
const devEnv = envs.find((e) => DEV_NAMES.includes(e)) ?? 'dev';
|
|
109
|
+
return {
|
|
110
|
+
name: from,
|
|
111
|
+
type: 'flutter',
|
|
112
|
+
root,
|
|
113
|
+
dir,
|
|
114
|
+
envs: devFirst(envs.includes(devEnv) ? envs : [devEnv, ...envs], devEnv),
|
|
115
|
+
devEnv,
|
|
116
|
+
baseHref,
|
|
117
|
+
tags,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`"${from}" is not an app pstack builds: an Angular/Ionic app with a ` +
|
|
121
|
+
`${angular_project_1.POLYSTACK_BUILD_EXECUTOR} release target, or a Flutter app with flutter-build targets ` +
|
|
122
|
+
'and an env/ folder.');
|
|
123
|
+
}
|
|
124
|
+
exports.describeBaseApp = describeBaseApp;
|
|
125
|
+
function devFirst(envs, dev) {
|
|
126
|
+
return [dev, ...envs.filter((e) => e !== dev)];
|
|
127
|
+
}
|
|
128
|
+
/** kebab, snake, camel, Pascal and Title forms of a kebab-case name (unique, longest first). */
|
|
129
|
+
function nameForms(name) {
|
|
130
|
+
const n = (0, devkit_1.names)(name);
|
|
131
|
+
const title = n.className.replace(/([A-Z])/g, ' $1').trim();
|
|
132
|
+
return [...new Set([name, name.replace(/-/g, '_'), n.propertyName, n.className, title])].sort((a, b) => b.length - a.length);
|
|
133
|
+
}
|
|
134
|
+
exports.nameForms = nameForms;
|
|
135
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
136
|
+
/** `text` with `from` replaced by `to` where it is a whole path (not part of a longer name). */
|
|
137
|
+
function replacePath(text, from, to) {
|
|
138
|
+
const re = new RegExp(`(?<![\\w.-])${escapeRe(from)}(?![\\w-])`, 'g');
|
|
139
|
+
return text.replace(re, to);
|
|
140
|
+
}
|
|
141
|
+
/** Replaces each base name form by the new one, as whole words. */
|
|
142
|
+
function replaceForms(text, base, name) {
|
|
143
|
+
const from = nameForms(base);
|
|
144
|
+
const to = new Map(from.map((f) => [f, formLike(f, base, name) ?? f]));
|
|
145
|
+
let out = text;
|
|
146
|
+
for (const f of from) {
|
|
147
|
+
const re = new RegExp(`(?<![A-Za-z0-9_-])${escapeRe(f)}(?![A-Za-z0-9_-])`, 'g');
|
|
148
|
+
out = out.replace(re, to.get(f) ?? f);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** The form of `name` matching `form` of `base` (kebab → kebab, Pascal → Pascal, …). */
|
|
153
|
+
function formLike(form, base, name) {
|
|
154
|
+
const b = (0, devkit_1.names)(base);
|
|
155
|
+
const n = (0, devkit_1.names)(name);
|
|
156
|
+
const bTitle = b.className.replace(/([A-Z])/g, ' $1').trim();
|
|
157
|
+
const nTitle = n.className.replace(/([A-Z])/g, ' $1').trim();
|
|
158
|
+
if (form === base)
|
|
159
|
+
return name;
|
|
160
|
+
if (form === base.replace(/-/g, '_'))
|
|
161
|
+
return name.replace(/-/g, '_');
|
|
162
|
+
if (form === b.propertyName)
|
|
163
|
+
return n.propertyName;
|
|
164
|
+
// A one-word base name has the same Pascal and Title form: the files renamed this way are
|
|
165
|
+
// titles and display names, so the Title form wins.
|
|
166
|
+
if (form === bTitle)
|
|
167
|
+
return nTitle;
|
|
168
|
+
if (form === b.className)
|
|
169
|
+
return n.className;
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
function isBinary(buf) {
|
|
173
|
+
const n = Math.min(buf.length, 8000);
|
|
174
|
+
for (let i = 0; i < n; i++)
|
|
175
|
+
if (buf[i] === 0)
|
|
176
|
+
return true;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
/** Files of the base app to copy, and what is left behind. */
|
|
180
|
+
function listFiles(tree, root, rules) {
|
|
181
|
+
const files = [];
|
|
182
|
+
const skipped = [];
|
|
183
|
+
const secrets = [];
|
|
184
|
+
const walk = (rel) => {
|
|
185
|
+
const dir = rel ? `${root}/${rel}` : root;
|
|
186
|
+
for (const child of [...tree.children(dir)].sort()) {
|
|
187
|
+
const r = rel ? `${rel}/${child}` : child;
|
|
188
|
+
const p = `${root}/${r}`;
|
|
189
|
+
const isFile = tree.isFile(p);
|
|
190
|
+
if ((0, copy_1.shouldSkip)(r, rules)) {
|
|
191
|
+
skipped.push(isFile ? r : `${r}/`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isFile) {
|
|
195
|
+
walk(r);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if ((0, copy_1.isPossibleSecret)(r)) {
|
|
199
|
+
secrets.push(r);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
files.push(r);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
walk('');
|
|
206
|
+
const listed = (0, copy_1.gitListedFiles)(path.join(tree.root, root));
|
|
207
|
+
let ignored;
|
|
208
|
+
if (listed) {
|
|
209
|
+
const set = new Set(listed);
|
|
210
|
+
// Files only in the tree (not on disk yet) are kept.
|
|
211
|
+
ignored = (rel) => !set.has(rel) && fs.existsSync(path.join(tree.root, root, rel));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const ig = gitignoreOf(tree, root);
|
|
215
|
+
ignored = (rel) => ig.ignores(rel);
|
|
216
|
+
}
|
|
217
|
+
const kept = [];
|
|
218
|
+
for (const f of files) {
|
|
219
|
+
if (ignored(f))
|
|
220
|
+
skipped.push(f);
|
|
221
|
+
else
|
|
222
|
+
kept.push(f);
|
|
223
|
+
}
|
|
224
|
+
return { files: kept, skipped: skipped.sort(), secrets };
|
|
225
|
+
}
|
|
226
|
+
/** The workspace and app .gitignore rules, as seen from the app folder (no git available). */
|
|
227
|
+
function gitignoreOf(tree, root) {
|
|
228
|
+
const ig = (0, devkit_deps_1.createIgnore)();
|
|
229
|
+
const rootIgnore = tree.read('.gitignore', 'utf-8');
|
|
230
|
+
if (rootIgnore) {
|
|
231
|
+
// Rules anchored at the workspace root that point into the app.
|
|
232
|
+
for (const line of rootIgnore.split(/\r?\n/)) {
|
|
233
|
+
const t = line.trim();
|
|
234
|
+
if (!t || t.startsWith('#'))
|
|
235
|
+
continue;
|
|
236
|
+
if (t.startsWith('/') || t.slice(0, -1).includes('/')) {
|
|
237
|
+
const anchored = t.replace(/^\//, '');
|
|
238
|
+
const neg = anchored.startsWith('!');
|
|
239
|
+
const body = neg ? anchored.slice(1) : anchored;
|
|
240
|
+
if (body.startsWith(`${root}/`))
|
|
241
|
+
ig.add(`${neg ? '!' : ''}/${body.slice(root.length + 1)}`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
ig.add(t);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const own = tree.read(`${root}/.gitignore`, 'utf-8');
|
|
249
|
+
if (own)
|
|
250
|
+
ig.add(own);
|
|
251
|
+
return ig;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Creates the new app from the base app: copies its files and configuration under the new name,
|
|
255
|
+
* empties its environment values and sets what must differ (port, base href, appId, env set).
|
|
256
|
+
*/
|
|
257
|
+
function appFrom(tree, o) {
|
|
258
|
+
const label = 'app --from';
|
|
259
|
+
const base = describeBaseApp(tree, o.from);
|
|
260
|
+
const { name } = o;
|
|
261
|
+
if (!app_dir_1.NAME_RE.test(name)) {
|
|
262
|
+
throw new Error(`${label}: --name="${name}" must be kebab-case (e.g. partner-portal).`);
|
|
263
|
+
}
|
|
264
|
+
if (name === base.name)
|
|
265
|
+
throw new Error(`${label}: the new app needs a name other than "${name}".`);
|
|
266
|
+
if (!base.root.includes('/')) {
|
|
267
|
+
throw new Error(`${label}: ${base.name} sits in a top-level folder (${base.root}); copy apps that live ` +
|
|
268
|
+
'under a folder such as apps/.');
|
|
269
|
+
}
|
|
270
|
+
const dir = o.dir === '.' ? undefined : (0, app_dir_1.parseDir)(o.dir ?? base.dir, label);
|
|
271
|
+
const root = (0, app_dir_1.appRoot)(name, dir);
|
|
272
|
+
if (dir)
|
|
273
|
+
(0, app_dir_1.assertDirOutsideProjects)(tree, dir, label);
|
|
274
|
+
if (tree.exists(root))
|
|
275
|
+
throw new Error(`${label}: "${root}" already exists. Pick another --name or --dir.`);
|
|
276
|
+
const projects = (0, project_json_files_1.scanProjects)(tree);
|
|
277
|
+
if (projects.some((p) => p.name === name)) {
|
|
278
|
+
throw new Error(`${label}: a project named "${name}" already exists. Pick another --name.`);
|
|
279
|
+
}
|
|
280
|
+
if (base.envProject) {
|
|
281
|
+
const owner = projects.find((p) => Object.values((p.json['targets'] ?? {})).some((t) => t.executor === angular_project_1.POLYSTACK_BUILD_EXECUTOR && t.options?.['envProject'] === name));
|
|
282
|
+
if (owner || tree.exists(`environments/${name}`)) {
|
|
283
|
+
throw new Error(`${label}: environments/${name} is already in use` +
|
|
284
|
+
`${owner ? ` (by ${owner.name})` : ''}. Pick another --name.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Port
|
|
288
|
+
const used = (0, serve_port_1.usedServePorts)(tree);
|
|
289
|
+
let port;
|
|
290
|
+
if (o.port !== undefined) {
|
|
291
|
+
const problem = (0, serve_port_1.portProblem)(o.port);
|
|
292
|
+
if (problem)
|
|
293
|
+
throw new Error(`${label}: ${problem}.`);
|
|
294
|
+
const owner = used.find((u) => u.port === o.port);
|
|
295
|
+
if (owner)
|
|
296
|
+
throw new Error(`${label}: port ${o.port} is used by ${owner.project}. Pick another --port.`);
|
|
297
|
+
port = o.port;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
port = (0, serve_port_1.nextFreePort)(used.map((u) => u.port));
|
|
301
|
+
}
|
|
302
|
+
// Environments
|
|
303
|
+
const wanted = o.envs ?? base.envs;
|
|
304
|
+
const unknown = wanted.filter((e) => !base.envs.includes(e));
|
|
305
|
+
if (unknown.length > 0) {
|
|
306
|
+
throw new Error(`${label}: ${base.name} has no environment ${unknown.join(', ')} (it has ${base.envs.join(', ')}).`);
|
|
307
|
+
}
|
|
308
|
+
const envs = base.envs.filter((e) => e === base.devEnv || wanted.includes(e));
|
|
309
|
+
const leftOut = base.envs.filter((e) => !envs.includes(e));
|
|
310
|
+
// Base href
|
|
311
|
+
let baseHref;
|
|
312
|
+
if (o.baseHref === undefined)
|
|
313
|
+
baseHref = base.baseHref ? `/${name}/` : undefined;
|
|
314
|
+
else if (o.baseHref !== null)
|
|
315
|
+
baseHref = o.baseHref;
|
|
316
|
+
if (baseHref !== undefined) {
|
|
317
|
+
try {
|
|
318
|
+
(0, base_href_1.checkBaseHref)(baseHref);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
throw new Error(`${label}: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Capacitor appId
|
|
325
|
+
let appId;
|
|
326
|
+
if (base.type === 'ionic') {
|
|
327
|
+
appId = o.appId ?? defaultAppId(name);
|
|
328
|
+
if (!exports.APP_ID_RE.test(appId)) {
|
|
329
|
+
throw new Error(`${label}: --app-id="${appId}" is not a valid Capacitor appId (reverse-DNS, e.g. com.acme.app).`);
|
|
330
|
+
}
|
|
331
|
+
if (appId === base.appId) {
|
|
332
|
+
throw new Error(`${label}: --app-id must differ from ${base.name}'s (${appId}).`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const result = {
|
|
336
|
+
base,
|
|
337
|
+
name,
|
|
338
|
+
root,
|
|
339
|
+
port,
|
|
340
|
+
baseHref,
|
|
341
|
+
appId,
|
|
342
|
+
envs,
|
|
343
|
+
leftOut,
|
|
344
|
+
copied: [],
|
|
345
|
+
envFiles: [],
|
|
346
|
+
expressions: [],
|
|
347
|
+
unkeyed: [],
|
|
348
|
+
notEmptied: [],
|
|
349
|
+
skipped: [],
|
|
350
|
+
secrets: [],
|
|
351
|
+
leftovers: [],
|
|
352
|
+
scripts: [],
|
|
353
|
+
};
|
|
354
|
+
const baseDepth = base.root.split('/').length;
|
|
355
|
+
const newDepth = root.split('/').length;
|
|
356
|
+
const oldPkg = base.type === 'flutter'
|
|
357
|
+
? /^name:\s*([A-Za-z0-9_]+)/m.exec(tree.read(`${base.root}/pubspec.yaml`, 'utf-8') ?? '')?.[1]
|
|
358
|
+
: undefined;
|
|
359
|
+
const newPkg = name.replace(/-/g, '_');
|
|
360
|
+
const rewrite = (rel, text) => {
|
|
361
|
+
let t = text;
|
|
362
|
+
if (baseDepth !== newDepth) {
|
|
363
|
+
const fileDepth = rel.split('/').length - 1;
|
|
364
|
+
const from = baseDepth + fileDepth;
|
|
365
|
+
const re = new RegExp(`(?<![\\w./])((?:\\.\\./){${from}})(?!\\.\\./)`, 'g');
|
|
366
|
+
t = t.replace(re, '../'.repeat(newDepth + fileDepth));
|
|
367
|
+
}
|
|
368
|
+
t = replacePath(t, base.root, root);
|
|
369
|
+
if (base.envProject)
|
|
370
|
+
t = replacePath(t, `environments/${base.envProject}`, `environments/${name}`);
|
|
371
|
+
return t;
|
|
372
|
+
};
|
|
373
|
+
// Ionic native projects carry the base app's appId: added again with cap-add instead.
|
|
374
|
+
const rules = base.type === 'ionic'
|
|
375
|
+
? { ...exports.APP_FROM_SKIP, topLevel: [...exports.APP_FROM_SKIP.topLevel, 'android', 'ios'] }
|
|
376
|
+
: exports.APP_FROM_SKIP;
|
|
377
|
+
const { files, skipped, secrets } = listFiles(tree, base.root, rules);
|
|
378
|
+
result.skipped = skipped;
|
|
379
|
+
result.secrets = secrets;
|
|
380
|
+
for (const rel of files) {
|
|
381
|
+
const src = `${base.root}/${rel}`;
|
|
382
|
+
const dest = `${root}/${rel}`;
|
|
383
|
+
const mode = fileMode(tree, src);
|
|
384
|
+
// Flutter env files: only the kept ones, every value emptied.
|
|
385
|
+
if (base.type === 'flutter' && /^env\/[^/]+\.json$/.test(rel)) {
|
|
386
|
+
const env = rel.slice('env/'.length, -'.json'.length);
|
|
387
|
+
if (!envs.includes(env))
|
|
388
|
+
continue;
|
|
389
|
+
let json;
|
|
390
|
+
try {
|
|
391
|
+
json = JSON.parse(tree.read(src, 'utf-8') ?? '{}');
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
throw new Error(`${label}: ${src} is not valid JSON: ${err.message}`);
|
|
395
|
+
}
|
|
396
|
+
tree.write(dest, JSON.stringify((0, empty_1.emptyJson)(json), null, 2) + '\n');
|
|
397
|
+
result.envFiles.push(dest);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const buf = tree.read(src);
|
|
401
|
+
if (!buf)
|
|
402
|
+
continue;
|
|
403
|
+
if (isBinary(buf)) {
|
|
404
|
+
tree.write(dest, buf, mode !== undefined ? { mode } : undefined);
|
|
405
|
+
result.copied.push(dest);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
let text = rewrite(rel, buf.toString('utf-8'));
|
|
409
|
+
const file = rel.split('/').pop() ?? rel;
|
|
410
|
+
if (rel === 'project.json') {
|
|
411
|
+
text = projectJson(text, base, o, result);
|
|
412
|
+
}
|
|
413
|
+
else if (rel === 'src/index.html' ||
|
|
414
|
+
rel === 'web/index.html' ||
|
|
415
|
+
rel === 'web/manifest.json') {
|
|
416
|
+
text = replaceForms(text, base.name, name);
|
|
417
|
+
}
|
|
418
|
+
else if (rel === 'capacitor.config.ts') {
|
|
419
|
+
text = text.replace(CAPACITOR_APP_ID, (_m, pre, q) => `${pre}${q}${appId}${q}`);
|
|
420
|
+
text = text.replace(/(\bappName\s*:\s*)(['"])([^'"\n]*)\2/, (m, pre, q, v) => `${pre}${q}${replaceForms(v, base.name, name)}${q}`);
|
|
421
|
+
}
|
|
422
|
+
else if (rel === 'package.json' && base.type === 'ionic') {
|
|
423
|
+
text = text.replace(/("name"\s*:\s*")([^"]*)"/, (_m, pre, v) => `${pre}${v === base.name || v.startsWith(`${base.name}-`) ? name + v.slice(base.name.length) : v}"`);
|
|
424
|
+
}
|
|
425
|
+
else if (rel === 'pubspec.yaml' && oldPkg) {
|
|
426
|
+
text = text.replace(/^name:\s*[A-Za-z0-9_]+/m, `name: ${newPkg}`);
|
|
427
|
+
}
|
|
428
|
+
else if (rel === 'csp-allowlist.json' && o.csp === 'clear') {
|
|
429
|
+
let permissionsPolicy;
|
|
430
|
+
try {
|
|
431
|
+
permissionsPolicy = (0, allowlist_1.parseAllowlist)(JSON.parse(text)).permissionsPolicy;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
permissionsPolicy = undefined;
|
|
435
|
+
}
|
|
436
|
+
const cleared = (0, allowlist_1.parseAllowlist)({});
|
|
437
|
+
text = (0, allowlist_1.serializeAllowlist)(permissionsPolicy ? { ...cleared, permissionsPolicy } : cleared);
|
|
438
|
+
}
|
|
439
|
+
if (oldPkg && file.endsWith('.dart')) {
|
|
440
|
+
text = text.replace(new RegExp(`(['"])package:${escapeRe(oldPkg)}/`, 'g'), `$1package:${newPkg}/`);
|
|
441
|
+
}
|
|
442
|
+
tree.write(dest, text, mode !== undefined ? { mode } : undefined);
|
|
443
|
+
result.copied.push(dest);
|
|
444
|
+
}
|
|
445
|
+
// Angular/Ionic env files live in environments/<envProject>/.
|
|
446
|
+
if (base.envProject)
|
|
447
|
+
copyAngularEnvFiles(tree, base, name, result);
|
|
448
|
+
// Server configs written by pstack: render them again for the new app.
|
|
449
|
+
if (base.type !== 'flutter') {
|
|
450
|
+
let allowlist;
|
|
451
|
+
try {
|
|
452
|
+
allowlist = (0, allowlist_1.parseAllowlist)(JSON.parse(tree.read(`${root}/csp-allowlist.json`, 'utf-8') ?? '{}'));
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
allowlist = undefined;
|
|
456
|
+
}
|
|
457
|
+
if (allowlist) {
|
|
458
|
+
for (const [file, render] of [
|
|
459
|
+
[`${root}/src/.htaccess`, render_1.renderHtaccess],
|
|
460
|
+
[`${root}/server/nginx.conf`, render_1.renderNginx],
|
|
461
|
+
]) {
|
|
462
|
+
if (tree.exists(file) && (tree.read(file, 'utf-8') ?? '').includes(render_1.GENERATED_MARKER)) {
|
|
463
|
+
tree.write(file, render(name, allowlist));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
copyEncryptionKeys(tree, base, name);
|
|
469
|
+
copyTsPaths(tree, base, name, root);
|
|
470
|
+
result.scripts = copyNpmScripts(tree, base, name, leftOut);
|
|
471
|
+
result.leftovers = findLeftovers(tree, base.name, [...result.copied, ...result.envFiles]);
|
|
472
|
+
(0, llm_block_1.refreshLlmBlocks)(tree);
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
exports.appFrom = appFrom;
|
|
476
|
+
function fileMode(tree, rel) {
|
|
477
|
+
try {
|
|
478
|
+
const stat = fs.statSync(path.join(tree.root, rel));
|
|
479
|
+
return stat.mode & 0o777;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** The new project.json: renamed, env set trimmed, port, base href, no domain lock. */
|
|
486
|
+
function projectJson(text, base, o, r) {
|
|
487
|
+
let json;
|
|
488
|
+
try {
|
|
489
|
+
json = JSON.parse(text);
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
throw new Error(`app --from: ${base.root}/project.json is not valid JSON: ${err.message}`);
|
|
493
|
+
}
|
|
494
|
+
json = renameTargets(json, base.name, r.name);
|
|
495
|
+
json['name'] = r.name;
|
|
496
|
+
if (typeof json['$schema'] === 'string') {
|
|
497
|
+
json['$schema'] = `${(0, app_dir_1.upTo)(r.root)}node_modules/nx/schemas/project-schema.json`;
|
|
498
|
+
}
|
|
499
|
+
if (o.tags)
|
|
500
|
+
json['tags'] = o.tags;
|
|
501
|
+
const targets = (json['targets'] ?? {});
|
|
502
|
+
for (const t of Object.values(targets)) {
|
|
503
|
+
if (t.executor !== angular_project_1.POLYSTACK_BUILD_EXECUTOR)
|
|
504
|
+
continue;
|
|
505
|
+
t.options ??= {};
|
|
506
|
+
if (base.envProject)
|
|
507
|
+
t.options['envProject'] = r.name;
|
|
508
|
+
for (const c of [t.options, ...Object.values(t.configurations ?? {})]) {
|
|
509
|
+
delete c['domains'];
|
|
510
|
+
delete c['includeWww'];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (const env of r.leftOut) {
|
|
514
|
+
if (base.type === 'flutter') {
|
|
515
|
+
for (const t of Object.values(targets)) {
|
|
516
|
+
if (t.executor !== FLUTTER_EXECUTOR || !t.configurations)
|
|
517
|
+
continue;
|
|
518
|
+
for (const [k, c] of Object.entries(t.configurations)) {
|
|
519
|
+
if (k === env || c?.['env'] === env)
|
|
520
|
+
delete t.configurations[k];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
for (const t of Object.values(targets)) {
|
|
526
|
+
if (t.executor !== angular_project_1.POLYSTACK_BUILD_EXECUTOR || !t.configurations)
|
|
527
|
+
continue;
|
|
528
|
+
const key = (0, plan_1.configurationFor)(t, env);
|
|
529
|
+
if (!key)
|
|
530
|
+
continue;
|
|
531
|
+
const buildConf = t.configurations[key]?.['buildConfiguration'] ??
|
|
532
|
+
{ prod: 'production', uat: 'uat' }[env] ??
|
|
533
|
+
env;
|
|
534
|
+
delete t.configurations[key];
|
|
535
|
+
const buildRef = t.options?.['buildTarget'];
|
|
536
|
+
const buildName = typeof buildRef === 'string' && buildRef.startsWith(`${r.name}:`)
|
|
537
|
+
? buildRef.split(':')[1]
|
|
538
|
+
: 'build';
|
|
539
|
+
const build = targets[buildName];
|
|
540
|
+
if (build?.configurations?.[buildConf])
|
|
541
|
+
delete build.configurations[buildConf];
|
|
542
|
+
for (const s of Object.values(targets)) {
|
|
543
|
+
if (!s.configurations)
|
|
544
|
+
continue;
|
|
545
|
+
for (const [k, c] of Object.entries(s.configurations)) {
|
|
546
|
+
const ref = c?.['browserTarget'] ?? c?.['buildTarget'];
|
|
547
|
+
if (typeof ref === 'string' && ref === `${r.name}:${buildName}:${buildConf}`) {
|
|
548
|
+
delete s.configurations[k];
|
|
549
|
+
if (s.defaultConfiguration === k)
|
|
550
|
+
delete s.defaultConfiguration;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// A default configuration that was left out would make `nx build <app>` fail.
|
|
557
|
+
for (const t of Object.values(targets)) {
|
|
558
|
+
if (t.defaultConfiguration && !t.configurations?.[t.defaultConfiguration]) {
|
|
559
|
+
delete t.defaultConfiguration;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const serve = (0, serve_port_1.serveTargetOf)(json);
|
|
563
|
+
if (serve)
|
|
564
|
+
serve.options = { ...(serve.options ?? {}), port: r.port };
|
|
565
|
+
const found = (0, base_href_1.baseHrefTarget)(json);
|
|
566
|
+
if (found) {
|
|
567
|
+
const configs = found.target.configurations ?? {};
|
|
568
|
+
const withHref = Object.keys(configs).filter((k) => configs[k]?.['baseHref'] !== undefined);
|
|
569
|
+
const targetsOf = withHref.length > 0 ? withHref : ['uat', 'prod'].filter((k) => configs[k] !== undefined);
|
|
570
|
+
if (r.baseHref !== undefined)
|
|
571
|
+
(0, base_href_1.setBaseHref)(found.target, targetsOf, r.baseHref);
|
|
572
|
+
else if (withHref.length > 0)
|
|
573
|
+
(0, base_href_1.setBaseHref)(found.target, withHref, undefined);
|
|
574
|
+
}
|
|
575
|
+
return JSON.stringify(json, null, 2) + '\n';
|
|
576
|
+
}
|
|
577
|
+
/** Every string with `<base>:` (a target reference) or `nx … <base>` turned to the new name. */
|
|
578
|
+
function renameTargets(value, base, name) {
|
|
579
|
+
if (typeof value === 'string') {
|
|
580
|
+
return value
|
|
581
|
+
.replace(new RegExp(`(^|[\\s'"=])${escapeRe(base)}:`, 'g'), `$1${name}:`)
|
|
582
|
+
.replace(new RegExp(`(\\bnx\\s+(?:run|serve|build|test|lint|export|e2e)\\s+)${escapeRe(base)}(?![\\w-])`, 'g'), `$1${name}`);
|
|
583
|
+
}
|
|
584
|
+
if (Array.isArray(value))
|
|
585
|
+
return value.map((v) => renameTargets(v, base, name));
|
|
586
|
+
if (value && typeof value === 'object') {
|
|
587
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renameTargets(v, base, name)]));
|
|
588
|
+
}
|
|
589
|
+
return value;
|
|
590
|
+
}
|
|
591
|
+
/** environments/<base>/ → environments/<name>/: the kept envs, every value emptied. */
|
|
592
|
+
function copyAngularEnvFiles(tree, base, name, r) {
|
|
593
|
+
const from = `environments/${base.envProject}`;
|
|
594
|
+
if (!tree.exists(from))
|
|
595
|
+
return;
|
|
596
|
+
const leftOutFiles = new Set(r.leftOut.flatMap((e) => base.envFiles?.[e] ?? []));
|
|
597
|
+
for (const file of [...tree.children(from)].sort()) {
|
|
598
|
+
const src = `${from}/${file}`;
|
|
599
|
+
if (!tree.isFile(src))
|
|
600
|
+
continue;
|
|
601
|
+
const dest = `environments/${name}/${file}`;
|
|
602
|
+
if (leftOutFiles.has(file))
|
|
603
|
+
continue;
|
|
604
|
+
const text = tree.read(src, 'utf-8') ?? '';
|
|
605
|
+
// Helper files (types, shared parts) are copied as they are.
|
|
606
|
+
if (!/\benvironment\b\s*(?::[^=]*)?=/.test(text)) {
|
|
607
|
+
tree.write(dest, text);
|
|
608
|
+
r.copied.push(dest);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!/\benvironment\b\s*(?::[^=]*)?=\s*\{/.test(text)) {
|
|
612
|
+
tree.write(dest, text);
|
|
613
|
+
r.copied.push(dest);
|
|
614
|
+
r.notEmptied.push(dest);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const emptied = (0, empty_1.emptyEnvironmentText)(text);
|
|
618
|
+
tree.write(dest, emptied.text);
|
|
619
|
+
r.envFiles.push(dest);
|
|
620
|
+
r.expressions.push(...emptied.expressions.map((k) => `${dest}: ${k}`));
|
|
621
|
+
r.unkeyed.push(...emptied.unkeyed.map((k) => `${dest}: ${k}`));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** nx.json pstack.envEncryption.keys.<base> (key names only) → keys.<name>. */
|
|
625
|
+
function copyEncryptionKeys(tree, base, name) {
|
|
626
|
+
if (!base.envProject || !tree.exists('nx.json'))
|
|
627
|
+
return;
|
|
628
|
+
const keys = (0, devkit_1.readJson)(tree, 'nx.json').pstack?.envEncryption?.keys;
|
|
629
|
+
const list = keys?.[base.envProject];
|
|
630
|
+
if (!Array.isArray(list) || keys?.[name])
|
|
631
|
+
return;
|
|
632
|
+
(0, devkit_1.updateJson)(tree, 'nx.json', (json) => {
|
|
633
|
+
json.pstack.envEncryption.keys[name] = [...list];
|
|
634
|
+
return json;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/** tsconfig.base.json aliases that point into the base app, for the new app. */
|
|
638
|
+
function copyTsPaths(tree, base, name, root) {
|
|
639
|
+
if (!tree.exists('tsconfig.base.json'))
|
|
640
|
+
return;
|
|
641
|
+
(0, devkit_1.updateJson)(tree, 'tsconfig.base.json', (json) => {
|
|
642
|
+
const opts = (json['compilerOptions'] ?? {});
|
|
643
|
+
const paths = (opts['paths'] ?? {});
|
|
644
|
+
for (const [alias, targets] of Object.entries({ ...paths })) {
|
|
645
|
+
if (!Array.isArray(targets) || targets.length === 0)
|
|
646
|
+
continue;
|
|
647
|
+
const inside = targets.every((t) => t === base.root || t.startsWith(`${base.root}/`));
|
|
648
|
+
if (!inside)
|
|
649
|
+
continue;
|
|
650
|
+
const next = replaceForms(alias, base.name, name);
|
|
651
|
+
if (next === alias || paths[next])
|
|
652
|
+
continue;
|
|
653
|
+
paths[next] = targets.map((t) => root + t.slice(base.root.length));
|
|
654
|
+
}
|
|
655
|
+
return json;
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
/** Root npm scripts of the base app (`serve:web`, `build:web:uat`, …) for the new app. */
|
|
659
|
+
function copyNpmScripts(tree, base, name, leftOut) {
|
|
660
|
+
if (!tree.exists('package.json'))
|
|
661
|
+
return [];
|
|
662
|
+
const scripts = ((0, devkit_1.readJson)(tree, 'package.json').scripts ?? {});
|
|
663
|
+
const add = {};
|
|
664
|
+
for (const [key, cmd] of Object.entries(scripts)) {
|
|
665
|
+
// <verb>:<app>[:<env>…]: only the app segment is the app's name.
|
|
666
|
+
const parts = key.split(':');
|
|
667
|
+
if (parts[1] !== base.name)
|
|
668
|
+
continue;
|
|
669
|
+
if (parts.slice(2).some((p) => leftOut.includes(p)))
|
|
670
|
+
continue;
|
|
671
|
+
const newKey = [parts[0], name, ...parts.slice(2)].join(':');
|
|
672
|
+
if (scripts[newKey] !== undefined)
|
|
673
|
+
continue;
|
|
674
|
+
add[newKey] = renameTargets(cmd, base.name, name);
|
|
675
|
+
}
|
|
676
|
+
if (Object.keys(add).length > 0)
|
|
677
|
+
(0, npm_scripts_1.addNpmScripts)(tree, add, { force: false });
|
|
678
|
+
return Object.keys(add);
|
|
679
|
+
}
|
|
680
|
+
const MAX_LEFTOVERS = 50;
|
|
681
|
+
/** Lines of the written text files that still name the base app. */
|
|
682
|
+
function findLeftovers(tree, base, files) {
|
|
683
|
+
const forms = nameForms(base);
|
|
684
|
+
const re = new RegExp(`(?<![A-Za-z0-9_-])(${forms.map(escapeRe).join('|')})(?![A-Za-z0-9_-])`);
|
|
685
|
+
const out = [];
|
|
686
|
+
for (const file of files) {
|
|
687
|
+
const buf = tree.read(file);
|
|
688
|
+
if (!buf || isBinary(buf))
|
|
689
|
+
continue;
|
|
690
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
691
|
+
for (let i = 0; i < lines.length; i++) {
|
|
692
|
+
if (re.test(lines[i])) {
|
|
693
|
+
out.push({ file, line: i + 1, text: lines[i].trim().slice(0, 120) });
|
|
694
|
+
if (out.length >= MAX_LEFTOVERS)
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
/** The report printed after (or, with --dry-run, instead of) creating the app. */
|
|
702
|
+
function appFromReport(r) {
|
|
703
|
+
const lines = [];
|
|
704
|
+
lines.push(`${r.name} (${r.root}) from ${r.base.name}: ${r.copied.length} file(s) copied, ` +
|
|
705
|
+
`${r.envFiles.length} env file(s) with every value emptied.`);
|
|
706
|
+
lines.push(` port ${r.port}${r.baseHref ? `, base href ${r.baseHref}` : ''}${r.appId ? `, appId ${r.appId}` : ''}`);
|
|
707
|
+
lines.push(` environments: ${r.envs.join(', ')}${r.leftOut.length ? ` (left out: ${r.leftOut.join(', ')})` : ''}`);
|
|
708
|
+
if (r.scripts.length > 0)
|
|
709
|
+
lines.push(` npm scripts: ${r.scripts.join(', ')}`);
|
|
710
|
+
if (r.expressions.length > 0) {
|
|
711
|
+
lines.push(' Values that were not literals, now empty strings:');
|
|
712
|
+
for (const e of r.expressions)
|
|
713
|
+
lines.push(` ${e}`);
|
|
714
|
+
}
|
|
715
|
+
if (r.notEmptied.length > 0) {
|
|
716
|
+
lines.push(' NOT emptied (the environment is not an object literal; values copied as they are):');
|
|
717
|
+
for (const e of r.notEmptied)
|
|
718
|
+
lines.push(` ${e}`);
|
|
719
|
+
}
|
|
720
|
+
if (r.unkeyed.length > 0) {
|
|
721
|
+
lines.push(' Entries left as written (spread / shorthand):');
|
|
722
|
+
for (const e of r.unkeyed)
|
|
723
|
+
lines.push(` ${e}`);
|
|
724
|
+
}
|
|
725
|
+
if (r.secrets.length > 0)
|
|
726
|
+
lines.push(` Not copied (possible secrets): ${r.secrets.join(', ')}`);
|
|
727
|
+
if (r.skipped.length > 0) {
|
|
728
|
+
const shown = r.skipped.slice(0, 20);
|
|
729
|
+
lines.push(` Not copied (build output, caches, ignored files): ${shown.join(', ')}` +
|
|
730
|
+
(r.skipped.length > shown.length ? `, … (${r.skipped.length - shown.length} more)` : ''));
|
|
731
|
+
}
|
|
732
|
+
if (r.leftovers.length > 0) {
|
|
733
|
+
lines.push(` Still naming ${r.base.name} (review these):`);
|
|
734
|
+
for (const l of r.leftovers)
|
|
735
|
+
lines.push(` ${l.file}:${l.line} ${l.text}`);
|
|
736
|
+
if (r.leftovers.length >= MAX_LEFTOVERS)
|
|
737
|
+
lines.push(' … (first 50 shown)');
|
|
738
|
+
}
|
|
739
|
+
lines.push('Next:');
|
|
740
|
+
lines.push(` - fill in the env files (every value is empty, production is false): ${r.base.type === 'flutter' ? `${r.root}/env/*.json` : `environments/${r.name}/`}`);
|
|
741
|
+
if (r.base.type !== 'flutter') {
|
|
742
|
+
lines.push(` - encrypted keys and domain locks: pstack encryption --app=${r.name} --domains=<env>=<hosts>`);
|
|
743
|
+
}
|
|
744
|
+
if (r.base.type === 'flutter' &&
|
|
745
|
+
r.copied.some((f) => f.startsWith(`${r.root}/android/`) || f.startsWith(`${r.root}/ios/`))) {
|
|
746
|
+
lines.push(` - android/ and ios/ still use ${r.base.name}'s applicationId / bundle id: change them ` +
|
|
747
|
+
`before installing ${r.name} next to ${r.base.name}`);
|
|
748
|
+
}
|
|
749
|
+
if (r.base.type === 'ionic' && r.skipped.some((s) => s === 'android/' || s === 'ios/')) {
|
|
750
|
+
lines.push(` - native projects were not copied (they hold ${r.base.name}'s appId): ` +
|
|
751
|
+
`nx run ${r.name}:cap-add --platform=android|ios`);
|
|
752
|
+
}
|
|
753
|
+
lines.push(` - nx run ${r.name}:serve`);
|
|
754
|
+
return lines.join('\n');
|
|
755
|
+
}
|
|
756
|
+
exports.appFromReport = appFromReport;
|
|
757
|
+
//# sourceMappingURL=from.js.map
|