@lowdefy/build 5.5.0 → 5.6.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/dist/build/buildAgents.js +9 -1
- package/dist/build/buildAuth/buildAgentAuth.js +56 -0
- package/dist/build/buildAuth/buildAuth.js +9 -0
- package/dist/build/buildAuth/getAgentRoles.js +39 -0
- package/dist/build/buildAuth/getProtectedAgents.js +29 -0
- package/dist/build/buildAuth/rejectReservedEntityIds.js +52 -0
- package/dist/build/buildPages/buildBlock/buildEvents.js +11 -1
- package/dist/build/buildPages/buildBlock/validateBlock.js +12 -1
- package/dist/build/buildRefs/getKey.js +16 -5
- package/dist/build/buildRefs/rebaseModuleRefPaths.js +34 -0
- package/dist/build/buildRefs/walker.js +26 -15
- package/dist/build/fetchModules.js +18 -4
- package/dist/build/jit/buildPageJit.js +18 -0
- package/dist/build/registerModules.js +25 -19
- package/dist/build/writeI18n.js +8 -1
- package/dist/build/writePluginImports/writeGlobalsCss.js +0 -1
- package/dist/defaultTypesMap.js +410 -385
- package/dist/test-utils/runBuild.js +8 -0
- package/dist/utils/logCollectedErrors.js +38 -10
- package/dist/utils/validateId.js +11 -0
- package/package.json +50 -50
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import fs from 'fs';
|
|
17
|
-
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import { isReserved, type } from '@lowdefy/helpers';
|
|
18
18
|
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
19
19
|
import { RESERVED_PLATFORM_TOOL_NAMES } from '@lowdefy/ai-utils';
|
|
20
20
|
import countOperators from '../utils/countOperators.js';
|
|
@@ -56,6 +56,14 @@ function buildAgents({ components, context }) {
|
|
|
56
56
|
});
|
|
57
57
|
components.agents.forEach((agent)=>{
|
|
58
58
|
const configKey = agent['~k'];
|
|
59
|
+
// agent.id becomes agent.agentId, which keys the detectCycles graph and the
|
|
60
|
+
// runtime agent registry - both plain objects. Reject a reserved name here,
|
|
61
|
+
// where the config location is still in hand.
|
|
62
|
+
if (isReserved(agent.id)) {
|
|
63
|
+
throw new ConfigError(`Agent id "${agent.id}" is a reserved name and cannot be used as an id.`, {
|
|
64
|
+
configKey
|
|
65
|
+
});
|
|
66
|
+
}
|
|
59
67
|
// Check duplicates
|
|
60
68
|
checkDuplicateAgentId({
|
|
61
69
|
id: agent.id,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */ /*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import getAgentRoles from './getAgentRoles.js';
|
|
18
|
+
import getProtectedAgents from './getProtectedAgents.js';
|
|
19
|
+
import { isInPatternList } from './matchPattern.js';
|
|
20
|
+
// Agents are served from the API surface, so auth.api patterns match agent ids too.
|
|
21
|
+
function buildAgentAuth({ components, context }) {
|
|
22
|
+
const protectedAgents = getProtectedAgents({
|
|
23
|
+
components
|
|
24
|
+
});
|
|
25
|
+
const agentRoles = getAgentRoles({
|
|
26
|
+
components
|
|
27
|
+
});
|
|
28
|
+
let configPublicApi = [];
|
|
29
|
+
if (type.isArray(components.auth.api.public)) {
|
|
30
|
+
configPublicApi = components.auth.api.public;
|
|
31
|
+
}
|
|
32
|
+
(components.agents ?? []).forEach((agent)=>{
|
|
33
|
+
if (agentRoles[agent.id]) {
|
|
34
|
+
if (isInPatternList(agent.id, configPublicApi)) {
|
|
35
|
+
throw new ConfigError(`Agent "${agent.id}" is both protected by roles and public.`, {
|
|
36
|
+
received: agentRoles[agent.id],
|
|
37
|
+
configKey: agent['~k']
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
agent.auth = {
|
|
41
|
+
public: false,
|
|
42
|
+
roles: agentRoles[agent.id]
|
|
43
|
+
};
|
|
44
|
+
} else if (protectedAgents.includes(agent.id)) {
|
|
45
|
+
agent.auth = {
|
|
46
|
+
public: false
|
|
47
|
+
};
|
|
48
|
+
} else {
|
|
49
|
+
agent.auth = {
|
|
50
|
+
public: true
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return components;
|
|
55
|
+
}
|
|
56
|
+
export default buildAgentAuth;
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
16
|
import buildAuthPlugins from './buildAuthPlugins.js';
|
|
17
|
+
import buildAgentAuth from './buildAgentAuth.js';
|
|
17
18
|
import buildApiAuth from './buildApiAuth.js';
|
|
18
19
|
import buildPageAuth from './buildPageAuth.js';
|
|
20
|
+
import rejectReservedEntityIds from './rejectReservedEntityIds.js';
|
|
19
21
|
import validateAuthConfig from './validateAuthConfig.js';
|
|
20
22
|
function buildAuth({ components, context }) {
|
|
21
23
|
const configured = !type.isNone(components.auth);
|
|
@@ -23,11 +25,18 @@ function buildAuth({ components, context }) {
|
|
|
23
25
|
components,
|
|
24
26
|
context
|
|
25
27
|
});
|
|
28
|
+
rejectReservedEntityIds({
|
|
29
|
+
components
|
|
30
|
+
});
|
|
26
31
|
components.auth.configured = configured;
|
|
27
32
|
buildApiAuth({
|
|
28
33
|
components,
|
|
29
34
|
context
|
|
30
35
|
});
|
|
36
|
+
buildAgentAuth({
|
|
37
|
+
components,
|
|
38
|
+
context
|
|
39
|
+
});
|
|
31
40
|
buildPageAuth({
|
|
32
41
|
components,
|
|
33
42
|
context
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { matchesPattern } from './matchPattern.js';
|
|
16
|
+
function getAgentRoles({ components }) {
|
|
17
|
+
const roles = components.auth.api.roles;
|
|
18
|
+
const agentIds = (components.agents ?? []).map((agent)=>agent.id);
|
|
19
|
+
const agentRoles = {};
|
|
20
|
+
Object.keys(roles).forEach((roleName)=>{
|
|
21
|
+
roles[roleName].forEach((pattern)=>{
|
|
22
|
+
agentIds.forEach((agentId)=>{
|
|
23
|
+
if (matchesPattern(agentId, pattern)) {
|
|
24
|
+
if (!agentRoles[agentId]) {
|
|
25
|
+
agentRoles[agentId] = new Set();
|
|
26
|
+
}
|
|
27
|
+
agentRoles[agentId].add(roleName);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
Object.keys(agentRoles).forEach((agentId)=>{
|
|
33
|
+
agentRoles[agentId] = [
|
|
34
|
+
...agentRoles[agentId]
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
return agentRoles;
|
|
38
|
+
}
|
|
39
|
+
export default getAgentRoles;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { isInPatternList } from './matchPattern.js';
|
|
17
|
+
function getProtectedAgents({ components }) {
|
|
18
|
+
const agentIds = (components.agents ?? []).map((agent)=>agent.id);
|
|
19
|
+
let protectedAgents = [];
|
|
20
|
+
if (type.isArray(components.auth.api.public)) {
|
|
21
|
+
protectedAgents = agentIds.filter((agentId)=>!isInPatternList(agentId, components.auth.api.public));
|
|
22
|
+
} else if (components.auth.api.protected === true) {
|
|
23
|
+
protectedAgents = agentIds;
|
|
24
|
+
} else if (type.isArray(components.auth.api.protected)) {
|
|
25
|
+
protectedAgents = agentIds.filter((agentId)=>isInPatternList(agentId, components.auth.api.protected));
|
|
26
|
+
}
|
|
27
|
+
return protectedAgents;
|
|
28
|
+
}
|
|
29
|
+
export default getProtectedAgents;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { isReserved } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
// buildAuth runs before validateId sees page, endpoint and agent ids, and it keys
|
|
18
|
+
// plain-object role maps on them: getPageRoles, getApiRoles and getAgentRoles all
|
|
19
|
+
// build `roles[id]` and read it back with a truthiness check. A reserved id reads
|
|
20
|
+
// through Object.prototype there, so the entity is silently marked protected with
|
|
21
|
+
// Object.prototype as its roles - and the object then travels into addKeys, which
|
|
22
|
+
// stamps a '~k' onto Object.prototype and corrupts every plain object in the
|
|
23
|
+
// process. Gate the ids here, where buildAuth first touches them.
|
|
24
|
+
//
|
|
25
|
+
// validateId reports the same ids again in buildApi, buildAgents and buildPages;
|
|
26
|
+
// logCollectedErrors collapses the pair on source line plus message so the author
|
|
27
|
+
// reads it once.
|
|
28
|
+
function rejectReservedEntityIds({ components }) {
|
|
29
|
+
const entityTypes = [
|
|
30
|
+
{
|
|
31
|
+
items: components.pages,
|
|
32
|
+
field: 'Page id'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
items: components.api,
|
|
36
|
+
field: 'Endpoint id'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
items: components.agents,
|
|
40
|
+
field: 'Agent id'
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
entityTypes.forEach(({ items, field })=>{
|
|
44
|
+
(items ?? []).forEach((item)=>{
|
|
45
|
+
if (!isReserved(item.id)) return;
|
|
46
|
+
throw new ConfigError(`${field} "${item.id}" is a reserved name and cannot be used as an id.`, {
|
|
47
|
+
configKey: item['~k']
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export default rejectReservedEntityIds;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { type } from '@lowdefy/helpers';
|
|
15
|
+
*/ import { isReserved, type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
17
|
import createCheckDuplicateId from '../../../utils/createCheckDuplicateId.js';
|
|
18
18
|
const BROWSER_DEFAULT_SHORTCUTS = new Set([
|
|
@@ -175,6 +175,16 @@ function buildEvents(block, pageContext) {
|
|
|
175
175
|
configKey: eventConfigKey
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
|
+
// The client's shortcut manager keys a plain object by the normalized
|
|
179
|
+
// shortcut. Normalization neither creates nor removes a reserved name:
|
|
180
|
+
// multi-character key names pass through unchanged, and no reserved
|
|
181
|
+
// name is a single character. So checking the raw string is enough, and
|
|
182
|
+
// a modified form like "Ctrl+__proto__" stays valid.
|
|
183
|
+
if (isReserved(shortcut)) {
|
|
184
|
+
throw new ConfigError(`Event shortcut "${shortcut}" on event "${key}" on block "${block.blockId}" on page "${pageContext.pageId}" is a reserved name and cannot be used as a shortcut.`, {
|
|
185
|
+
configKey: eventConfigKey
|
|
186
|
+
});
|
|
187
|
+
}
|
|
178
188
|
if (BROWSER_DEFAULT_SHORTCUTS.has(shortcut.toLowerCase())) {
|
|
179
189
|
pageContext.context.handleWarning(new ConfigWarning(`Shortcut "${shortcut}" on event "${key}" on block "${block.blockId}" on page "${pageContext.pageId}" conflicts with a browser default.`, {
|
|
180
190
|
configKey: eventConfigKey
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { type } from '@lowdefy/helpers';
|
|
15
|
+
*/ import { isReserved, splitPath, type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
function validateBlock(block, { pageId }, parentConfigKey) {
|
|
18
18
|
const configKey = block?.['~k'] ?? parentConfigKey;
|
|
@@ -33,6 +33,17 @@ function validateBlock(block, { pageId }, parentConfigKey) {
|
|
|
33
33
|
configKey
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
// Block ids are dot-paths that nest state, so they bypass validateId's shape gate. Every segment
|
|
37
|
+
// still becomes an object key on the way through get/set, so check per segment: a block
|
|
38
|
+
// `id: a.constructor.b` would reach `get` with a reserved segment and throw mid-render, which
|
|
39
|
+
// ErrorBoundary cannot catch because the throw happens during the render that would mount it.
|
|
40
|
+
const reservedSegment = splitPath(block.id).find(isReserved);
|
|
41
|
+
if (!type.isNone(reservedSegment)) {
|
|
42
|
+
throw new ConfigError(`Block id "${block.id}" uses reserved name "${reservedSegment}" as a path segment, at page "${pageId}".`, {
|
|
43
|
+
received: block.id,
|
|
44
|
+
configKey
|
|
45
|
+
});
|
|
46
|
+
}
|
|
36
47
|
if (type.isNone(block.type)) {
|
|
37
48
|
throw new ConfigError(`Block type is not defined at "${block.id}" on page "${pageId}".`, {
|
|
38
49
|
configKey
|
|
@@ -12,12 +12,23 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { get } from '@lowdefy/helpers';
|
|
16
|
-
|
|
15
|
+
*/ import { get, ReservedKeyError } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
function getKey({ input, refDef, filePath }) {
|
|
17
18
|
if (refDef.key) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
return get(input, refDef.key, {
|
|
21
|
+
default: null
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof ReservedKeyError)) throw error;
|
|
25
|
+
// The ref key is author-written YAML, so a reserved name is a config mistake. Without
|
|
26
|
+
// this the bare ReservedKeyError reaches the build with no file and no line.
|
|
27
|
+
throw new ConfigError(`_ref key "${refDef.key}" is a reserved name.`, {
|
|
28
|
+
cause: error,
|
|
29
|
+
filePath
|
|
30
|
+
});
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
33
|
return input;
|
|
23
34
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import path from 'path';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
// Module refs author their path/resolver/transformer relative to the module
|
|
18
|
+
// (e.g. "resolvers/makeActionPages.js"). Rebase those relative paths against the
|
|
19
|
+
// module root so they resolve from the module, not the app config dir. Mutates
|
|
20
|
+
// refDef in place. No-op when moduleRoot is not set or a path is already absolute.
|
|
21
|
+
function rebaseModuleRefPaths({ refDef, moduleRoot }) {
|
|
22
|
+
if (!moduleRoot) return refDef;
|
|
23
|
+
for (const field of [
|
|
24
|
+
'path',
|
|
25
|
+
'resolver',
|
|
26
|
+
'transformer'
|
|
27
|
+
]){
|
|
28
|
+
if (type.isString(refDef[field]) && !path.isAbsolute(refDef[field])) {
|
|
29
|
+
refDef[field] = path.resolve(moduleRoot, refDef[field]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return refDef;
|
|
33
|
+
}
|
|
34
|
+
export default rebaseModuleRefPaths;
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
|
-
import { get, serializer, type } from '@lowdefy/helpers';
|
|
16
|
+
import { get, ReservedKeyError, serializer, type } from '@lowdefy/helpers';
|
|
17
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
18
18
|
import { evaluateOperators } from '@lowdefy/operators';
|
|
19
19
|
import makeRefDefinition from './makeRefDefinition.js';
|
|
20
|
+
import rebaseModuleRefPaths from './rebaseModuleRefPaths.js';
|
|
20
21
|
import getRefContent from './getRefContent.js';
|
|
21
22
|
import getModuleRefContent from './getModuleRefContent.js';
|
|
22
23
|
import runTransformer from './runTransformer.js';
|
|
@@ -202,19 +203,35 @@ function evaluateBuildOperator(node, ctx) {
|
|
|
202
203
|
}
|
|
203
204
|
return output;
|
|
204
205
|
}
|
|
206
|
+
// _var keys are author-written YAML, so a reserved name (e.g. "constructor") is a config
|
|
207
|
+
// mistake, not a missing var. Wraps both call shapes for that check. The object form call
|
|
208
|
+
// site below passes no `options` argument at all, so a missing key comes back from `get` as
|
|
209
|
+
// `undefined` instead of a default — resolveVar relies on that `undefined` to tell "var not
|
|
210
|
+
// provided" apart from "var provided as null".
|
|
211
|
+
function readVar(key, ctx, options) {
|
|
212
|
+
try {
|
|
213
|
+
return get(ctx.vars, key, options);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!(error instanceof ReservedKeyError)) throw error;
|
|
216
|
+
throw new ConfigError(`_var key "${key}" is a reserved name.`, {
|
|
217
|
+
cause: error,
|
|
218
|
+
filePath: ctx.currentFile
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
205
222
|
// Resolve a _var node
|
|
206
223
|
function resolveVar(node, ctx) {
|
|
207
224
|
const varDef = node._var;
|
|
208
225
|
// String form: { _var: "key" }
|
|
209
226
|
if (type.isString(varDef)) {
|
|
210
|
-
const value =
|
|
227
|
+
const value = readVar(varDef, ctx, {
|
|
211
228
|
default: null
|
|
212
229
|
});
|
|
213
230
|
return cloneVarValue(value, ctx.sourceRefId);
|
|
214
231
|
}
|
|
215
232
|
// Object form: { _var: { key, default } }
|
|
216
233
|
if (type.isObject(varDef) && type.isString(varDef.key)) {
|
|
217
|
-
const varFromParent =
|
|
234
|
+
const varFromParent = readVar(varDef.key, ctx);
|
|
218
235
|
// Var provided (even if null) → use parent's sourceRefId for location
|
|
219
236
|
if (!type.isUndefined(varFromParent)) {
|
|
220
237
|
return cloneVarValue(varFromParent, ctx.sourceRefId);
|
|
@@ -470,17 +487,10 @@ async function resolveRef(node, ctx) {
|
|
|
470
487
|
refDef.key = await resolve(cloneForResolve(refDef.key), ctx);
|
|
471
488
|
}
|
|
472
489
|
// 4. Module path resolution: resolve relative paths from the module root
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (type.isString(refDef.resolver) && !path.isAbsolute(refDef.resolver)) {
|
|
478
|
-
refDef.resolver = path.resolve(ctx.moduleRoot, refDef.resolver);
|
|
479
|
-
}
|
|
480
|
-
if (type.isString(refDef.transformer) && !path.isAbsolute(refDef.transformer)) {
|
|
481
|
-
refDef.transformer = path.resolve(ctx.moduleRoot, refDef.transformer);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
490
|
+
rebaseModuleRefPaths({
|
|
491
|
+
refDef,
|
|
492
|
+
moduleRoot: ctx.moduleRoot
|
|
493
|
+
});
|
|
484
494
|
// 5. Update refMap with resolved path; store original for resolver refs
|
|
485
495
|
ctx.refMap[refDef.id].path = refDef.path;
|
|
486
496
|
if (!refDef.path) {
|
|
@@ -616,7 +626,8 @@ async function resolveRef(node, ctx) {
|
|
|
616
626
|
// 14. Extract key
|
|
617
627
|
content = getKey({
|
|
618
628
|
input: content,
|
|
619
|
-
refDef
|
|
629
|
+
refDef,
|
|
630
|
+
filePath: ctx.currentFile
|
|
620
631
|
});
|
|
621
632
|
// 15. Tag all nodes with ~r for provenance
|
|
622
633
|
tagRefDeep(content, refDef.id);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
+
import { ReservedKeyError, setKey } from '@lowdefy/helpers';
|
|
17
18
|
import { ConfigError } from '@lowdefy/errors';
|
|
18
19
|
import fetchGitHubModule from './fetchGitHubModule.js';
|
|
19
20
|
import parseModuleSource from './parseModuleSource.js';
|
|
@@ -28,6 +29,19 @@ function findGitRoot(startPath) {
|
|
|
28
29
|
dir = parent;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
// Module ids come from the app's lowdefy.yaml, so they key this map from user
|
|
33
|
+
// config. Surface a reserved id as the build's ConfigError rather than a bare
|
|
34
|
+
// ReservedKeyError.
|
|
35
|
+
function setResolvedPaths(resolved, id, paths) {
|
|
36
|
+
try {
|
|
37
|
+
setKey(resolved, id, paths);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!(error instanceof ReservedKeyError)) throw error;
|
|
40
|
+
throw new ConfigError(`Module entry id "${id}" is a reserved name.`, {
|
|
41
|
+
cause: error
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
31
45
|
async function fetchModules({ moduleEntries, context }) {
|
|
32
46
|
const resolved = {};
|
|
33
47
|
for (const entry of moduleEntries){
|
|
@@ -37,22 +51,22 @@ async function fetchModules({ moduleEntries, context }) {
|
|
|
37
51
|
if (!fs.existsSync(path.join(resolvedPath, 'module.lowdefy.yaml'))) {
|
|
38
52
|
throw new ConfigError(`Module "${entry.id}": module.lowdefy.yaml not found at ${resolvedPath}`);
|
|
39
53
|
}
|
|
40
|
-
resolved
|
|
54
|
+
setResolvedPaths(resolved, entry.id, {
|
|
41
55
|
packageRoot: findGitRoot(resolvedPath) ?? resolvedPath,
|
|
42
56
|
moduleRoot: resolvedPath,
|
|
43
57
|
isLocal: true
|
|
44
|
-
};
|
|
58
|
+
});
|
|
45
59
|
} else if (source.type === 'github') {
|
|
46
60
|
const cached = await fetchGitHubModule(source, context);
|
|
47
61
|
const moduleRoot = source.path ? path.join(cached.packageRoot, source.path) : cached.packageRoot;
|
|
48
62
|
if (!fs.existsSync(path.join(moduleRoot, 'module.lowdefy.yaml'))) {
|
|
49
63
|
throw new ConfigError(`Module "${entry.id}": module.lowdefy.yaml not found at path "${source.path || '/'}" in ${source.owner}/${source.repo}@${source.ref}`);
|
|
50
64
|
}
|
|
51
|
-
resolved
|
|
65
|
+
setResolvedPaths(resolved, entry.id, {
|
|
52
66
|
packageRoot: cached.packageRoot,
|
|
53
67
|
moduleRoot,
|
|
54
68
|
isLocal: false
|
|
55
|
-
};
|
|
69
|
+
});
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
72
|
return resolved;
|
|
@@ -31,6 +31,7 @@ import evaluateStaticOperators from '../buildRefs/evaluateStaticOperators.js';
|
|
|
31
31
|
import getRefContent from '../buildRefs/getRefContent.js';
|
|
32
32
|
import jsMapParser from '../buildJs/jsMapParser.js';
|
|
33
33
|
import makeRefDefinition from '../buildRefs/makeRefDefinition.js';
|
|
34
|
+
import rebaseModuleRefPaths from '../buildRefs/rebaseModuleRefPaths.js';
|
|
34
35
|
import { resolve, WalkContext, cloneForResolve, tagRefDeep } from '../buildRefs/walker.js';
|
|
35
36
|
import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
|
|
36
37
|
import writeMaps from '../writeMaps.js';
|
|
@@ -151,6 +152,23 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
|
|
|
151
152
|
refDef = makeRefDefinition(refDefinition, null, buildContext.refMap);
|
|
152
153
|
buildContext.refMap[refDef.id].path = refDef.path;
|
|
153
154
|
}
|
|
155
|
+
// Module path resolution: resolve relative path/resolver/transformer from the
|
|
156
|
+
// module root. The full build does this in walker.js step 4 when an _ref node
|
|
157
|
+
// is encountered, but the JIT path builds the page refDef directly from
|
|
158
|
+
// resolverOriginal (the un-rebased authored _ref) and calls getRefContent
|
|
159
|
+
// without going through the walker — so a module resolver like
|
|
160
|
+
// "resolvers/makeActionPages.js" would otherwise resolve against the app
|
|
161
|
+
// config dir instead of the module root. (File-based module pages are
|
|
162
|
+
// unaffected: their paths are stored already-rebased in refMap.)
|
|
163
|
+
if (moduleEntry?.moduleRoot) {
|
|
164
|
+
rebaseModuleRefPaths({
|
|
165
|
+
refDef,
|
|
166
|
+
moduleRoot: moduleEntry.moduleRoot
|
|
167
|
+
});
|
|
168
|
+
if (type.isString(refDef.path)) {
|
|
169
|
+
buildContext.refMap[refDef.id].path = refDef.path;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
154
172
|
const pageContent = await getRefContent({
|
|
155
173
|
context: buildContext,
|
|
156
174
|
refDef,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import semver from 'semver';
|
|
17
|
-
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import { ReservedKeyError, setKey, type } from '@lowdefy/helpers';
|
|
18
18
|
import { ConfigError } from '@lowdefy/errors';
|
|
19
19
|
import operators from '@lowdefy/operators-js/operators/build';
|
|
20
20
|
import { resolve, WalkContext } from './buildRefs/walker.js';
|
|
@@ -69,9 +69,6 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
69
69
|
if (entry.id.includes('/')) {
|
|
70
70
|
throw new ConfigError(`Module entry id "${entry.id}" must not contain '/'. ` + `Use a flat identifier like "team-users".`);
|
|
71
71
|
}
|
|
72
|
-
if (entry.id === '__proto__' || entry.id === 'constructor' || entry.id === 'prototype') {
|
|
73
|
-
throw new ConfigError(`Module entry id "${entry.id}" is a reserved name.`);
|
|
74
|
-
}
|
|
75
72
|
if (!entry.source || !type.isString(entry.source)) {
|
|
76
73
|
throw new ConfigError(`Module entry "${entry.id}": 'source' is required and must be a string.`);
|
|
77
74
|
}
|
|
@@ -151,21 +148,30 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
151
148
|
throw new ConfigError(`Module "${entry.id}" requires plugin "${plugin.name}" version "${plugin.version}" ` + `but the app has version "${appVersion}" installed. ` + `Update the plugin to a compatible version.`);
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
151
|
+
try {
|
|
152
|
+
setKey(context.modules, entry.id, {
|
|
153
|
+
id: entry.id,
|
|
154
|
+
source: entry.source,
|
|
155
|
+
packageRoot,
|
|
156
|
+
moduleRoot,
|
|
157
|
+
isLocal,
|
|
158
|
+
consumerVars: entry.vars ?? {},
|
|
159
|
+
varDefs,
|
|
160
|
+
resolvedVarCache: {},
|
|
161
|
+
connections: entry.connections ?? {},
|
|
162
|
+
manifest,
|
|
163
|
+
dependencies,
|
|
164
|
+
moduleDependencies: entry.dependencies ?? {},
|
|
165
|
+
refDef
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof ReservedKeyError) {
|
|
169
|
+
throw new ConfigError(`Module entry id "${entry.id}" is a reserved name.`, {
|
|
170
|
+
cause: error
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
169
175
|
}
|
|
170
176
|
async function resolveFullManifest({ entryId, context }) {
|
|
171
177
|
const moduleEntry = context.modules[entryId];
|
package/dist/build/writeI18n.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
16
|
-
import { mergeObjects, serializer, type } from '@lowdefy/helpers';
|
|
16
|
+
import { isReserved, mergeObjects, serializer, type } from '@lowdefy/helpers';
|
|
17
17
|
function collectPluginMessages({ messagesMap, declaredCodes }) {
|
|
18
18
|
const collected = {};
|
|
19
19
|
Object.values(messagesMap ?? {}).forEach((pluginCatalog)=>{
|
|
@@ -57,6 +57,13 @@ async function writeI18n({ components, context }) {
|
|
|
57
57
|
configKey: locale?.['~k'] ?? i18n['~k']
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
// The code keys the collected plugin catalog and the merged messages
|
|
61
|
+
// artifact, both plain objects.
|
|
62
|
+
if (isReserved(locale.code)) {
|
|
63
|
+
throw new ConfigError(`Locale code "${locale.code}" in "config.i18n.locales" is a reserved name and cannot be used as a locale code.`, {
|
|
64
|
+
configKey: locale['~k']
|
|
65
|
+
});
|
|
66
|
+
}
|
|
60
67
|
if (codes.has(locale.code)) {
|
|
61
68
|
throw new ConfigError(`Duplicate locale code "${locale.code}" in "config.i18n.locales".`, {
|
|
62
69
|
configKey: locale['~k']
|