@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.91
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/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +27 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +86 -47
- package/biomeBase.ts +6 -5
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +129 -4
- package/executors.ts +105 -24
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +67 -0
- package/frontendBuild/watchRootResolver.ts +13 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
package/qualityScanner.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
|
5
5
|
import ignore from "ignore";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
8
|
+
import { FormSetterScanner } from "./formSetterScanner";
|
|
8
9
|
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
9
|
-
import {
|
|
10
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
10
11
|
|
|
11
12
|
type QualitySeverity = "warning";
|
|
12
|
-
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr";
|
|
13
|
+
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr" | "agent";
|
|
13
14
|
|
|
14
15
|
export interface QualityWarning {
|
|
15
16
|
rule: string;
|
|
@@ -88,7 +89,8 @@ const SUGGESTED_RULES = [
|
|
|
88
89
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
89
90
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
90
91
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
91
|
-
"
|
|
92
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
|
|
93
|
+
"Move shared app and lib utilities to common/ instead of creating apps/*/base or libs/*/base.",
|
|
92
94
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline.",
|
|
93
95
|
];
|
|
94
96
|
|
|
@@ -115,6 +117,8 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
115
117
|
"Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
|
|
116
118
|
"akan.file.placeholder-export":
|
|
117
119
|
"Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
120
|
+
"akan.file.bang-comment-in-client":
|
|
121
|
+
"Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
118
122
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
119
123
|
"akan.file.global-declaration":
|
|
120
124
|
"Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
|
|
@@ -122,14 +126,22 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
122
126
|
"Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
123
127
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
124
128
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
129
|
+
"akan.agent.unpublished-form-setter":
|
|
130
|
+
"Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
|
|
125
131
|
"akan.file.component-internal-declaration":
|
|
126
132
|
"Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
127
133
|
"akan.file.component-export":
|
|
128
134
|
"Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix — only PascalCase components and their `<Component>Props` interface belong here.",
|
|
129
135
|
"akan.layout.app-root-file":
|
|
130
136
|
"Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
137
|
+
"akan.layout.app-root-folder":
|
|
138
|
+
"Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
|
|
131
139
|
"akan.layout.lib-root-file":
|
|
132
|
-
"Move the file into a
|
|
140
|
+
"Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
|
|
141
|
+
"akan.layout.lib-root-folder":
|
|
142
|
+
"Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
|
|
143
|
+
"akan.layout.lib-facet-file":
|
|
144
|
+
"Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
|
|
133
145
|
"akan.layout.module-ui-file":
|
|
134
146
|
"Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
|
|
135
147
|
"akan.ssr.unnecessary-use-client":
|
|
@@ -173,6 +185,7 @@ export class AkanQualityScanner {
|
|
|
173
185
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
174
186
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
175
187
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
188
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
176
189
|
...ssr.warnings,
|
|
177
190
|
];
|
|
178
191
|
|
|
@@ -303,6 +316,7 @@ export class AkanQualityScanner {
|
|
|
303
316
|
});
|
|
304
317
|
}
|
|
305
318
|
|
|
319
|
+
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
306
320
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
307
321
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
308
322
|
warnings.push(...getGlobalMutationWarnings(sourceFile));
|
|
@@ -408,26 +422,31 @@ export class AkanQualityScanner {
|
|
|
408
422
|
}
|
|
409
423
|
|
|
410
424
|
#scanLayoutQuality(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
411
|
-
const segments = sourceFile.file.split("/");
|
|
412
425
|
const warnings: QualityWarning[] = [];
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
const rootEntry = getSysRootEntry(sourceFile.file);
|
|
427
|
+
if (rootEntry) {
|
|
428
|
+
const { type, name, isDir } = rootEntry;
|
|
429
|
+
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
430
|
+
const kind = isDir ? "folder" : "file";
|
|
431
|
+
if (!allowed) {
|
|
432
|
+
warnings.push({
|
|
433
|
+
rule: `akan.layout.${type}-root-${kind}`,
|
|
434
|
+
scope: "layout",
|
|
435
|
+
severity: "warning",
|
|
436
|
+
file: sourceFile.file,
|
|
437
|
+
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
421
440
|
}
|
|
422
441
|
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
442
|
+
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
443
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
|
|
425
444
|
warnings.push({
|
|
426
|
-
rule: "akan.layout.lib-
|
|
445
|
+
rule: "akan.layout.lib-facet-file",
|
|
427
446
|
scope: "layout",
|
|
428
447
|
severity: "warning",
|
|
429
448
|
file: sourceFile.file,
|
|
430
|
-
message: `Unexpected lib root file "${
|
|
449
|
+
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
|
|
431
450
|
});
|
|
432
451
|
}
|
|
433
452
|
|
|
@@ -580,9 +599,10 @@ function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement
|
|
|
580
599
|
|
|
581
600
|
function getExportedClassNames(sourceFile: ts.SourceFile) {
|
|
582
601
|
return sourceFile.statements
|
|
583
|
-
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement)
|
|
602
|
+
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement))
|
|
584
603
|
.filter((statement) => isExported(statement))
|
|
585
|
-
.map((statement) => statement.name
|
|
604
|
+
.map((statement) => statement.name?.text)
|
|
605
|
+
.filter((name): name is string => !!name);
|
|
586
606
|
}
|
|
587
607
|
|
|
588
608
|
function isComponentDeclarationFile(file: string) {
|
|
@@ -662,7 +682,43 @@ function isPascalCaseName(name: string) {
|
|
|
662
682
|
|
|
663
683
|
function isDefaultExportStatement(statement: ts.Statement) {
|
|
664
684
|
if (ts.isExportAssignment(statement)) return !statement.isExportEquals;
|
|
665
|
-
|
|
685
|
+
if (!ts.canHaveModifiers(statement)) return false;
|
|
686
|
+
return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Bun's bundler keeps `//!` and `/*!` through minification (it reads them as the `@license` class), so a
|
|
691
|
+
* bang marker in browser-reachable code ships verbatim to every visitor. `no-bang-comment-in-client.grit`
|
|
692
|
+
* is the build gate; this exists because that rule cannot see the whole file.
|
|
693
|
+
*
|
|
694
|
+
* A comment is trivia, which Biome's GritQL exposes to no node pattern, so the rule matches through
|
|
695
|
+
* `file($name, $body)` — and `$body` is the module's *token* span. A marker above the first statement or
|
|
696
|
+
* below the last one sits in leading or trailing trivia and is unreachable from GritQL at all. Reading the
|
|
697
|
+
* raw text here covers both, and re-reporting the markers grit already catches costs nothing: the two run
|
|
698
|
+
* from different commands, and a file with one is failing lint either way.
|
|
699
|
+
*/
|
|
700
|
+
function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
701
|
+
if (!isClientReachableFile(sourceFile.file)) return [];
|
|
702
|
+
// Anchored to line start or to whitespace after code, so a literal like `"https://host//!path"` is not a hit.
|
|
703
|
+
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => ({
|
|
704
|
+
rule: "akan.file.bang-comment-in-client",
|
|
705
|
+
scope: "file" as const,
|
|
706
|
+
severity: "warning" as const,
|
|
707
|
+
file: sourceFile.file,
|
|
708
|
+
line,
|
|
709
|
+
message: "A `//!` or `/*!` marker survives minification and ships to the browser.",
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`. */
|
|
714
|
+
function isClientReachableFile(file: string) {
|
|
715
|
+
if (/\.(test|spec)\.tsx?$/.test(file)) return false;
|
|
716
|
+
if (/\.(constant|store)\.ts$/.test(file)) return true;
|
|
717
|
+
if (/\.(Template|Unit|Util|View|Zone)\.tsx$/.test(file)) return true;
|
|
718
|
+
const segments = file.split("/");
|
|
719
|
+
if (segments.includes("ui") || segments.includes("webkit")) return /\.tsx?$/.test(file);
|
|
720
|
+
if (segments.includes("common")) return file.endsWith(".ts");
|
|
721
|
+
return segments.includes("page") && file.endsWith(".tsx");
|
|
666
722
|
}
|
|
667
723
|
|
|
668
724
|
function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
@@ -821,7 +877,19 @@ function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number],
|
|
|
821
877
|
return `${modelName}Store`;
|
|
822
878
|
}
|
|
823
879
|
|
|
824
|
-
|
|
880
|
+
const sysRootTypes = { apps: "app", libs: "lib" } as const;
|
|
881
|
+
|
|
882
|
+
function getSysRootEntry(file: string) {
|
|
883
|
+
const segments = file.split("/");
|
|
884
|
+
const root = segments[0];
|
|
885
|
+
const name = segments[2];
|
|
886
|
+
if (!root || !name || segments.length < 3) return null;
|
|
887
|
+
const type = sysRootTypes[root as keyof typeof sysRootTypes];
|
|
888
|
+
if (!type) return null;
|
|
889
|
+
return { type, name, isDir: segments.length > 3 };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getLibFacetRootFile(file: string) {
|
|
825
893
|
const segments = file.split("/");
|
|
826
894
|
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
827
895
|
return segments[3];
|
package/recipeScanner.ts
CHANGED
|
@@ -118,7 +118,10 @@ const extractVariants = (config: ts.ObjectLiteralExpression) => {
|
|
|
118
118
|
if (!ts.isPropertyAssignment(variant) || !isNamed(variant.name)) continue;
|
|
119
119
|
if (!ts.isObjectLiteralExpression(variant.initializer)) continue;
|
|
120
120
|
variants[propName(variant.name)] = variant.initializer.properties
|
|
121
|
-
.filter(
|
|
121
|
+
.filter(
|
|
122
|
+
(option): option is ts.PropertyAssignment & { name: ts.Identifier | ts.StringLiteral } =>
|
|
123
|
+
ts.isPropertyAssignment(option) && isNamed(option.name),
|
|
124
|
+
)
|
|
122
125
|
.map((option) => propName(option.name));
|
|
123
126
|
}
|
|
124
127
|
} else if (key === "defaultVariants" && ts.isObjectLiteralExpression(property.initializer)) {
|
package/scanInfo.ts
CHANGED
|
@@ -9,9 +9,9 @@ import type {
|
|
|
9
9
|
PkgScanResult,
|
|
10
10
|
ScanResult,
|
|
11
11
|
} from "./akanConfig";
|
|
12
|
-
|
|
12
|
+
import { AkanAppConfig } from "./akanConfig";
|
|
13
13
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
14
|
-
import {
|
|
14
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
15
15
|
|
|
16
16
|
const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
|
|
17
17
|
type ScalarFileType = (typeof scalarFileTypes)[number];
|
|
@@ -57,7 +57,6 @@ const moduleUiFileTypes = {
|
|
|
57
57
|
scalar: new Set(["Template", "Unit"]),
|
|
58
58
|
} satisfies Record<ModuleKind, Set<string>>;
|
|
59
59
|
const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
|
|
60
|
-
const rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
|
|
61
60
|
|
|
62
61
|
// The dependency scanner needs `typescript` (~65MB resident). Only the scan/sync commands run it, so
|
|
63
62
|
// load it on demand rather than through the module graph of every process that imports scan info.
|
|
@@ -65,8 +64,6 @@ const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExec
|
|
|
65
64
|
(await import("./dependencyScanner")).TypeScriptDependencyScanner.from(exec);
|
|
66
65
|
|
|
67
66
|
const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
|
|
68
|
-
const isAllowedLibRootFile = (filename: string) =>
|
|
69
|
-
libFacetRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
70
67
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
71
68
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
72
69
|
async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
|
|
@@ -85,24 +82,29 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
|
|
|
85
82
|
violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
|
|
86
83
|
};
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
|
|
85
|
+
const allowedRootFiles: ReadonlySet<string> = rootAllowedFiles[exec.type];
|
|
86
|
+
const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
|
|
87
|
+
const { files, dirs } = await exec.getFilesAndDirs(".");
|
|
88
|
+
files
|
|
89
|
+
.filter((filename) => !allowedRootFiles.has(filename))
|
|
90
|
+
.forEach((filename) => {
|
|
91
|
+
addViolation(filename, `unsupported ${exec.type} root file`);
|
|
92
|
+
});
|
|
93
|
+
dirs
|
|
94
|
+
.filter((dirname) => !allowedRootDirs.has(dirname))
|
|
95
|
+
.forEach((dirname) => {
|
|
96
|
+
addViolation(dirname, `unsupported ${exec.type} root folder`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
//* An app validates its page tree in `getPageKeys`, which a lib has no equivalent of — so a lib's own
|
|
100
|
+
//* routes went unchecked until whichever app opted into them with `syncPageLibs` blew up on the sync.
|
|
101
|
+
if (exec.type === "lib")
|
|
102
|
+
for (const { relativePath, reason } of await exec.getPageConventionViolations()) addViolation(relativePath, reason);
|
|
101
103
|
|
|
102
104
|
libRoot.files
|
|
103
|
-
.filter((filename) => !
|
|
105
|
+
.filter((filename) => !isAllowedLibFacetRootFile(filename))
|
|
104
106
|
.forEach((filename) => {
|
|
105
|
-
addViolation(path.join("lib", filename), "unsupported lib root file");
|
|
107
|
+
addViolation(path.join("lib", filename), "unsupported lib facet root file");
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
libRoot.dirs
|
|
@@ -284,12 +286,11 @@ class ScanInfo {
|
|
|
284
286
|
}),
|
|
285
287
|
]);
|
|
286
288
|
const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
|
|
287
|
-
const
|
|
289
|
+
const common = {
|
|
288
290
|
name: exec.name,
|
|
289
291
|
type: exec.type,
|
|
290
292
|
repoName: exec.workspace.repoName,
|
|
291
293
|
serveDomain: WorkspaceExecutor.getBaseDevEnv(path.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
|
|
292
|
-
akanConfig,
|
|
293
294
|
files,
|
|
294
295
|
libDeps,
|
|
295
296
|
pkgDeps,
|
|
@@ -297,6 +298,11 @@ class ScanInfo {
|
|
|
297
298
|
devDependencies: npmDevDeps.filter((dep) => !isAkanFrameworkDependency(dep)),
|
|
298
299
|
routes,
|
|
299
300
|
};
|
|
301
|
+
//? `exec.type` and the resolved config class are correlated, which the union alone cannot express.
|
|
302
|
+
const scanResult: AppScanResult | LibScanResult =
|
|
303
|
+
akanConfig instanceof AkanAppConfig
|
|
304
|
+
? ({ ...common, akanConfig } satisfies AppScanResult)
|
|
305
|
+
: ({ ...common, akanConfig } satisfies LibScanResult);
|
|
300
306
|
return scanResult;
|
|
301
307
|
}
|
|
302
308
|
|
|
@@ -461,7 +467,7 @@ export class LibInfo extends ScanInfo {
|
|
|
461
467
|
LibInfo.libInfos.set(libName, await LibInfo.fromExecutor(libExecutor));
|
|
462
468
|
}),
|
|
463
469
|
);
|
|
464
|
-
const libInfo = new LibInfo(exec, scanResult);
|
|
470
|
+
const libInfo = new LibInfo(exec, scanResult as LibScanResult);
|
|
465
471
|
LibInfo.libInfos.set(exec.name, libInfo);
|
|
466
472
|
LibInfo.#sortedLibIndices = null;
|
|
467
473
|
return libInfo;
|
|
@@ -512,7 +518,7 @@ export class LibInfo extends ScanInfo {
|
|
|
512
518
|
export class PkgInfo {
|
|
513
519
|
readonly exec: PkgExecutor;
|
|
514
520
|
readonly name: string;
|
|
515
|
-
|
|
521
|
+
#scanResult: PkgScanResult;
|
|
516
522
|
|
|
517
523
|
static async scanExecutor(exec: PkgExecutor) {
|
|
518
524
|
const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
|
|
@@ -554,10 +560,10 @@ export class PkgInfo {
|
|
|
554
560
|
constructor(exec: PkgExecutor, scanResult: PkgScanResult) {
|
|
555
561
|
this.exec = exec;
|
|
556
562
|
this.name = exec.name;
|
|
557
|
-
this
|
|
563
|
+
this.#scanResult = scanResult;
|
|
558
564
|
}
|
|
559
565
|
getScanResult() {
|
|
560
|
-
return this
|
|
566
|
+
return this.#scanResult;
|
|
561
567
|
}
|
|
562
568
|
}
|
|
563
569
|
|
package/semver.test.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { compareSemver } from "./semver";
|
|
3
|
+
|
|
4
|
+
describe("compareSemver", () => {
|
|
5
|
+
test("orders plain versions", () => {
|
|
6
|
+
expect(compareSemver("1.4.0", "1.3.13")).toBe(1);
|
|
7
|
+
expect(compareSemver("1.3.13", "1.4.0")).toBe(-1);
|
|
8
|
+
expect(compareSemver("19.2.7", "19.2.7")).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("compares the version behind a range operator, not the range", () => {
|
|
12
|
+
expect(compareSemver("^3.1049.0", "^3.721.0")).toBe(1);
|
|
13
|
+
expect(compareSemver("~1.2.0", "^1.10.0")).toBe(-1);
|
|
14
|
+
expect(compareSemver(">=1.3.13", "1.4.0")).toBe(-1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("sorts a prerelease below its release", () => {
|
|
18
|
+
expect(compareSemver("1.0.0-alpha", "1.0.0")).toBe(-1);
|
|
19
|
+
expect(compareSemver("0.0.0-experimental-603e6108-20241029", "0.0.0")).toBe(-1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("falls back to numeric comparison for specs that are not versions", () => {
|
|
23
|
+
expect(compareSemver("workspace:*", "0.0.0")).toBe(0);
|
|
24
|
+
expect(compareSemver("workspace:*", "1.0.0")).toBe(-1);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/semver.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `Bun.semver.order` compares range strings loosely — `^3.1049.0` vs `^3.721.0` answers 0 — so the
|
|
2
|
+
// operator prefix has to come off before it sees the version.
|
|
3
|
+
const stripRangeOperator = (version: string) => version.replace(/^[\s^~>=<v]+/, "").trim();
|
|
4
|
+
|
|
5
|
+
const parseVersion = (version: string): number[] => {
|
|
6
|
+
return version
|
|
7
|
+
.replace(/^[^\d]*/, "")
|
|
8
|
+
.split(/[.-]/)
|
|
9
|
+
.map((part) => Number.parseInt(part, 10))
|
|
10
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const compareNumeric = (a: string, b: string): number => {
|
|
14
|
+
const left = parseVersion(a);
|
|
15
|
+
const right = parseVersion(b);
|
|
16
|
+
const length = Math.max(left.length, right.length);
|
|
17
|
+
for (let i = 0; i < length; i++) {
|
|
18
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
19
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const compareSemver = (a: string, b: string): number => {
|
|
25
|
+
try {
|
|
26
|
+
return Bun.semver.order(stripRangeOperator(a), stripRangeOperator(b));
|
|
27
|
+
} catch {
|
|
28
|
+
// Not every dependency spec is a version — `workspace:*`, a git URL, a tarball path.
|
|
29
|
+
return compareNumeric(a, b);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AppExecutor, Executor, WorkspaceExecutor } from "./executors";
|
|
6
|
+
import { formatSlicePlan, SlicePlanner } from "./slicePlanner";
|
|
7
|
+
import type { PackageJson } from "./types";
|
|
8
|
+
|
|
9
|
+
const tempRoots: string[] = [];
|
|
10
|
+
const originalEnv = { ...process.env };
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
process.env = { ...originalEnv };
|
|
14
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
|
|
15
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
16
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
process.env = { ...originalEnv };
|
|
21
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const write = async (filePath: string, content: string) => {
|
|
25
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
26
|
+
await writeFile(filePath, content);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const gitignore = [
|
|
30
|
+
"node_modules",
|
|
31
|
+
"apps/*/lib/cnst.ts",
|
|
32
|
+
"libs/*/common/index.ts",
|
|
33
|
+
"**/env.server.local.ts",
|
|
34
|
+
"**/akan.app.json",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
// `AppExecutor.from` and `AppInfo.fromExecutor` both memoise by name, so each fixture needs a fresh one.
|
|
38
|
+
const makeWorkspace = async (appName: string, libName: string) => {
|
|
39
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-slice-"));
|
|
40
|
+
tempRoots.push(root);
|
|
41
|
+
const rootPackageJson: PackageJson = {
|
|
42
|
+
name: "workspace",
|
|
43
|
+
version: "0.0.1",
|
|
44
|
+
description: "workspace",
|
|
45
|
+
workspaces: ["pkgs/*"],
|
|
46
|
+
dependencies: { lodash: "4.0.0", "unused-dep": "1.0.0", ioredis: "5.0.0" },
|
|
47
|
+
devDependencies: { typescript: "6.0.0", "@types/lodash": "4.1.0", "@types/qrcode": "1.0.0" },
|
|
48
|
+
patchedDependencies: {
|
|
49
|
+
"lodash@4.0.0": "patches/lodash@4.0.0.patch",
|
|
50
|
+
"unused-dep@1.0.0": "patches/unused-dep@1.0.0.patch",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await write(path.join(root, "package.json"), `${JSON.stringify(rootPackageJson, null, 2)}\n`);
|
|
54
|
+
await write(path.join(root, ".gitignore"), `${gitignore}\n`);
|
|
55
|
+
await write(path.join(root, "biome.json"), "{}\n");
|
|
56
|
+
//? The framework's peers are declared by akanjs, not by any import, so the planner reads them here.
|
|
57
|
+
await write(
|
|
58
|
+
path.join(root, "node_modules/akanjs/package.json"),
|
|
59
|
+
'{ "name": "akanjs", "version": "3.0.0", "peerDependencies": { "react": "19.0.0", "ioredis": "5.0.0" } }\n',
|
|
60
|
+
);
|
|
61
|
+
await write(path.join(root, "patches/lodash@4.0.0.patch"), "");
|
|
62
|
+
await write(path.join(root, "patches/unused-dep@1.0.0.patch"), "");
|
|
63
|
+
|
|
64
|
+
await write(path.join(root, `apps/${appName}/akan.config.ts`), "export default {};\n");
|
|
65
|
+
await write(path.join(root, `apps/${appName}/tsconfig.json`), "{}\n");
|
|
66
|
+
await write(
|
|
67
|
+
path.join(root, `apps/${appName}/lib/task/task.constant.ts`),
|
|
68
|
+
[
|
|
69
|
+
`import { helper } from "@libs/${libName}/common";`,
|
|
70
|
+
'import lodash from "lodash";',
|
|
71
|
+
"",
|
|
72
|
+
"export { helper, lodash };",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n"),
|
|
75
|
+
);
|
|
76
|
+
await write(path.join(root, `apps/${appName}/lib/cnst.ts`), "export {};\n");
|
|
77
|
+
await write(path.join(root, `apps/${appName}/env/env.server.local.ts`), "export const env = {};\n");
|
|
78
|
+
|
|
79
|
+
await write(path.join(root, `libs/${libName}/akan.config.ts`), "export default {};\n");
|
|
80
|
+
await write(path.join(root, `libs/${libName}/tsconfig.json`), "{}\n");
|
|
81
|
+
await mkdir(path.join(root, `libs/${libName}/lib`), { recursive: true });
|
|
82
|
+
await write(path.join(root, `libs/${libName}/common/helper.ts`), "export const helper = 1;\n");
|
|
83
|
+
await write(path.join(root, `libs/${libName}/common/index.ts`), 'export * from "./helper";\n');
|
|
84
|
+
|
|
85
|
+
await write(path.join(root, "pkgs/in-tree/package.json"), '{ "name": "in-tree" }\n');
|
|
86
|
+
|
|
87
|
+
const git = new Executor("fixture", root);
|
|
88
|
+
await git.spawn("git", ["init", "--quiet"]);
|
|
89
|
+
await git.spawn("git", ["add", "-A"]);
|
|
90
|
+
await git.spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "--quiet", "-m", "fixture"]);
|
|
91
|
+
|
|
92
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: `workspace-${appName}` });
|
|
93
|
+
return { root, plan: async () => await new SlicePlanner(AppExecutor.from(workspace, appName)).plan() };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
describe("SlicePlanner", () => {
|
|
97
|
+
test("resolves the lib closure and lists only git-tracked slice files", async () => {
|
|
98
|
+
const { plan } = await makeWorkspace("slice-app", "slice-lib");
|
|
99
|
+
const result = await plan();
|
|
100
|
+
|
|
101
|
+
expect(result.app).toBe("slice-app");
|
|
102
|
+
expect(result.libs).toEqual(["slice-lib"]);
|
|
103
|
+
expect(result.appFiles).toContain("apps/slice-app/lib/task/task.constant.ts");
|
|
104
|
+
expect(result.libFiles["slice-lib"]).toContain("libs/slice-lib/common/helper.ts");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("leaves out generated barrels, env values and every other workspace member", async () => {
|
|
108
|
+
const { plan } = await makeWorkspace("ignore-app", "ignore-lib");
|
|
109
|
+
const result = await plan();
|
|
110
|
+
|
|
111
|
+
expect(result.appFiles).not.toContain("apps/ignore-app/lib/cnst.ts");
|
|
112
|
+
expect(result.appFiles).not.toContain("apps/ignore-app/env/env.server.local.ts");
|
|
113
|
+
expect(result.libFiles["ignore-lib"]).not.toContain("libs/ignore-lib/common/index.ts");
|
|
114
|
+
expect(result.rootFiles).toEqual(expect.arrayContaining(["package.json", ".gitignore", "biome.json"]));
|
|
115
|
+
expect(result.rootFiles.filter((file) => file.startsWith("pkgs/"))).toEqual([]);
|
|
116
|
+
expect(result.rootFiles.filter((file) => /^(apps|libs)\//.test(file))).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("drops `workspaces` from the slice manifest and reports it", async () => {
|
|
120
|
+
const { plan } = await makeWorkspace("manifest-app", "manifest-lib");
|
|
121
|
+
const result = await plan();
|
|
122
|
+
|
|
123
|
+
expect(result.packageJson.workspaces).toBeUndefined();
|
|
124
|
+
expect(result.warnings.join("\n")).toContain("workspaces");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("prunes what the slice does not import, keeping the toolchain and the type packages", async () => {
|
|
128
|
+
const { plan } = await makeWorkspace("deps-app", "deps-lib");
|
|
129
|
+
const result = await plan();
|
|
130
|
+
|
|
131
|
+
expect(result.requiredDependencies).toContain("lodash");
|
|
132
|
+
//? `ioredis` is an akanjs peer: nothing imports it, and pruning it would break the server at runtime.
|
|
133
|
+
expect(result.packageJson.dependencies).toEqual({ lodash: "4.0.0", ioredis: "5.0.0" });
|
|
134
|
+
//? Nothing imports `@types/lodash` by name, so only its companion keeps it.
|
|
135
|
+
expect(result.packageJson.devDependencies).toEqual({ typescript: "6.0.0", "@types/lodash": "4.1.0" });
|
|
136
|
+
expect(result.packageJson.patchedDependencies).toEqual({ "lodash@4.0.0": "patches/lodash@4.0.0.patch" });
|
|
137
|
+
expect(result.unusedDependencies).toEqual(["@types/qrcode", "unused-dep"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("carries the dependencies whole when there is no akanjs manifest to read the peers from", async () => {
|
|
141
|
+
const { root, plan } = await makeWorkspace("nopeer-app", "nopeer-lib");
|
|
142
|
+
await rm(path.join(root, "node_modules"), { recursive: true, force: true });
|
|
143
|
+
const result = await plan();
|
|
144
|
+
|
|
145
|
+
expect(result.unusedDependencies).toEqual([]);
|
|
146
|
+
expect(result.packageJson.dependencies).toEqual({ lodash: "4.0.0", "unused-dep": "1.0.0", ioredis: "5.0.0" });
|
|
147
|
+
expect(result.warnings.join("\n")).toContain("carried whole");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("names a type package the way DefinitelyTyped does", () => {
|
|
151
|
+
expect(SlicePlanner.typesPackageOf("lodash")).toBe("@types/lodash");
|
|
152
|
+
expect(SlicePlanner.typesPackageOf("@capacitor/core")).toBe("@types/capacitor__core");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("warns about untracked files under the slice paths", async () => {
|
|
156
|
+
const { root, plan } = await makeWorkspace("untracked-app", "untracked-lib");
|
|
157
|
+
await write(path.join(root, "apps/untracked-app/lib/task/task.service.ts"), "export {};\n");
|
|
158
|
+
const result = await plan();
|
|
159
|
+
|
|
160
|
+
expect(result.warnings.join("\n")).toContain("apps/untracked-app/lib/task/task.service.ts");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("formatSlicePlan", () => {
|
|
165
|
+
test("collapses the workspace shell to its top-level entries", () => {
|
|
166
|
+
const text = formatSlicePlan({
|
|
167
|
+
app: "demo",
|
|
168
|
+
libs: ["util"],
|
|
169
|
+
appFiles: ["apps/demo/main.ts"],
|
|
170
|
+
libFiles: { util: ["libs/util/index.ts"] },
|
|
171
|
+
rootFiles: ["package.json", "infra/app/values/main.yaml", "infra/app/templates/app.yaml"],
|
|
172
|
+
packageJson: { name: "demo", version: "0.0.1", description: "demo" },
|
|
173
|
+
requiredDependencies: [],
|
|
174
|
+
unusedDependencies: [],
|
|
175
|
+
warnings: [],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(text).toContain("app: demo (apps/demo: 1 files)");
|
|
179
|
+
expect(text).toContain("3 files across infra, package.json");
|
|
180
|
+
expect(text).toContain("(none)");
|
|
181
|
+
});
|
|
182
|
+
});
|