@cardstack/boxel-cli 0.6.0 → 0.6.1-unstable.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardstack/boxel-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-unstable.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI tools for Boxel workspace management",
6
6
  "main": "./dist/index.js",
@@ -102,9 +102,9 @@
102
102
  "semver": "^7.7.0",
103
103
  "vite": "^6.3.2",
104
104
  "vitest": "^2.1.9",
105
- "@cardstack/postgres": "0.0.0",
105
+ "@cardstack/local-types": "0.0.0",
106
106
  "@cardstack/runtime-common": "1.0.0",
107
- "@cardstack/local-types": "0.0.0"
107
+ "@cardstack/postgres": "0.0.0"
108
108
  },
109
109
  "publishConfig": {
110
110
  "access": "public",
@@ -15,6 +15,12 @@ import { tmpdir } from 'node:os';
15
15
  import { dirname, join, relative, resolve, sep } from 'node:path';
16
16
 
17
17
  import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
18
+ import {
19
+ REALM_CACHE_DIR,
20
+ createStagedDiagnosticSink,
21
+ isSilentSetupFailure,
22
+ stageRealmSources,
23
+ } from '@cardstack/runtime-common/realm-source-staging';
18
24
  import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type';
19
25
 
20
26
  import {
@@ -121,6 +127,20 @@ const BXL_PATH = BUNDLED_TYPES_DIR
121
127
  const SHIMS_PATH = BUNDLED_TYPES_DIR
122
128
  ? join(BUNDLED_TYPES_DIR, 'shims')
123
129
  : undefined;
130
+ // In a published install the shims above type `@cardstack/boxel-icons/*`
131
+ // as `any` — a deliberate trade against shipping 50MB of declarations.
132
+ // Monorepo dev has no shim: the import resolves through the package's
133
+ // exports map into the gitignored `declarations/` build output, and when
134
+ // that isn't built it falls through to the untyped rollup JS — every icon
135
+ // types as `object`, and each unannotated `static icon = X` in fetched
136
+ // catalog sources breaks its class's `typeof BaseDef` constraint. Alias
137
+ // the committed sources instead, the same mapping the factory twin uses.
138
+ const BOXEL_ICONS_PATHS = BUNDLED_TYPES_DIR
139
+ ? undefined
140
+ : [
141
+ `${join(PACKAGES_PATH, 'boxel-icons', 'src', 'icons')}/*`,
142
+ `${join(PACKAGES_PATH, 'boxel-icons', 'src')}/*`,
143
+ ];
124
144
 
125
145
  // The temp parse workspace needs the CLI's runtime deps resolvable so
126
146
  // glint can type-check card code: `@glint/ember-tsc` (and its
@@ -183,7 +203,25 @@ const CLI_DEPENDENCY_PACKAGES = (() => {
183
203
  }
184
204
  })();
185
205
 
186
- let cachedTsconfigContent: string | undefined;
206
+ /**
207
+ * The realm prefixes a card may import and this command can fetch.
208
+ *
209
+ * Spelled out here rather than imported from
210
+ * `@cardstack/runtime-common/realm-prefixes`: that module's type-only imports
211
+ * reach `@cardstack/base/*`, which this package's deliberately dependency-light
212
+ * `lint:types` cannot resolve. The cost of the copy is that a realm added to
213
+ * the declaration has to be added here too; the check that catches it is this
214
+ * package's own type-check staying green.
215
+ *
216
+ * The base realm is deliberately absent: it is already aliased to real sources
217
+ * below and needs no network call.
218
+ */
219
+ const RESOLVABLE_PREFIXES = [
220
+ '@cardstack/catalog/',
221
+ '@cardstack/skills/',
222
+ '@cardstack/openrouter/',
223
+ '@cardstack/pretui/',
224
+ ];
187
225
 
188
226
  export interface ParseError {
189
227
  file: string;
@@ -200,6 +238,13 @@ export interface ParseRealmResult {
200
238
  durationMs: number;
201
239
  parseableFiles: string[];
202
240
  errors: ParseError[];
241
+ /**
242
+ * Degradations that did not fail the run but changed what it validated:
243
+ * realm modules the staging walk could not fetch, prefixes that fell back
244
+ * to the `any` shim, and diagnostics anchored in staged realm sources. A
245
+ * green result with warnings is a pass over an incomplete module graph.
246
+ */
247
+ warnings: string[];
203
248
  errorMessage?: string;
204
249
  }
205
250
 
@@ -252,6 +297,21 @@ async function retryWithPoll<T>(
252
297
  return result;
253
298
  }
254
299
 
300
+ /**
301
+ * The origin realm prefixes resolve against.
302
+ *
303
+ * When `--realm` was passed, that realm's own origin serves the prefix realms
304
+ * alongside it. A workspace parse has no realm, so fall back to the active
305
+ * profile's realm server — the same host `boxel search` would query. Neither
306
+ * available means the imports get the shim.
307
+ */
308
+ function realmOriginForPrefixes(
309
+ realmUrl: string | undefined,
310
+ pm: ProfileManager,
311
+ ): string | undefined {
312
+ return realmUrl || pm.getActiveProfile()?.profile.realmServerUrl;
313
+ }
314
+
255
315
  // ---------------------------------------------------------------------------
256
316
  // Public entry point
257
317
  // ---------------------------------------------------------------------------
@@ -344,6 +404,7 @@ export async function parseRealm(
344
404
  durationMs: Date.now() - startedAt,
345
405
  parseableFiles: [],
346
406
  errors: [],
407
+ warnings: [],
347
408
  };
348
409
  }
349
410
 
@@ -353,6 +414,7 @@ export async function parseRealm(
353
414
  : fetchSource(normalizedRealmUrl, path, pm);
354
415
 
355
416
  let errors: ParseError[] = [];
417
+ let warnings: string[] = [];
356
418
  let filesWithErrors = new Set<string>();
357
419
 
358
420
  if (gtsFiles.length > 0) {
@@ -374,8 +436,16 @@ export async function parseRealm(
374
436
 
375
437
  if (gtsContents.length > 0) {
376
438
  try {
377
- let glintErrors = await runGlintCheck(gtsContents);
378
- for (let e of glintErrors) {
439
+ let glintResult = await runGlintCheck(gtsContents, {
440
+ realmOrigin: realmOriginForPrefixes(normalizedRealmUrl, pm),
441
+ // The staging walk authenticates the same way fetchSource above
442
+ // does, so a non-public prefix realm resolves to real sources
443
+ // instead of degrading to the shim.
444
+ fetchFn: (input, init) => pm.authedRealmFetch(input, init),
445
+ cacheScope: pm.getActiveProfile()?.id,
446
+ });
447
+ warnings.push(...glintResult.warnings);
448
+ for (let e of glintResult.errors) {
379
449
  errors.push(e);
380
450
  filesWithErrors.add(e.file);
381
451
  }
@@ -419,6 +489,7 @@ export async function parseRealm(
419
489
  durationMs: Date.now() - startedAt,
420
490
  parseableFiles,
421
491
  errors,
492
+ warnings,
422
493
  };
423
494
  }
424
495
 
@@ -617,9 +688,25 @@ function linkResolvedDeps(nodeModulesDir: string): void {
617
688
  * workspace and writes a tsconfig with the same monorepo path mappings
618
689
  * the realm uses at runtime, then parses TS diagnostics from stdout.
619
690
  */
620
- async function runGlintCheck(
691
+ interface RunGlintCheckOptions {
692
+ /**
693
+ * Origin the realm prefixes are served from — the realm being parsed, or the
694
+ * active profile's realm server when parsing a local workspace. Without it,
695
+ * realm-prefixed imports fall back to the ambient shim.
696
+ */
697
+ realmOrigin?: string;
698
+ fetchFn?: typeof globalThis.fetch;
699
+ /** Identity behind `fetchFn`, partitioning the realm-source memo. */
700
+ cacheScope?: string;
701
+ }
702
+
703
+ // Exported for tests: the realm-prefixed staging behavior can only be pinned
704
+ // with an injected fetch, and the CLI surface (a subprocess) has nowhere to
705
+ // inject one.
706
+ export async function runGlintCheck(
621
707
  files: { path: string; content: string }[],
622
- ): Promise<ParseError[]> {
708
+ glintOptions: RunGlintCheckOptions = {},
709
+ ): Promise<{ errors: ParseError[]; warnings: string[] }> {
623
710
  let tempDir = mkdtempSync(join(tmpdir(), 'boxel-parse-'));
624
711
 
625
712
  try {
@@ -639,80 +726,93 @@ async function runGlintCheck(
639
726
  writeFileSync(resolved, file.content, 'utf8');
640
727
  }
641
728
 
642
- if (!cachedTsconfigContent) {
643
- let tsconfig = {
644
- compilerOptions: {
645
- target: 'es2022',
646
- allowJs: true,
647
- moduleResolution: 'bundler',
648
- allowSyntheticDefaultImports: true,
649
- noEmit: true,
650
- baseUrl: '.',
651
- module: 'es2022',
652
- strict: true,
653
- experimentalDecorators: true,
654
- skipLibCheck: true,
655
- noUnusedLocals: false,
656
- noUnusedParameters: false,
657
- // Bundled `@cardstack/bxl` source imports its siblings by
658
- // explicit `.ts` specifier. Without this each of those is a
659
- // TS5097 — none reaches the caller (they're outside the temp
660
- // dir, so the loop below drops them), but they still count
661
- // toward `totalDiagnosticLines`, which is what tells a real
662
- // "glint resolved nothing" breakage from a clean run. Safe
663
- // under `noEmit`, and it keeps that signal meaningful.
664
- allowImportingTsExtensions: true,
665
- // `qunit-dom` augments QUnit's `Assert` with `.dom(...)`.
666
- // Workspaces routinely include `.test.gts` files that call
667
- // `assert.dom(...)` without importing qunit-dom directly (they
668
- // rely on the ambient augmentation), and parse type-checks every
669
- // discovered `.gts` — so the type lib has to be loaded here or
670
- // those tests fail with "Property 'dom' does not exist on type
671
- // 'Assert'". It resolves because qunit-dom is a runtime
672
- // dependency of boxel-cli, reachable via the node_modules
673
- // resolution above. `@cardstack/local-types` is workspace-only
674
- // and fed via `include` below instead of here.
675
- types: ['qunit-dom'],
676
- paths: {
677
- '@cardstack/base/*': [`${BASE_PKG_PATH}/*`],
678
- 'https://cardstack.com/base/*': [`${BASE_PKG_PATH}/*`],
679
- '@cardstack/runtime-common': [`${RUNTIME_COMMON_PATH}/index`],
680
- '@cardstack/runtime-common/*': [`${RUNTIME_COMMON_PATH}/*`],
681
- '@cardstack/bxl': [`${BXL_PATH}/index`],
682
- '@cardstack/bxl/*': [`${BXL_PATH}/*`],
683
- '@cardstack/host/tests/*': [`${HOST_TESTS_PATH}/*`],
684
- '@cardstack/host/*': [`${HOST_APP_PATH}/*`],
685
- // The host registers each tool module under both its
686
- // `tools/` specifier and the pre-rename `commands/` one, so
687
- // card content that still carries the old spelling loads.
688
- // Both aliases therefore target `tools/`, which is where the
689
- // modules live.
690
- '@cardstack/boxel-host/commands/*': [`${HOST_APP_PATH}/tools/*`],
691
- // Card code imports host tools as
692
- // `@cardstack/boxel-host/tools/<name>`.
693
- '@cardstack/boxel-host/tools/*': [`${HOST_APP_PATH}/tools/*`],
694
- // Host library modules card code may import (e.g.
695
- // `lib/pdfjs-loader`); `lib` is among the bundled host-app
696
- // subdirs.
697
- '@cardstack/boxel-host/lib/*': [`${HOST_APP_PATH}/lib/*`],
698
- '@cardstack/boxel-ui/*': [`${BOXEL_UI_PATH}/*`],
699
- '*': [`${HOST_TYPES_PATH}/*`],
700
- },
729
+ let { realmPaths, warnings: stagingWarnings } = await stageRealmSources({
730
+ tempDir,
731
+ files,
732
+ prefixes: RESOLVABLE_PREFIXES,
733
+ origin: glintOptions.realmOrigin,
734
+ fetchFn: glintOptions.fetchFn,
735
+ cacheScope: glintOptions.cacheScope,
736
+ onWarn: (message) => cliLog.warn(message),
737
+ });
738
+
739
+ let warnings: string[] = [...stagingWarnings];
740
+
741
+ let tsconfig = {
742
+ compilerOptions: {
743
+ target: 'es2022',
744
+ allowJs: true,
745
+ moduleResolution: 'bundler',
746
+ allowSyntheticDefaultImports: true,
747
+ noEmit: true,
748
+ baseUrl: '.',
749
+ module: 'es2022',
750
+ strict: true,
751
+ experimentalDecorators: true,
752
+ skipLibCheck: true,
753
+ noUnusedLocals: false,
754
+ noUnusedParameters: false,
755
+ // Bundled `@cardstack/bxl` source imports its siblings by
756
+ // explicit `.ts` specifier. Without this each of those is a
757
+ // TS5097 — none reaches the caller (they're outside the temp
758
+ // dir, so the loop below drops them), but they still count
759
+ // toward `inputDiagnosticLines`, which is what tells a real
760
+ // "glint resolved nothing" breakage from a clean run. Safe
761
+ // under `noEmit`, and it keeps that signal meaningful.
762
+ allowImportingTsExtensions: true,
763
+ // `qunit-dom` augments QUnit's `Assert` with `.dom(...)`.
764
+ // Workspaces routinely include `.test.gts` files that call
765
+ // `assert.dom(...)` without importing qunit-dom directly (they
766
+ // rely on the ambient augmentation), and parse type-checks every
767
+ // discovered `.gts` — so the type lib has to be loaded here or
768
+ // those tests fail with "Property 'dom' does not exist on type
769
+ // 'Assert'". It resolves because qunit-dom is a runtime
770
+ // dependency of boxel-cli, reachable via the node_modules
771
+ // resolution above. `@cardstack/local-types` is workspace-only
772
+ // and fed via `include` below instead of here.
773
+ types: ['qunit-dom'],
774
+ paths: {
775
+ ...realmPaths,
776
+ '@cardstack/base/*': [`${BASE_PKG_PATH}/*`],
777
+ 'https://cardstack.com/base/*': [`${BASE_PKG_PATH}/*`],
778
+ '@cardstack/runtime-common': [`${RUNTIME_COMMON_PATH}/index`],
779
+ '@cardstack/runtime-common/*': [`${RUNTIME_COMMON_PATH}/*`],
780
+ '@cardstack/bxl': [`${BXL_PATH}/index`],
781
+ '@cardstack/bxl/*': [`${BXL_PATH}/*`],
782
+ '@cardstack/host/tests/*': [`${HOST_TESTS_PATH}/*`],
783
+ '@cardstack/host/*': [`${HOST_APP_PATH}/*`],
784
+ // The host registers each tool module under both its
785
+ // `tools/` specifier and the pre-rename `commands/` one, so
786
+ // card content that still carries the old spelling loads.
787
+ // Both aliases therefore target `tools/`, which is where the
788
+ // modules live.
789
+ '@cardstack/boxel-host/commands/*': [`${HOST_APP_PATH}/tools/*`],
790
+ // Card code imports host tools as
791
+ // `@cardstack/boxel-host/tools/<name>`.
792
+ '@cardstack/boxel-host/tools/*': [`${HOST_APP_PATH}/tools/*`],
793
+ // Host library modules card code may import (e.g.
794
+ // `lib/pdfjs-loader`); `lib` is among the bundled host-app
795
+ // subdirs.
796
+ '@cardstack/boxel-host/lib/*': [`${HOST_APP_PATH}/lib/*`],
797
+ '@cardstack/boxel-ui/*': [`${BOXEL_UI_PATH}/*`],
798
+ ...(BOXEL_ICONS_PATHS
799
+ ? { '@cardstack/boxel-icons/*': BOXEL_ICONS_PATHS }
800
+ : {}),
801
+ '*': [`${HOST_TYPES_PATH}/*`],
701
802
  },
702
- include: [
703
- '**/*.ts',
704
- '**/*.gts',
705
- '**/*.gjs',
706
- `${LOCAL_TYPES_PATH}/**/*.d.ts`,
707
- ...(SHIMS_PATH ? [`${SHIMS_PATH}/**/*.d.ts`] : []),
708
- ],
709
- exclude: ['node_modules'],
710
- };
711
- cachedTsconfigContent = JSON.stringify(tsconfig, null, 2);
712
- }
803
+ },
804
+ include: [
805
+ '**/*.ts',
806
+ '**/*.gts',
807
+ '**/*.gjs',
808
+ `${LOCAL_TYPES_PATH}/**/*.d.ts`,
809
+ ...(SHIMS_PATH ? [`${SHIMS_PATH}/**/*.d.ts`] : []),
810
+ ],
811
+ exclude: ['node_modules', REALM_CACHE_DIR],
812
+ };
713
813
  writeFileSync(
714
814
  join(tempDir, 'tsconfig.json'),
715
- cachedTsconfigContent,
815
+ JSON.stringify(tsconfig, null, 2),
716
816
  'utf8',
717
817
  );
718
818
 
@@ -780,17 +880,29 @@ async function runGlintCheck(
780
880
  });
781
881
 
782
882
  let errors: ParseError[] = [];
783
- let totalDiagnosticLines = 0;
883
+ let inputDiagnosticLines = 0;
884
+ let stagedDiagnostics = createStagedDiagnosticSink(tempDir);
784
885
  for (let line of output.split('\n')) {
785
886
  let match = line.match(
786
887
  /^(.+?)\((\d+),(\d+)\):\s*error\s+(TS\d+):\s*(.+)/,
787
888
  );
788
889
  if (!match) continue;
789
890
 
790
- totalDiagnosticLines++;
791
-
792
891
  let [, filePath, lineStr, colStr, tsCode, message] = match;
793
892
  let absolutePath = resolve(tempDir, filePath);
893
+ if (
894
+ stagedDiagnostics.trackIfStaged(absolutePath, {
895
+ line: lineStr,
896
+ column: colStr,
897
+ tsCode,
898
+ message,
899
+ })
900
+ ) {
901
+ continue;
902
+ }
903
+
904
+ inputDiagnosticLines++;
905
+
794
906
  if (!absolutePath.startsWith(tempDir)) continue;
795
907
 
796
908
  if (tsCode === 'TS2353' && message.includes("'scoped'")) continue;
@@ -807,7 +919,19 @@ async function runGlintCheck(
807
919
  });
808
920
  }
809
921
 
810
- if (exitedWithError && errors.length === 0 && totalDiagnosticLines === 0) {
922
+ let stagedWarning = stagedDiagnostics.warning();
923
+ if (stagedWarning) {
924
+ warnings.push(stagedWarning);
925
+ }
926
+
927
+ if (
928
+ isSilentSetupFailure({
929
+ exitedWithError,
930
+ errorCount: errors.length,
931
+ inputDiagnosticLines,
932
+ stagedDiagnosticLines: stagedDiagnostics.count(),
933
+ })
934
+ ) {
811
935
  let truncatedOutput = output.slice(0, 500).trim();
812
936
  errors.push({
813
937
  file: files[0]?.path ?? 'unknown',
@@ -817,7 +941,7 @@ async function runGlintCheck(
817
941
  });
818
942
  }
819
943
 
820
- return errors;
944
+ return { errors, warnings };
821
945
  } finally {
822
946
  try {
823
947
  rmSync(tempDir, { recursive: true, force: true });
@@ -1069,6 +1193,7 @@ function emptyErrorResult(message: string): ParseRealmResult {
1069
1193
  durationMs: 0,
1070
1194
  parseableFiles: [],
1071
1195
  errors: [],
1196
+ warnings: [],
1072
1197
  errorMessage: message,
1073
1198
  };
1074
1199
  }
@@ -1129,9 +1254,21 @@ export function registerParseCommand(program: Command): void {
1129
1254
  process.exit(1);
1130
1255
  }
1131
1256
 
1257
+ // Warnings are part of the verdict, not stderr noise: a green run with
1258
+ // warnings passed against an incomplete realm module graph, and the
1259
+ // author deciding whether that matters needs them next to the verdict
1260
+ // line rather than scrolled past above it.
1261
+ for (let warning of result.warnings) {
1262
+ console.log(`${DIM}warning${RESET} ${warning}`);
1263
+ }
1264
+
1132
1265
  if (result.errors.length === 0) {
1133
1266
  console.log(
1134
- `${DIM}No parse errors (${result.filesChecked} file(s) checked).${RESET}`,
1267
+ `${DIM}No parse errors (${result.filesChecked} file(s) checked${
1268
+ result.warnings.length > 0
1269
+ ? `; ${result.warnings.length} warning(s) above`
1270
+ : ''
1271
+ }).${RESET}`,
1135
1272
  );
1136
1273
  return;
1137
1274
  }