@auraindustry/aurajs 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/authored-project.mjs +498 -2
  6. package/src/build-contract/capabilities.mjs +87 -1
  7. package/src/build-contract/constants.mjs +1 -0
  8. package/src/build-contract.mjs +2 -0
  9. package/src/bundler.mjs +143 -13
  10. package/src/cli.mjs +681 -13
  11. package/src/commands/packs.mjs +741 -0
  12. package/src/commands/project-authoring.mjs +128 -1
  13. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  14. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  15. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  16. package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
  17. package/src/conformance-mobile.mjs +166 -0
  18. package/src/conformance.mjs +89 -30
  19. package/src/evidence-bundle.mjs +242 -0
  20. package/src/headless-test/runtime-coordinator.mjs +186 -33
  21. package/src/headless-test.mjs +2 -0
  22. package/src/helpers/2d/index.mjs +183 -0
  23. package/src/helpers/index.mjs +26 -0
  24. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  25. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  26. package/src/helpers/starter-utils/animation-2d.js +337 -0
  27. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  28. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  29. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  30. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  31. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  32. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  33. package/src/helpers/starter-utils/core.js +150 -0
  34. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  35. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  36. package/src/helpers/starter-utils/index.js +26 -0
  37. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  38. package/src/helpers/starter-utils/journal-2d.js +267 -0
  39. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  40. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  41. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  42. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  43. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  44. package/src/helpers/starter-utils/triggers.js +662 -0
  45. package/src/helpers/starter-utils/tween-2d.js +615 -0
  46. package/src/helpers/starter-utils/wave-director.js +101 -0
  47. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  48. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  49. package/src/mobile/android/build.mjs +606 -0
  50. package/src/mobile/android/host-artifact.mjs +280 -0
  51. package/src/mobile/ios/build.mjs +1323 -0
  52. package/src/mobile/ios/host-artifact.mjs +819 -0
  53. package/src/mobile/shared/capabilities.mjs +174 -0
  54. package/src/packs/catalog.mjs +259 -0
  55. package/src/perf-benchmark-runner.mjs +17 -12
  56. package/src/perf-benchmark.mjs +408 -4
  57. package/src/publish-command.mjs +303 -6
  58. package/src/replay-runtime.mjs +257 -0
  59. package/src/scaffold/config.mjs +2 -0
  60. package/src/scaffold/fs.mjs +8 -1
  61. package/src/scaffold/project-docs.mjs +43 -1
  62. package/src/scaffold.mjs +4 -0
  63. package/src/session-runtime.mjs +4 -3
  64. package/src/web-conformance.mjs +0 -36
  65. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  66. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  67. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  68. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  69. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  70. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  71. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  72. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  73. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  74. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  75. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  76. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  77. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  78. package/templates/create/3d/src/runtime/materials.js +10 -0
  79. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  80. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  81. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  82. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  83. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  84. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  85. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  86. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  87. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  88. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  89. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  90. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  91. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  92. package/templates/create/shared/src/starter-utils/index.js +15 -1
  93. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  94. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  95. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  96. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  97. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  98. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  99. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  100. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  101. package/templates/create-bin/play.js +36 -7
  102. package/templates/skills/auramaxx/SKILL.md +46 -0
  103. package/templates/skills/auramaxx/project-requirements.md +68 -0
  104. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  105. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  106. package/templates/skills/aurajs/SKILL.md +0 -96
  107. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  108. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -2,11 +2,44 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  import {
5
+ MOBILE_CAPABILITY_REPORT_SCHEMA,
5
6
  PROJECT_CAPABILITY_DECLARATION_FILE,
6
7
  WEB_CAPABILITY_DECLARATION_SCHEMA,
7
8
  WEB_RUNTIME_CONFIG_SCHEMA,
8
9
  } from './constants.mjs';
9
10
  import { normalizePositiveInt } from './helpers.mjs';
11
+ import { buildMobileCapabilityReport } from '../mobile/shared/capabilities.mjs';
12
+
13
+ const MOBILE_UNSUPPORTED_API_RULES = Object.freeze([
14
+ Object.freeze({
15
+ api: 'aura.window.setCursorLocked',
16
+ reasonCode: 'mobile_cursor_lock_unsupported',
17
+ }),
18
+ Object.freeze({
19
+ api: 'aura.input.getMouseDelta',
20
+ reasonCode: 'mobile_cursor_lock_unsupported',
21
+ }),
22
+ Object.freeze({
23
+ api: 'aura.input.getMouseWheel',
24
+ reasonCode: 'mobile_mouse_wheel_unsupported',
25
+ }),
26
+ Object.freeze({
27
+ api: 'aura.window.setSize',
28
+ reasonCode: 'mobile_window_management_unsupported',
29
+ }),
30
+ Object.freeze({
31
+ api: 'aura.window.setFullscreen',
32
+ reasonCode: 'mobile_window_management_unsupported',
33
+ }),
34
+ Object.freeze({
35
+ api: 'aura.window.setTitle',
36
+ reasonCode: 'mobile_window_management_unsupported',
37
+ }),
38
+ Object.freeze({
39
+ api: 'aura.window.setCursorVisible',
40
+ reasonCode: 'mobile_window_management_unsupported',
41
+ }),
42
+ ]);
10
43
 
