@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.
@@ -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
- */ function setStepId(step, { endpointId }) {
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 === 'CallApi' ? 'endpoint' : 'request';
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
@@ -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
- try {
34
- components.app.git_sha = execSync('git rev-parse HEAD').toString().trim();
35
- } catch (_) {
36
- //pass
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, secrets, state, step, user }`
23
+ functionPrototype: `{ args, item, lowdefyApp, payload, secret, state, step, user }`
24
24
  }));
25
25
  }
26
26
  export default writeJs;
@@ -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 does not export a connection with that id.`);
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.join(context.directories.config, filePath));
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
- return (await nativeImport(fileUrl.href)).default;
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: refDef.transformer
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 && type.isString(refDef.path) && !path.isAbsolute(refDef.path)) {
507
- refDef.path = path.resolve(ctx.moduleRoot, 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 && refDef.path) {
516
- if (!refDef.path.startsWith(ctx.packageRoot + '/') && refDef.path !== ctx.packageRoot) {
517
- throw new ConfigError(`Module ref path "${refDef.path}" escapes the package root.`);
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
- // Parse exports object from manifest
126
- const rawExports = manifest.exports ?? {};
127
- const exportSections = [
128
- 'pages',
129
- 'components',
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;