@kirigami/php-prepros 1.9.3 → 3.0.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 +502 -21
- package/README.md +1988 -1817
- package/index.d.ts +62 -26
- package/index.js +1 -1
- package/package.json +59 -59
- package/src/imagebatch.php +68 -67
- package/src/libraries/aliases.inc.php +879 -879
- package/src/libraries/curl.class.php +7 -7
- package/src/libraries/ld.class.php +803 -804
- package/src/libraries/md-legacy.class.php +1257 -0
- package/src/libraries/md.class.php +51 -1249
- package/src/libraries/meta.class.php +496 -506
- package/src/libraries/{normalizer.class.php → normalizer-legacy.class.php} +11 -1
- package/src/libraries/prepros.class.php +85 -8
- package/src/libraries/schema-legacy.class.php +549 -0
- package/src/libraries/schema.class.php +99 -472
- package/src/libraries/yaml-legacy.class.php +598 -0
- package/src/libraries/yaml.class.php +41 -406
- package/src/prepros.js +152 -48
- package/src/prepros.php +4 -5
- package/src/runenv.php +2 -2
- package/src/utils.inc.php +12 -2
package/src/prepros.js
CHANGED
|
@@ -5,7 +5,7 @@ import path, { dirname } from "path";
|
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
7
|
import { walkFile } from '@kirigami/struct-walker';
|
|
8
|
-
import {
|
|
8
|
+
import { createPHPRuntime } from "@kirigami/php-wasm";
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const __modules = new Map;
|
|
@@ -16,6 +16,23 @@ let __root = null;
|
|
|
16
16
|
let __php = null;
|
|
17
17
|
let __config = null;
|
|
18
18
|
|
|
19
|
+
// Mounts, PHP requests and resets share mutable state. Queue the entire
|
|
20
|
+
// operation so reload cannot dispose a runtime between mounting and execution.
|
|
21
|
+
let pending = Promise.resolve();
|
|
22
|
+
const serial = operation => (...args) => {
|
|
23
|
+
const result = pending.then(() => operation(...args));
|
|
24
|
+
pending = result.catch(() => {});
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const reset = async () => {
|
|
29
|
+
const php = __php;
|
|
30
|
+
__php = null;
|
|
31
|
+
__root = null;
|
|
32
|
+
__config = null;
|
|
33
|
+
if (php) php.exit();
|
|
34
|
+
};
|
|
35
|
+
|
|
19
36
|
|
|
20
37
|
// Load and cache kirigami.yaml on first use. Deferred (not run at import) so
|
|
21
38
|
// `import '@kirigami/php-prepros'` has no side effects — e.g. `kiri build -h`
|
|
@@ -29,7 +46,7 @@ const loadConfig = async () => {
|
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
|
|
32
|
-
const
|
|
49
|
+
const initializePHPInstance = async () => {
|
|
33
50
|
const config = await loadConfig();
|
|
34
51
|
if(!__php) {
|
|
35
52
|
if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
|
|
@@ -46,8 +63,9 @@ const getPHPInstance = async () => {
|
|
|
46
63
|
preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
47
64
|
preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
|
|
48
65
|
preprosConfig.data = config.kirigami || {};
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
// The SEO block, read by both META and LD; `jsonld: true` switches
|
|
67
|
+
// LD's JSON-LD on (see META/LD's docblocks).
|
|
68
|
+
preprosConfig.seo = config.seo ?? null;
|
|
51
69
|
// META auto-detects favicon / apple-touch-icon / humans.txt at the
|
|
52
70
|
// source root; those extensions aren't mounted into the sandbox, so the
|
|
53
71
|
// presence check is done here on the real filesystem instead.
|
|
@@ -60,16 +78,21 @@ const getPHPInstance = async () => {
|
|
|
60
78
|
// page's <head> with a <link>/<script> per sass/esbuild task output.
|
|
61
79
|
preprosConfig.tasks = config.tasks || [];
|
|
62
80
|
|
|
63
|
-
__php = await (preprosConfig.network
|
|
81
|
+
__php = await createPHPRuntime({ network: Boolean(preprosConfig.network) });
|
|
64
82
|
__php.setSpawnHandler((command, args, options) => spawn(command, args, options));
|
|
65
83
|
__php.preprosConfig = preprosConfig;
|
|
66
84
|
__php.setIniValues({
|
|
67
|
-
log_errors:
|
|
68
|
-
html_errors:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
log_errors: 1,
|
|
86
|
+
html_errors: 0,
|
|
87
|
+
// Diagnostics belong on stderr, never inside generated HTML.
|
|
88
|
+
display_errors: 0,
|
|
89
|
+
error_reporting: 32767,
|
|
90
|
+
error_log: 'php://stderr',
|
|
91
|
+
memory_limit: '2G',
|
|
92
|
+
// The framework bootstrap (autoloader, $argv/$config, aliases,
|
|
93
|
+
// the `boot` hook) — every entrypoint used to `include` this by
|
|
94
|
+
// hand; auto_prepend_file makes it a single config point instead.
|
|
95
|
+
auto_prepend_file: '/prepros/utils.inc.php',
|
|
73
96
|
});
|
|
74
97
|
|
|
75
98
|
await mountPath(__dirname, '/prepros', __php);
|
|
@@ -97,22 +120,34 @@ const getPHPInstance = async () => {
|
|
|
97
120
|
results: results
|
|
98
121
|
});
|
|
99
122
|
} catch(e) {
|
|
100
|
-
return {
|
|
123
|
+
return JSON.stringify({
|
|
101
124
|
success: false,
|
|
102
125
|
error: typeof e == 'string' ? e : e.message
|
|
103
|
-
}
|
|
126
|
+
});
|
|
104
127
|
}
|
|
105
128
|
});
|
|
106
129
|
}
|
|
107
130
|
return __php;
|
|
108
131
|
}
|
|
109
132
|
|
|
133
|
+
const getPHPInstance = async () => {
|
|
134
|
+
try {
|
|
135
|
+
return await initializePHPInstance();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await reset();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
110
142
|
|
|
111
143
|
const mountPath = async (localPath, virtualDir, php) => {
|
|
112
144
|
const config = await loadConfig();
|
|
113
145
|
php = php || await getPHPInstance();
|
|
114
146
|
if(!path.isAbsolute(localPath)) localPath = path.join(__project, localPath);
|
|
115
|
-
|
|
147
|
+
// POSIX separators: on Windows a nested path ("src\\about\\_index.php")
|
|
148
|
+
// would otherwise land at "/project/src\\about\\_index.php", a separate
|
|
149
|
+
// file, leaving the real page's VFS copy stale.
|
|
150
|
+
virtualDir = virtualDir || path.posix.join('/project', path.relative(__project, localPath).split(path.sep).join('/'));
|
|
116
151
|
const includeExtensions = new Set(['.php', '.json', '.yaml', '.yml', '.md', '.db', '.txt', ...(config?.prepros?.mountext || [])]);
|
|
117
152
|
const stat = fs.statSync(localPath);
|
|
118
153
|
if (stat.isDirectory()) {
|
|
@@ -140,27 +175,37 @@ const mountPath = async (localPath, virtualDir, php) => {
|
|
|
140
175
|
const run = async (args = [], script = null, mountfiles = []) => {
|
|
141
176
|
const php = await getPHPInstance();
|
|
142
177
|
|
|
143
|
-
await Promise.all(mountfiles.map(async
|
|
144
|
-
const file = path.resolve(item);
|
|
145
|
-
if(!fs.existsSync(file)) return;
|
|
146
|
-
if(!path.relative(__project, file)) return;
|
|
147
|
-
const dest = path.join('/project', file.replace(__project, '')).replaceAll('\\', '/');
|
|
178
|
+
await Promise.all(mountfiles.map(async ({ file, dest }) => {
|
|
148
179
|
const buf = fs.readFileSync(file);
|
|
149
180
|
const parentDir = dest.substring(0, dest.lastIndexOf('/'));
|
|
150
181
|
if (parentDir) php.mkdirTree(parentDir);
|
|
151
182
|
php.writeFile(dest, isBinary(buf) ? buf : buf.toString('utf8'));
|
|
152
183
|
}));
|
|
153
184
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
let stdout, stderr;
|
|
186
|
+
try {
|
|
187
|
+
const output = await php.runStream({
|
|
188
|
+
scriptPath: script || '/prepros/prepros.php',
|
|
189
|
+
env: {
|
|
190
|
+
PREPROS_ARGS: JSON.stringify(args),
|
|
191
|
+
PREPROS_CONFIG: JSON.stringify(php.preprosConfig)
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
stdout = await output.stdoutText;
|
|
195
|
+
stderr = await output.stderrText;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// The WASM runtime aborted (e.g. `RuntimeError: unreachable`): its
|
|
198
|
+
// heap and request state are gone, and reusing it only yields
|
|
199
|
+
// follow-up errors ("Cannot redeclare function ..."). Drop it so the
|
|
200
|
+
// next operation starts a fresh one, and report this run as failed.
|
|
201
|
+
try { await reset(); } catch { /* already dead */ }
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
files: [],
|
|
205
|
+
error: `PHP runtime crashed: ${e?.message || e}. It has been restarted; run the build again.`,
|
|
206
|
+
stderr: String(e?.stack || e),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
164
209
|
|
|
165
210
|
let retobj;
|
|
166
211
|
const resultPath = '/internal/prepros_result.json';
|
|
@@ -236,14 +281,60 @@ const extractPhpError = (text) => {
|
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
|
|
284
|
+
const inside = (root, target) => {
|
|
285
|
+
const relative = path.relative(root, target);
|
|
286
|
+
return relative && relative !== '..' && !relative.startsWith('..' + path.sep)
|
|
287
|
+
&& !path.isAbsolute(relative);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// Require both the authored path and its real target to stay inside the project.
|
|
292
|
+
// Validate the whole input list before booting PHP or copying any file.
|
|
293
|
+
const projectFile = (input, optional = false) => {
|
|
294
|
+
const file = path.resolve(__project, input);
|
|
295
|
+
if (!inside(__project, file)) throw new Error('PHP file outside project');
|
|
296
|
+
if (!fs.existsSync(file)) {
|
|
297
|
+
if (optional) return null;
|
|
298
|
+
throw new Error("Can't find PHP file");
|
|
299
|
+
}
|
|
300
|
+
if (!inside(fs.realpathSync(__project), fs.realpathSync(file))) {
|
|
301
|
+
throw new Error('PHP file outside project');
|
|
302
|
+
}
|
|
303
|
+
if (!fs.statSync(file).isFile()) throw new TypeError('Expected a project file, not a directory');
|
|
304
|
+
return { file, dest: '/project/' + path.relative(__project, file).split(path.sep).join('/') };
|
|
305
|
+
};
|
|
306
|
+
|
|
239
307
|
const runenv = async (script, paths = [], ...args) => {
|
|
240
|
-
if(!script) throw
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
308
|
+
if (!script) throw new Error('Missing PHP file.');
|
|
309
|
+
const entry = projectFile(script);
|
|
310
|
+
const mounts = [entry, ...paths.map(file => projectFile(file, true)).filter(Boolean)];
|
|
311
|
+
return run([entry.dest, ...args], '/prepros/runenv.php', mounts);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
// A plugin-registered script lives in its plugin's package, which a linked or
|
|
316
|
+
// workspace install places outside the project. The caller vouches for
|
|
317
|
+
// `pluginRoot` (an active plugin's resolved package directory); the script's
|
|
318
|
+
// authored and real paths must both stay inside it. Extra `paths` are still
|
|
319
|
+
// project files.
|
|
320
|
+
const runPluginScript = async (script, pluginRoot, paths = [], ...args) => {
|
|
321
|
+
if (!script) throw new Error('Missing PHP file.');
|
|
322
|
+
if (!pluginRoot) throw new Error('Missing plugin package directory.');
|
|
323
|
+
const root = path.resolve(pluginRoot);
|
|
324
|
+
const file = path.resolve(root, script);
|
|
325
|
+
if (!inside(root, file)) throw new Error('PHP file outside plugin package');
|
|
326
|
+
if (!fs.existsSync(file)) throw new Error("Can't find PHP file");
|
|
327
|
+
const realRoot = fs.realpathSync(root);
|
|
328
|
+
const realFile = fs.realpathSync(file);
|
|
329
|
+
if (!inside(realRoot, realFile)) throw new Error('PHP file outside plugin package');
|
|
330
|
+
if (!fs.statSync(realFile).isFile()) throw new TypeError('Expected a plugin file, not a directory');
|
|
331
|
+
const entry = {
|
|
332
|
+
file: realFile,
|
|
333
|
+
dest: '/plugin-scripts/' + path.basename(realRoot) + '/' + path.relative(realRoot, realFile).split(path.sep).join('/'),
|
|
334
|
+
};
|
|
335
|
+
const mounts = [entry, ...paths.map(p => projectFile(p, true)).filter(Boolean)];
|
|
336
|
+
return run([entry.dest, ...args], '/prepros/runenv.php', mounts);
|
|
337
|
+
};
|
|
247
338
|
|
|
248
339
|
|
|
249
340
|
// Run a batch of image jobs (resize / palette) through /prepros/imagebatch.php,
|
|
@@ -267,22 +358,25 @@ const render = async (file = '.', phpIncludes = []) => {
|
|
|
267
358
|
await mountPath(target);
|
|
268
359
|
if(config?.prepros?.before) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.before));
|
|
269
360
|
if(config?.prepros?.after) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.after));
|
|
361
|
+
for (const type of Object.values(config?.prepros?.types || {})) {
|
|
362
|
+
if (type?.before) await mountPath(path.resolve(config?.kirigami?.root, type.before));
|
|
363
|
+
if (type?.after) await mountPath(path.resolve(config?.kirigami?.root, type.after));
|
|
364
|
+
}
|
|
270
365
|
|
|
271
366
|
// Extra PHP files contributed by plugins (the kiri 'prepros:php' hook):
|
|
272
367
|
// mounted outside /project and include_once'd once, before any page
|
|
273
368
|
// renders, so they can PREPROS::registerTag()/registerHook() from PHP.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
php.preprosConfig.phpIncludes = mounted;
|
|
369
|
+
const php = await getPHPInstance();
|
|
370
|
+
const mounted = [];
|
|
371
|
+
const includes = Array.isArray(phpIncludes) ? phpIncludes : [];
|
|
372
|
+
for (let i = 0; i < includes.length; i++) {
|
|
373
|
+
const abs = path.resolve(includes[i]);
|
|
374
|
+
if (!fs.existsSync(abs)) continue;
|
|
375
|
+
const virt = `/plugins/${i}_${path.basename(abs)}`;
|
|
376
|
+
await mountPath(abs, virt, php);
|
|
377
|
+
mounted.push(virt);
|
|
285
378
|
}
|
|
379
|
+
php.preprosConfig.phpIncludes = mounted;
|
|
286
380
|
|
|
287
381
|
return run([fsvm]);
|
|
288
382
|
}
|
|
@@ -295,4 +389,14 @@ const sitemap = async () => {
|
|
|
295
389
|
}
|
|
296
390
|
|
|
297
391
|
|
|
298
|
-
|
|
392
|
+
const queuedRunenv = serial(runenv);
|
|
393
|
+
const queuedRunPluginScript = serial(runPluginScript);
|
|
394
|
+
const queuedRender = serial(render);
|
|
395
|
+
const queuedSitemap = serial(sitemap);
|
|
396
|
+
const queuedMountPath = serial(mountPath);
|
|
397
|
+
const queuedProcessImages = serial(processImages);
|
|
398
|
+
const resetRuntime = serial(reset);
|
|
399
|
+
export {
|
|
400
|
+
queuedRunenv as runenv, queuedRunPluginScript as runPluginScript, queuedRender as render, queuedSitemap as sitemap,
|
|
401
|
+
queuedMountPath as mountPath, queuedProcessImages as processImages, resetRuntime,
|
|
402
|
+
};
|
package/src/prepros.php
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Framework bootstrap (autoloader, $argv/$config, aliases, `boot` hook) is
|
|
4
|
+
// loaded via php.ini's auto_prepend_file, not an explicit include here.
|
|
4
5
|
|
|
5
6
|
try {
|
|
6
7
|
if (!isset($argv[1])) STD::error("Invalid argument.");
|
|
@@ -13,12 +14,10 @@ try {
|
|
|
13
14
|
else if (is_dir($target)) {
|
|
14
15
|
$prj = new PREPROS($config);
|
|
15
16
|
foreach (FS::dig($target . '/*.php', true) as $file) {
|
|
16
|
-
|
|
17
|
-
if (strpos($parent, '_') === 0) continue;
|
|
18
|
-
if (strpos(pathinfo($file, PATHINFO_FILENAME), '_') !== 0) continue;
|
|
17
|
+
if (!PREPROS::isPage($file)) continue;
|
|
19
18
|
PREPROS::render($file);
|
|
20
19
|
}
|
|
21
|
-
} elseif (
|
|
20
|
+
} elseif (PREPROS::isPage($target)) {
|
|
22
21
|
PREPROS::render($target);
|
|
23
22
|
} else {
|
|
24
23
|
throw new Exception("Invalid target.");
|
package/src/runenv.php
CHANGED
package/src/utils.inc.php
CHANGED
|
@@ -26,7 +26,6 @@ spl_autoload_register(function ($class) {
|
|
|
26
26
|
'LD' => 'ld.class.php',
|
|
27
27
|
'MD' => 'md.class.php',
|
|
28
28
|
'META' => 'meta.class.php',
|
|
29
|
-
'NORM' => 'norm.class.php',
|
|
30
29
|
'OBF' => 'obf.class.php',
|
|
31
30
|
'PREPROS' => 'prepros.class.php',
|
|
32
31
|
'SCHEMA' => 'schema.class.php',
|
|
@@ -36,7 +35,10 @@ spl_autoload_register(function ($class) {
|
|
|
36
35
|
'YAML' => 'yaml.class.php',
|
|
37
36
|
|
|
38
37
|
// Fallbacks
|
|
39
|
-
'
|
|
38
|
+
'MD_LEGACY' => 'md-legacy.class.php',
|
|
39
|
+
'NORMALIZER_LEGACY' => 'normalizer-legacy.class.php',
|
|
40
|
+
'SCHEMA_LEGACY' => 'schema-legacy.class.php',
|
|
41
|
+
'YAML_LEGACY' => 'yaml-legacy.class.php'
|
|
40
42
|
];
|
|
41
43
|
if (isset($catalog[$class])) require_once(__DIR__ . '/libraries/' . $catalog[$class]);
|
|
42
44
|
}, true, true);
|
|
@@ -58,3 +60,11 @@ PREPROS::loadConfig($config);
|
|
|
58
60
|
// `includes` file (or prepros.plugins.php) can hook here to pull in extra
|
|
59
61
|
// PHP files or wire itself up.
|
|
60
62
|
PREPROS::runHook('boot', $config);
|
|
63
|
+
|
|
64
|
+
// Symmetric `shutdown` hook, fired via register_shutdown_function rather
|
|
65
|
+
// than auto_append_file: every entrypoint ends in STD::succeed()/STD::error(),
|
|
66
|
+
// both of which exit() — and PHP skips auto_append_file whenever the script
|
|
67
|
+
// terminates through exit()/die(). A shutdown function has no such gap.
|
|
68
|
+
register_shutdown_function(function () {
|
|
69
|
+
PREPROS::runHook('shutdown');
|
|
70
|
+
});
|