11
44
  export function buildRuntimeConfig(options = {}) {
12
45
  const windowConfig = options.windowConfig && typeof options.windowConfig === 'object'
@@ -67,6 +100,40 @@ function normalizeOptionalModules(modules) {
67
100
  };
68
101
  }
69
102
 
103
+ function buildMobileTargetAssessment(requiredApis) {
104
+ const matchedApis = new Set();
105
+ const reasonCodes = new Set();
106
+
107
+ for (const api of requiredApis) {
108
+ for (const rule of MOBILE_UNSUPPORTED_API_RULES) {
109
+ if (api !== rule.api) continue;
110
+ matchedApis.add(api);
111
+ reasonCodes.add(rule.reasonCode);
112
+ }
113
+ }
114
+
115
+ const normalizedMatchedApis = [...matchedApis].sort((a, b) => a.localeCompare(b));
116
+ const normalizedReasonCodes = [...reasonCodes].sort((a, b) => a.localeCompare(b));
117
+ const portable = normalizedReasonCodes.length === 0;
118
+
119
+ return {
120
+ schema: 'aurajs.mobile-capability-assessment.v1',
121
+ portable,
122
+ matchedDesktopApis: normalizedMatchedApis,
123
+ unsupportedReasonCodes: normalizedReasonCodes,
124
+ targets: {
125
+ android: {
126
+ portable,
127
+ unsupportedReasonCodes: normalizedReasonCodes,
128
+ },
129
+ ios: {
130
+ portable,
131
+ unsupportedReasonCodes: normalizedReasonCodes,
132
+ },
133
+ },
134
+ };
135
+ }
136
+
70
137
  function normalizeWebCapabilityDeclaration(value, baseModules = {}, source = null) {
71
138
  const declaration = value && typeof value === 'object' && !Array.isArray(value)
72
139
  ? value
@@ -85,16 +152,18 @@ function normalizeWebCapabilityDeclaration(value, baseModules = {}, source = nul
85
152
  }
86
153
 
87
154
  const declaredModules = normalizeOptionalModules(declaration.optionalModules);
155
+ const requiredApis = normalizeRequiredApis(declaration.requiredApis);
88
156
  return {
89
157
  schema: WEB_CAPABILITY_DECLARATION_SCHEMA,
90
158
  source,
91
- requiredApis: normalizeRequiredApis(declaration.requiredApis),
159
+ requiredApis,
92
160
  optionalModules: {
93
161
  physics: baseModules.physics || declaredModules.physics,
94
162
  network: baseModules.network || declaredModules.network,
95
163
  multiplayer: baseModules.multiplayer || declaredModules.multiplayer,
96
164
  steam: baseModules.steam || declaredModules.steam,
97
165
  },
166
+ mobileTargetAssessment: buildMobileTargetAssessment(requiredApis),
98
167
  };
99
168
  }
100
169
 
@@ -114,3 +183,20 @@ export function readProjectCapabilityDeclaration({ projectRoot, modules }) {
114
183
 
115
184
  return normalizeWebCapabilityDeclaration(parsed, baseModules, PROJECT_CAPABILITY_DECLARATION_FILE);
116
185
  }
186
+
187
+ export function buildMobileCapabilityAssertion({ projectRoot, modules, target }) {
188
+ const capabilityDeclaration = readProjectCapabilityDeclaration({
189
+ projectRoot,
190
+ modules,
191
+ });
192
+ const report = buildMobileCapabilityReport({
193
+ target,
194
+ capabilityDeclaration,
195
+ });
196
+ return {
197
+ ...report,
198
+ schema: MOBILE_CAPABILITY_REPORT_SCHEMA,
199
+ };
200
+ }
201
+
202
+ export { buildMobileCapabilityReport } from '../mobile/shared/capabilities.mjs';
@@ -2,5 +2,6 @@ export const BUILD_MANIFEST_SCHEMA = 'aurajs.build-manifest.v1';
2
2
  export const WEB_BUILD_MANIFEST_SCHEMA = 'aurajs.web-build-manifest.v1';
