@lowdefy/build 5.3.0 → 5.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.
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +6 -2
- package/dist/build/buildApi/buildRoutine/validateStep.js +18 -0
- package/dist/build/buildApp.js +18 -5
- package/dist/build/buildJs/writeJs.js +2 -2
- package/dist/build/buildMenu.js +5 -0
- package/dist/build/buildModuleDefs.js +52 -1
- package/dist/build/buildModules.js +1 -1
- package/dist/build/buildPages/validateCallApiRefs.js +9 -0
- package/dist/build/buildRefs/getUserJavascriptFunction.js +6 -9
- package/dist/build/buildRefs/runTransformer.js +3 -2
- package/dist/build/buildRefs/walker.js +22 -39
- package/dist/build/codegenI18nLocales.js +53 -0
- package/dist/build/jit/shallowBuild.js +16 -0
- package/dist/build/registerModules.js +6 -33
- package/dist/build/writeAppMeta.js +19 -0
- package/dist/build/writeI18n.js +117 -0
- package/dist/build/writePluginImports/writePluginImports.js +5 -0
- package/dist/build/writePluginImports/writeServerExternalPackages.js +121 -0
- package/dist/createContext.js +6 -1
- package/dist/defaultMessagesMap.js +3 -0
- package/dist/defaultTypesMap.js +532 -389
- package/dist/index.js +15 -0
- package/dist/lowdefySchema.js +170 -0
- package/dist/scripts/generateDefaultMessages.js +37 -0
- package/package.json +51 -51
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ function countStepTypes(step, { typeCounters }) {
|
|
16
|
-
if (step.type === 'CallApi') {
|
|
16
|
+
if (step.type === 'CallApi' || step.type === 'ValidateSchema') {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
typeCounters.requests.increment(step.type, step['~k']);
|
|
@@ -12,10 +12,14 @@
|
|
|
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
|
-
*/
|
|
15
|
+
*/ const prefixByType = {
|
|
16
|
+
CallApi: 'endpoint',
|
|
17
|
+
ValidateSchema: 'validate'
|
|
18
|
+
};
|
|
19
|
+
function setStepId(step, { endpointId }) {
|
|
16
20
|
step.stepId = step.id;
|
|
17
21
|
step.endpointId = endpointId;
|
|
18
|
-
const prefix = step.type
|
|
22
|
+
const prefix = prefixByType[step.type] ?? 'request';
|
|
19
23
|
step.id = `${prefix}:${endpointId}:${step.stepId}`;
|
|
20
24
|
}
|
|
21
25
|
export default setStepId;
|
|
@@ -69,6 +69,24 @@ function validateStep(step, { endpointId }) {
|
|
|
69
69
|
}
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
+
if (step.type === 'ValidateSchema') {
|
|
73
|
+
if (type.isNone(step.properties?.schema)) {
|
|
74
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" requires properties.schema.`, {
|
|
75
|
+
configKey
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (type.isNone(step.properties?.data)) {
|
|
79
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" requires properties.data.`, {
|
|
80
|
+
configKey
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!type.isNone(step.connectionId)) {
|
|
84
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
|
|
85
|
+
configKey
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
72
90
|
if (type.isUndefined(step.connectionId)) {
|
|
73
91
|
throw new ConfigError(`Step connectionId missing at endpoint "${endpointId}".`, {
|
|
74
92
|
configKey
|
package/dist/build/buildApp.js
CHANGED
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { execSync } from 'child_process';
|
|
16
16
|
import { type } from '@lowdefy/helpers';
|
|
17
|
+
function computeGitSha() {
|
|
18
|
+
const fromEnv = process.env.LOWDEFY_GIT_SHA?.trim();
|
|
19
|
+
if (fromEnv) return fromEnv;
|
|
20
|
+
try {
|
|
21
|
+
return execSync('git rev-parse HEAD').toString().trim();
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
function buildApp({ components }) {
|
|
18
27
|
if (type.isNone(components.app)) {
|
|
19
28
|
components.app = {};
|
|
@@ -30,11 +39,15 @@ function buildApp({ components }) {
|
|
|
30
39
|
if (type.isNone(components.app.html.appendHead)) {
|
|
31
40
|
components.app.html.appendHead = '';
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
components.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
components.appMeta = {
|
|
43
|
+
slug: components.slug ?? null,
|
|
44
|
+
name: components.name ?? null,
|
|
45
|
+
version: components.version ?? null,
|
|
46
|
+
description: components.description ?? null,
|
|
47
|
+
license: components.license ?? null,
|
|
48
|
+
lowdefyVersion: components.lowdefy ?? null,
|
|
49
|
+
gitSha: computeGitSha()
|
|
50
|
+
};
|
|
38
51
|
return components;
|
|
39
52
|
}
|
|
40
53
|
export default buildApp;
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
async function writeJs({ context }) {
|
|
17
17
|
await context.writeBuildArtifact('plugins/operators/clientJsMap.js', generateJsFile({
|
|
18
18
|
map: context.jsMap.client,
|
|
19
|
-
functionPrototype: `{ actions, args, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
|
|
19
|
+
functionPrototype: `{ actions, args, event, input, location, lowdefyApp, lowdefyGlobal, request, state, urlQuery, user }`
|
|
20
20
|
}));
|
|
21
21
|
await context.writeBuildArtifact('plugins/operators/serverJsMap.js', generateJsFile({
|
|
22
22
|
map: context.jsMap.server,
|
|
23
|
-
functionPrototype: `{ args, item, payload,
|
|
23
|
+
functionPrototype: `{ args, item, lowdefyApp, payload, secret, state, step, user }`
|
|
24
24
|
}));
|
|
25
25
|
}
|
|
26
26
|
export default writeJs;
|
package/dist/build/buildMenu.js
CHANGED
|
@@ -103,6 +103,11 @@ function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateM
|
|
|
103
103
|
public: true
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
if (menuItem.type === 'MenuDivider') {
|
|
107
|
+
menuItem.auth = {
|
|
108
|
+
public: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
106
111
|
checkDuplicateMenuItemId({
|
|
107
112
|
id: menuItem.id,
|
|
108
113
|
menuId,
|
|
@@ -20,7 +20,7 @@ import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
|
20
20
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
21
21
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
22
22
|
import fetchModules from './fetchModules.js';
|
|
23
|
-
import { resolveLocalManifest, resolveFullManifest } from './registerModules.js';
|
|
23
|
+
import { resolveLocalManifest, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
24
24
|
import resolveModuleDependencies from './resolveModuleDependencies.js';
|
|
25
25
|
validateOperatorsDynamic({
|
|
26
26
|
operators
|
|
@@ -30,6 +30,9 @@ const dynamicIdentifiers = collectDynamicIdentifiers({
|
|
|
30
30
|
});
|
|
31
31
|
async function parseLowdefyYaml({ context }) {
|
|
32
32
|
const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
|
|
33
|
+
// Stash for Phase 2.5 — consumer vars come from lowdefy.yaml, so refs
|
|
34
|
+
// within them must be parented to lowdefy.yaml's refDef.
|
|
35
|
+
context.lowdefyYamlRefDef = refDef;
|
|
33
36
|
const content = await getRefContent({
|
|
34
37
|
context,
|
|
35
38
|
refDef,
|
|
@@ -49,6 +52,10 @@ async function parseLowdefyYaml({ context }) {
|
|
|
49
52
|
env: process.env,
|
|
50
53
|
dynamicIdentifiers,
|
|
51
54
|
shouldStop: (path)=>{
|
|
55
|
+
// Defer entry vars and connections: they may contain cross-module
|
|
56
|
+
// refs that require modules to be registered first.
|
|
57
|
+
if (/^modules\.\d+\.vars$/.test(path)) return 'preserve';
|
|
58
|
+
if (/^modules\.\d+\.connections$/.test(path)) return 'preserve';
|
|
52
59
|
if (path.startsWith('modules')) return false;
|
|
53
60
|
return 'preserve';
|
|
54
61
|
}
|
|
@@ -61,6 +68,42 @@ async function parseLowdefyYaml({ context }) {
|
|
|
61
68
|
});
|
|
62
69
|
return config ?? {};
|
|
63
70
|
}
|
|
71
|
+
async function resolveEntryConfig({ entry, context }) {
|
|
72
|
+
const moduleEntry = context.modules[entry.id];
|
|
73
|
+
const lowdefyYamlRefDef = context.lowdefyYamlRefDef;
|
|
74
|
+
function makeAppLevelCtx() {
|
|
75
|
+
return new WalkContext({
|
|
76
|
+
buildContext: context,
|
|
77
|
+
refId: lowdefyYamlRefDef.id,
|
|
78
|
+
sourceRefId: null,
|
|
79
|
+
vars: {},
|
|
80
|
+
path: '',
|
|
81
|
+
currentFile: lowdefyYamlRefDef.path,
|
|
82
|
+
refChain: new Set(lowdefyYamlRefDef.path ? [
|
|
83
|
+
lowdefyYamlRefDef.path
|
|
84
|
+
] : []),
|
|
85
|
+
operators,
|
|
86
|
+
env: process.env,
|
|
87
|
+
dynamicIdentifiers
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const refDef = lowdefyYamlRefDef;
|
|
91
|
+
let resolvedVars = await resolve(moduleEntry.consumerVars, makeAppLevelCtx());
|
|
92
|
+
resolvedVars = evaluateStaticOperators({
|
|
93
|
+
context,
|
|
94
|
+
input: resolvedVars,
|
|
95
|
+
refDef
|
|
96
|
+
});
|
|
97
|
+
moduleEntry.consumerVars = resolvedVars ?? {};
|
|
98
|
+
let resolvedConnections = await resolve(moduleEntry.connections, makeAppLevelCtx());
|
|
99
|
+
resolvedConnections = evaluateStaticOperators({
|
|
100
|
+
context,
|
|
101
|
+
input: resolvedConnections,
|
|
102
|
+
refDef
|
|
103
|
+
});
|
|
104
|
+
moduleEntry.connections = resolvedConnections ?? {};
|
|
105
|
+
validateRequiredVars(moduleEntry.varDefs, moduleEntry.consumerVars, entry.id, entry.source);
|
|
106
|
+
}
|
|
64
107
|
async function buildModuleDefs({ context }) {
|
|
65
108
|
const lowdefyConfig = await parseLowdefyYaml({
|
|
66
109
|
context
|
|
@@ -86,6 +129,14 @@ async function buildModuleDefs({ context }) {
|
|
|
86
129
|
resolveModuleDependencies({
|
|
87
130
|
context
|
|
88
131
|
});
|
|
132
|
+
// Step 2.5: Resolve deferred entry vars and connections at app level,
|
|
133
|
+
// then validate required vars against the resolved values.
|
|
134
|
+
for (const entry of moduleEntries){
|
|
135
|
+
await resolveEntryConfig({
|
|
136
|
+
entry,
|
|
137
|
+
context
|
|
138
|
+
});
|
|
139
|
+
}
|
|
89
140
|
// Step 3: Full resolve — cross-module refs, preserved content
|
|
90
141
|
for (const entryId of Object.keys(context.modules)){
|
|
91
142
|
await resolveFullManifest({
|
|
@@ -45,7 +45,7 @@ function buildModules({ components, context }) {
|
|
|
45
45
|
const moduleConnIds = new Set((manifest.connections ?? []).map((c)=>c.id));
|
|
46
46
|
for (const remapKey of Object.keys(remapping)){
|
|
47
47
|
if (!moduleConnIds.has(remapKey)) {
|
|
48
|
-
throw new ConfigError(`Module "${entry.id}" connection remapping references "${remapKey}", ` + `but the module
|
|
48
|
+
throw new ConfigError(`Module "${entry.id}" connection remapping references "${remapKey}", ` + `but the module has no connection with that id.`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// Validate secret whitelist on non-remapped content
|
|
@@ -14,11 +14,20 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigWarning } from '@lowdefy/errors';
|
|
16
16
|
function validateCallApiRefs({ callApiActionRefs, endpointConfigs, context }) {
|
|
17
|
+
const existingEndpointIds = new Set(endpointConfigs.map((config)=>config.endpointId));
|
|
17
18
|
const internalEndpoints = new Set(endpointConfigs.filter((config)=>config.type === 'InternalApi').map((config)=>config.endpointId));
|
|
18
19
|
callApiActionRefs.forEach(({ endpointId, action, sourcePageId })=>{
|
|
19
20
|
if (action.skip === true) {
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
23
|
+
if (!existingEndpointIds.has(endpointId)) {
|
|
24
|
+
context.handleWarning(new ConfigWarning(`CallAPI action on page "${sourcePageId}" references non-existent endpoint "${endpointId}".`, {
|
|
25
|
+
configKey: action['~k'],
|
|
26
|
+
prodError: true,
|
|
27
|
+
checkSlug: 'callapi-refs'
|
|
28
|
+
}));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
22
31
|
if (internalEndpoints.has(endpointId)) {
|
|
23
32
|
context.handleWarning(new ConfigWarning(`CallAPI action on page "${sourcePageId}" targets InternalApi endpoint "${endpointId}". InternalApi endpoints are not accessible from client pages.`, {
|
|
24
33
|
configKey: action['~k'],
|
|
@@ -15,20 +15,17 @@
|
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import { pathToFileURL } from 'url';
|
|
17
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
18
|
-
// Create a native import() that survives webpack bundling. When this module is
|
|
19
|
-
// bundled by Next.js webpack for server-dev API routes, webpack transforms
|
|
20
|
-
// import() into __webpack_require__() which can't handle file:// URLs for
|
|
21
|
-
// loading user-provided resolver and transformer JS files from the config
|
|
22
|
-
// directory. The Function constructor creates the import call at runtime,
|
|
23
|
-
// bypassing webpack's static analysis.
|
|
24
|
-
const nativeImport = new Function('specifier', 'return import(specifier)');
|
|
25
18
|
async function getUserJavascriptFunction({ context, filePath }) {
|
|
26
19
|
try {
|
|
27
|
-
const fileUrl = pathToFileURL(path.
|
|
20
|
+
const fileUrl = pathToFileURL(path.resolve(context.directories.config, filePath));
|
|
28
21
|
// Bust Node.js module cache so edits to resolver/transformer JS files are
|
|
29
22
|
// picked up during dev rebuilds. Each import gets a unique URL.
|
|
30
23
|
fileUrl.searchParams.set('t', Date.now());
|
|
31
|
-
|
|
24
|
+
// webpackIgnore tells Next.js webpack to leave this dynamic import alone
|
|
25
|
+
// when bundling server-dev API routes — otherwise webpack rewrites import()
|
|
26
|
+
// into __webpack_require__() which can't handle file:// URLs for loading
|
|
27
|
+
// user-provided resolver/transformer JS files from the config directory.
|
|
28
|
+
return (await import(/* webpackIgnore: true */ fileUrl.href)).default;
|
|
32
29
|
} catch (error) {
|
|
33
30
|
throw new ConfigError(`Error importing ${filePath}.`, {
|
|
34
31
|
cause: error,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
16
|
import getUserJavascriptFunction from './getUserJavascriptFunction.js';
|
|
17
|
-
async function runTransformer({ context, input, refDef }) {
|
|
17
|
+
async function runTransformer({ context, input, refDef, referencedFrom }) {
|
|
18
18
|
if (refDef.transformer) {
|
|
19
19
|
const transformerFn = await getUserJavascriptFunction({
|
|
20
20
|
context,
|
|
@@ -25,7 +25,8 @@ async function runTransformer({ context, input, refDef }) {
|
|
|
25
25
|
} catch (error) {
|
|
26
26
|
throw new ConfigError(`Error calling transformer "${refDef.transformer}" from "${refDef.path}".`, {
|
|
27
27
|
cause: error,
|
|
28
|
-
filePath:
|
|
28
|
+
filePath: referencedFrom,
|
|
29
|
+
lineNumber: refDef.lineNumber
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -330,11 +330,6 @@ function resolveModulePageId(arg, moduleEntry, context, configKey) {
|
|
|
330
330
|
configKey
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
|
-
if (!(moduleEntry.exports?.pages ?? []).some((p)=>p.id === arg)) {
|
|
334
|
-
throw new ConfigError(`Module "${moduleEntry.id}" does not export page "${arg}".`, {
|
|
335
|
-
configKey
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
333
|
return `${moduleEntry.id}/${arg}`;
|
|
339
334
|
}
|
|
340
335
|
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
@@ -345,12 +340,6 @@ function resolveModulePageId(arg, moduleEntry, context, configKey) {
|
|
|
345
340
|
configKey,
|
|
346
341
|
usage: `_module.pageId { id: "${arg.id}", module: "${arg.module}" }`
|
|
347
342
|
});
|
|
348
|
-
if (!(targetEntry.exports?.pages ?? []).some((p)=>p.id === arg.id)) {
|
|
349
|
-
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
350
|
-
throw new ConfigError(`${caller} references page "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export page "${arg.id}".`, {
|
|
351
|
-
configKey
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
343
|
return `${targetEntry.id}/${arg.id}`;
|
|
355
344
|
}
|
|
356
345
|
throw new ConfigError('_module.pageId requires a string or object { id, module }.', {
|
|
@@ -365,11 +354,6 @@ function resolveModuleConnectionId(arg, moduleEntry, context, configKey) {
|
|
|
365
354
|
configKey
|
|
366
355
|
});
|
|
367
356
|
}
|
|
368
|
-
if (!(moduleEntry.exports?.connections ?? []).some((c)=>c.id === arg)) {
|
|
369
|
-
throw new ConfigError(`Module "${moduleEntry.id}" does not export connection "${arg}".`, {
|
|
370
|
-
configKey
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
357
|
const remapping = moduleEntry.connections ?? {};
|
|
374
358
|
if (remapping[arg]) {
|
|
375
359
|
return remapping[arg];
|
|
@@ -384,12 +368,6 @@ function resolveModuleConnectionId(arg, moduleEntry, context, configKey) {
|
|
|
384
368
|
configKey,
|
|
385
369
|
usage: `_module.connectionId { id: "${arg.id}", module: "${arg.module}" }`
|
|
386
370
|
});
|
|
387
|
-
if (!(targetEntry.exports?.connections ?? []).some((c)=>c.id === arg.id)) {
|
|
388
|
-
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
389
|
-
throw new ConfigError(`${caller} references connection "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export connection "${arg.id}".`, {
|
|
390
|
-
configKey
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
371
|
const targetRemapping = targetEntry.connections ?? {};
|
|
394
372
|
if (targetRemapping[arg.id]) {
|
|
395
373
|
return targetRemapping[arg.id];
|
|
@@ -408,11 +386,6 @@ function resolveModuleEndpointId(arg, moduleEntry, context, configKey) {
|
|
|
408
386
|
configKey
|
|
409
387
|
});
|
|
410
388
|
}
|
|
411
|
-
if (!(moduleEntry.exports?.api ?? []).some((e)=>e.id === arg)) {
|
|
412
|
-
throw new ConfigError(`Module "${moduleEntry.id}" does not export endpoint "${arg}".`, {
|
|
413
|
-
configKey
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
389
|
return `${moduleEntry.id}/${arg}`;
|
|
417
390
|
}
|
|
418
391
|
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
@@ -423,12 +396,6 @@ function resolveModuleEndpointId(arg, moduleEntry, context, configKey) {
|
|
|
423
396
|
configKey,
|
|
424
397
|
usage: `_module.endpointId { id: "${arg.id}", module: "${arg.module}" }`
|
|
425
398
|
});
|
|
426
|
-
if (!(targetEntry.exports?.api ?? []).some((e)=>e.id === arg.id)) {
|
|
427
|
-
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
428
|
-
throw new ConfigError(`${caller} references endpoint "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export endpoint "${arg.id}".`, {
|
|
429
|
-
configKey
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
399
|
return `${targetEntry.id}/${arg.id}`;
|
|
433
400
|
}
|
|
434
401
|
throw new ConfigError('_module.endpointId requires a string or object { id, module }.', {
|
|
@@ -503,8 +470,16 @@ async function resolveRef(node, ctx) {
|
|
|
503
470
|
refDef.key = await resolve(cloneForResolve(refDef.key), ctx);
|
|
504
471
|
}
|
|
505
472
|
// 4. Module path resolution: resolve relative paths from the module root
|
|
506
|
-
if (ctx.moduleRoot
|
|
507
|
-
refDef.path
|
|
473
|
+
if (ctx.moduleRoot) {
|
|
474
|
+
if (type.isString(refDef.path) && !path.isAbsolute(refDef.path)) {
|
|
475
|
+
refDef.path = path.resolve(ctx.moduleRoot, refDef.path);
|
|
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
|
+
}
|
|
508
483
|
}
|
|
509
484
|
// 5. Update refMap with resolved path; store original for resolver refs
|
|
510
485
|
ctx.refMap[refDef.id].path = refDef.path;
|
|
@@ -512,9 +487,16 @@ async function resolveRef(node, ctx) {
|
|
|
512
487
|
ctx.refMap[refDef.id].original = refDef.original;
|
|
513
488
|
}
|
|
514
489
|
// 6. Path escape constraint: module refs cannot escape the package root
|
|
515
|
-
if (ctx.packageRoot
|
|
516
|
-
|
|
517
|
-
|
|
490
|
+
if (ctx.packageRoot) {
|
|
491
|
+
for (const field of [
|
|
492
|
+
'path',
|
|
493
|
+
'resolver',
|
|
494
|
+
'transformer'
|
|
495
|
+
]){
|
|
496
|
+
const value = refDef[field];
|
|
497
|
+
if (type.isString(value) && !value.startsWith(ctx.packageRoot + '/') && value !== ctx.packageRoot) {
|
|
498
|
+
throw new ConfigError(`Module ref ${field} "${value}" escapes the package root.`);
|
|
499
|
+
}
|
|
518
500
|
}
|
|
519
501
|
}
|
|
520
502
|
// 7. Circular detection
|
|
@@ -628,7 +610,8 @@ async function resolveRef(node, ctx) {
|
|
|
628
610
|
content = await runTransformer({
|
|
629
611
|
context: ctx.buildContext,
|
|
630
612
|
input: content,
|
|
631
|
-
refDef
|
|
613
|
+
refDef,
|
|
614
|
+
referencedFrom: ctx.currentFile
|
|
632
615
|
});
|
|
633
616
|
// 14. Extract key
|
|
634
617
|
content = getKey({
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
const HEADER = `// Generated by @lowdefy/build. Do not edit.\n`;
|
|
17
|
+
const SAFE_IDENTIFIER_REGEX = /^[A-Za-z0-9_-]+$/;
|
|
18
|
+
function isSafeIdentifier(value) {
|
|
19
|
+
return type.isString(value) && SAFE_IDENTIFIER_REGEX.test(value);
|
|
20
|
+
}
|
|
21
|
+
function buildAntdLoaders(locales) {
|
|
22
|
+
const entries = locales.filter((locale)=>isSafeIdentifier(locale.code) && isSafeIdentifier(locale.antd)).map((locale)=>` '${locale.code}': () => import('antd/locale/${locale.antd}.js').then((m) => m.default ?? m),`);
|
|
23
|
+
return `${HEADER}const loaders = {\n${entries.join('\n')}\n};\nexport default loaders;\n`;
|
|
24
|
+
}
|
|
25
|
+
function buildDayjsImports(locales) {
|
|
26
|
+
const validDayjsLocales = locales.filter((locale)=>isSafeIdentifier(locale.code) && isSafeIdentifier(locale.dayjs));
|
|
27
|
+
const ids = Array.from(new Set(validDayjsLocales.map((locale)=>locale.dayjs)));
|
|
28
|
+
const imports = ids.map((id)=>`import 'dayjs/locale/${id}.js';`).join('\n');
|
|
29
|
+
const localeMap = validDayjsLocales.map((locale)=>` '${locale.code}': '${locale.dayjs}',`).join('\n');
|
|
30
|
+
return `${HEADER}${imports}\nconst localeMap = {\n${localeMap}\n};\nexport default localeMap;\n`;
|
|
31
|
+
}
|
|
32
|
+
// antd X (@ant-design/x@2.7.x) only ships two locale packs: en_US and zh_CN.
|
|
33
|
+
// Other locales fall back to en_US for X-native strings (New chat, OK/Cancel,
|
|
34
|
+
// Stop loading, Like/Dislike, etc.). App authors override per-locale via
|
|
35
|
+
// config.i18n.messages.{locale}.agent.antdx.* keys handled outside this codegen.
|
|
36
|
+
const ANTD_X_SUPPORTED = new Set([
|
|
37
|
+
'en_US',
|
|
38
|
+
'zh_CN'
|
|
39
|
+
]);
|
|
40
|
+
function buildAntdXLoaders(locales) {
|
|
41
|
+
const entries = locales.filter((locale)=>isSafeIdentifier(locale.code) && isSafeIdentifier(locale.antd)).map((locale)=>{
|
|
42
|
+
const pack = ANTD_X_SUPPORTED.has(locale.antd) ? locale.antd : 'en_US';
|
|
43
|
+
return ` '${locale.code}': () => import('@ant-design/x/locale/${pack}.js').then((m) => m.default ?? m),`;
|
|
44
|
+
});
|
|
45
|
+
return `${HEADER}const loaders = {\n${entries.join('\n')}\n};\nexport default loaders;\n`;
|
|
46
|
+
}
|
|
47
|
+
async function codegenI18nLocales({ components, context }) {
|
|
48
|
+
const locales = components.config?.i18n?.locales ?? [];
|
|
49
|
+
await context.writeBuildArtifact('i18n/antdLocales.js', buildAntdLoaders(locales));
|
|
50
|
+
await context.writeBuildArtifact('i18n/antdXLocales.js', buildAntdXLoaders(locales));
|
|
51
|
+
await context.writeBuildArtifact('i18n/dayjsLocales.js', buildDayjsImports(locales));
|
|
52
|
+
}
|
|
53
|
+
export default codegenI18nLocales;
|
|
@@ -38,6 +38,7 @@ import copyPublicFolder from '../copyPublicFolder.js';
|
|
|
38
38
|
import testSchema from '../testSchema.js';
|
|
39
39
|
import validateConfig from '../validateConfig.js';
|
|
40
40
|
import writeApp from '../writeApp.js';
|
|
41
|
+
import writeAppMeta from '../writeAppMeta.js';
|
|
41
42
|
import writeAuth from '../writeAuth.js';
|
|
42
43
|
import writeConfig from '../writeConfig.js';
|
|
43
44
|
import writeConnections from '../writeConnections.js';
|
|
@@ -46,6 +47,8 @@ import writeApi from '../writeApi.js';
|
|
|
46
47
|
import writeGlobal from '../writeGlobal.js';
|
|
47
48
|
import writeJs from '../buildJs/writeJs.js';
|
|
48
49
|
import writeLogger from '../writeLogger.js';
|
|
50
|
+
import codegenI18nLocales from '../codegenI18nLocales.js';
|
|
51
|
+
import writeI18n from '../writeI18n.js';
|
|
49
52
|
import writeTheme from '../writeTheme.js';
|
|
50
53
|
import writeMaps from '../writeMaps.js';
|
|
51
54
|
import updateServerPackageJson from '../full/updateServerPackageJson.js';
|
|
@@ -202,6 +205,10 @@ async function shallowBuild(options) {
|
|
|
202
205
|
components,
|
|
203
206
|
context
|
|
204
207
|
});
|
|
208
|
+
await writeAppMeta({
|
|
209
|
+
components,
|
|
210
|
+
context
|
|
211
|
+
});
|
|
205
212
|
await writeAuth({
|
|
206
213
|
components,
|
|
207
214
|
context
|
|
@@ -230,6 +237,14 @@ async function shallowBuild(options) {
|
|
|
230
237
|
components,
|
|
231
238
|
context
|
|
232
239
|
});
|
|
240
|
+
await writeI18n({
|
|
241
|
+
components,
|
|
242
|
+
context
|
|
243
|
+
});
|
|
244
|
+
await codegenI18nLocales({
|
|
245
|
+
components,
|
|
246
|
+
context
|
|
247
|
+
});
|
|
233
248
|
await writeLogger({
|
|
234
249
|
components,
|
|
235
250
|
context
|
|
@@ -253,6 +268,7 @@ async function shallowBuild(options) {
|
|
|
253
268
|
await context.writeBuildArtifact('jsMap.json', JSON.stringify(context.jsMap));
|
|
254
269
|
await context.writeBuildArtifact('idCounter.json', JSON.stringify(makeId.counter));
|
|
255
270
|
await context.writeBuildArtifact('customTypesMap.json', JSON.stringify(options.customTypesMap ?? {}));
|
|
271
|
+
await context.writeBuildArtifact('customMessagesMap.json', JSON.stringify(options.customMessagesMap ?? {}));
|
|
256
272
|
// Persist snapshot of installed packages for JIT missing-package detection.
|
|
257
273
|
// Written as a build artifact so JIT builds compare against the skeleton
|
|
258
274
|
// build state, not a potentially-updated package.json (race condition).
|
|
@@ -122,38 +122,12 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
122
122
|
throw new ConfigError(`Module "${entry.id}": each item in "dependencies" must have a string "id".`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'menus',
|
|
131
|
-
'connections',
|
|
132
|
-
'api'
|
|
133
|
-
];
|
|
134
|
-
const exports = {};
|
|
135
|
-
for (const section of exportSections){
|
|
136
|
-
const items = rawExports[section] ?? [];
|
|
137
|
-
if (!type.isArray(items)) {
|
|
138
|
-
throw new ConfigError(`Module "${entry.id}": exports.${section} must be an array.`);
|
|
139
|
-
}
|
|
140
|
-
for (const item of items){
|
|
141
|
-
if (!type.isString(item.id)) {
|
|
142
|
-
throw new ConfigError(`Module "${entry.id}": each item in exports.${section} must have a string "id".`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
exports[section] = items;
|
|
146
|
-
}
|
|
147
|
-
// Reject unknown keys in exports
|
|
148
|
-
for (const key of Object.keys(rawExports)){
|
|
149
|
-
if (!exportSections.includes(key)) {
|
|
150
|
-
throw new ConfigError(`Module "${entry.id}": unknown exports section "${key}". ` + `Valid sections: ${exportSections.join(', ')}.`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Validate required vars without defaults (needs raw defs + consumer values only).
|
|
154
|
-
// Type validation moves to after Phase 2 because defaults are resolved lazily.
|
|
125
|
+
// Capture var definitions for the registered module entry.
|
|
126
|
+
// Required-var validation is deferred to Phase 2.5 (buildModuleDefs.js)
|
|
127
|
+
// because entry.vars may contain unresolved _refs at this point.
|
|
128
|
+
// Type validation runs at the end of Phase 3 against the lazily-populated
|
|
129
|
+
// resolvedVarCache.
|
|
155
130
|
const varDefs = manifest.vars ?? {};
|
|
156
|
-
validateRequiredVars(varDefs, entry.vars ?? {}, entry.id, entry.source);
|
|
157
131
|
// Validate plugin dependencies against app's declared plugins
|
|
158
132
|
const requiredPlugins = manifest.plugins ?? [];
|
|
159
133
|
for (const plugin of requiredPlugins){
|
|
@@ -189,7 +163,6 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
189
163
|
connections: entry.connections ?? {},
|
|
190
164
|
manifest,
|
|
191
165
|
dependencies,
|
|
192
|
-
exports,
|
|
193
166
|
moduleDependencies: entry.dependencies ?? {},
|
|
194
167
|
refDef
|
|
195
168
|
};
|
|
@@ -244,4 +217,4 @@ async function resolveFullManifest({ entryId, context }) {
|
|
|
244
217
|
validateVarTypes(varDefs, moduleEntry.resolvedVarCache, entryId, moduleEntry.source);
|
|
245
218
|
}
|
|
246
219
|
}
|
|
247
|
-
export { resolveLocalManifest, resolveFullManifest };
|
|
220
|
+
export { resolveLocalManifest, resolveFullManifest, validateRequiredVars };
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { serializer } from '@lowdefy/helpers';
|
|
16
|
+
async function writeAppMeta({ components, context }) {
|
|
17
|
+
await context.writeBuildArtifact('appMeta.json', serializer.serializeToString(components.appMeta));
|
|
18
|
+
}
|
|
19
|
+
export default writeAppMeta;
|