laravel-workers 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -2
  3. package/bin/laravel-workers.mjs +141 -0
  4. package/build.mjs +2599 -0
  5. package/compiler/bin/controller.mjs +92 -0
  6. package/compiler/bin/transpile.mjs +65 -0
  7. package/compiler/runtime/rt-from-context.mjs +2399 -0
  8. package/compiler/src/classes.mjs +1232 -0
  9. package/compiler/src/controller.mjs +1029 -0
  10. package/compiler/src/dist-swap.mjs +76 -0
  11. package/compiler/src/mail-reach.mjs +104 -0
  12. package/compiler/src/policy.mjs +345 -0
  13. package/compiler/src/transpiler.mjs +3094 -0
  14. package/compiler/src/view-name.mjs +21 -0
  15. package/kernel/auth.mjs +1502 -0
  16. package/kernel/cache.mjs +405 -0
  17. package/kernel/collections.mjs +1615 -0
  18. package/kernel/cookies.mjs +474 -0
  19. package/kernel/csrf.mjs +698 -0
  20. package/kernel/db.mjs +703 -0
  21. package/kernel/digest.mjs +360 -0
  22. package/kernel/encrypter.mjs +106 -0
  23. package/kernel/form-request.mjs +106 -0
  24. package/kernel/hash.mjs +113 -0
  25. package/kernel/html-entities.mjs +268 -0
  26. package/kernel/http.mjs +1803 -0
  27. package/kernel/image.mjs +341 -0
  28. package/kernel/mail.mjs +269 -0
  29. package/kernel/mailable.mjs +172 -0
  30. package/kernel/model.mjs +1226 -0
  31. package/kernel/multipart.mjs +161 -0
  32. package/kernel/paginator.mjs +336 -0
  33. package/kernel/php-json.mjs +220 -0
  34. package/kernel/php-serialize.mjs +245 -0
  35. package/kernel/queue.mjs +96 -0
  36. package/kernel/ratelimit.mjs +855 -0
  37. package/kernel/redis.mjs +264 -0
  38. package/kernel/resources.mjs +857 -0
  39. package/kernel/response.mjs +17 -0
  40. package/kernel/router.mjs +276 -0
  41. package/kernel/scheduler.mjs +236 -0
  42. package/kernel/service-adapters.mjs +49 -0
  43. package/kernel/session.mjs +934 -0
  44. package/kernel/storage.mjs +265 -0
  45. package/kernel/str.mjs +445 -0
  46. package/kernel/tests/auth.test.mjs +1074 -0
  47. package/kernel/tests/budget.test.mjs +527 -0
  48. package/kernel/tests/cache.test.mjs +290 -0
  49. package/kernel/tests/collections.test.mjs +534 -0
  50. package/kernel/tests/cookies.test.mjs +302 -0
  51. package/kernel/tests/csrf.test.mjs +597 -0
  52. package/kernel/tests/db.test.mjs +1069 -0
  53. package/kernel/tests/encrypter.test.mjs +90 -0
  54. package/kernel/tests/fixtures/laravel-1337-cookies.json +120 -0
  55. package/kernel/tests/hash.test.mjs +149 -0
  56. package/kernel/tests/http.test.mjs +1182 -0
  57. package/kernel/tests/image.test.mjs +235 -0
  58. package/kernel/tests/login-flow.test.mjs +509 -0
  59. package/kernel/tests/model.test.mjs +324 -0
  60. package/kernel/tests/oracle-app.mjs +32 -0
  61. package/kernel/tests/paginator.test.mjs +132 -0
  62. package/kernel/tests/php-json.test.mjs +263 -0
  63. package/kernel/tests/php-serialize.test.mjs +210 -0
  64. package/kernel/tests/ports.test.mjs +47 -0
  65. package/kernel/tests/ratelimit.test.mjs +975 -0
  66. package/kernel/tests/readiness.test.mjs +155 -0
  67. package/kernel/tests/redis.test.mjs +175 -0
  68. package/kernel/tests/registry-json.test.mjs +28 -0
  69. package/kernel/tests/resources.test.mjs +275 -0
  70. package/kernel/tests/router.test.mjs +281 -0
  71. package/kernel/tests/routes-json.test.mjs +32 -0
  72. package/kernel/tests/services.test.mjs +474 -0
  73. package/kernel/tests/session.test.mjs +709 -0
  74. package/kernel/tests/sqlite-integration.test.mjs +390 -0
  75. package/kernel/tests/storage.test.mjs +133 -0
  76. package/kernel/tests/str.test.mjs +83 -0
  77. package/kernel/tests/upload.test.mjs +193 -0
  78. package/kernel/tests/validation-messages.test.mjs +117 -0
  79. package/kernel/tests/validation.test.mjs +340 -0
  80. package/kernel/tests/validator-hooks.test.mjs +343 -0
  81. package/kernel/tests/validator.test.mjs +805 -0
  82. package/kernel/tests/vendor-lite.test.mjs +39 -0
  83. package/kernel/validation-messages.data.mjs +168 -0
  84. package/kernel/validation-messages.mjs +249 -0
  85. package/kernel/validation.mjs +123 -0
  86. package/kernel/validator.mjs +1538 -0
  87. package/package.json +68 -5
  88. package/shadow/run.mjs +784 -0
  89. package/shadow/scenarios/fresh-app.mjs +60 -0
  90. package/shadow/scenarios/lw-constructs-app.mjs +669 -0
  91. package/src/ports/dbcheck.mjs +22 -0
  92. package/src/worker.mjs +200 -0
  93. package/tools/_root.php +135 -0
  94. package/tools/bench.mjs +129 -0
  95. package/tools/class-dump.php +87 -0
  96. package/tools/compile-view.php +263 -0
  97. package/tools/config-dump.php +192 -0
  98. package/tools/constants-dump.php +46 -0
  99. package/tools/construct-audit.mjs +326 -0
  100. package/tools/constructs.json +1510 -0
  101. package/tools/docs-check.mjs +149 -0
  102. package/tools/event-dump.php +320 -0
  103. package/tools/formrequest-dump.php +229 -0
  104. package/tools/gate.mjs +36 -0
  105. package/tools/http-oracle.php +200 -0
  106. package/tools/init.mjs +73 -0
  107. package/tools/integrations.json +69 -0
  108. package/tools/mailable-views.php +116 -0
  109. package/tools/make-cookie-fixture.mjs +87 -0
  110. package/tools/make-html-entities.mjs +106 -0
  111. package/tools/model-dump.php +198 -0
  112. package/tools/php-auth-oracle.php +701 -0
  113. package/tools/php-cache-oracle.php +166 -0
  114. package/tools/php-crypto-oracle.php +113 -0
  115. package/tools/php-csrf-oracle.php +271 -0
  116. package/tools/php-http-pipeline-oracle.php +358 -0
  117. package/tools/php-json-oracle.php +102 -0
  118. package/tools/php-login-flow-oracle.php +342 -0
  119. package/tools/php-provision.mjs +172 -0
  120. package/tools/php-ratelimit-oracle.php +508 -0
  121. package/tools/php-runtime.mjs +342 -0
  122. package/tools/php-serialize-oracle.php +114 -0
  123. package/tools/php-session-oracle.php +266 -0
  124. package/tools/php-shadow-oracle.php +588 -0
  125. package/tools/policy-dump.php +240 -0
  126. package/tools/ports.mjs +51 -0
  127. package/tools/prerender.php +68 -0
  128. package/tools/readiness.mjs +255 -0
  129. package/tools/registry-json.mjs +22 -0
  130. package/tools/routes-dump.php +27 -0
  131. package/tools/routes-json.mjs +48 -0
  132. package/tools/schedule-dump.php +82 -0
  133. package/tools/schema-dump.php +158 -0
  134. package/tools/validation-messages.php +54 -0
  135. package/tools/vendor-lite.mjs +299 -0
  136. package/tools/verify-s0.php +209 -0
