@auraindustry/aurajs 0.1.1 → 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 (144) 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/asset-pack.mjs +2 -1
  6. package/src/authored-project.mjs +498 -2
  7. package/src/authored-runtime.mjs +14 -0
  8. package/src/bin-integrity.mjs +33 -26
  9. package/src/build-contract/capabilities.mjs +87 -1
  10. package/src/build-contract/constants.mjs +1 -0
  11. package/src/build-contract.mjs +2 -0
  12. package/src/bundler.mjs +143 -13
  13. package/src/cli.mjs +681 -13
  14. package/src/commands/packs.mjs +741 -0
  15. package/src/commands/project-authoring.mjs +128 -1
  16. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  17. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  18. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  19. package/src/conformance/cases/systems-and-gameplay-cases.mjs +1126 -10
  20. package/src/conformance-mobile.mjs +166 -0
  21. package/src/conformance.mjs +89 -30
  22. package/src/evidence-bundle.mjs +242 -0
  23. package/src/external-package-surface.mjs +1 -1
  24. package/src/headless-test/runtime-coordinator.mjs +186 -33
  25. package/src/headless-test.mjs +2 -0
  26. package/src/helpers/2d/index.mjs +183 -0
  27. package/src/helpers/index.mjs +26 -0
  28. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  29. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  30. package/src/helpers/starter-utils/animation-2d.js +337 -0
  31. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  32. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  33. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  34. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  35. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  36. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  37. package/src/helpers/starter-utils/core.js +150 -0
  38. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  39. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  40. package/src/helpers/starter-utils/index.js +26 -0
  41. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  42. package/src/helpers/starter-utils/journal-2d.js +267 -0
  43. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  44. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  45. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  46. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  47. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  48. package/src/helpers/starter-utils/triggers.js +662 -0
  49. package/src/helpers/starter-utils/tween-2d.js +615 -0
  50. package/src/helpers/starter-utils/wave-director.js +101 -0
  51. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  52. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  53. package/src/mobile/android/build.mjs +606 -0
  54. package/src/mobile/android/host-artifact.mjs +280 -0
  55. package/src/mobile/ios/build.mjs +1323 -0
  56. package/src/mobile/ios/host-artifact.mjs +819 -0
  57. package/src/mobile/shared/capabilities.mjs +174 -0
  58. package/src/package-integrity.mjs +18 -4
  59. package/src/packs/catalog.mjs +259 -0
  60. package/src/perf-benchmark-runner.mjs +17 -12
  61. package/src/perf-benchmark.mjs +408 -4
  62. package/src/publish-command.mjs +434 -17
  63. package/src/publish-validation.mjs +22 -11
  64. package/src/replay-runtime.mjs +257 -0
  65. package/src/scaffold/config.mjs +2 -0
  66. package/src/scaffold/fs.mjs +8 -1
  67. package/src/scaffold/project-docs.mjs +101 -41
  68. package/src/scaffold.mjs +4 -0
  69. package/src/session-runtime.mjs +4 -3
  70. package/src/web-conformance.mjs +0 -36
  71. package/templates/create/2d/src/runtime/app.js +4 -0
  72. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  73. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  74. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  75. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  76. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  77. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  78. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  79. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  80. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  81. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  82. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  83. package/templates/create/2d-survivor/src/runtime/app.js +4 -0
  84. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  85. package/templates/create/3d/src/runtime/app.js +4 -0
  86. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  87. package/templates/create/3d/src/runtime/materials.js +10 -0
  88. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  89. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  90. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  91. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  92. package/templates/create/3d-collectathon/src/runtime/app.js +4 -0
  93. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  94. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  95. package/templates/create/blank/assets/splash/aurajs-gg-wordmark.webp +0 -0
  96. package/templates/create/blank/assets/splash/bg.webp +0 -0
  97. package/templates/create/blank/assets/splash/boot-loop.wav +0 -0
  98. package/templates/create/blank/assets/splash/boot-sting.wav +0 -0
  99. package/templates/create/blank/assets/splash/logo-mascot-sheet.webp +0 -0
  100. package/templates/create/blank/assets/splash/logoholo.webp +0 -0
  101. package/templates/create/blank/src/main.js +5 -1
  102. package/templates/create/blank/src/runtime/splash.js +305 -0
  103. package/templates/create/local-multiplayer/scenes/gameplay.scene.js +186 -12
  104. package/templates/create/local-multiplayer/src/runtime/capabilities.js +8 -1
  105. package/templates/create/shared/assets/splash/aurajs-gg-wordmark.webp +0 -0
  106. package/templates/create/shared/assets/splash/bg.webp +0 -0
  107. package/templates/create/shared/assets/splash/boot-loop.wav +0 -0
  108. package/templates/create/shared/assets/splash/boot-sting.wav +0 -0
  109. package/templates/create/shared/assets/splash/logo-mascot-sheet.webp +0 -0
  110. package/templates/create/shared/assets/splash/logoholo.webp +0 -0
  111. package/templates/create/shared/src/runtime/splash.js +305 -0
  112. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  113. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  114. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  115. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  116. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  117. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  118. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  119. package/templates/create/shared/src/starter-utils/index.js +15 -1
  120. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  121. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  122. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  123. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  124. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  125. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  126. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  127. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  128. package/templates/create/video-cutscene/src/runtime/app.js +4 -0
  129. package/templates/create-bin/play.js +148 -7
  130. package/templates/skills/auramaxx/SKILL.md +46 -0
  131. package/templates/skills/auramaxx/project-requirements.md +68 -0
  132. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  133. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  134. package/templates/starter/assets/splash/aurajs-gg-wordmark.webp +0 -0
  135. package/templates/starter/assets/splash/bg.webp +0 -0
  136. package/templates/starter/assets/splash/boot-loop.wav +0 -0
  137. package/templates/starter/assets/splash/boot-sting.wav +0 -0
  138. package/templates/starter/assets/splash/logo-mascot-sheet.webp +0 -0
  139. package/templates/starter/assets/splash/logoholo.webp +0 -0
  140. package/templates/starter/src/main.js +4 -0
  141. package/templates/starter/src/runtime/splash.js +305 -0
  142. package/templates/skills/aurajs/SKILL.md +0 -96
  143. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  144. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
