@kirigami/php-prepros 2.0.0 → 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 -1887
- 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 -807
- package/src/libraries/md-legacy.class.php +1257 -0
- package/src/libraries/md.class.php +51 -1249
- package/src/libraries/meta.class.php +496 -509
- 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 +151 -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,8 @@ 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
|
-
// The
|
|
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).
|
|
51
68
|
preprosConfig.seo = config.seo ?? null;
|
|
52
69
|
// META auto-detects favicon / apple-touch-icon / humans.txt at the
|
|
53
70
|
// source root; those extensions aren't mounted into the sandbox, so the
|
|
@@ -61,16 +78,21 @@ const getPHPInstance = async () => {
|
|
|
61
78
|
// page's <head> with a <link>/<script> per sass/esbuild task output.
|
|
62
79
|
preprosConfig.tasks = config.tasks || [];
|
|
63
80
|
|
|
64
|
-
__php = await (preprosConfig.network
|
|
81
|
+
__php = await createPHPRuntime({ network: Boolean(preprosConfig.network) });
|
|
65
82
|
__php.setSpawnHandler((command, args, options) => spawn(command, args, options));
|
|
66
83
|
__php.preprosConfig = preprosConfig;
|
|
67
84
|
__php.setIniValues({
|
|
68
|
-
log_errors:
|
|
69
|
-
html_errors:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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',
|
|
74
96
|
});
|
|
75
97
|
|
|
76
98
|
await mountPath(__dirname, '/prepros', __php);
|
|
@@ -98,22 +120,34 @@ const getPHPInstance = async () => {
|
|
|
98
120
|
results: results
|
|
99
121
|
});
|
|
100
122
|
} catch(e) {
|
|
101
|
-
return {
|
|
123
|
+
return JSON.stringify({
|
|
102
124
|
success: false,
|
|
103
125
|
error: typeof e == 'string' ? e : e.message
|
|
104
|
-
}
|
|
126
|
+
});
|
|
105
127
|
}
|
|
106
128
|
});
|
|
107
129
|
}
|
|
108
130
|
return __php;
|
|
109
131
|
}
|
|
110
132
|
|
|
133
|
+
const getPHPInstance = async () => {
|
|
134
|
+
try {
|
|
135
|
+
return await initializePHPInstance();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await reset();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
111
142
|
|
|
112
143
|
const mountPath = async (localPath, virtualDir, php) => {
|
|
113
144
|
const config = await loadConfig();
|
|
114
145
|
php = php || await getPHPInstance();
|
|
115
146
|
if(!path.isAbsolute(localPath)) localPath = path.join(__project, localPath);
|
|
116
|
-
|
|
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('/'));
|
|
117
151
|
const includeExtensions = new Set(['.php', '.json', '.yaml', '.yml', '.md', '.db', '.txt', ...(config?.prepros?.mountext || [])]);
|
|
118
152
|
const stat = fs.statSync(localPath);
|
|
119
153
|
if (stat.isDirectory()) {
|
|
@@ -141,27 +175,37 @@ const mountPath = async (localPath, virtualDir, php) => {
|
|
|
141
175
|
const run = async (args = [], script = null, mountfiles = []) => {
|
|
142
176
|
const php = await getPHPInstance();
|
|
143
177
|
|
|
144
|
-
await Promise.all(mountfiles.map(async
|
|
145
|
-
const file = path.resolve(item);
|
|
146
|
-
if(!fs.existsSync(file)) return;
|
|
147
|
-
if(!path.relative(__project, file)) return;
|
|
148
|
-
const dest = path.join('/project', file.replace(__project, '')).replaceAll('\\', '/');
|
|
178
|
+
await Promise.all(mountfiles.map(async ({ file, dest }) => {
|
|
149
179
|
const buf = fs.readFileSync(file);
|
|
150
180
|
const parentDir = dest.substring(0, dest.lastIndexOf('/'));
|
|
151
181
|
if (parentDir) php.mkdirTree(parentDir);
|
|
152
182
|
php.writeFile(dest, isBinary(buf) ? buf : buf.toString('utf8'));
|
|
153
183
|
}));
|
|
154
184
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
}
|
|
165
209
|
|
|
166
210
|
let retobj;
|
|
167
211
|
const resultPath = '/internal/prepros_result.json';
|
|
@@ -237,14 +281,60 @@ const extractPhpError = (text) => {
|
|
|
237
281
|
}
|
|
238
282
|
|
|
239
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
|
+
|
|
240
307
|
const runenv = async (script, paths = [], ...args) => {
|
|
241
|
-
if(!script) throw
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
};
|
|
248
338
|
|
|
249
339
|
|
|
250
340
|
// Run a batch of image jobs (resize / palette) through /prepros/imagebatch.php,
|
|
@@ -268,22 +358,25 @@ const render = async (file = '.', phpIncludes = []) => {
|
|
|
268
358
|
await mountPath(target);
|
|
269
359
|
if(config?.prepros?.before) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.before));
|
|
270
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
|
+
}
|
|
271
365
|
|
|
272
366
|
// Extra PHP files contributed by plugins (the kiri 'prepros:php' hook):
|
|
273
367
|
// mounted outside /project and include_once'd once, before any page
|
|
274
368
|
// renders, so they can PREPROS::registerTag()/registerHook() from PHP.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
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);
|
|
286
378
|
}
|
|
379
|
+
php.preprosConfig.phpIncludes = mounted;
|
|
287
380
|
|
|
288
381
|
return run([fsvm]);
|
|
289
382
|
}
|
|
@@ -296,4 +389,14 @@ const sitemap = async () => {
|
|
|
296
389
|
}
|
|
297
390
|
|
|
298
391
|
|
|
299
|
-
|
|
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
|
+
});
|