@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/bundled-test-harness/index.html +1 -1
- package/bundled-test-harness/tests/index.html +1 -1
- package/bundled-types/runtime-common/realm-source-cache.d.ts +107 -0
- package/bundled-types/runtime-common/realm-source-staging.d.ts +147 -0
- package/dist/index.js +257 -255
- package/package.json +3 -3
- package/src/commands/parse.ts +219 -82
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/
|
|
105
|
+
"@cardstack/local-types": "0.0.0",
|
|
106
106
|
"@cardstack/runtime-common": "1.0.0",
|
|
107
|
-
"@cardstack/
|
|
107
|
+
"@cardstack/postgres": "0.0.0"
|
|
108
108
|
},
|
|
109
109
|
"publishConfig": {
|
|
110
110
|
"access": "public",
|
package/src/commands/parse.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
],
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|