@rangka/core 0.1.1 → 0.1.3
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/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -25
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { extractSourceModels, validatePageSources } from '../page-utils.js';
|
|
3
|
-
import type { PageDefinition } from '@rangka/shared';
|
|
4
|
-
|
|
5
|
-
describe('extractSourceModels', () => {
|
|
6
|
-
it('extracts models from data widget source', () => {
|
|
7
|
-
const page: PageDefinition = {
|
|
8
|
-
key: 'sales.orders',
|
|
9
|
-
label: 'Orders',
|
|
10
|
-
type: 'collection',
|
|
11
|
-
body: [
|
|
12
|
-
{
|
|
13
|
-
type: 'data',
|
|
14
|
-
source: { model: 'sales.order' },
|
|
15
|
-
children: [{ type: 'text', bind: { field: 'name' } }],
|
|
16
|
-
},
|
|
17
|
-
],
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const models = extractSourceModels(page);
|
|
21
|
-
expect(models).toContain('sales.order');
|
|
22
|
-
expect(models).toHaveLength(1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('extracts models from nested data widgets', () => {
|
|
26
|
-
const page: PageDefinition = {
|
|
27
|
-
key: 'sales.orders',
|
|
28
|
-
label: 'Orders',
|
|
29
|
-
type: 'collection',
|
|
30
|
-
body: [
|
|
31
|
-
{
|
|
32
|
-
type: 'data',
|
|
33
|
-
source: { model: 'sales.order' },
|
|
34
|
-
children: [
|
|
35
|
-
{
|
|
36
|
-
type: 'data',
|
|
37
|
-
source: { model: 'sales.order_item' },
|
|
38
|
-
children: [{ type: 'text', bind: { field: 'quantity' } }],
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const models = extractSourceModels(page);
|
|
46
|
-
expect(models).toContain('sales.order');
|
|
47
|
-
expect(models).toContain('sales.order_item');
|
|
48
|
-
expect(models).toHaveLength(2);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('extracts models from table with model binding', () => {
|
|
52
|
-
const page: PageDefinition = {
|
|
53
|
-
key: 'sales.orders',
|
|
54
|
-
label: 'Orders',
|
|
55
|
-
type: 'collection',
|
|
56
|
-
body: [
|
|
57
|
-
{
|
|
58
|
-
type: 'table',
|
|
59
|
-
bind: { model: { name: 'sales.invoice' } },
|
|
60
|
-
children: [{ type: 'column', bind: { field: 'name' } }],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const models = extractSourceModels(page);
|
|
66
|
-
expect(models).toContain('sales.invoice');
|
|
67
|
-
expect(models).toHaveLength(1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('extracts models from multiple siblings', () => {
|
|
71
|
-
const page: PageDefinition = {
|
|
72
|
-
key: 'sales.orders',
|
|
73
|
-
label: 'Orders',
|
|
74
|
-
type: 'collection',
|
|
75
|
-
body: [
|
|
76
|
-
{ type: 'data', source: { model: 'sales.order' }, children: [] },
|
|
77
|
-
{ type: 'data', source: { model: 'contacts.contact' }, children: [] },
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const models = extractSourceModels(page);
|
|
82
|
-
expect(models).toContain('sales.order');
|
|
83
|
-
expect(models).toContain('contacts.contact');
|
|
84
|
-
expect(models).toHaveLength(2);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('returns empty for body without data sources', () => {
|
|
88
|
-
const page: PageDefinition = {
|
|
89
|
-
key: 'sales.dashboard',
|
|
90
|
-
label: 'Dashboard',
|
|
91
|
-
type: 'dashboard',
|
|
92
|
-
body: [
|
|
93
|
-
{ type: 'text', props: { content: 'Hello' } },
|
|
94
|
-
{ type: 'button', props: { label: 'Click' } },
|
|
95
|
-
],
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const models = extractSourceModels(page);
|
|
99
|
-
expect(models).toHaveLength(0);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('deduplicates repeated model references', () => {
|
|
103
|
-
const page: PageDefinition = {
|
|
104
|
-
key: 'sales.orders',
|
|
105
|
-
label: 'Orders',
|
|
106
|
-
type: 'collection',
|
|
107
|
-
body: [
|
|
108
|
-
{ type: 'data', source: { model: 'sales.order' }, children: [] },
|
|
109
|
-
{ type: 'table', bind: { model: { name: 'sales.order' } }, children: [] },
|
|
110
|
-
],
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const models = extractSourceModels(page);
|
|
114
|
-
expect(models).toEqual(['sales.order']);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('validatePageSources', () => {
|
|
119
|
-
const knownModels = new Set(['sales.order', 'sales.customer', 'contacts.contact']);
|
|
120
|
-
|
|
121
|
-
it('catches unresolved models in data widget', () => {
|
|
122
|
-
const pages: Array<{ module: string; page: PageDefinition }> = [
|
|
123
|
-
{
|
|
124
|
-
module: 'sales',
|
|
125
|
-
page: {
|
|
126
|
-
key: 'sales.broken',
|
|
127
|
-
label: 'Broken',
|
|
128
|
-
type: 'collection',
|
|
129
|
-
body: [
|
|
130
|
-
{
|
|
131
|
-
type: 'data',
|
|
132
|
-
source: { model: 'sales.nonexistent' },
|
|
133
|
-
children: [],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
const warnings = validatePageSources(pages, knownModels);
|
|
141
|
-
expect(warnings).toHaveLength(1);
|
|
142
|
-
expect(warnings[0].pageKey).toBe('sales.broken');
|
|
143
|
-
expect(warnings[0].location).toBe('body[0]');
|
|
144
|
-
expect(warnings[0].message).toContain('sales.nonexistent');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('catches unresolved models in nested children', () => {
|
|
148
|
-
const pages: Array<{ module: string; page: PageDefinition }> = [
|
|
149
|
-
{
|
|
150
|
-
module: 'sales',
|
|
151
|
-
page: {
|
|
152
|
-
key: 'sales.nested',
|
|
153
|
-
label: 'Nested',
|
|
154
|
-
type: 'collection',
|
|
155
|
-
body: [
|
|
156
|
-
{
|
|
157
|
-
type: 'data',
|
|
158
|
-
source: { model: 'sales.order' },
|
|
159
|
-
children: [
|
|
160
|
-
{
|
|
161
|
-
type: 'data',
|
|
162
|
-
source: { model: 'sales.ghost' },
|
|
163
|
-
children: [],
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
const warnings = validatePageSources(pages, knownModels);
|
|
173
|
-
expect(warnings).toHaveLength(1);
|
|
174
|
-
expect(warnings[0].location).toBe('body[0].children[0]');
|
|
175
|
-
expect(warnings[0].message).toContain('sales.ghost');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('catches unresolved models in table bind.model', () => {
|
|
179
|
-
const pages: Array<{ module: string; page: PageDefinition }> = [
|
|
180
|
-
{
|
|
181
|
-
module: 'sales',
|
|
182
|
-
page: {
|
|
183
|
-
key: 'sales.table',
|
|
184
|
-
label: 'Table',
|
|
185
|
-
type: 'collection',
|
|
186
|
-
body: [
|
|
187
|
-
{
|
|
188
|
-
type: 'table',
|
|
189
|
-
bind: { model: { name: 'sales.missing' } },
|
|
190
|
-
children: [],
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
const warnings = validatePageSources(pages, knownModels);
|
|
198
|
-
expect(warnings).toHaveLength(1);
|
|
199
|
-
expect(warnings[0].location).toBe('body[0]');
|
|
200
|
-
expect(warnings[0].message).toContain('sales.missing');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('passes when all models exist', () => {
|
|
204
|
-
const pages: Array<{ module: string; page: PageDefinition }> = [
|
|
205
|
-
{
|
|
206
|
-
module: 'sales',
|
|
207
|
-
page: {
|
|
208
|
-
key: 'sales.orders',
|
|
209
|
-
label: 'Orders',
|
|
210
|
-
type: 'collection',
|
|
211
|
-
body: [
|
|
212
|
-
{
|
|
213
|
-
type: 'data',
|
|
214
|
-
source: { model: 'sales.order' },
|
|
215
|
-
children: [{ type: 'text', bind: { field: 'name' } }],
|
|
216
|
-
},
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
const warnings = validatePageSources(pages, knownModels);
|
|
223
|
-
expect(warnings).toHaveLength(0);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { ProjectScanner } from '../project-scanner.js';
|
|
4
|
-
|
|
5
|
-
const FIXTURE_ROOT = path.resolve(__dirname, '../../../../../tests/fixtures/basic-app');
|
|
6
|
-
|
|
7
|
-
describe('ProjectScanner', () => {
|
|
8
|
-
it('discovers modules from modules/ directory', async () => {
|
|
9
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
10
|
-
const result = await scanner.scan();
|
|
11
|
-
|
|
12
|
-
expect(result.app.schemas.length).toBeGreaterThan(0);
|
|
13
|
-
const modules = new Set(result.app.schemas.map((s) => s.module));
|
|
14
|
-
expect(modules.has('sales')).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('discovers model schemas from flat files', async () => {
|
|
18
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
19
|
-
const result = await scanner.scan();
|
|
20
|
-
|
|
21
|
-
const schemaNames = result.app.schemas.map((s) => s.schema.name);
|
|
22
|
-
expect(schemaNames).toContain('customer');
|
|
23
|
-
expect(schemaNames).toContain('invoice');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('loads hooks from hooks/ directory', async () => {
|
|
27
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
28
|
-
const result = await scanner.scan();
|
|
29
|
-
|
|
30
|
-
expect(result.app.hooks).toBeDefined();
|
|
31
|
-
const invoiceHooks = result.app.hooks!.find((h) => h.model === 'sales.invoice');
|
|
32
|
-
expect(invoiceHooks).toBeDefined();
|
|
33
|
-
expect(invoiceHooks!.hooks.validate).toBeTypeOf('function');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('loads roles from per-module roles.ts', async () => {
|
|
37
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
38
|
-
const result = await scanner.scan();
|
|
39
|
-
|
|
40
|
-
expect(result.app.roles).toBeDefined();
|
|
41
|
-
expect(result.app.roles!.length).toBeGreaterThan(0);
|
|
42
|
-
const salesRoles = result.app.roles!.find((r) => r.app === 'sales');
|
|
43
|
-
expect(salesRoles).toBeDefined();
|
|
44
|
-
expect(salesRoles!.config['Sales User']).toBeDefined();
|
|
45
|
-
expect(salesRoles!.config['Sales Manager']).toBeDefined();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('loads rangka.config.ts', async () => {
|
|
49
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
50
|
-
const result = await scanner.scan();
|
|
51
|
-
|
|
52
|
-
expect(result.rangkaConfig).toBeDefined();
|
|
53
|
-
expect(result.rangkaConfig.database.dialect).toBe('pg');
|
|
54
|
-
expect(result.rangkaConfig.database.host).toBe('localhost');
|
|
55
|
-
expect(result.rangkaConfig.database.port).toBe(5433);
|
|
56
|
-
expect(result.rangkaConfig.database.database).toBe('rangka_test');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('returns empty extensions when extensions/ dir does not exist', async () => {
|
|
60
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
61
|
-
const result = await scanner.scan();
|
|
62
|
-
|
|
63
|
-
expect(result.app.extensions).toEqual([]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('throws when rangka.config.ts is missing', async () => {
|
|
67
|
-
const scanner = new ProjectScanner('/tmp/nonexistent-rangka-project');
|
|
68
|
-
await expect(scanner.scan()).rejects.toThrow('No rangka.config.ts found');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('produces DiscoveredApp compatible with boot()', async () => {
|
|
72
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
73
|
-
const result = await scanner.scan();
|
|
74
|
-
|
|
75
|
-
expect(result.app.packageInfo.path).toBe(FIXTURE_ROOT);
|
|
76
|
-
expect(result.app.packageInfo.rangka.type).toBe('app');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('discovers pages from pages/ directory', async () => {
|
|
80
|
-
const scanner = new ProjectScanner(FIXTURE_ROOT);
|
|
81
|
-
const result = await scanner.scan();
|
|
82
|
-
|
|
83
|
-
expect(result.app.pages).toBeDefined();
|
|
84
|
-
const pageKeys = result.app.pages!.map((p) => p.page.key);
|
|
85
|
-
expect(pageKeys).toContain('sales.customers');
|
|
86
|
-
expect(pageKeys).toContain('sales.orders');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { ModuleConfig } from '@rangka/shared';
|
|
2
|
-
import { CircularDependencyError, MissingDependencyError } from './types.js';
|
|
3
|
-
|
|
4
|
-
// Topological sort of app modules by their declared dependencies.
|
|
5
|
-
// Core is always first; remaining apps are sorted so dependencies load before dependents.
|
|
6
|
-
export function dependencySort(apps: ModuleConfig[]): ModuleConfig[] {
|
|
7
|
-
const appsByName = new Map<string, ModuleConfig>(apps.map((app) => [app.name, app]));
|
|
8
|
-
|
|
9
|
-
const coreApp = appsByName.get('core');
|
|
10
|
-
if (!coreApp) {
|
|
11
|
-
throw new Error('Core app must be present in the app list');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const nonCoreApps = apps.filter((a) => a.name !== 'core');
|
|
15
|
-
validateDependenciesExist(nonCoreApps, appsByName);
|
|
16
|
-
|
|
17
|
-
const sorted = topologicalSort(nonCoreApps, appsByName);
|
|
18
|
-
return [coreApp, ...sorted];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function validateDependenciesExist(
|
|
22
|
-
apps: ModuleConfig[],
|
|
23
|
-
appsByName: Map<string, ModuleConfig>,
|
|
24
|
-
): void {
|
|
25
|
-
for (const app of apps) {
|
|
26
|
-
for (const dep of app.depends ?? []) {
|
|
27
|
-
if (dep !== 'core' && !appsByName.has(dep)) {
|
|
28
|
-
throw new MissingDependencyError(app.name, dep);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Kahn's algorithm — processes apps with no remaining dependencies first.
|
|
35
|
-
function topologicalSort(
|
|
36
|
-
apps: ModuleConfig[],
|
|
37
|
-
appsByName: Map<string, ModuleConfig>,
|
|
38
|
-
): ModuleConfig[] {
|
|
39
|
-
const inDegree = new Map<string, number>();
|
|
40
|
-
const dependents = new Map<string, string[]>();
|
|
41
|
-
|
|
42
|
-
for (const app of apps) {
|
|
43
|
-
inDegree.set(app.name, 0);
|
|
44
|
-
dependents.set(app.name, []);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
for (const app of apps) {
|
|
48
|
-
for (const dep of app.depends ?? []) {
|
|
49
|
-
if (dep === 'core') continue;
|
|
50
|
-
dependents.get(dep)!.push(app.name);
|
|
51
|
-
inDegree.set(app.name, inDegree.get(app.name)! + 1);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const ready = apps
|
|
56
|
-
.filter((app) => inDegree.get(app.name) === 0)
|
|
57
|
-
.map((app) => app.name)
|
|
58
|
-
.sort();
|
|
59
|
-
|
|
60
|
-
const sorted: ModuleConfig[] = [];
|
|
61
|
-
|
|
62
|
-
while (ready.length > 0) {
|
|
63
|
-
const current = ready.shift()!;
|
|
64
|
-
sorted.push(appsByName.get(current)!);
|
|
65
|
-
|
|
66
|
-
for (const dependent of dependents.get(current) ?? []) {
|
|
67
|
-
const remaining = inDegree.get(dependent)! - 1;
|
|
68
|
-
inDegree.set(dependent, remaining);
|
|
69
|
-
if (remaining === 0) {
|
|
70
|
-
ready.push(dependent);
|
|
71
|
-
ready.sort();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (sorted.length < apps.length) {
|
|
77
|
-
const unsorted = apps.filter((a) => !sorted.includes(a)).map((a) => a.name);
|
|
78
|
-
throw new CircularDependencyError(unsorted);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return sorted;
|
|
82
|
-
}
|
package/src/boot/discovery.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import type { DiscoverySource, RangkaPackageInfo } from './types.js';
|
|
4
|
-
|
|
5
|
-
// Scans node_modules for packages that declare themselves as rangka apps in package.json.
|
|
6
|
-
export class NodeModulesDiscoverySource implements DiscoverySource {
|
|
7
|
-
constructor(private readonly projectRoot: string) {}
|
|
8
|
-
|
|
9
|
-
async findRangkaPackages(): Promise<RangkaPackageInfo[]> {
|
|
10
|
-
const nodeModulesPath = path.join(this.projectRoot, 'node_modules');
|
|
11
|
-
const packageDirs = await this.listPackageDirs(nodeModulesPath);
|
|
12
|
-
|
|
13
|
-
const results: RangkaPackageInfo[] = [];
|
|
14
|
-
for (const { dirPath, packageName } of packageDirs) {
|
|
15
|
-
const info = await this.tryReadRangkaConfig(dirPath, packageName);
|
|
16
|
-
if (info) results.push(info);
|
|
17
|
-
}
|
|
18
|
-
return results;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Lists all package directories, expanding scoped (@org/pkg) folders.
|
|
22
|
-
private async listPackageDirs(
|
|
23
|
-
nodeModulesPath: string,
|
|
24
|
-
): Promise<{ dirPath: string; packageName: string }[]> {
|
|
25
|
-
const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true });
|
|
26
|
-
const dirs: { dirPath: string; packageName: string }[] = [];
|
|
27
|
-
|
|
28
|
-
for (const entry of entries) {
|
|
29
|
-
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
30
|
-
|
|
31
|
-
if (entry.name.startsWith('@')) {
|
|
32
|
-
const scopePath = path.join(nodeModulesPath, entry.name);
|
|
33
|
-
const scopedEntries = await fs.readdir(scopePath, { withFileTypes: true });
|
|
34
|
-
for (const scoped of scopedEntries) {
|
|
35
|
-
if (!scoped.isDirectory()) continue;
|
|
36
|
-
dirs.push({
|
|
37
|
-
dirPath: path.join(scopePath, scoped.name),
|
|
38
|
-
packageName: `${entry.name}/${scoped.name}`,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
dirs.push({
|
|
43
|
-
dirPath: path.join(nodeModulesPath, entry.name),
|
|
44
|
-
packageName: entry.name,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return dirs;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Reads package.json and returns info only if it declares rangka.type === 'app'.
|
|
53
|
-
private async tryReadRangkaConfig(
|
|
54
|
-
dirPath: string,
|
|
55
|
-
packageName: string,
|
|
56
|
-
): Promise<RangkaPackageInfo | null> {
|
|
57
|
-
try {
|
|
58
|
-
const content = await fs.readFile(path.join(dirPath, 'package.json'), 'utf-8');
|
|
59
|
-
const pkgJson = JSON.parse(content);
|
|
60
|
-
|
|
61
|
-
if (pkgJson.rangka?.type === 'app') {
|
|
62
|
-
return {
|
|
63
|
-
packageName,
|
|
64
|
-
path: dirPath,
|
|
65
|
-
rangka: {
|
|
66
|
-
type: pkgJson.rangka.type,
|
|
67
|
-
entrypoint: pkgJson.rangka.entrypoint || './app.ts',
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
// package.json doesn't exist or is unreadable — skip
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// In-memory discovery source for testing.
|
|
79
|
-
export class MemoryDiscoverySource implements DiscoverySource {
|
|
80
|
-
constructor(private readonly packages: RangkaPackageInfo[]) {}
|
|
81
|
-
|
|
82
|
-
async findRangkaPackages(): Promise<RangkaPackageInfo[]> {
|
|
83
|
-
return this.packages;
|
|
84
|
-
}
|
|
85
|
-
}
|