@@ -0,0 +1,76 @@
1
+ // compiler/src/dist-swap.mjs — the atomic swap of the build output.
2
+ //
3
+ // The build writes every artifact into `<dist>.building` and calls commitDist()
4
+ // only after its LAST check passed. A build that fails therefore leaves the
5
+ // previous `dist/` untouched: an operator (or a watcher, or CI) can never pick
6
+ // up a half-written artifact, and the bundler never sees a `dist` whose
7
+ // generated modules were not emitted yet.
8
+ //
9
+ // Windows detail: a running `wrangler dev` (or an editor) can hold a handle on
10
+ // `dist/` or a subdirectory, so the rename can fail even though every file is
11
+ // closed. The fallback wipes the contents and keeps the locked directory
12
+ // shells — the build rewrites everything anyway.
13
+ import { existsSync, readdirSync, renameSync, rmSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ /** Remove a directory tree, tolerating a locked entry. */
17
+ function wipe(dir, onLocked) {
18
+ try {
19
+ rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
20
+ return;
21
+ } catch {
22
+ /* a handle is held — wipe what can be wiped */
23
+ }
24
+ if (!existsSync(dir)) return;
25
+ for (const entry of readdirSync(dir)) {
26
+ try {
27
+ rmSync(join(dir, entry), { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
28
+ } catch {
29
+ if (onLocked) onLocked(join(dir, entry));
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Move the finished staging directory into place.
36
+ *
37
+ * The previous `dist/` is renamed aside first and deleted only AFTER the new
38
+ * one landed: a crash between the two renames leaves the old build recoverable
39
+ * under `<dist>.old-*` (the next build sweeps those), never a mix of both.
40
+ *
41
+ * @param {string} staging the directory the build wrote
42
+ * @param {string} final the directory the app (and wrangler) reads
43
+ * @param {(path: string) => void} [onLocked] told about each entry it cannot delete
44
+ */
45
+ export function commitDist(staging, final, onLocked) {
46
+ if (!existsSync(staging)) {
47
+ throw new Error('commitDist: the staging directory ' + staging + ' does not exist');
48
+ }
49
+ const previous = final + '.old-' + Date.now();
50
+ let moved = false;
51
+ if (existsSync(final)) {
52
+ try {
53
+ renameSync(final, previous);
54
+ moved = true;
55
+ } catch {
56
+ wipe(final, onLocked);
57
+ }
58
+ }
59
+ renameSync(staging, final);
60
+ if (moved) {
61
+ try {
62
+ rmSync(previous, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
63
+ } catch {
64
+ /* locked — the `dist.old-*` sweep removes it on the next build */
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Throw the staging directory away (a failed build). */
70
+ export function discardStaging(staging) {
71
+ try {
72
+ rmSync(staging, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
73
+ } catch {
74
+ /* still locked — the next build wipes it */
75
+ }
76
+ }
@@ -0,0 +1,104 @@
1
+ // compiler/src/mail-reach.mjs — the class graph a route reaches.
2
+ //
3
+ // A route does not only render its OWN views: a mailable renders ITS view,
4
+ // and at request time the mailable renders it through the ROUTE's runtime
5
+ // (kernel/mailable.mjs: the mailable's __lwMail() calls the runtime's
6
+ // view()). So the view of every mailable a route can REACH must be in that
7
+ // route's compiled view map - otherwise the request answers 500 ("view not
8
+ // compiled") where Laravel answers 200 and sends the mail.
9
+ //
10
+ // "Reach" is the class graph, and the build knows it exactly:
11
+ // - the route's own `new X()` sites,
12
+ // - a class's constructor dependencies (the runtime builds them: the exact
13
+ // edge - a service a listener injects is ALWAYS built),
14
+ // - a class's `new X()` sites (the compiler resolved them to their FQCN),
15
+ // - the listeners of an event class (the runtime dispatches to them).
16
+ //
17
+ // The graph is walked with a visited set: a listener can dispatch an event
18
+ // whose listener dispatches back.
19
+ //
20
+ // The walk is deliberately over-approximating on the METHOD-CONDITIONAL edge
21
+ // (a class's `new` sites): over-attaching a view is harmless (the route
22
+ // imports a view it may not render), while under-attaching one is a
23
+ // request-time 500 where Laravel answers 200.
24
+
25
+ // The classes a route instantiates, resolved the way the COMPILER resolved
26
+ // them: the build's alias map (a short name -> the FQCN of the compiled
27
+ // class; an ambiguous short name is absent) and the set of compiled FQCNs (the
28
+ // FQCN-written form). The same rule the transpiler applies
29
+ // (compiler/src/transpiler.mjs: the `new` case), so the graph and the
30
+ // compiled code agree on WHICH class the route instantiates.
31
+ //
32
+ // `newClasses` is the extract's list: { alias, fqcn } per `new` site, the
33
+ // alias being the name as written (a leading backslash stripped).
34
+ // `classesMeta` is the build's compiled-class metadata: { fqcns, aliases }.
35
+ export function resolveNewSites(newClasses, classesMeta) {
36
+ const fqcns = new Set(classesMeta && classesMeta.fqcns ? classesMeta.fqcns : []);
37
+ const aliases = (classesMeta && classesMeta.aliases) || {};
38
+ const out = [];
39
+ for (const entry of newClasses || []) {
40
+ const alias = String(entry.alias || '').replace(/^\\/, '');
41
+ if (!alias) continue;
42
+ // The short name of a compiled class (the alias map), else the class
43
+ // itself (the FQCN-written form). A name the compiler could not resolve
44
+ // (an unknown class) resolves to nothing here: the route was stopped.
45
+ const fqcn = aliases[alias] || (fqcns.has(alias) ? alias : null);
46
+ if (fqcn && !out.includes(fqcn)) out.push(fqcn);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // The classes a route reaches, from the route's own `new` sites.
52
+ // `ctx` carries the class graph:
53
+ // classMeta - the compiled classes: { FQCN: { instantiated: [FQCN] } }
54
+ // dumpClasses - the dumped classes: { FQCN: { kind, ctor: [...] } }
55
+ // events - the dumped events: { FQCN: { listeners: [FQCN] } }
56
+ export function reachableClasses(newClasses, classesMeta, ctx) {
57
+ const seeds = resolveNewSites(newClasses, classesMeta);
58
+ const reached = new Set();
59
+ const queue = [...seeds];
60
+ while (queue.length) {
61
+ const fqcn = queue.shift();
62
+ if (!fqcn || reached.has(fqcn)) continue;
63
+ reached.add(fqcn);
64
+ for (const next of classEdges(fqcn, ctx)) {
65
+ if (next && !reached.has(next)) queue.push(next);
66
+ }
67
+ }
68
+ return { reached, seeds };
69
+ }
70
+
71
+ // The classes one class reaches: its constructor dependencies, the classes it
72
+ // instantiates, and (for an event class) its listeners.
73
+ function classEdges(fqcn, ctx) {
74
+ const edges = [];
75
+ // The constructor dependencies (the runtime builds them: rt.newClass for
76
+ // every class-typed dependency - the exact edge). The dump records the
77
+ // dependency's class (and the concrete binding of an interface).
78
+ const dumped = (ctx.dumpClasses && ctx.dumpClasses[fqcn]) || null;
79
+ for (const p of (dumped && dumped.ctor) || []) {
80
+ if (!p || p.kind !== 'class') continue;
81
+ edges.push(p.impl || p.type);
82
+ }
83
+ // The classes the class instantiates (`new X()` in its methods).
84
+ const meta = (ctx.classMeta && ctx.classMeta[fqcn]) || null;
85
+ for (const inst of (meta && meta.instantiated) || []) edges.push(inst);
86
+ // The listeners of an event class (the runtime dispatches the event to them).
87
+ const event = (ctx.events && ctx.events[fqcn]) || null;
88
+ for (const listener of (event && event.listeners) || []) edges.push(listener);
89
+ return edges;
90
+ }
91
+
92
+ // The mailables among the reached classes: the app's mailable classes (the
93
+ // dump records their kind). Each mailable's views must be in the route's view
94
+ // map.
95
+ export function reachedMailables(reached, dumpClasses) {
96
+ const mailables = [];
97
+ for (const fqcn of reached) {
98
+ const info = (dumpClasses && dumpClasses[fqcn]) || null;
99
+ if (info && info.kind === 'mailable' && !mailables.includes(fqcn)) mailables.push(fqcn);
100
+ }
101
+ return mailables;
102
+ }
103
+
104
+ export default { resolveNewSites, reachableClasses, reachedMailables };
@@ -0,0 +1,345 @@
1
+ // compiler/src/policy.mjs — the policy compiler (build time).
2
+ //
3
+ // A Laravel policy is a class: `App\Policies\PostPolicy` with the ability
4
+ // methods (`viewAny`, `create`, `update`, ...) and an optional `before()` hook.
5
+ // The compiled rung compiles the policy methods into plain async functions and
6
+ // ships them in `dist/src/policies.generated.mjs`; the Gate check at request
7
+ // time calls them with Laravel's own rules (the guest rules, the before() hook,
8
+ // the missing-ability denial).
9
+ //
10
+ // The metadata comes from tools/policy-dump.php (the real Gate of the booted
11
+ // app): which policy answers for a model, which abilities it has, whether a
12
+ // guest may call them, and whether the app registers global Gate callbacks.
13
+ //
14
+ // Anything the compiled rung cannot answer exactly fails loud here — the
15
+ // policy is not compiled, and the routes that check it stay blocked.
16
+ import parser from 'php-parser';
17
+ import { readFileSync } from 'node:fs';
18
+ import { transpileControllerMethod } from './transpiler.mjs';
19
+
20
+ const phpParser = new parser({ parser: { extractDoc: false }, ast: { withPositions: true } });
21
+
22
+ const vname = (name) => 'v_' + String(name).replace(/[^A-Za-z0-9_]/g, '_');
23
+ const shortName = (fqcn) => String(fqcn).split('\\').pop();
24
+
25
+ const nodeName = (n) => {
26
+ if (n == null) return '';
27
+ if (typeof n === 'string') return n;
28
+ if (typeof n.name === 'string') return n.name;
29
+ if (n.name && typeof n.name.name === 'string') return n.name.name;
30
+ return '';
31
+ };
32
+
33
+ const normPath = (p) => String(p || '').replace(/\\/g, '/').toLowerCase();
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // the AST of the policy file
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function parsePolicyFile(file) {
40
+ let source;
41
+ try {
42
+ source = readFileSync(file, 'utf8');
43
+ } catch (e) {
44
+ return { error: 'the policy file could not be read: ' + (e.message || e) };
45
+ }
46
+ let ast;
47
+ try {
48
+ ast = phpParser.parseCode(source);
49
+ } catch (e) {
50
+ return { error: 'parse error: ' + (e.message || String(e)) };
51
+ }
52
+ if (ast && ast.errors && ast.errors.length) {
53
+ const first = ast.errors[0];
54
+ return { error: 'parse error: ' + (first.msg || first.message || JSON.stringify(first)) };
55
+ }
56
+ let classNode = null;
57
+ const walk = (node) => {
58
+ if (!node || typeof node !== 'object') return;
59
+ if (Array.isArray(node)) {
60
+ for (const n of node) walk(n);
61
+ return;
62
+ }
63
+ if (!classNode && node.kind === 'class') {
64
+ classNode = node;
65
+ return;
66
+ }
67
+ for (const k of Object.keys(node)) {
68
+ if (k === 'loc') continue;
69
+ walk(node[k]);
70
+ }
71
+ };
72
+ walk(ast.children || []);
73
+ if (!classNode) return { error: 'the policy file has no class' };
74
+ return { classNode };
75
+ }
76
+
77
+ function findMethod(classNode, name) {
78
+ for (const n of classNode.body || []) {
79
+ if (n.kind === 'method' && nodeName(n.name) === name) return n;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // one policy method
86
+ // ---------------------------------------------------------------------------
87
+
88
+ // The parameters of a policy method: the user, then (for an ability) the
89
+ // subject; for `before()`: the user, the ability, then the subject.
90
+ function planParams(entry, methodMeta, isBefore, opts) {
91
+ const params = methodMeta.params || [];
92
+ const seeds = [];
93
+ params.forEach((p, i) => {
94
+ const isUser = i === 0;
95
+ const isSubject = isBefore ? i >= 2 : i === 1;
96
+ if (isSubject) {
97
+ const fqcn =
98
+ p.type && !p.builtin && opts.models.has(p.type) ? p.type : entry.model;
99
+ seeds.push({ var: vname(p.name), kind: 'model', alias: shortName(fqcn), fqcn });
100
+ return;
101
+ }
102
+ if (!isUser) return;
103
+ // the user: its declared type when it is a model the build knows, else the
104
+ // app's auth provider model (an untyped `$user` is still that user).
105
+ let fqcn = null;
106
+ if (p.type && !p.builtin && opts.models.has(p.type)) fqcn = p.type;
107
+ else if (!p.type && opts.userModel && opts.models.has(opts.userModel)) fqcn = opts.userModel;
108
+ if (fqcn) seeds.push({ var: vname(p.name), kind: 'model', alias: shortName(fqcn), fqcn });
109
+ });
110
+ return seeds;
111
+ }
112
+
113
+ function compileMethod(entry, methodMeta, isBefore, opts, classNode) {
114
+ const name = methodMeta.name;
115
+ const label = (entry.policy || 'the policy') + '::' + name + '()';
116
+ if (methodMeta.static) {
117
+ return { error: 'the policy method ' + label + ' is static — Laravel calls the policy as an instance method' };
118
+ }
119
+ if (methodMeta.abstract) {
120
+ return { error: 'the policy method ' + label + ' is abstract' };
121
+ }
122
+ if (methodMeta.file && entry.file && normPath(methodMeta.file) !== normPath(entry.file)) {
123
+ return {
124
+ error:
125
+ 'the policy method ' + label + ' is declared in ' + methodMeta.file + ' (a trait or a base class) — the compiled rung compiles the policy file only',
126
+ };
127
+ }
128
+ if (methodMeta.returnType && !methodMeta.returnBuiltin && methodMeta.returnType !== 'mixed') {
129
+ return {
130
+ error:
131
+ 'the policy method ' + label + ' returns ' + methodMeta.returnType + ' — the compiled rung compiles the boolean policies only (an authorization Response carries a message the edge does not render)',
132
+ };
133
+ }
134
+ const params = methodMeta.params || [];
135
+ if (isBefore && params.length > 3) {
136
+ return { error: 'the policy before() may take the user, the ability and one subject — ' + params.length + ' parameters are declared' };
137
+ }
138
+ if (!isBefore && params.length > 2) {
139
+ return {
140
+ error:
141
+ 'the policy method ' + label + ' may take the user and one subject — Laravel hands the policy the Gate arguments',
142
+ };
143
+ }
144
+ for (const p of params) {
145
+ if (p.variadic) {
146
+ return { error: 'the policy method ' + label + ' has a variadic parameter — the compiled rung does not compile variadics' };
147
+ }
148
+ if (p.byRef) {
149
+ return { error: 'the policy method ' + label + ' has a by-reference parameter' };
150
+ }
151
+ }
152
+ const methodNode = findMethod(classNode, name);
153
+ if (!methodNode) {
154
+ return { error: 'the policy method ' + label + ' is not in the policy file' };
155
+ }
156
+ const seeds = planParams(entry, methodMeta, isBefore, opts);
157
+ const paramVars = new Set(params.map((p) => vname(p.name)));
158
+ const emitted = transpileControllerMethod((methodNode.body && methodNode.body.children) || [], {
159
+ models: opts.modelAliases,
160
+ modelNotCompiled: opts.modelNotCompiled,
161
+ modelClassFqcns: opts.modelClassFqcns,
162
+ views: new Set(),
163
+ indent: '',
164
+ paramVars,
165
+ requestVars: new Set(),
166
+ // The class constants the app declares (`Post::PUBLISHED`): dumped at
167
+ // build time, so a policy body that reads one compiles instead of
168
+ // stopping on an unavailable constant.
169
+ constants: opts.constants || {},
170
+ formRequests: new Map(),
171
+ modelVarSeeds: seeds,
172
+ });
173
+ if (emitted.errors.length) {
174
+ return { error: 'the policy method ' + label + ' did not compile: ' + emitted.errors[0].message };
175
+ }
176
+ return {
177
+ fn: emitted.code,
178
+ params: params.map((p) => vname(p.name)),
179
+ body: emitted.code,
180
+ };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // the whole registry
185
+ // ---------------------------------------------------------------------------
186
+
187
+ export function compilePolicies(dump, opts = {}) {
188
+ const globals = (dump && dump.globals) || { before: 0, after: 0, abilities: [] };
189
+ const models = (dump && dump.models) || [];
190
+ const modelSet = opts.models instanceof Set ? opts.models : new Set(opts.models || []);
191
+ const modelClassFqcns = opts.modelClassFqcns || {};
192
+ // The class names the policy body may use (the aliases the app writes +
193
+ // the FQCNs): a model chain inside a policy (`Post::where(...)`) resolves
194
+ // against them.
195
+ const modelAliases =
196
+ opts.modelAliases instanceof Set ? opts.modelAliases : new Set(Object.keys(modelClassFqcns));
197
+ const options = {
198
+ models: modelSet,
199
+ modelSet,
200
+ modelAliases,
201
+ modelNotCompiled: opts.modelNotCompiled || new Map(),
202
+ modelClassFqcns,
203
+ userModel: opts.userModel || null,
204
+ // The dumped class constants, by alias and by FQCN.
205
+ constants: opts.constants || {},
206
+ };
207
+
208
+ const meta = {};
209
+ const entries = [];
210
+ const errors = [];
211
+ // The GLOBAL Gate::define() abilities, under a reserved key: a subject-less
212
+ // `Gate::allows('x')` resolves here (the policy map is per model).
213
+ meta.__global__ = {
214
+ definitions: Object.fromEntries(
215
+ Object.entries((globals && globals.definitions) || {}).filter(
216
+ ([, d]) => d && d.kind === 'string' && d.callback
217
+ )
218
+ ),
219
+ definedAbilities: Array.isArray(globals.abilities) ? globals.abilities : [],
220
+ globalBefore: Number(globals.before) || 0,
221
+ globalAfter: Number(globals.after) || 0,
222
+ };
223
+
224
+ for (const entry of models) {
225
+ const modelMeta = {
226
+ policy: entry.policy || null,
227
+ error: entry.error || null,
228
+ hasMagicCall: !!entry.hasMagicCall,
229
+ globalBefore: Number(globals.before) || 0,
230
+ globalAfter: Number(globals.after) || 0,
231
+ definedAbilities: Array.isArray(globals.abilities) ? globals.abilities : [],
232
+ before: null,
233
+ methods: {},
234
+ compileError: null,
235
+ };
236
+ meta[entry.model] = modelMeta;
237
+
238
+ if (entry.error && entry.error !== 'no policy') {
239
+ // The policy could not be resolved (an exception, a missing class): the
240
+ // Gate check cannot be answered -> the route stays blocked.
241
+ errors.push({ model: entry.model, message: 'the policy for ' + entry.model + ' could not be read: ' + entry.error });
242
+ entries.push({ model: entry.model, policy: null, before: null, methods: {} });
243
+ continue;
244
+ }
245
+ if (!entry.policy) {
246
+ // No policy: Laravel denies every ability (the build fails loud on a
247
+ // global Gate::define() that would answer instead).
248
+ entries.push({ model: entry.model, policy: null, before: null, methods: {} });
249
+ continue;
250
+ }
251
+
252
+ const parsed = parsePolicyFile(entry.file);
253
+ if (parsed.error) {
254
+ modelMeta.compileError = parsed.error;
255
+ errors.push({ model: entry.model, message: parsed.error });
256
+ entries.push({ model: entry.model, policy: entry.policy, before: null, methods: {} });
257
+ continue;
258
+ }
259
+
260
+ const compiled = { before: null, methods: {} };
261
+ let failed = null;
262
+
263
+ if (entry.before) {
264
+ const r = compileMethod(entry, entry.before, true, options, parsed.classNode);
265
+ if (r.error) failed = r.error;
266
+ else {
267
+ compiled.before = { guest: !!entry.before.guest, fn: r.fn, params: r.params };
268
+ modelMeta.before = { guest: !!entry.before.guest, params: (entry.before.params || []).length };
269
+ }
270
+ }
271
+
272
+ if (!failed) {
273
+ for (const [name, methodMeta] of Object.entries(entry.methods || {})) {
274
+ const r = compileMethod(entry, methodMeta, false, options, parsed.classNode);
275
+ if (r.error) {
276
+ failed = r.error;
277
+ break;
278
+ }
279
+ compiled.methods[name] = { guest: !!methodMeta.guest, fn: r.fn, params: r.params };
280
+ modelMeta.methods[name] = {
281
+ guest: !!methodMeta.guest,
282
+ params: (methodMeta.params || []).map((p) => ({ name: p.name, type: p.type })),
283
+ };
284
+ }
285
+ }
286
+
287
+ if (failed) {
288
+ modelMeta.compileError = failed;
289
+ errors.push({ model: entry.model, message: failed });
290
+ entries.push({ model: entry.model, policy: entry.policy, before: null, methods: {} });
291
+ continue;
292
+ }
293
+
294
+ entries.push({ model: entry.model, policy: entry.policy, before: compiled.before, methods: compiled.methods });
295
+ }
296
+
297
+ return { code: emitRegistry(entries, meta.__global__ ? meta.__global__.definitions : {}), errors, meta };
298
+ }
299
+
300
+ // The generated module: the compiled policies, keyed by the model FQCN.
301
+ function emitRegistry(entries, globals) {
302
+ const lines = [];
303
+ lines.push('// generated by laravel-workers — the compiled policies (do not edit)');
304
+ lines.push('// The Gate checks the edge answers: the abilities, the guest rules, before().');
305
+ lines.push('');
306
+ lines.push('export const policies = {');
307
+ for (const e of entries) {
308
+ lines.push(' ' + JSON.stringify(e.model) + ': {');
309
+ lines.push(' policy: ' + JSON.stringify(e.policy) + ',');
310
+ if (e.before) {
311
+ lines.push(' before: {');
312
+ lines.push(' guest: ' + (e.before.guest ? 'true' : 'false') + ',');
313
+ lines.push(' fn: async (' + ['rt'].concat(e.before.params).join(', ') + ') => {');
314
+ for (const l of String(e.before.fn || '').split('\n')) lines.push(l ? ' ' + l : l);
315
+ lines.push(' },');
316
+ lines.push(' },');
317
+ } else {
318
+ lines.push(' before: null,');
319
+ }
320
+ lines.push(' methods: {');
321
+ for (const [name, m] of Object.entries(e.methods)) {
322
+ lines.push(' ' + JSON.stringify(name) + ': {');
323
+ lines.push(' guest: ' + (m.guest ? 'true' : 'false') + ',');
324
+ lines.push(' fn: async (' + ['rt'].concat(m.params).join(', ') + ') => {');
325
+ for (const l of String(m.fn || '').split('\n')) lines.push(l ? ' ' + l : l);
326
+ lines.push(' },');
327
+ lines.push(' },');
328
+ }
329
+ lines.push(' },');
330
+ lines.push(' },');
331
+ }
332
+ lines.push('};');
333
+ lines.push('');
334
+ // The GLOBAL Gate::define() abilities (`Gate::allows('x')` with no subject):
335
+ // the kernel's `rt.gateGlobal` reads this map, and only the compilable ones
336
+ // (a 'Class@method' string callback) are here - a closure never reaches it.
337
+ const globalsMap = globals || {};
338
+ lines.push('export const gateDefinitions = ' + JSON.stringify(globalsMap, null, 2) + ';');
339
+ lines.push('');
340
+ lines.push('export default policies;');
341
+ lines.push('');
342
+ return lines.join('\n');
343
+ }
344
+
345
+ export default { compilePolicies };