3
3
  export const WEB_RUNTIME_CONFIG_SCHEMA = 'aurajs.web-runtime-config.v1';
4
4
  export const WEB_CAPABILITY_DECLARATION_SCHEMA = 'aurajs.web-capability-declaration.v1';
5
+ export const MOBILE_CAPABILITY_REPORT_SCHEMA = 'aurajs.mobile-capability-report.v1';
5
6
 
6
7
  export const PROJECT_CAPABILITY_DECLARATION_FILE = 'aura.capabilities.json';
@@ -16,11 +16,13 @@ import { WEB_INDEX_HTML, WEB_LOADER_SOURCE } from './build-contract/web-template
16
16
 
17
17
  export {
18
18
  BUILD_MANIFEST_SCHEMA,
19
+ MOBILE_CAPABILITY_REPORT_SCHEMA,
19
20
  WEB_BUILD_MANIFEST_SCHEMA,
20
21
  WEB_CAPABILITY_DECLARATION_SCHEMA,
21
22
  WEB_RUNTIME_CONFIG_SCHEMA,
22
23
  } from './build-contract/constants.mjs';
23
24
  export { writeCanonicalJsonFile } from './build-contract/helpers.mjs';
25
+ export { buildRuntimeConfig, readProjectCapabilityDeclaration } from './build-contract/capabilities.mjs';
24
26
 