2
  import { dirname, join, resolve } from 'node:path';
4
3
  import { fileURLToPath } from 'node:url';
@@ -28,14 +27,37 @@ function normalizeText(value) {
28
27
  return String(value || '').replace(/\r\n?/g, '\n');
29
28
  }
30
29
 
31
- function sha256Text(value) {
32
- return createHash('sha256').update(normalizeText(value)).digest('hex');
33
- }
34
-
35
30
  function readJsonFile(path) {
36
31
  return JSON.parse(readFileSync(path, 'utf8'));
37
32
  }
38
33
 
34
+ const PLAY_WRAPPER_REQUIRED_MARKERS = [
35
+ '#!/usr/bin/env node',
36
+ 'const fallbackAuraPackage =',
37
+ "const MINIMAL_COMMANDS = ['dev', 'join', 'play', 'fork', 'publish', 'session'];",
38
+ "const ALL_COMMANDS = ['dev', 'join', 'play', 'fork', 'publish', 'session', 'state', 'inspect', 'action'];",
39
+ "args: ['exec', '--yes', '--package', fallbackAuraPackage, '--', 'aura', ...commandArgs]",
40
+ ];
41
+
42
+ function assertPlayWrapperContract(relativePath, fileContent) {
43
+ const normalized = normalizeText(fileContent);
44
+ const missingMarkers = PLAY_WRAPPER_REQUIRED_MARKERS.filter((marker) => !normalized.includes(marker));
45
+ if (missingMarkers.length > 0) {
46
+ throw new ProjectBinIntegrityError(
47
+ 'project_bin_wrapper_contract_invalid',
48
+ `Bin target "${relativePath}" does not satisfy the AuraJS play wrapper contract.`,
49
+ {
50
+ relativePath,
51
+ missingMarkers,
52
+ },
53
+ );
54
+ }
55
+
56
+ return {
57
+ markers: [...PLAY_WRAPPER_REQUIRED_MARKERS],
58
+ };
59
+ }
60
+
39
61
  function listRelativeFiles(root, current = root, acc = []) {
40
62
  for (const entry of readdirSync(current, { withFileTypes: true })) {
41
63
  const fullPath = join(current, entry.name);
@@ -84,6 +106,7 @@ export function assertProjectBinIntegrity(
84
106
  aurajsPackageRoot = DEFAULT_AURAJS_PACKAGE_ROOT,
85
107
  expectedAurajsVersion = null,
86
108
  enforceExactAurajsDependency = true,
109
+ enforceResolvedAurajsVersionMatch = false,
87
110
  } = {},
88
111
  ) {
89
112
  const resolvedProjectRoot = resolve(projectRoot || process.cwd());
@@ -146,7 +169,7 @@ export function assertProjectBinIntegrity(
146
169
 
147
170
  const aurajsPackage = readJsonFile(aurajsPackageJsonPath);
148
171
  const resolvedAurajsVersion = String(aurajsPackage?.version || '').trim();
149
- if (expectedAurajsVersion && resolvedAurajsVersion !== expectedAurajsVersion) {
172
+ if (enforceResolvedAurajsVersionMatch && expectedAurajsVersion && resolvedAurajsVersion !== expectedAurajsVersion) {
150
173
  throw new ProjectBinIntegrityError(
151
174
  'project_aurajs_resolved_version_mismatch',
152
175
  `Resolved @auraindustry/aurajs ${resolvedAurajsVersion || '<missing>'}, expected ${expectedAurajsVersion}.`,
@@ -167,9 +190,6 @@ export function assertProjectBinIntegrity(
167
190
  );
168
191
  }
169
192
 
170
- const canonicalTemplate = normalizeText(readFileSync(templatePath, 'utf8'));
171
- const canonicalTemplateHash = sha256Text(canonicalTemplate);
172
-
173
193
  const binEntries = resolveProjectBinEntries(resolvedProjectPackage, resolvedPackageName);
174
194
  if (binEntries.length === 0) {
175
195
  throw new ProjectBinIntegrityError(
@@ -227,26 +247,13 @@ export function assertProjectBinIntegrity(
227
247
  );
228
248
  }
229
249
 
230
- const fileContent = normalizeText(readFileSync(absolutePath, 'utf8'));
231
- const fileHash = sha256Text(fileContent);
232
- if (fileContent !== canonicalTemplate) {
233
- throw new ProjectBinIntegrityError(
234
- 'project_bin_template_modified',
235
- `Bin target "${relativePath}" does not match the canonical AuraJS play.js template.`,
236
- {
237
- relativePath,
238
- absolutePath,
239
- expectedTemplatePath: templatePath,
240
- expectedTemplateHash: canonicalTemplateHash,
241
- actualHash: fileHash,
242
- },
243
- );
244
- }
250
+ const fileContent = readFileSync(absolutePath, 'utf8');
251
+ const wrapperContract = assertPlayWrapperContract(relativePath, fileContent);
245
252
 
246
253
  verifiedFiles.push({
247
254
  relativePath,
248
255
  absolutePath,
249
- hash: fileHash,
256
+ wrapperContract,
250
257
  });
251
258
  }
252
259
 
@@ -260,7 +267,7 @@ export function assertProjectBinIntegrity(
260
267
  resolvedVersion: resolvedAurajsVersion,
261
268
  packageRoot: aurajsPackageRoot,
262
269
  templatePath,
263
- templateHash: canonicalTemplateHash,
270
+ wrapperContract: 'aurajs.play-wrapper.v1',
264
271
  },
265
272
  bin: {
266
273
  entries: binEntries,
@@ -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
  }