@notur/sdk 1.4.6 → 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 +44 -0
- package/bin/notur-create.js +19 -13
- package/bin/notur-doctor.js +123 -0
- package/bin/notur-pack.js +13 -7
- package/bin/notur-sync.js +199 -0
- package/bin/notur-validate.js +200 -0
- package/bin/notur.js +7 -1
- 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/extension.yaml +0 -10
- package/examples/red-button/package.json +5 -2
- package/package.json +5 -2
- package/examples/red-button/src/RedButtonExtension.php +0 -11
package/README.md
CHANGED
|
@@ -145,6 +145,50 @@ Options:
|
|
|
145
145
|
- `--with-api-routes` — include a client API route stub.
|
|
146
146
|
- `--force` — allow writing into an existing empty directory.
|
|
147
147
|
|
|
148
|
+
Frontend-only extensions are manifest-only by default. They do not need a PHP entrypoint unless you choose backend features such as API routes.
|
|
149
|
+
|
|
150
|
+
### `notur-sync` — Sync generated metadata
|
|
151
|
+
|
|
152
|
+
Reads `extension.yaml` and updates derived local development files.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx notur-sync
|
|
156
|
+
npx @notur/sdk sync
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
It keeps `package.json` name/version/scripts aligned, creates `.env.example` if missing, and creates `webpack.config.js` for frontend extensions that do not already have one.
|
|
160
|
+
|
|
161
|
+
Options:
|
|
162
|
+
- `--force-webpack` — regenerate `webpack.config.js` from `extension.yaml`.
|
|
163
|
+
|
|
164
|
+
### `notur-validate` — Validate extension structure
|
|
165
|
+
|
|
166
|
+
Runs local checks before packaging or pushing:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx notur-validate
|
|
170
|
+
npx @notur/sdk validate
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Checks include manifest shape, id format, route file paths, bundle path, package metadata drift, missing scripts, `createExtension()` usage, and unknown slot IDs.
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
- `--strict` — treat warnings as errors.
|
|
177
|
+
|
|
178
|
+
### `notur-doctor` — Diagnose local and remote setup
|
|
179
|
+
|
|
180
|
+
Summarizes whether the local extension is buildable and ready to push:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx notur-doctor
|
|
184
|
+
npx @notur/sdk doctor
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It checks manifest/package/build files, runs `notur-validate`, and verifies local `.env` or shell variables for remote push.
|
|
188
|
+
|
|
189
|
+
Options:
|
|
190
|
+
- `--no-remote` — skip remote host/key checks.
|
|
191
|
+
|
|
148
192
|
### `notur-pack` — Package extensions
|
|
149
193
|
|
|
150
194
|
Creates a `.notur` archive (tar.gz) from your extension directory, ready to upload to the Pterodactyl admin panel.
|
package/bin/notur-create.js
CHANGED
|
@@ -281,11 +281,19 @@ function ensureTarget(target, force) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
function manifestTemplate({ id,
|
|
284
|
+
function manifestTemplate({ id, className, namespace, frontend, apiRoutes, display, description, phpEntrypoint }) {
|
|
285
285
|
const frontendSection = frontend
|
|
286
286
|
? `
|
|
287
287
|
frontend:
|
|
288
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/"
|
|
289
297
|
`
|
|
290
298
|
: '';
|
|
291
299
|
const backendSection = apiRoutes
|
|
@@ -302,16 +310,7 @@ name: "${yamlString(display)}"
|
|
|
302
310
|
version: "1.0.0"
|
|
303
311
|
description: "${yamlString(description)}"
|
|
304
312
|
license: "MIT"
|
|
305
|
-
|
|
306
|
-
requires:
|
|
307
|
-
notur: "^1.0"
|
|
308
|
-
pterodactyl: "^1.12"
|
|
309
|
-
php: "^8.2"
|
|
310
|
-
|
|
311
|
-
entrypoint: "${namespace.replace(/\\/g, '\\\\')}\\\\${className}"
|
|
312
|
-
autoload:
|
|
313
|
-
psr-4:
|
|
314
|
-
"${namespace.replace(/\\/g, '\\\\')}\\\\": "src/"
|
|
313
|
+
${phpSection}
|
|
315
314
|
${backendSection}
|
|
316
315
|
${frontendSection}`;
|
|
317
316
|
}
|
|
@@ -390,13 +389,16 @@ function packageTemplate(id) {
|
|
|
390
389
|
dev: 'webpack-cli --mode development --watch --config webpack.config.js',
|
|
391
390
|
pack: 'notur-pack',
|
|
392
391
|
push: 'notur-push',
|
|
392
|
+
sync: 'notur-sync',
|
|
393
|
+
validate: 'notur-validate',
|
|
394
|
+
doctor: 'notur-doctor',
|
|
393
395
|
},
|
|
394
396
|
peerDependencies: {
|
|
395
397
|
react: '^16.14.0',
|
|
396
398
|
'react-dom': '^16.14.0',
|
|
397
399
|
},
|
|
398
400
|
devDependencies: {
|
|
399
|
-
'@notur/sdk': '^1.4.
|
|
401
|
+
'@notur/sdk': '^1.4.7',
|
|
400
402
|
'@types/react': '^16.14.0',
|
|
401
403
|
'@types/react-dom': '^16.9.0',
|
|
402
404
|
react: '^16.14.0',
|
|
@@ -507,6 +509,7 @@ async function main() {
|
|
|
507
509
|
const namespace = `${studly(vendor)}\\${studly(name)}`;
|
|
508
510
|
const className = `${studly(name)}Extension`;
|
|
509
511
|
const target = path.resolve(options.path, name);
|
|
512
|
+
const needsPhpEntrypoint = options.withApiRoutes;
|
|
510
513
|
|
|
511
514
|
ensureTarget(target, options.force);
|
|
512
515
|
|
|
@@ -522,8 +525,11 @@ async function main() {
|
|
|
522
525
|
apiRoutes: options.withApiRoutes,
|
|
523
526
|
display: options.displayName || displayName(name),
|
|
524
527
|
description: options.description || 'A Notur extension',
|
|
528
|
+
phpEntrypoint: needsPhpEntrypoint,
|
|
525
529
|
}));
|
|
526
|
-
|
|
530
|
+
if (needsPhpEntrypoint) {
|
|
531
|
+
writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
|
|
532
|
+
}
|
|
527
533
|
if (options.withApiRoutes) {
|
|
528
534
|
writeFile(path.join(target, 'src/routes/api-client.php'), apiRouteTemplate());
|
|
529
535
|
}
|
|
@@ -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();
|
package/bin/notur-pack.js
CHANGED
|
@@ -257,13 +257,19 @@ async function pack(sourceDir, outputPath, options = {}) {
|
|
|
257
257
|
];
|
|
258
258
|
|
|
259
259
|
if (options.dryRun) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
try {
|
|
261
|
+
console.log('\nDry run mode (no archive written).');
|
|
262
|
+
console.log(`Would create: ${outputFullPath}`);
|
|
263
|
+
console.log(`Would include: ${archiveEntries.length} files`);
|
|
264
|
+
if (deterministicArgs.length > 0) {
|
|
265
|
+
console.log('Deterministic archive mode: enabled (GNU tar flags).');
|
|
266
|
+
} else {
|
|
267
|
+
console.log('Deterministic archive mode: not available (GNU tar not detected).');
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
if (!checksumsExisted && fs.existsSync(checksumsPath)) {
|
|
271
|
+
fs.unlinkSync(checksumsPath);
|
|
272
|
+
}
|
|
267
273
|
}
|
|
268
274
|
return;
|
|
269
275
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('yaml');
|
|
6
|
+
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const options = {
|
|
10
|
+
path: '.',
|
|
11
|
+
forceWebpack: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === '--force-webpack') {
|
|
17
|
+
options.forceWebpack = true;
|
|
18
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
19
|
+
usage(0);
|
|
20
|
+
} else if (!arg.startsWith('-')) {
|
|
21
|
+
options.path = arg;
|
|
22
|
+
} else {
|
|
23
|
+
console.error(`Unknown argument: ${arg}`);
|
|
24
|
+
usage(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usage(code) {
|
|
32
|
+
console.log(`Usage:
|
|
33
|
+
npx notur-sync [path]
|
|
34
|
+
npx @notur/sdk sync [path]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--force-webpack Regenerate webpack.config.js from extension.yaml`);
|
|
38
|
+
process.exit(code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readManifest(extensionPath) {
|
|
42
|
+
const manifestPath = ['extension.yaml', 'extension.yml']
|
|
43
|
+
.map(file => path.join(extensionPath, file))
|
|
44
|
+
.find(file => fs.existsSync(file));
|
|
45
|
+
|
|
46
|
+
if (!manifestPath) {
|
|
47
|
+
console.error(`Error: extension.yaml not found in ${extensionPath}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const manifest = yaml.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
52
|
+
if (!manifest?.id || !manifest?.version) {
|
|
53
|
+
console.error('Error: extension.yaml must contain id and version.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return manifest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packageName(id) {
|
|
61
|
+
return id.replace('/', '-');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function studly(value) {
|
|
65
|
+
return value
|
|
66
|
+
.split('-')
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
69
|
+
.join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function libraryName(id) {
|
|
73
|
+
return id
|
|
74
|
+
.split(/[\/-]/)
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map(studly)
|
|
77
|
+
.join('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function bundlePath(manifest) {
|
|
81
|
+
return manifest?.frontend?.bundle || 'resources/frontend/dist/extension.js';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function syncPackageJson(extensionPath, manifest) {
|
|
85
|
+
const packagePath = path.join(extensionPath, 'package.json');
|
|
86
|
+
const current = fs.existsSync(packagePath)
|
|
87
|
+
? JSON.parse(fs.readFileSync(packagePath, 'utf8'))
|
|
88
|
+
: {};
|
|
89
|
+
|
|
90
|
+
const next = {
|
|
91
|
+
...current,
|
|
92
|
+
name: packageName(manifest.id),
|
|
93
|
+
version: manifest.version,
|
|
94
|
+
private: current.private ?? true,
|
|
95
|
+
scripts: {
|
|
96
|
+
...(current.scripts || {}),
|
|
97
|
+
build: current.scripts?.build || 'webpack-cli --mode production --config webpack.config.js',
|
|
98
|
+
dev: current.scripts?.dev || 'webpack-cli --mode development --watch --config webpack.config.js',
|
|
99
|
+
pack: current.scripts?.pack || 'notur-pack',
|
|
100
|
+
push: current.scripts?.push || 'notur-push',
|
|
101
|
+
sync: current.scripts?.sync || 'notur-sync',
|
|
102
|
+
validate: current.scripts?.validate || 'notur-validate',
|
|
103
|
+
doctor: current.scripts?.doctor || 'notur-doctor',
|
|
104
|
+
},
|
|
105
|
+
peerDependencies: {
|
|
106
|
+
react: '^16.14.0',
|
|
107
|
+
'react-dom': '^16.14.0',
|
|
108
|
+
...(current.peerDependencies || {}),
|
|
109
|
+
},
|
|
110
|
+
devDependencies: {
|
|
111
|
+
'@notur/sdk': '^1.4.7',
|
|
112
|
+
'@types/react': '^16.14.0',
|
|
113
|
+
'@types/react-dom': '^16.9.0',
|
|
114
|
+
react: '^16.14.0',
|
|
115
|
+
'react-dom': '^16.14.0',
|
|
116
|
+
'ts-loader': '^9.5.0',
|
|
117
|
+
typescript: '^5.3.0',
|
|
118
|
+
webpack: '^5.90.0',
|
|
119
|
+
'webpack-cli': '^6.0.0',
|
|
120
|
+
...(current.devDependencies || {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(packagePath, JSON.stringify(next, null, 2) + '\n');
|
|
125
|
+
console.log(` synced ${path.relative(process.cwd(), packagePath)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function webpackTemplate(manifest) {
|
|
129
|
+
const bundle = bundlePath(manifest);
|
|
130
|
+
const filename = path.basename(bundle);
|
|
131
|
+
const outputDir = path.dirname(bundle);
|
|
132
|
+
|
|
133
|
+
return `const path = require('path');
|
|
134
|
+
const base = require('@notur/sdk/webpack.extension.config');
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
...base,
|
|
138
|
+
entry: './resources/frontend/src/index.tsx',
|
|
139
|
+
output: {
|
|
140
|
+
...base.output,
|
|
141
|
+
filename: '${filename}',
|
|
142
|
+
path: path.resolve(__dirname, '${outputDir.replace(/\\/g, '/')}'),
|
|
143
|
+
library: {
|
|
144
|
+
...base.output.library,
|
|
145
|
+
name: '__NOTUR_EXT_${libraryName(manifest.id)}__',
|
|
146
|
+
type: 'umd',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function syncWebpack(extensionPath, manifest, force) {
|
|
154
|
+
if (!manifest.frontend) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const webpackPath = path.join(extensionPath, 'webpack.config.js');
|
|
159
|
+
if (fs.existsSync(webpackPath) && !force) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(webpackPath, webpackTemplate(manifest));
|
|
164
|
+
console.log(` synced ${path.relative(process.cwd(), webpackPath)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function syncEnvExample(extensionPath) {
|
|
168
|
+
const envExamplePath = path.join(extensionPath, '.env.example');
|
|
169
|
+
if (fs.existsSync(envExamplePath)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(envExamplePath, `NOTUR_HOST=https://panel.example.com
|
|
174
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
175
|
+
`);
|
|
176
|
+
console.log(` synced ${path.relative(process.cwd(), envExamplePath)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function main() {
|
|
180
|
+
const options = parseArgs();
|
|
181
|
+
const extensionPath = path.resolve(options.path);
|
|
182
|
+
if (!fs.existsSync(extensionPath) || !fs.statSync(extensionPath).isDirectory()) {
|
|
183
|
+
console.error(`Error: extension path does not exist: ${extensionPath}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const manifest = readManifest(extensionPath);
|
|
188
|
+
console.log(`Syncing ${manifest.id} from extension.yaml`);
|
|
189
|
+
|
|
190
|
+
if (manifest.frontend) {
|
|
191
|
+
syncPackageJson(extensionPath, manifest);
|
|
192
|
+
syncWebpack(extensionPath, manifest, options.forceWebpack);
|
|
193
|
+
syncEnvExample(extensionPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('Done.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('yaml');
|
|
6
|
+
|
|
7
|
+
const KNOWN_SLOTS = new Set([
|
|
8
|
+
'navbar', 'navbar.left', 'navbar.before', 'navbar.after',
|
|
9
|
+
'server.subnav', 'server.subnav.before', 'server.subnav.after', 'server.header', 'server.page', 'server.footer',
|
|
10
|
+
'server.terminal.buttons', 'server.console.header', 'server.console.info.before', 'server.console.info.after',
|
|
11
|
+
'server.console.sidebar', 'server.console.command', 'server.console.footer', 'server.files.actions',
|
|
12
|
+
'server.files.header', 'server.files.footer', 'server.files.dropdown', 'server.files.edit.before',
|
|
13
|
+
'server.files.edit.after', 'server.databases.before', 'server.databases.after', 'server.schedules.before',
|
|
14
|
+
'server.schedules.after', 'server.schedules.edit.before', 'server.schedules.edit.after', 'server.users.before',
|
|
15
|
+
'server.users.after', 'server.backups.before', 'server.backups.after', 'server.backups.dropdown',
|
|
16
|
+
'server.network.before', 'server.network.after', 'server.startup.before', 'server.startup.after',
|
|
17
|
+
'server.settings.before', 'server.settings.after', 'dashboard.header', 'dashboard.widgets',
|
|
18
|
+
'dashboard.serverlist.before', 'dashboard.serverlist.after', 'dashboard.serverrow.name.before',
|
|
19
|
+
'dashboard.serverrow.name.after', 'dashboard.serverrow.description.before', 'dashboard.serverrow.description.after',
|
|
20
|
+
'dashboard.serverrow.limits', 'dashboard.footer', 'dashboard.page', 'account.header', 'account.page',
|
|
21
|
+
'account.footer', 'account.subnav', 'account.subnav.before', 'account.subnav.after',
|
|
22
|
+
'account.overview.before', 'account.overview.after', 'account.api.before', 'account.api.after',
|
|
23
|
+
'account.ssh.before', 'account.ssh.after', 'auth.container.before', 'auth.container.after',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function parseArgs() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const options = { path: '.', strict: false };
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === '--strict') {
|
|
32
|
+
options.strict = true;
|
|
33
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
34
|
+
usage(0);
|
|
35
|
+
} else if (!arg.startsWith('-')) {
|
|
36
|
+
options.path = arg;
|
|
37
|
+
} else {
|
|
38
|
+
console.error(`Unknown argument: ${arg}`);
|
|
39
|
+
usage(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function usage(code) {
|
|
46
|
+
console.log(`Usage:
|
|
47
|
+
npx notur-validate [path]
|
|
48
|
+
npx @notur/sdk validate [path]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--strict Treat warnings as errors`);
|
|
52
|
+
process.exit(code);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readManifest(basePath, result) {
|
|
56
|
+
const manifestPath = ['extension.yaml', 'extension.yml']
|
|
57
|
+
.map(file => path.join(basePath, file))
|
|
58
|
+
.find(file => fs.existsSync(file));
|
|
59
|
+
if (!manifestPath) {
|
|
60
|
+
result.errors.push('extension.yaml not found.');
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return yaml.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
result.errors.push(`extension.yaml could not be parsed: ${error.message}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateManifest(basePath, manifest, result) {
|
|
73
|
+
if (!manifest) return;
|
|
74
|
+
|
|
75
|
+
for (const field of ['id', 'name', 'version']) {
|
|
76
|
+
if (!manifest[field]) {
|
|
77
|
+
result.errors.push(`extension.yaml is missing required field: ${field}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (manifest.id && !/^[a-z0-9-]+\/[a-z0-9-]+$/.test(manifest.id)) {
|
|
82
|
+
result.errors.push(`extension id "${manifest.id}" must use vendor/name with lowercase letters, numbers, and hyphens.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (manifest.frontend?.bundle) {
|
|
86
|
+
const bundlePath = path.join(basePath, manifest.frontend.bundle);
|
|
87
|
+
if (!fs.existsSync(bundlePath)) {
|
|
88
|
+
result.warnings.push(`frontend.bundle does not exist yet: ${manifest.frontend.bundle}. Run your build before packaging.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const routes = manifest.backend?.routes;
|
|
93
|
+
if (routes && typeof routes === 'object') {
|
|
94
|
+
for (const [group, routeFile] of Object.entries(routes)) {
|
|
95
|
+
if (typeof routeFile !== 'string') {
|
|
96
|
+
result.errors.push(`backend.routes.${group} must be a string path.`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!fs.existsSync(path.join(basePath, routeFile))) {
|
|
100
|
+
result.errors.push(`backend.routes.${group} points to missing file: ${routeFile}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readPackage(basePath, result) {
|
|
107
|
+
const packagePath = path.join(basePath, 'package.json');
|
|
108
|
+
if (!fs.existsSync(packagePath)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
result.errors.push(`package.json could not be parsed: ${error.message}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validatePackage(manifest, pkg, result) {
|
|
120
|
+
if (!manifest || !pkg) return;
|
|
121
|
+
|
|
122
|
+
const expectedName = manifest.id?.replace('/', '-');
|
|
123
|
+
if (expectedName && pkg.name && pkg.name !== expectedName) {
|
|
124
|
+
result.warnings.push(`package.json name "${pkg.name}" differs from manifest-derived "${expectedName}". Run npx notur-sync.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (manifest.version && pkg.version && pkg.version !== manifest.version) {
|
|
128
|
+
result.warnings.push(`package.json version "${pkg.version}" differs from extension.yaml version "${manifest.version}". Run npx notur-sync.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const script of ['build', 'pack', 'push', 'sync']) {
|
|
132
|
+
if (!pkg.scripts?.[script]) {
|
|
133
|
+
result.warnings.push(`package.json is missing "${script}" script.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateFrontendSource(basePath, manifest, result) {
|
|
139
|
+
const indexPath = path.join(basePath, 'resources/frontend/src/index.tsx');
|
|
140
|
+
if (!fs.existsSync(indexPath)) {
|
|
141
|
+
if (manifest?.frontend) {
|
|
142
|
+
result.warnings.push('frontend is configured but resources/frontend/src/index.tsx was not found.');
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const source = fs.readFileSync(indexPath, 'utf8');
|
|
148
|
+
if (!source.includes('createExtension')) {
|
|
149
|
+
result.warnings.push('frontend entry does not appear to call createExtension().');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (manifest?.id && !source.includes(manifest.id)) {
|
|
153
|
+
result.warnings.push(`frontend entry does not appear to reference manifest id "${manifest.id}".`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const slotMatches = source.matchAll(/slot:\s*['"`]([^'"`]+)['"`]/g);
|
|
157
|
+
for (const match of slotMatches) {
|
|
158
|
+
if (!KNOWN_SLOTS.has(match[1])) {
|
|
159
|
+
result.warnings.push(`unknown slot id "${match[1]}". Check docs/slot-reference.md or SDK SlotId.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function printResult(result, strict) {
|
|
165
|
+
for (const warning of result.warnings) {
|
|
166
|
+
console.warn(`warning: ${warning}`);
|
|
167
|
+
}
|
|
168
|
+
for (const error of result.errors) {
|
|
169
|
+
console.error(`error: ${error}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const failed = result.errors.length > 0 || (strict && result.warnings.length > 0);
|
|
173
|
+
if (failed) {
|
|
174
|
+
console.error(`Notur validation failed (${result.errors.length} errors, ${result.warnings.length} warnings).`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`Notur validation passed (${result.warnings.length} warnings).`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function main() {
|
|
182
|
+
const options = parseArgs();
|
|
183
|
+
const basePath = path.resolve(options.path);
|
|
184
|
+
const result = { errors: [], warnings: [] };
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
|
|
187
|
+
result.errors.push(`path does not exist or is not a directory: ${basePath}`);
|
|
188
|
+
printResult(result, options.strict);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const manifest = readManifest(basePath, result);
|
|
193
|
+
validateManifest(basePath, manifest, result);
|
|
194
|
+
const pkg = readPackage(basePath, result);
|
|
195
|
+
validatePackage(manifest, pkg, result);
|
|
196
|
+
validateFrontendSource(basePath, manifest, result);
|
|
197
|
+
printResult(result, options.strict);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|