create-ortha-app 0.4.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +277 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/lib/conditionals.d.ts +37 -0
- package/dist/lib/conditionals.d.ts.map +1 -0
- package/dist/lib/conditionals.js +115 -0
- package/dist/lib/features.d.ts +185 -0
- package/dist/lib/features.d.ts.map +1 -0
- package/dist/lib/features.js +328 -0
- package/dist/lib/template.d.ts +46 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +115 -0
- package/dist/lib/ui.d.ts +76 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +310 -0
- package/dist/lib/validate.d.ts +13 -0
- package/dist/lib/validate.d.ts.map +1 -0
- package/dist/lib/validate.js +51 -0
- package/package.json +37 -0
- package/templates/default/README.md.tmpl +199 -0
- package/templates/default/_gitignore +8 -0
- package/templates/default/apps/admin/index.html +38 -0
- package/templates/default/apps/admin/src/main.tsx +5 -0
- package/templates/default/apps/admin/src/plugins.spec.ts +58 -0
- package/templates/default/apps/admin/src/plugins.ts +57 -0
- package/templates/default/apps/admin/src/styles.css +254 -0
- package/templates/default/apps/admin/tsconfig.json +29 -0
- package/templates/default/apps/admin/vite.config.mts +67 -0
- package/templates/default/apps/admin-e2e/playwright.config.ts +51 -0
- package/templates/default/apps/admin-e2e/src/auth.spec.ts +55 -0
- package/templates/default/apps/admin-e2e/src/support/seed.ts +62 -0
- package/templates/default/apps/admin-e2e/tsconfig.json +23 -0
- package/templates/default/apps/server/jest.config.js +38 -0
- package/templates/default/apps/server/jest.setup.js +20 -0
- package/templates/default/apps/server/ortha.config.ts +505 -0
- package/templates/default/apps/server/src/main.ts +26 -0
- package/templates/default/apps/server/src/plugins.spec.ts +71 -0
- package/templates/default/apps/server/src/plugins.ts +229 -0
- package/templates/default/apps/server/tsconfig.json +31 -0
- package/templates/default/apps/server-e2e/jest.config.js +47 -0
- package/templates/default/apps/server-e2e/src/api.spec.ts +107 -0
- package/templates/default/apps/server-e2e/src/global-setup.ts +41 -0
- package/templates/default/apps/server-e2e/src/jest.setup.ts +28 -0
- package/templates/default/apps/server-e2e/src/support/db.ts +143 -0
- package/templates/default/apps/server-e2e/src/support/test-app.ts +64 -0
- package/templates/default/apps/server-e2e/tsconfig.json +26 -0
- package/templates/default/docker-compose.yml +21 -0
- package/templates/default/env.tmpl +140 -0
- package/templates/default/package.json.tmpl +51 -0
- package/templates/default/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ortha CMS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const promises_1 = require("node:readline/promises");
|
|
10
|
+
const features_1 = require("./lib/features");
|
|
11
|
+
const template_1 = require("./lib/template");
|
|
12
|
+
const ui = tslib_1.__importStar(require("./lib/ui"));
|
|
13
|
+
const validate_1 = require("./lib/validate");
|
|
14
|
+
const USAGE = `create-ortha-app — scaffold an Ortha CMS app
|
|
15
|
+
|
|
16
|
+
Usage: npx create-ortha-app <directory> [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--yes Accept every default, asking nothing
|
|
20
|
+
--media <id> Storage adapter (default: media-local)
|
|
21
|
+
--copilot <ids> Comma-separated copilot providers, or "none"
|
|
22
|
+
--sso <ids> Comma-separated identity providers (sso-oidc), or "none"
|
|
23
|
+
--protocols <ids> Comma-separated protocols beyond REST (graphql, mcp), or "none"
|
|
24
|
+
--no-install Skip installing dependencies
|
|
25
|
+
--no-git Skip initialising a git repository
|
|
26
|
+
-h, --help Show this message
|
|
27
|
+
`;
|
|
28
|
+
/** Whether a bare `--flag` is present. */
|
|
29
|
+
function flag(argv, name) {
|
|
30
|
+
return argv.includes(`--${name}`);
|
|
31
|
+
}
|
|
32
|
+
/** Reads `--name=value` or `--name value` from argv. */
|
|
33
|
+
function option(argv, name) {
|
|
34
|
+
const inline = argv.find((arg) => arg.startsWith(`--${name}=`));
|
|
35
|
+
if (inline)
|
|
36
|
+
return inline.slice(`--${name}=`.length);
|
|
37
|
+
const index = argv.indexOf(`--${name}`);
|
|
38
|
+
const next = index === -1 ? undefined : argv[index + 1];
|
|
39
|
+
return next && !next.startsWith('-') ? next : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Splits a comma-separated flag value; `none` means an empty selection. */
|
|
42
|
+
function idList(raw) {
|
|
43
|
+
if (raw === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (raw.trim() === 'none')
|
|
46
|
+
return [];
|
|
47
|
+
return raw
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((id) => id.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* This package's own version, which every `@orthacms/*` dependency in the
|
|
54
|
+
* generated app is pinned to.
|
|
55
|
+
*
|
|
56
|
+
* The scaffolder is released in lockstep with the packages it scaffolds, so its
|
|
57
|
+
* version *is* the matching set — which is the whole mechanism keeping a
|
|
58
|
+
* generated app internally consistent. Reading it from the manifest rather than
|
|
59
|
+
* resolving `latest` from the registry also means `npx create-ortha-app@0.3.0`
|
|
60
|
+
* generates a 0.3.0 app, not whatever shipped since.
|
|
61
|
+
*/
|
|
62
|
+
function ownVersion() {
|
|
63
|
+
const manifest = (0, node_path_1.join)(__dirname, '../package.json');
|
|
64
|
+
return JSON.parse((0, node_fs_1.readFileSync)(manifest, 'utf8'))
|
|
65
|
+
.version;
|
|
66
|
+
}
|
|
67
|
+
/** Runs a command in `cwd`, returning whether it succeeded. */
|
|
68
|
+
function runCommand(command, args, cwd) {
|
|
69
|
+
return ((0, node_child_process_1.spawnSync)(command, args, {
|
|
70
|
+
cwd,
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
shell: process.platform === 'win32'
|
|
73
|
+
}).status === 0);
|
|
74
|
+
}
|
|
75
|
+
/** Turns a feature into a picker row. */
|
|
76
|
+
function toChoice(feature) {
|
|
77
|
+
return {
|
|
78
|
+
value: feature.id,
|
|
79
|
+
label: feature.label,
|
|
80
|
+
hint: feature.hint,
|
|
81
|
+
selected: feature.enabledByDefault,
|
|
82
|
+
disabled: !feature.available,
|
|
83
|
+
locked: feature.locked
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The ids a feature group falls back to when nothing is picked.
|
|
88
|
+
*
|
|
89
|
+
* A locked feature is in every answer, including `--protocols none`: REST is
|
|
90
|
+
* not something the flag can switch off.
|
|
91
|
+
*/
|
|
92
|
+
function defaultsOf(features) {
|
|
93
|
+
return features
|
|
94
|
+
.filter((feature) => (feature.enabledByDefault || feature.locked) &&
|
|
95
|
+
feature.available)
|
|
96
|
+
.map((feature) => feature.id);
|
|
97
|
+
}
|
|
98
|
+
/** The ids that are on regardless of what was asked or passed. */
|
|
99
|
+
function lockedOf(features) {
|
|
100
|
+
return features
|
|
101
|
+
.filter((feature) => feature.locked && feature.available)
|
|
102
|
+
.map((feature) => feature.id);
|
|
103
|
+
}
|
|
104
|
+
/** Asks a free-text question, re-asking until it validates. */
|
|
105
|
+
async function askText(label, fallback, validate) {
|
|
106
|
+
const rl = (0, promises_1.createInterface)({
|
|
107
|
+
input: process.stdin,
|
|
108
|
+
output: process.stdout
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
for (;;) {
|
|
112
|
+
const raw = await rl.question(`${ui.cyan('◆')} ${ui.bold(label)} ${ui.dim(`(${fallback})`)} `);
|
|
113
|
+
const answer = raw.trim() || fallback;
|
|
114
|
+
const problem = validate?.(answer);
|
|
115
|
+
if (!problem)
|
|
116
|
+
return answer;
|
|
117
|
+
ui.error(problem);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
rl.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolves the answers, asking only when there is a terminal to ask in.
|
|
126
|
+
*
|
|
127
|
+
* Non-interactive is not an error case to warn about — it is CI, a piped
|
|
128
|
+
* install, and `--yes`. A scaffolder that blocks on a prompt nobody can answer
|
|
129
|
+
* hangs a pipeline until it times out, which is a far worse failure than
|
|
130
|
+
* defaulting, so the flags and the defaults cover every question.
|
|
131
|
+
*/
|
|
132
|
+
async function resolveAnswers(argv, defaultName) {
|
|
133
|
+
const asked = !flag(argv, 'yes') && ui.interactive();
|
|
134
|
+
const defaultDatabaseUrl = `postgresql://ortha:ortha@localhost:5432/${defaultName.replace(/[^a-z0-9_]/gi, '_')}`;
|
|
135
|
+
const media = idList(option(argv, 'media')) ?? defaultsOf(features_1.MEDIA_PROVIDERS);
|
|
136
|
+
const copilot = idList(option(argv, 'copilot')) ?? defaultsOf(features_1.COPILOT_PROVIDERS);
|
|
137
|
+
const sso = idList(option(argv, 'sso')) ?? defaultsOf(features_1.SSO_PROVIDERS);
|
|
138
|
+
const protocols = [
|
|
139
|
+
...lockedOf(features_1.PROTOCOLS),
|
|
140
|
+
...(idList(option(argv, 'protocols')) ?? defaultsOf(features_1.PROTOCOLS))
|
|
141
|
+
];
|
|
142
|
+
if (!asked) {
|
|
143
|
+
return {
|
|
144
|
+
appName: defaultName,
|
|
145
|
+
databaseUrl: defaultDatabaseUrl,
|
|
146
|
+
adminEmail: 'admin@example.com',
|
|
147
|
+
selection: {
|
|
148
|
+
enabled: new Set([...media, ...copilot, ...sso, ...protocols])
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
ui.section('Project');
|
|
153
|
+
const appName = await askText('App name', defaultName, validate_1.validateAppName);
|
|
154
|
+
const databaseUrl = await askText('Database URL', defaultDatabaseUrl, validate_1.validateDatabaseUrl);
|
|
155
|
+
const adminEmail = await askText('Admin email', 'admin@example.com');
|
|
156
|
+
ui.section('Features');
|
|
157
|
+
ui.note('Everything else is installed for you. These are the choices.');
|
|
158
|
+
console.log('');
|
|
159
|
+
// Only ask when there is more than one answer available. S3 is not
|
|
160
|
+
// published, so today this is a question with a single possible reply, and
|
|
161
|
+
// asking it would be noise pretending to be a choice.
|
|
162
|
+
const selectable = features_1.MEDIA_PROVIDERS.filter((provider) => provider.available);
|
|
163
|
+
const chosenMedia = selectable.length > 1
|
|
164
|
+
? [
|
|
165
|
+
(await ui.select('Where should uploads be stored?', features_1.MEDIA_PROVIDERS.map(toChoice))) ?? media[0]
|
|
166
|
+
]
|
|
167
|
+
: media;
|
|
168
|
+
const chosenCopilot = (await ui.multiselect('AI copilot — which model backends?', features_1.COPILOT_PROVIDERS.map(toChoice))) ?? copilot;
|
|
169
|
+
const chosenSso = (await ui.multiselect('Single sign-on — which identity providers?', features_1.SSO_PROVIDERS.map(toChoice))) ?? sso;
|
|
170
|
+
const chosenProtocols = (await ui.multiselect('Which protocols should the content API speak?', features_1.PROTOCOLS.map(toChoice))) ?? protocols;
|
|
171
|
+
return {
|
|
172
|
+
appName,
|
|
173
|
+
databaseUrl,
|
|
174
|
+
adminEmail,
|
|
175
|
+
selection: {
|
|
176
|
+
enabled: new Set([
|
|
177
|
+
...chosenMedia.filter(Boolean),
|
|
178
|
+
...chosenCopilot,
|
|
179
|
+
...chosenSso,
|
|
180
|
+
...chosenProtocols
|
|
181
|
+
])
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Human-readable summary of what was chosen. */
|
|
186
|
+
function describeSelection(selection) {
|
|
187
|
+
const labelsFor = (features) => {
|
|
188
|
+
const chosen = features
|
|
189
|
+
.filter((feature) => selection.enabled.has(feature.id))
|
|
190
|
+
.map((feature) => feature.label);
|
|
191
|
+
return chosen.length > 0 ? chosen.join(', ') : ui.dim('none');
|
|
192
|
+
};
|
|
193
|
+
const providers = features_1.COPILOT_PROVIDERS.filter((provider) => selection.enabled.has(provider.id));
|
|
194
|
+
return [
|
|
195
|
+
['Storage', labelsFor(features_1.MEDIA_PROVIDERS)],
|
|
196
|
+
['Protocols', labelsFor(features_1.PROTOCOLS)],
|
|
197
|
+
[
|
|
198
|
+
'Copilot',
|
|
199
|
+
providers.length > 0
|
|
200
|
+
? `${providers.map((p) => p.label).join(', ')} + offline fake`
|
|
201
|
+
: 'offline fake only'
|
|
202
|
+
],
|
|
203
|
+
['Sign-in', labelsFor(features_1.SSO_PROVIDERS)],
|
|
204
|
+
['Ortha packages', String((0, features_1.resolvePackages)(selection).length + 1)]
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
async function main() {
|
|
208
|
+
const argv = process.argv.slice(2);
|
|
209
|
+
if (flag(argv, 'help') || argv.includes('-h')) {
|
|
210
|
+
console.log(USAGE);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const positional = argv.filter((arg) => !arg.startsWith('-'));
|
|
214
|
+
const target = (0, node_path_1.resolve)(positional[0] ?? '.');
|
|
215
|
+
ui.banner('Ortha CMS', `Creating an app in ${target}`);
|
|
216
|
+
if ((0, template_1.isNonEmptyDirectory)(target)) {
|
|
217
|
+
throw new Error(`${target} already exists and is not empty. Pick a new directory, ` +
|
|
218
|
+
`or empty that one first.`);
|
|
219
|
+
}
|
|
220
|
+
const answers = await resolveAnswers(argv, (0, node_path_1.basename)(target));
|
|
221
|
+
// Generated rather than prompted: a password typed at a prompt is echoed to
|
|
222
|
+
// the terminal and lands in shell history. This one is written only to the
|
|
223
|
+
// git-ignored .env, and printed once below.
|
|
224
|
+
const adminPassword = (0, node_crypto_1.randomBytes)(12).toString('base64url');
|
|
225
|
+
ui.summary('Your app', [
|
|
226
|
+
['Name', answers.appName],
|
|
227
|
+
['Database', (0, validate_1.databaseNameFrom)(answers.databaseUrl)],
|
|
228
|
+
...describeSelection(answers.selection)
|
|
229
|
+
]);
|
|
230
|
+
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
231
|
+
(0, template_1.renderTemplate)((0, node_path_1.join)(__dirname, '../templates/default'), target, {
|
|
232
|
+
appName: answers.appName,
|
|
233
|
+
appTitle: answers.appName,
|
|
234
|
+
databaseUrl: answers.databaseUrl,
|
|
235
|
+
databaseName: (0, validate_1.databaseNameFrom)(answers.databaseUrl),
|
|
236
|
+
adminEmail: answers.adminEmail,
|
|
237
|
+
adminPassword,
|
|
238
|
+
orthaVersion: ownVersion(),
|
|
239
|
+
selection: answers.selection
|
|
240
|
+
});
|
|
241
|
+
console.log('');
|
|
242
|
+
ui.success('Files written');
|
|
243
|
+
if (!flag(argv, 'no-git') && !(0, node_fs_1.existsSync)((0, node_path_1.join)(target, '.git'))) {
|
|
244
|
+
if (runCommand('git', ['init', '--quiet'], target)) {
|
|
245
|
+
ui.success('Git repository initialised');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let installed = true;
|
|
249
|
+
if (!flag(argv, 'no-install')) {
|
|
250
|
+
console.log('');
|
|
251
|
+
ui.note('Installing dependencies…');
|
|
252
|
+
installed = runCommand('npm', ['install', '--no-audit', '--no-fund'], target);
|
|
253
|
+
if (installed)
|
|
254
|
+
ui.success('Dependencies installed');
|
|
255
|
+
}
|
|
256
|
+
if (!installed) {
|
|
257
|
+
ui.error('Dependency installation failed — fix the error above, then run ' +
|
|
258
|
+
'`npm install` in the new directory.');
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
}
|
|
261
|
+
ui.summary('Your admin account', [
|
|
262
|
+
['Email', answers.adminEmail],
|
|
263
|
+
['Password', ui.bold(adminPassword)],
|
|
264
|
+
['', ui.dim('Also written to .env')]
|
|
265
|
+
]);
|
|
266
|
+
ui.nextSteps([
|
|
267
|
+
`cd ${(0, node_path_1.basename)(target)}`,
|
|
268
|
+
...(flag(argv, 'no-install') ? ['npm install'] : []),
|
|
269
|
+
'docker compose up -d',
|
|
270
|
+
'npm run migrate',
|
|
271
|
+
`npm run dev ${ui.dim('→ http://localhost:4200')}`
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
main().catch((error) => {
|
|
275
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of `create-ortha-app`.
|
|
3
|
+
*
|
|
4
|
+
* The package is used through `npx`; these exports exist so the rendering,
|
|
5
|
+
* feature resolution and validation are testable without spawning the binary.
|
|
6
|
+
*/
|
|
7
|
+
export { renderTemplate, renderManifest, render, isNonEmptyDirectory, type TemplateValues } from './lib/template';
|
|
8
|
+
export { applyConditionals } from './lib/conditionals';
|
|
9
|
+
export { CORE_PACKAGES, CORE_DEV_PACKAGES, TRANSITIVE_PACKAGES, MEDIA_PROVIDERS, COPILOT_PROVIDERS, SSO_PROVIDERS, PROTOCOLS, ALL_FEATURES, resolvePackages, resolveDevPackages, resolveFlags, type Feature, type FeatureSelection } from './lib/features';
|
|
10
|
+
export { validateAppName, validateDatabaseUrl, databaseNameFrom } from './lib/validate';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACH,cAAc,EACd,cAAc,EACd,MAAM,EACN,mBAAmB,EACnB,KAAK,cAAc,EACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EACH,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACH,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EACnB,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.databaseNameFrom = exports.validateDatabaseUrl = exports.validateAppName = exports.resolveFlags = exports.resolveDevPackages = exports.resolvePackages = exports.ALL_FEATURES = exports.PROTOCOLS = exports.SSO_PROVIDERS = exports.COPILOT_PROVIDERS = exports.MEDIA_PROVIDERS = exports.TRANSITIVE_PACKAGES = exports.CORE_DEV_PACKAGES = exports.CORE_PACKAGES = exports.applyConditionals = exports.isNonEmptyDirectory = exports.render = exports.renderManifest = exports.renderTemplate = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Public API of `create-ortha-app`.
|
|
6
|
+
*
|
|
7
|
+
* The package is used through `npx`; these exports exist so the rendering,
|
|
8
|
+
* feature resolution and validation are testable without spawning the binary.
|
|
9
|
+
*/
|
|
10
|
+
var template_1 = require("./lib/template");
|
|
11
|
+
Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
|
|
12
|
+
Object.defineProperty(exports, "renderManifest", { enumerable: true, get: function () { return template_1.renderManifest; } });
|
|
13
|
+
Object.defineProperty(exports, "render", { enumerable: true, get: function () { return template_1.render; } });
|
|
14
|
+
Object.defineProperty(exports, "isNonEmptyDirectory", { enumerable: true, get: function () { return template_1.isNonEmptyDirectory; } });
|
|
15
|
+
var conditionals_1 = require("./lib/conditionals");
|
|
16
|
+
Object.defineProperty(exports, "applyConditionals", { enumerable: true, get: function () { return conditionals_1.applyConditionals; } });
|
|
17
|
+
var features_1 = require("./lib/features");
|
|
18
|
+
Object.defineProperty(exports, "CORE_PACKAGES", { enumerable: true, get: function () { return features_1.CORE_PACKAGES; } });
|
|
19
|
+
Object.defineProperty(exports, "CORE_DEV_PACKAGES", { enumerable: true, get: function () { return features_1.CORE_DEV_PACKAGES; } });
|
|
20
|
+
Object.defineProperty(exports, "TRANSITIVE_PACKAGES", { enumerable: true, get: function () { return features_1.TRANSITIVE_PACKAGES; } });
|
|
21
|
+
Object.defineProperty(exports, "MEDIA_PROVIDERS", { enumerable: true, get: function () { return features_1.MEDIA_PROVIDERS; } });
|
|
22
|
+
Object.defineProperty(exports, "COPILOT_PROVIDERS", { enumerable: true, get: function () { return features_1.COPILOT_PROVIDERS; } });
|
|
23
|
+
Object.defineProperty(exports, "SSO_PROVIDERS", { enumerable: true, get: function () { return features_1.SSO_PROVIDERS; } });
|
|
24
|
+
Object.defineProperty(exports, "PROTOCOLS", { enumerable: true, get: function () { return features_1.PROTOCOLS; } });
|
|
25
|
+
Object.defineProperty(exports, "ALL_FEATURES", { enumerable: true, get: function () { return features_1.ALL_FEATURES; } });
|
|
26
|
+
Object.defineProperty(exports, "resolvePackages", { enumerable: true, get: function () { return features_1.resolvePackages; } });
|
|
27
|
+
Object.defineProperty(exports, "resolveDevPackages", { enumerable: true, get: function () { return features_1.resolveDevPackages; } });
|
|
28
|
+
Object.defineProperty(exports, "resolveFlags", { enumerable: true, get: function () { return features_1.resolveFlags; } });
|
|
29
|
+
var validate_1 = require("./lib/validate");
|
|
30
|
+
Object.defineProperty(exports, "validateAppName", { enumerable: true, get: function () { return validate_1.validateAppName; } });
|
|
31
|
+
Object.defineProperty(exports, "validateDatabaseUrl", { enumerable: true, get: function () { return validate_1.validateDatabaseUrl; } });
|
|
32
|
+
Object.defineProperty(exports, "databaseNameFrom", { enumerable: true, get: function () { return validate_1.databaseNameFrom; } });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ortha:if` blocks — how one template serves every feature combination.
|
|
3
|
+
*
|
|
4
|
+
* A marker is any line containing the directive, whatever comment syntax wraps
|
|
5
|
+
* it, so the same three forms work in TypeScript, YAML, `.env` and Markdown:
|
|
6
|
+
*
|
|
7
|
+
* // ortha:if copilot
|
|
8
|
+
* import { CopilotPlugin } from '@orthacms/copilot-server';
|
|
9
|
+
* // ortha:end
|
|
10
|
+
*
|
|
11
|
+
* # ortha:ifnot graphql
|
|
12
|
+
* # (REST only — no GraphQL endpoint is mounted.)
|
|
13
|
+
* # ortha:end
|
|
14
|
+
*
|
|
15
|
+
* The marker lines are always removed; the body survives only when the
|
|
16
|
+
* condition holds. Blocks may nest, and an inner block is only reached when
|
|
17
|
+
* the outer one is kept.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately line-based rather than an expression language. The templates are
|
|
20
|
+
* read far more often than they are written — someone opening
|
|
21
|
+
* `plugins.ts` should see the real file with a few comments in it, not a
|
|
22
|
+
* templating dialect — and every condition this needs is "is this feature on".
|
|
23
|
+
*
|
|
24
|
+
* **Not used for `package.json`.** Dropping lines from JSON produces a trailing
|
|
25
|
+
* comma and an app that will not install, blaming the template rather than the
|
|
26
|
+
* feature that was switched off; dependencies are assembled in `features.ts`
|
|
27
|
+
* instead.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Applies every `ortha:if` block in `contents` against the enabled `flags`.
|
|
31
|
+
*
|
|
32
|
+
* @throws when a block is left open, which would otherwise silently swallow the
|
|
33
|
+
* rest of the file — a missing `ortha:end` in `plugins.ts` deletes every plugin
|
|
34
|
+
* below it, and the app boots with no API rather than failing to render.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyConditionals(contents: string, flags: ReadonlySet<string>, file?: string): string;
|
|
37
|
+
//# sourceMappingURL=conditionals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditionals.d.ts","sourceRoot":"","sources":["../../src/lib/conditionals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAmCH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,IAAI,SAAa,GAClB,MAAM,CA0CR"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `ortha:if` blocks — how one template serves every feature combination.
|
|
4
|
+
*
|
|
5
|
+
* A marker is any line containing the directive, whatever comment syntax wraps
|
|
6
|
+
* it, so the same three forms work in TypeScript, YAML, `.env` and Markdown:
|
|
7
|
+
*
|
|
8
|
+
* // ortha:if copilot
|
|
9
|
+
* import { CopilotPlugin } from '@orthacms/copilot-server';
|
|
10
|
+
* // ortha:end
|
|
11
|
+
*
|
|
12
|
+
* # ortha:ifnot graphql
|
|
13
|
+
* # (REST only — no GraphQL endpoint is mounted.)
|
|
14
|
+
* # ortha:end
|
|
15
|
+
*
|
|
16
|
+
* The marker lines are always removed; the body survives only when the
|
|
17
|
+
* condition holds. Blocks may nest, and an inner block is only reached when
|
|
18
|
+
* the outer one is kept.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately line-based rather than an expression language. The templates are
|
|
21
|
+
* read far more often than they are written — someone opening
|
|
22
|
+
* `plugins.ts` should see the real file with a few comments in it, not a
|
|
23
|
+
* templating dialect — and every condition this needs is "is this feature on".
|
|
24
|
+
*
|
|
25
|
+
* **Not used for `package.json`.** Dropping lines from JSON produces a trailing
|
|
26
|
+
* comma and an app that will not install, blaming the template rather than the
|
|
27
|
+
* feature that was switched off; dependencies are assembled in `features.ts`
|
|
28
|
+
* instead.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.applyConditionals = applyConditionals;
|
|
32
|
+
/**
|
|
33
|
+
* A trailing comment closer, so a block-comment directive works too — `*\/`
|
|
34
|
+
* for CSS and C-style block comments, `-->` for HTML.
|
|
35
|
+
*/
|
|
36
|
+
const CLOSER = String.raw `(?:\s*(?:\*\/|-->))?`;
|
|
37
|
+
const IF = new RegExp(String.raw `(^|\s)ortha:if\s+([a-z0-9-]+)${CLOSER}\s*$`);
|
|
38
|
+
const IFNOT = new RegExp(String.raw `(^|\s)ortha:ifnot\s+([a-z0-9-]+)${CLOSER}\s*$`);
|
|
39
|
+
const END = new RegExp(String.raw `(^|\s)ortha:end${CLOSER}\s*$`);
|
|
40
|
+
/** Reads the directive on `line`, or `undefined` for an ordinary line. */
|
|
41
|
+
function directiveOf(line) {
|
|
42
|
+
const trimmed = line.trimEnd();
|
|
43
|
+
const ifMatch = IF.exec(trimmed);
|
|
44
|
+
if (ifMatch)
|
|
45
|
+
return { kind: 'if', flag: ifMatch[2] };
|
|
46
|
+
const ifNotMatch = IFNOT.exec(trimmed);
|
|
47
|
+
if (ifNotMatch)
|
|
48
|
+
return { kind: 'ifnot', flag: ifNotMatch[2] };
|
|
49
|
+
if (END.test(trimmed))
|
|
50
|
+
return { kind: 'end' };
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Applies every `ortha:if` block in `contents` against the enabled `flags`.
|
|
55
|
+
*
|
|
56
|
+
* @throws when a block is left open, which would otherwise silently swallow the
|
|
57
|
+
* rest of the file — a missing `ortha:end` in `plugins.ts` deletes every plugin
|
|
58
|
+
* below it, and the app boots with no API rather than failing to render.
|
|
59
|
+
*/
|
|
60
|
+
function applyConditionals(contents, flags, file = 'template') {
|
|
61
|
+
const lines = contents.split('\n');
|
|
62
|
+
const kept = [];
|
|
63
|
+
// One entry per open block: whether its body is being written out. A nested
|
|
64
|
+
// block inside a dropped one stays dropped whatever its own condition says.
|
|
65
|
+
const stack = [];
|
|
66
|
+
const writing = () => stack.every(Boolean);
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const directive = directiveOf(line);
|
|
69
|
+
if (!directive) {
|
|
70
|
+
if (writing())
|
|
71
|
+
kept.push(line);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (directive.kind === 'end') {
|
|
75
|
+
if (stack.length === 0) {
|
|
76
|
+
throw new Error(`${file}: an \`ortha:end\` with no matching \`ortha:if\`.`);
|
|
77
|
+
}
|
|
78
|
+
stack.pop();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const holds = directive.kind === 'if'
|
|
82
|
+
? flags.has(directive.flag)
|
|
83
|
+
: !flags.has(directive.flag);
|
|
84
|
+
stack.push(holds);
|
|
85
|
+
}
|
|
86
|
+
if (stack.length > 0) {
|
|
87
|
+
throw new Error(`${file}: ${stack.length} unclosed \`ortha:if\` block(s) — every ` +
|
|
88
|
+
`one needs an \`ortha:end\`, or the rest of the file is dropped.`);
|
|
89
|
+
}
|
|
90
|
+
return collapseBlankRuns(kept).join('\n');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Collapses runs of three or more blank lines into one.
|
|
94
|
+
*
|
|
95
|
+
* Removing a block almost always leaves the blank line that separated it behind
|
|
96
|
+
* next to the one after it. Without this every generated file carries a scar
|
|
97
|
+
* wherever a feature was switched off, which reads as sloppiness in the very
|
|
98
|
+
* first file anyone opens.
|
|
99
|
+
*/
|
|
100
|
+
function collapseBlankRuns(lines) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let blanks = 0;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
if (line.trim() === '') {
|
|
105
|
+
blanks += 1;
|
|
106
|
+
if (blanks > 1)
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
blanks = 0;
|
|
111
|
+
}
|
|
112
|
+
out.push(line);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|