@notur/sdk 1.4.5 → 1.4.7
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 +136 -1
- package/bin/notur-create.js +586 -0
- package/bin/notur-doctor.js +123 -0
- package/bin/notur-pack.js +13 -7
- package/bin/notur-push.js +331 -0
- package/bin/notur-sync.js +199 -0
- package/bin/notur-validate.js +200 -0
- package/bin/notur.js +46 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -0
- package/dist/events.js.map +1 -1
- package/dist/hooks/useExtensionConfig.d.ts +23 -4
- package/dist/hooks/useExtensionConfig.d.ts.map +1 -1
- package/dist/hooks/useExtensionConfig.js +8 -1
- package/dist/hooks/useExtensionConfig.js.map +1 -1
- package/dist/hooks/useNavigate.d.ts +27 -6
- package/dist/hooks/useNavigate.d.ts.map +1 -1
- package/dist/hooks/useNavigate.js +11 -2
- package/dist/hooks/useNavigate.js.map +1 -1
- package/dist/hooks/useNoturEvent.d.ts +12 -0
- package/dist/hooks/useNoturEvent.d.ts.map +1 -1
- package/dist/hooks/useNoturEvent.js +12 -0
- package/dist/hooks/useNoturEvent.js.map +1 -1
- package/dist/hooks/usePermission.d.ts +10 -1
- package/dist/hooks/usePermission.d.ts.map +1 -1
- package/dist/hooks/usePermission.js +10 -1
- package/dist/hooks/usePermission.js.map +1 -1
- package/dist/hooks/useServerContext.d.ts +18 -3
- package/dist/hooks/useServerContext.d.ts.map +1 -1
- package/dist/hooks/useServerContext.js +8 -1
- package/dist/hooks/useServerContext.js.map +1 -1
- package/dist/hooks/useUserContext.d.ts +10 -2
- package/dist/hooks/useUserContext.d.ts.map +1 -1
- package/dist/hooks/useUserContext.js +2 -0
- package/dist/hooks/useUserContext.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +179 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/examples/red-button/.env.example +2 -0
- package/examples/red-button/README.md +30 -0
- package/examples/red-button/extension.yaml +9 -0
- package/examples/red-button/package.json +29 -0
- package/examples/red-button/resources/frontend/src/index.tsx +32 -0
- package/examples/red-button/tsconfig.json +13 -0
- package/examples/red-button/webpack.config.js +17 -0
- package/package.json +11 -3
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const options = {
|
|
11
|
+
id: null,
|
|
12
|
+
path: process.cwd(),
|
|
13
|
+
preset: null,
|
|
14
|
+
displayName: null,
|
|
15
|
+
description: null,
|
|
16
|
+
withFrontend: true,
|
|
17
|
+
withApiRoutes: false,
|
|
18
|
+
force: false,
|
|
19
|
+
slot: 'dashboard.widgets',
|
|
20
|
+
packageManager: null,
|
|
21
|
+
install: null,
|
|
22
|
+
createEnv: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg === '--path') {
|
|
28
|
+
options.path = args[++i];
|
|
29
|
+
} else if (arg === '--preset') {
|
|
30
|
+
options.preset = args[++i];
|
|
31
|
+
} else if (arg === '--name' || arg === '--display-name') {
|
|
32
|
+
options.displayName = args[++i];
|
|
33
|
+
} else if (arg === '--description') {
|
|
34
|
+
options.description = args[++i];
|
|
35
|
+
} else if (arg === '--with-frontend') {
|
|
36
|
+
options.withFrontend = true;
|
|
37
|
+
} else if (arg === '--no-frontend') {
|
|
38
|
+
options.withFrontend = false;
|
|
39
|
+
} else if (arg === '--with-api-routes') {
|
|
40
|
+
options.withApiRoutes = true;
|
|
41
|
+
} else if (arg === '--no-api-routes') {
|
|
42
|
+
options.withApiRoutes = false;
|
|
43
|
+
} else if (arg === '--force') {
|
|
44
|
+
options.force = true;
|
|
45
|
+
} else if (arg === '--slot') {
|
|
46
|
+
options.slot = args[++i];
|
|
47
|
+
} else if (arg === '--package-manager') {
|
|
48
|
+
options.packageManager = args[++i];
|
|
49
|
+
} else if (arg === '--install') {
|
|
50
|
+
options.install = true;
|
|
51
|
+
} else if (arg === '--no-install') {
|
|
52
|
+
options.install = false;
|
|
53
|
+
} else if (arg === '--env') {
|
|
54
|
+
options.createEnv = true;
|
|
55
|
+
} else if (arg === '--no-env') {
|
|
56
|
+
options.createEnv = false;
|
|
57
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
58
|
+
usage(0);
|
|
59
|
+
} else if (!arg.startsWith('-') && !options.id) {
|
|
60
|
+
options.id = arg;
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`Unknown argument: ${arg}`);
|
|
63
|
+
usage(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.preset && !['frontend', 'backend', 'full', 'minimal'].includes(options.preset)) {
|
|
68
|
+
console.error('Error: preset must be one of frontend, backend, full, or minimal.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.packageManager && !['npm', 'pnpm', 'yarn', 'bun'].includes(options.packageManager)) {
|
|
73
|
+
console.error('Error: package manager must be one of npm, pnpm, yarn, or bun.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return options;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function usage(code) {
|
|
81
|
+
console.log(`Usage:
|
|
82
|
+
npx notur-create acme/red-button [options]
|
|
83
|
+
npx @notur/sdk create acme/red-button [options]
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
--path <dir> Parent directory for the generated extension
|
|
87
|
+
--preset <name> frontend, backend, full, or minimal
|
|
88
|
+
--name <name> Display name for extension.yaml
|
|
89
|
+
--description <txt> Description for extension.yaml
|
|
90
|
+
--slot <slot> Initial frontend slot (default: dashboard.widgets)
|
|
91
|
+
--package-manager npm, pnpm, yarn, or bun
|
|
92
|
+
--install Install frontend dependencies after scaffolding
|
|
93
|
+
--env Create .env from .env.example
|
|
94
|
+
--no-frontend Generate PHP/manifest only
|
|
95
|
+
--with-api-routes Include a client API route stub
|
|
96
|
+
--force Allow writing into an existing empty directory`);
|
|
97
|
+
process.exit(code);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isInteractive() {
|
|
101
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function prompt(question, defaultValue = '') {
|
|
105
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
106
|
+
const rl = readline.createInterface({
|
|
107
|
+
input: process.stdin,
|
|
108
|
+
output: process.stdout,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return new Promise(resolve => {
|
|
112
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
113
|
+
rl.close();
|
|
114
|
+
resolve(answer.trim() || defaultValue);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function select(question, choices, defaultChoice) {
|
|
120
|
+
const labels = choices.map(choice => choice.value).join('/');
|
|
121
|
+
while (true) {
|
|
122
|
+
const answer = await prompt(`${question} [${labels}]`, defaultChoice);
|
|
123
|
+
const match = choices.find(choice => choice.value === answer);
|
|
124
|
+
if (match) {
|
|
125
|
+
return match.value;
|
|
126
|
+
}
|
|
127
|
+
console.log(`Choose one of: ${labels}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function confirm(question, defaultValue = false) {
|
|
132
|
+
const answer = await prompt(`${question} [${defaultValue ? 'Y/n' : 'y/N'}]`, defaultValue ? 'y' : 'n');
|
|
133
|
+
return ['y', 'yes'].includes(answer.toLowerCase());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateId(id) {
|
|
137
|
+
return /^[a-z0-9-]+\/[a-z0-9-]+$/.test(id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyPreset(options) {
|
|
141
|
+
const preset = options.preset || 'frontend';
|
|
142
|
+
const features = {
|
|
143
|
+
frontend: true,
|
|
144
|
+
apiRoutes: false,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (preset === 'minimal') {
|
|
148
|
+
features.frontend = false;
|
|
149
|
+
features.apiRoutes = false;
|
|
150
|
+
} else if (preset === 'backend') {
|
|
151
|
+
features.frontend = false;
|
|
152
|
+
features.apiRoutes = true;
|
|
153
|
+
} else if (preset === 'full') {
|
|
154
|
+
features.frontend = true;
|
|
155
|
+
features.apiRoutes = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.withFrontend === false) {
|
|
159
|
+
features.frontend = false;
|
|
160
|
+
} else if (options.withFrontend === true && options.preset === null) {
|
|
161
|
+
features.frontend = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options.withApiRoutes === true) {
|
|
165
|
+
features.apiRoutes = true;
|
|
166
|
+
} else if (options.withApiRoutes === false && options.preset === null) {
|
|
167
|
+
features.apiRoutes = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
options.withFrontend = features.frontend;
|
|
171
|
+
options.withApiRoutes = features.apiRoutes;
|
|
172
|
+
options.preset = preset;
|
|
173
|
+
|
|
174
|
+
return options;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function resolveOptions(options) {
|
|
178
|
+
if (!options.id && !isInteractive()) {
|
|
179
|
+
console.error('Error: extension id is required in non-interactive mode.');
|
|
180
|
+
usage(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (options.id && !validateId(options.id)) {
|
|
184
|
+
console.error('Error: extension id must use vendor/name format with lowercase letters, numbers, and hyphens.');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!options.id) {
|
|
189
|
+
console.log('Notur extension setup');
|
|
190
|
+
while (!options.id) {
|
|
191
|
+
const id = await prompt('Extension id', 'acme/red-button');
|
|
192
|
+
if (validateId(id)) {
|
|
193
|
+
options.id = id;
|
|
194
|
+
} else {
|
|
195
|
+
console.log('Use vendor/name format with lowercase letters, numbers, and hyphens.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const [, name] = options.id.split('/');
|
|
201
|
+
if (!options.displayName && isInteractive()) {
|
|
202
|
+
options.displayName = await prompt('Display name', displayName(name));
|
|
203
|
+
}
|
|
204
|
+
if (!options.description && isInteractive()) {
|
|
205
|
+
options.description = await prompt('Description', 'A Notur extension');
|
|
206
|
+
}
|
|
207
|
+
if (!options.preset && isInteractive()) {
|
|
208
|
+
options.preset = await select('Preset', [
|
|
209
|
+
{ value: 'frontend' },
|
|
210
|
+
{ value: 'backend' },
|
|
211
|
+
{ value: 'full' },
|
|
212
|
+
{ value: 'minimal' },
|
|
213
|
+
], 'frontend');
|
|
214
|
+
}
|
|
215
|
+
if (isInteractive() && ['frontend', 'full'].includes(options.preset) && options.withFrontend !== false) {
|
|
216
|
+
options.slot = await prompt('Initial frontend slot', options.slot);
|
|
217
|
+
}
|
|
218
|
+
if (!options.packageManager && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
|
|
219
|
+
options.packageManager = await select('Package manager', [
|
|
220
|
+
{ value: 'npm' },
|
|
221
|
+
{ value: 'pnpm' },
|
|
222
|
+
{ value: 'yarn' },
|
|
223
|
+
{ value: 'bun' },
|
|
224
|
+
], 'npm');
|
|
225
|
+
}
|
|
226
|
+
if (options.createEnv === null && isInteractive()) {
|
|
227
|
+
options.createEnv = await confirm('Create .env from .env.example?', false);
|
|
228
|
+
}
|
|
229
|
+
if (options.install === null && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
|
|
230
|
+
options.install = await confirm('Install frontend dependencies now?', false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return applyPreset(options);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function studly(value) {
|
|
237
|
+
return value
|
|
238
|
+
.split('-')
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
241
|
+
.join('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function displayName(value) {
|
|
245
|
+
return value
|
|
246
|
+
.split('-')
|
|
247
|
+
.filter(Boolean)
|
|
248
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
249
|
+
.join(' ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function libraryName(id) {
|
|
253
|
+
return id
|
|
254
|
+
.split(/[\/-]/)
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map(studly)
|
|
257
|
+
.join('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function yamlString(value) {
|
|
261
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeFile(filePath, content) {
|
|
265
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
266
|
+
fs.writeFileSync(filePath, content);
|
|
267
|
+
console.log(` created ${path.relative(process.cwd(), filePath)}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function ensureTarget(target, force) {
|
|
271
|
+
if (!fs.existsSync(target)) {
|
|
272
|
+
fs.mkdirSync(target, { recursive: true });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = fs.readdirSync(target);
|
|
277
|
+
if (entries.length > 0 || !force) {
|
|
278
|
+
console.error(`Error: target directory already exists: ${target}`);
|
|
279
|
+
console.error('Use --force only for an existing empty directory.');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function manifestTemplate({ id, className, namespace, frontend, apiRoutes, display, description, phpEntrypoint }) {
|
|
285
|
+
const frontendSection = frontend
|
|
286
|
+
? `
|
|
287
|
+
frontend:
|
|
288
|
+
bundle: "resources/frontend/dist/extension.js"
|
|
289
|
+
`
|
|
290
|
+
: '';
|
|
291
|
+
const phpSection = phpEntrypoint
|
|
292
|
+
? `
|
|
293
|
+
entrypoint: "${namespace.replace(/\\/g, '\\\\')}\\\\${className}"
|
|
294
|
+
autoload:
|
|
295
|
+
psr-4:
|
|
296
|
+
"${namespace.replace(/\\/g, '\\\\')}\\\\": "src/"
|
|
297
|
+
`
|
|
298
|
+
: '';
|
|
299
|
+
const backendSection = apiRoutes
|
|
300
|
+
? `
|
|
301
|
+
backend:
|
|
302
|
+
routes:
|
|
303
|
+
api-client: "src/routes/api-client.php"
|
|
304
|
+
`
|
|
305
|
+
: '';
|
|
306
|
+
|
|
307
|
+
return `notur: "1.0"
|
|
308
|
+
id: "${id}"
|
|
309
|
+
name: "${yamlString(display)}"
|
|
310
|
+
version: "1.0.0"
|
|
311
|
+
description: "${yamlString(description)}"
|
|
312
|
+
license: "MIT"
|
|
313
|
+
${phpSection}
|
|
314
|
+
${backendSection}
|
|
315
|
+
${frontendSection}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function phpTemplate({ namespace, className }) {
|
|
319
|
+
return `<?php
|
|
320
|
+
|
|
321
|
+
declare(strict_types=1);
|
|
322
|
+
|
|
323
|
+
namespace ${namespace};
|
|
324
|
+
|
|
325
|
+
use Notur\\Support\\NoturExtension;
|
|
326
|
+
|
|
327
|
+
class ${className} extends NoturExtension
|
|
328
|
+
{
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function apiRouteTemplate() {
|
|
334
|
+
return `<?php
|
|
335
|
+
|
|
336
|
+
use Illuminate\\Support\\Facades\\Route;
|
|
337
|
+
|
|
338
|
+
Route::get('/ping', function () {
|
|
339
|
+
return response()->json([
|
|
340
|
+
'message' => 'pong',
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function frontendTemplate({ id, slot }) {
|
|
347
|
+
return `import * as React from 'react';
|
|
348
|
+
import { createExtension } from '@notur/sdk';
|
|
349
|
+
|
|
350
|
+
const ExampleButton: React.FC = () => {
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
style={{
|
|
354
|
+
background: '#dc2626',
|
|
355
|
+
color: '#fff',
|
|
356
|
+
border: 0,
|
|
357
|
+
borderRadius: '6px',
|
|
358
|
+
padding: '8px 12px',
|
|
359
|
+
fontWeight: 600,
|
|
360
|
+
cursor: 'pointer',
|
|
361
|
+
}}
|
|
362
|
+
onClick={() => alert('Hello from Notur')}
|
|
363
|
+
>
|
|
364
|
+
Red Button
|
|
365
|
+
</button>
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
createExtension({
|
|
370
|
+
id: '${id}',
|
|
371
|
+
slots: [
|
|
372
|
+
{
|
|
373
|
+
slot: '${slot}',
|
|
374
|
+
component: ExampleButton,
|
|
375
|
+
order: 10,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function packageTemplate(id) {
|
|
383
|
+
return `${JSON.stringify({
|
|
384
|
+
name: id.replace('/', '-'),
|
|
385
|
+
version: '1.0.0',
|
|
386
|
+
private: true,
|
|
387
|
+
scripts: {
|
|
388
|
+
build: 'webpack-cli --mode production --config webpack.config.js',
|
|
389
|
+
dev: 'webpack-cli --mode development --watch --config webpack.config.js',
|
|
390
|
+
pack: 'notur-pack',
|
|
391
|
+
push: 'notur-push',
|
|
392
|
+
sync: 'notur-sync',
|
|
393
|
+
validate: 'notur-validate',
|
|
394
|
+
doctor: 'notur-doctor',
|
|
395
|
+
},
|
|
396
|
+
peerDependencies: {
|
|
397
|
+
react: '^16.14.0',
|
|
398
|
+
'react-dom': '^16.14.0',
|
|
399
|
+
},
|
|
400
|
+
devDependencies: {
|
|
401
|
+
'@notur/sdk': '^1.4.7',
|
|
402
|
+
'@types/react': '^16.14.0',
|
|
403
|
+
'@types/react-dom': '^16.9.0',
|
|
404
|
+
react: '^16.14.0',
|
|
405
|
+
'react-dom': '^16.14.0',
|
|
406
|
+
'ts-loader': '^9.5.0',
|
|
407
|
+
typescript: '^5.3.0',
|
|
408
|
+
webpack: '^5.90.0',
|
|
409
|
+
'webpack-cli': '^6.0.0',
|
|
410
|
+
},
|
|
411
|
+
}, null, 2)}
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function tsconfigTemplate() {
|
|
416
|
+
return `{
|
|
417
|
+
"compilerOptions": {
|
|
418
|
+
"target": "ES2019",
|
|
419
|
+
"module": "ESNext",
|
|
420
|
+
"moduleResolution": "Node",
|
|
421
|
+
"jsx": "react",
|
|
422
|
+
"strict": true,
|
|
423
|
+
"esModuleInterop": true,
|
|
424
|
+
"skipLibCheck": true,
|
|
425
|
+
"forceConsistentCasingInFileNames": true
|
|
426
|
+
},
|
|
427
|
+
"include": ["resources/frontend/src/**/*"]
|
|
428
|
+
}
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function webpackTemplate(libName) {
|
|
433
|
+
return `const path = require('path');
|
|
434
|
+
const base = require('@notur/sdk/webpack.extension.config');
|
|
435
|
+
|
|
436
|
+
module.exports = {
|
|
437
|
+
...base,
|
|
438
|
+
entry: './resources/frontend/src/index.tsx',
|
|
439
|
+
output: {
|
|
440
|
+
...base.output,
|
|
441
|
+
filename: 'extension.js',
|
|
442
|
+
path: path.resolve(__dirname, 'resources/frontend/dist'),
|
|
443
|
+
library: {
|
|
444
|
+
...base.output.library,
|
|
445
|
+
name: '__NOTUR_EXT_${libName}__',
|
|
446
|
+
type: 'umd',
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readmeTemplate(id) {
|
|
454
|
+
return `# ${id}
|
|
455
|
+
|
|
456
|
+
Development:
|
|
457
|
+
|
|
458
|
+
\`\`\`bash
|
|
459
|
+
npm install
|
|
460
|
+
npm run build
|
|
461
|
+
npx notur-pack
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
Remote push to a Notur-enabled panel:
|
|
465
|
+
|
|
466
|
+
\`\`\`bash
|
|
467
|
+
cp .env.example .env
|
|
468
|
+
npm run push
|
|
469
|
+
\`\`\`
|
|
470
|
+
|
|
471
|
+
Or pass values directly:
|
|
472
|
+
|
|
473
|
+
\`\`\`bash
|
|
474
|
+
npx notur-push --host https://panel.example.com --key notur_xxx
|
|
475
|
+
\`\`\`
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function installCommand(packageManager) {
|
|
480
|
+
if (packageManager === 'pnpm') return ['pnpm', ['install']];
|
|
481
|
+
if (packageManager === 'yarn') return ['yarn', ['install']];
|
|
482
|
+
if (packageManager === 'bun') return ['bun', ['install']];
|
|
483
|
+
return ['npm', ['install']];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function runScriptCommand(packageManager, script) {
|
|
487
|
+
if (packageManager === 'bun') return `bun run ${script}`;
|
|
488
|
+
if (packageManager === 'pnpm') return `pnpm run ${script}`;
|
|
489
|
+
if (packageManager === 'yarn') return `yarn ${script}`;
|
|
490
|
+
return `npm run ${script}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function runInstall(target, packageManager) {
|
|
494
|
+
const [command, args] = installCommand(packageManager || 'npm');
|
|
495
|
+
console.log(`\nRunning ${command} ${args.join(' ')}...`);
|
|
496
|
+
const result = spawnSync(command, args, {
|
|
497
|
+
cwd: target,
|
|
498
|
+
stdio: 'inherit',
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (result.status !== 0) {
|
|
502
|
+
console.warn(`Dependency install failed. Run ${command} ${args.join(' ')} manually in ${target}.`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function main() {
|
|
507
|
+
const options = await resolveOptions(parseArgs());
|
|
508
|
+
const [vendor, name] = options.id.split('/');
|
|
509
|
+
const namespace = `${studly(vendor)}\\${studly(name)}`;
|
|
510
|
+
const className = `${studly(name)}Extension`;
|
|
511
|
+
const target = path.resolve(options.path, name);
|
|
512
|
+
const needsPhpEntrypoint = options.withApiRoutes;
|
|
513
|
+
|
|
514
|
+
ensureTarget(target, options.force);
|
|
515
|
+
|
|
516
|
+
console.log(`Scaffolding ${options.id} in ${target}`);
|
|
517
|
+
|
|
518
|
+
writeFile(path.join(target, 'extension.yaml'), manifestTemplate({
|
|
519
|
+
id: options.id,
|
|
520
|
+
vendor,
|
|
521
|
+
name,
|
|
522
|
+
namespace,
|
|
523
|
+
className,
|
|
524
|
+
frontend: options.withFrontend,
|
|
525
|
+
apiRoutes: options.withApiRoutes,
|
|
526
|
+
display: options.displayName || displayName(name),
|
|
527
|
+
description: options.description || 'A Notur extension',
|
|
528
|
+
phpEntrypoint: needsPhpEntrypoint,
|
|
529
|
+
}));
|
|
530
|
+
if (needsPhpEntrypoint) {
|
|
531
|
+
writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
|
|
532
|
+
}
|
|
533
|
+
if (options.withApiRoutes) {
|
|
534
|
+
writeFile(path.join(target, 'src/routes/api-client.php'), apiRouteTemplate());
|
|
535
|
+
}
|
|
536
|
+
writeFile(path.join(target, 'README.md'), readmeTemplate(options.id));
|
|
537
|
+
writeFile(path.join(target, '.env.example'), `NOTUR_HOST=https://panel.example.com
|
|
538
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
539
|
+
`);
|
|
540
|
+
if (options.createEnv) {
|
|
541
|
+
writeFile(path.join(target, '.env'), `NOTUR_HOST=https://panel.example.com
|
|
542
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
543
|
+
`);
|
|
544
|
+
}
|
|
545
|
+
writeFile(path.join(target, '.gitignore'), `node_modules/
|
|
546
|
+
vendor/
|
|
547
|
+
resources/frontend/dist/
|
|
548
|
+
.env
|
|
549
|
+
*.notur
|
|
550
|
+
*.notur.sha256
|
|
551
|
+
*.notur.sig
|
|
552
|
+
`);
|
|
553
|
+
|
|
554
|
+
if (options.withFrontend) {
|
|
555
|
+
writeFile(path.join(target, 'resources/frontend/src/index.tsx'), frontendTemplate({
|
|
556
|
+
id: options.id,
|
|
557
|
+
slot: options.slot,
|
|
558
|
+
}));
|
|
559
|
+
writeFile(path.join(target, 'package.json'), packageTemplate(options.id));
|
|
560
|
+
writeFile(path.join(target, 'tsconfig.json'), tsconfigTemplate());
|
|
561
|
+
writeFile(path.join(target, 'webpack.config.js'), webpackTemplate(libraryName(options.id)));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (options.install && options.withFrontend) {
|
|
565
|
+
runInstall(target, options.packageManager);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log('\nNext steps:');
|
|
569
|
+
if (options.withFrontend) {
|
|
570
|
+
console.log(` cd ${target}`);
|
|
571
|
+
if (!options.install) {
|
|
572
|
+
const [command, args] = installCommand(options.packageManager || 'npm');
|
|
573
|
+
console.log(` ${command} ${args.join(' ')}`);
|
|
574
|
+
}
|
|
575
|
+
console.log(` ${runScriptCommand(options.packageManager || 'npm', 'build')}`);
|
|
576
|
+
console.log(' npx notur-pack');
|
|
577
|
+
} else {
|
|
578
|
+
console.log(` cd ${target}`);
|
|
579
|
+
console.log(' npx notur-pack');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
main().catch(error => {
|
|
584
|
+
console.error(error?.message || String(error));
|
|
585
|
+
process.exit(1);
|
|
586
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const options = { path: '.', remote: true };
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const arg = args[i];
|
|
12
|
+
if (arg === '--no-remote') {
|
|
13
|
+
options.remote = false;
|
|
14
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
15
|
+
usage(0);
|
|
16
|
+
} else if (!arg.startsWith('-')) {
|
|
17
|
+
options.path = arg;
|
|
18
|
+
} else {
|
|
19
|
+
console.error(`Unknown argument: ${arg}`);
|
|
20
|
+
usage(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return options;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function usage(code) {
|
|
27
|
+
console.log(`Usage:
|
|
28
|
+
npx notur-doctor [path]
|
|
29
|
+
npx @notur/sdk doctor [path]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--no-remote Skip remote host/key checks`);
|
|
33
|
+
process.exit(code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseEnv(basePath) {
|
|
37
|
+
const envPath = path.join(basePath, '.env');
|
|
38
|
+
if (!fs.existsSync(envPath)) return {};
|
|
39
|
+
const values = {};
|
|
40
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
43
|
+
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
44
|
+
if (!match) continue;
|
|
45
|
+
values[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '');
|
|
46
|
+
}
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function check(label, ok, detail = '') {
|
|
51
|
+
const mark = ok ? 'ok' : 'fail';
|
|
52
|
+
console.log(`${mark}: ${label}${detail ? ` - ${detail}` : ''}`);
|
|
53
|
+
return ok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function warn(label, ok, detail = '') {
|
|
57
|
+
const mark = ok ? 'ok' : 'warn';
|
|
58
|
+
console.log(`${mark}: ${label}${detail ? ` - ${detail}` : ''}`);
|
|
59
|
+
return ok;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function commandExists(command) {
|
|
63
|
+
const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
|
|
64
|
+
return !result.error && result.status === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main() {
|
|
68
|
+
const options = parseArgs();
|
|
69
|
+
const basePath = path.resolve(options.path);
|
|
70
|
+
let ok = true;
|
|
71
|
+
|
|
72
|
+
console.log(`Notur doctor for ${basePath}`);
|
|
73
|
+
|
|
74
|
+
ok = check('extension directory exists', fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) && ok;
|
|
75
|
+
ok = check('extension.yaml exists', fs.existsSync(path.join(basePath, 'extension.yaml')) || fs.existsSync(path.join(basePath, 'extension.yml'))) && ok;
|
|
76
|
+
|
|
77
|
+
const packagePath = path.join(basePath, 'package.json');
|
|
78
|
+
const hasPackage = fs.existsSync(packagePath);
|
|
79
|
+
check('package.json exists', hasPackage);
|
|
80
|
+
if (hasPackage) {
|
|
81
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
82
|
+
ok = check('build script', Boolean(pkg.scripts?.build)) && ok;
|
|
83
|
+
check('push script', Boolean(pkg.scripts?.push));
|
|
84
|
+
check('@notur/sdk dependency', Boolean(pkg.dependencies?.['@notur/sdk'] || pkg.devDependencies?.['@notur/sdk']));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
check('node available', Boolean(process.version), process.version);
|
|
88
|
+
check('npm available', commandExists('npm'));
|
|
89
|
+
check('webpack config exists', fs.existsSync(path.join(basePath, 'webpack.config.js')));
|
|
90
|
+
|
|
91
|
+
const bundleCandidates = [
|
|
92
|
+
'resources/frontend/dist/extension.js',
|
|
93
|
+
'resources/frontend/dist/bundle.js',
|
|
94
|
+
'dist/extension.js',
|
|
95
|
+
'dist/bundle.js',
|
|
96
|
+
];
|
|
97
|
+
warn('frontend bundle exists', bundleCandidates.some(file => fs.existsSync(path.join(basePath, file))), 'run npm run build before packaging');
|
|
98
|
+
|
|
99
|
+
const validateResult = spawnSync(process.execPath, [path.join(__dirname, 'notur-validate.js'), basePath], {
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
encoding: 'utf8',
|
|
102
|
+
});
|
|
103
|
+
ok = check('notur validate', validateResult.status === 0) && ok;
|
|
104
|
+
if (validateResult.stdout.trim()) console.log(validateResult.stdout.trim());
|
|
105
|
+
if (validateResult.stderr.trim()) console.log(validateResult.stderr.trim());
|
|
106
|
+
|
|
107
|
+
if (options.remote) {
|
|
108
|
+
const env = parseEnv(basePath);
|
|
109
|
+
const host = process.env.NOTUR_HOST || env.NOTUR_HOST;
|
|
110
|
+
const key = process.env.NOTUR_PUSH_KEY || process.env.NOTUR_API_KEY || env.NOTUR_PUSH_KEY || env.NOTUR_API_KEY;
|
|
111
|
+
check('remote host configured', Boolean(host), host ? new URL(host).origin : '');
|
|
112
|
+
check('remote push key configured', Boolean(key));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!ok) {
|
|
116
|
+
console.error('Notur doctor found blocking issues.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('Notur doctor completed.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main();
|