25
27
  export function writeBuildManifest(options = {}) {
26
28
  const outRoot = resolve(options.outRoot || process.cwd());
package/src/bundler.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
3
4
  import {
4
5
  existsSync,
5
6
  mkdirSync,
@@ -9,7 +10,8 @@ import {
9
10
  readdirSync,
10
11
  watch,
11
12
  } from 'node:fs';
12
- import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
13
+ import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
13
15
  import { buildGameActionRuntimeBootstrapSource } from './game-action-runtime.mjs';
14
16
  import { buildStateRestoreRuntimeBootstrapSource } from './game-state-runtime.mjs';
15
17
 
@@ -42,6 +44,22 @@ const AUTHORED_SOURCE_ROOT_DIRS = Object.freeze([
42
44
  'data',
43
45
  ]);
44
46
 
47
+ const BUNDLER_DIR = dirname(fileURLToPath(import.meta.url));
48
+ const CLI_PACKAGE_ROOT = resolve(BUNDLER_DIR, '..');
49
+ const CLI_PACKAGE = JSON.parse(readFileSync(resolve(CLI_PACKAGE_ROOT, 'package.json'), 'utf8'));
50
+ const CLI_PACKAGE_NAME = CLI_PACKAGE.name;
51
+ const SUPPORTED_HELPER_SPECIFIER = `${CLI_PACKAGE_NAME}/helpers`;
52
+ const SELF_PACKAGE_EXPORTS = new Map(
53
+ Object.entries(CLI_PACKAGE.exports || {})
54
+ .filter(([, target]) => typeof target === 'string' && target.length > 0)
55
+ .map(([subpath, target]) => {
56
+ const packageSpecifier = subpath === '.'
57
+ ? CLI_PACKAGE_NAME
58
+ : `${CLI_PACKAGE_NAME}/${subpath.replace(/^\.\//, '')}`;
59
+ return [packageSpecifier, resolve(CLI_PACKAGE_ROOT, target)];
60
+ }),
61
+ );
62
+
45
63
  export function isBundleError(error) {
46
64
  return error instanceof BundleError;
47
65
  }
@@ -78,6 +96,13 @@ export function formatBundleError(error, projectRoot = process.cwd()) {
78
96
  }
79
97
  }
80
98
 
99
+ if (Array.isArray(details.hints) && details.hints.length > 0) {
100
+ parts.push('hints:');
101
+ for (const hint of details.hints) {
102
+ parts.push(` - ${hint}`);
103
+ }
104
+ }
105
+
81
106
  if (details.cycle) {
82
107
  const rendered = details.cycle.map((entry) => relPath(projectRoot, entry)).join(' -> ');
83
108
  parts.push(`cycle: ${rendered}`);
@@ -247,6 +272,7 @@ function buildModuleGraph(ctx) {
247
272
  filePath,
248
273
  code: parsed.code,
249
274
  imports: dependencies,
275
+ moduleType: parsed.moduleType,
250
276
  });
251
277
 
252
278
  state.set(filePath, 2);
@@ -298,6 +324,10 @@ function emitBundle(graph) {
298
324
  }
299
325
 
300
326
  function transformModule(moduleMeta, srcDir) {
327
+ if (moduleMeta.moduleType === 'json') {
328
+ return `__exports.default = ${moduleMeta.code.trim()};`;
329
+ }
330
+
301
331
  const importByLine = new Map();
302
332
  const importContinuationLines = new Set();
303
333
  for (const item of moduleMeta.imports) {
@@ -483,15 +513,23 @@ function readParsedModule(ctx, filePath) {
483
513
  const content = readFileSync(filePath, 'utf8');
484
514
  const hash = sha1(content);
485
515
  const cached = ctx.cache.files.get(filePath);
516
+ const moduleType = extname(filePath).toLowerCase() === '.json' ? 'json' : 'js';
486
517
 
487
518
  if (cached && cached.hash === hash) {
488
519
  return cached;
489
520
  }
490
521
 
522
+ if (moduleType === 'json') {
523
+ validateJsonModule(content, filePath);
524
+ const parsed = { hash, code: content, imports: [], moduleType };
525
+ ctx.cache.files.set(filePath, parsed);
526
+ return parsed;
527
+ }
528
+
491
529
  validateSyntax(content, filePath);
492
530
 
493
531
  const imports = scanImports(content, filePath);
494
- const parsed = { hash, code: content, imports };
532
+ const parsed = { hash, code: content, imports, moduleType };
495
533
  ctx.cache.files.set(filePath, parsed);
496
534
  return parsed;
497
535
  }
@@ -559,7 +597,8 @@ function scanImports(code, filePath) {
559
597
 
560
598
  function parseImportStatement(statement) {
561
599
  const normalized = statement.replace(/\s+/g, ' ').trim();
562
- const sideEffect = normalized.match(/^import\s+['"]([^'"\n]+)['"]\s*;?$/);
600
+ const importAttributes = String.raw`(?:\s+(?:with|assert)\s+\{[^}]*\})?`;
601
+ const sideEffect = normalized.match(new RegExp(`^import\\s+['"]([^'"\\n]+)['"]${importAttributes}\\s*;?$`));
563
602
  if (sideEffect) {
564
603
  return {
565
604
  clause: null,
@@ -567,7 +606,7 @@ function parseImportStatement(statement) {
567
606
  };
568
607
  }
569
608
 
570
- const fromImport = normalized.match(/^import\s+(.+?)\s+from\s+['"]([^'"\n]+)['"]\s*;?$/);
609
+ const fromImport = normalized.match(new RegExp(`^import\\s+(.+?)\\s+from\\s+['"]([^'"\\n]+)['"]${importAttributes}\\s*;?$`));
571
610
  if (fromImport) {
572
611
  return {
573
612
  clause: fromImport[1].trim(),
@@ -582,15 +621,12 @@ function resolveSpecifier(projectRoot, allowedSourceRoots, srcDir, importer, imp
582
621
  const specifier = importEntry.specifier;
583
622
 
584
623
  if (!(specifier.startsWith('./') || specifier.startsWith('../'))) {
585
- throw new BundleError(
586
- BUNDLE_ERROR_CODES.UNSUPPORTED_SPECIFIER,
587
- `Unsupported module specifier "${specifier}". Use relative imports only.`,
588
- {
589
- file: importer,
590
- importer,
591
- specifier,
592
- line: importEntry.line,
593
- },
624
+ return resolveSupportedPackageSpecifier(
625
+ projectRoot,
626
+ allowedSourceRoots,
627
+ srcDir,
628
+ importer,
629
+ importEntry,
594
630
  );
595
631
  }
596
632
 
@@ -623,6 +659,60 @@ function resolveSpecifier(projectRoot, allowedSourceRoots, srcDir, importer, imp
623
659
  );
624
660
  }
625
661
 
662
+ function resolveSupportedPackageSpecifier(projectRoot, allowedSourceRoots, srcDir, importer, importEntry) {
663
+ const specifier = importEntry.specifier;
664
+ let resolved = null;
665
+ let packageRoot = null;
666
+
667
+ if (specifier === CLI_PACKAGE_NAME || specifier.startsWith(`${CLI_PACKAGE_NAME}/`)) {
668
+ try {
669
+ resolved = createRequire(importer).resolve(specifier);
670
+ } catch {
671
+ resolved = null;
672
+ }
673
+
674
+ if (!resolved && SELF_PACKAGE_EXPORTS.has(specifier)) {
675
+ resolved = SELF_PACKAGE_EXPORTS.get(specifier);
676
+ packageRoot = CLI_PACKAGE_ROOT;
677
+ }
678
+ }
679
+
680
+ if (!resolved) {
681
+ throw new BundleError(
682
+ BUNDLE_ERROR_CODES.UNSUPPORTED_SPECIFIER,
683
+ `Unsupported module specifier "${specifier}".`,
684
+ {
685
+ file: importer,
686
+ importer,
687
+ specifier,
688
+ line: importEntry.line,
689
+ hints: [
690
+ 'Use relative imports for authored project files.',
691
+ `Use the local src/starter-utils copy or supported package imports from "${SUPPORTED_HELPER_SPECIFIER}".`,
692
+ ],
693
+ },
694
+ );
695
+ }
696
+
697
+ if (!(existsSync(resolved) && statSync(resolved).isFile())) {
698
+ throw new BundleError(
699
+ BUNDLE_ERROR_CODES.MODULE_NOT_FOUND,
700
+ `Module not found for specifier "${specifier}".`,
701
+ {
702
+ file: importer,
703
+ importer,
704
+ specifier,
705
+ line: importEntry.line,
706
+ candidates: [relPath(srcDir, resolved)],
707
+ },
708
+ );
709
+ }
710
+
711
+ packageRoot ||= findNearestPackageRoot(resolved) || CLI_PACKAGE_ROOT;
712
+ appendAllowedRoot(allowedSourceRoots, packageRoot);
713
+ return resolved;
714
+ }
715
+
626
716
  function resolveAllowedSourceRoots(projectRoot, explicitRoots = null) {
627
717
  if (Array.isArray(explicitRoots) && explicitRoots.length > 0) {
628
718
  return explicitRoots
@@ -640,6 +730,28 @@ function resolveAllowedSourceRoots(projectRoot, explicitRoots = null) {
640
730
  return roots;
641
731
  }
642
732
 
733
+ function appendAllowedRoot(allowedSourceRoots, root) {
734
+ const normalizedRoot = resolve(root);
735
+ if (!allowedSourceRoots.some((candidate) => candidate === normalizedRoot)) {
736
+ allowedSourceRoots.push(normalizedRoot);
737
+ }
738
+ }
739
+
740
+ function findNearestPackageRoot(filePath) {
741
+ let current = dirname(filePath);
742
+ while (true) {
743
+ const packageJsonPath = join(current, 'package.json');
744
+ if (existsSync(packageJsonPath) && statSync(packageJsonPath).isFile()) {
745
+ return current;
746
+ }
747
+ const parent = resolve(current, '..');
748
+ if (parent === current) {
749
+ return null;
750
+ }
751
+ current = parent;
752
+ }
753
+ }
754
+
643
755
  function ensureEntryWithinSource(projectRoot, allowedSourceRoots, entryFile) {
644
756
  if (allowedSourceRoots.some((root) => pathIsWithinRoot(root, entryFile))) {
645
757
  return;
@@ -651,6 +763,10 @@ function ensureEntryWithinSource(projectRoot, allowedSourceRoots, entryFile) {
651
763
  {
652
764
  file: entryFile,
653
765
  candidates: allowedSourceRoots.map((root) => relPath(projectRoot, root)),
766
+ hints: [
767
+ 'Entrypoints must stay inside the authored project source roots.',
768
+ `Use "${SUPPORTED_HELPER_SPECIFIER}" through local src/starter-utils copies for shared helper code instead of moving entrypoints outside src/.`,
769
+ ],
654
770
  },
655
771
  );
656
772
  }
@@ -674,6 +790,10 @@ function ensureWithinSource(projectRoot, allowedSourceRoots, candidate, importer
674
790
  specifier: importEntry.specifier,
675
791
  line: importEntry.line,
676
792
  candidates: allowedSourceRoots.map((root) => relPath(projectRoot, root)),
793
+ hints: [
794
+ 'Move shared authored helper code into src/ if it is project-specific.',
795
+ `For supported shared helper lanes, import from "${SUPPORTED_HELPER_SPECIFIER}" or keep the local src/starter-utils copy.`,
796
+ ],
677
797
  },
678
798
  );
679
799
  }
@@ -710,6 +830,16 @@ function validateSyntax(code, filePath) {
710
830
  });
711
831
  }
712
832
 
833
+ function validateJsonModule(code, filePath) {
834
+ try {
835
+ JSON.parse(code);
836
+ } catch {
837
+ throw new BundleError(BUNDLE_ERROR_CODES.SYNTAX_ERROR, 'Invalid JSON module.', {
838
+ file: filePath,
839
+ });
840
+ }
841
+ }
842
+
713
843
  function hasDynamicImport(code) {
714
844
  return /(^|[^\w$.])import\s*\(/m.test(code);
715
845
  }