oxlint-plugin-arch 0.12.0 → 0.12.1
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 +26 -0
- package/dist/index.js +6 -2
- package/dist/preset/preset-detect.d.ts +1 -2
- package/dist/preset/preset-stylesheet.d.ts +1 -0
- package/dist/preset/preset-units.d.ts +2 -0
- package/dist/preset/preset.constants.d.ts +2 -0
- package/dist/preset/preset.js +566 -338
- package/dist/preset/preset.utils.d.ts +3 -0
- package/dist/types/preset.types.d.ts +7 -1
- package/dist/vendor/dillon-anti-slop/index.js +32 -3
- package/package.json +3 -3
- package/src/vendor/dillon-anti-slop/UPSTREAM.md +5 -0
package/dist/preset/preset.js
CHANGED
|
@@ -2,8 +2,34 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
4
|
// src/preset/groups/backend.ts
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import { posix as posix2, resolve as resolve3 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/preset/preset-detect.ts
|
|
8
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
9
|
+
import { matchesGlob, posix, resolve as resolve2 } from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/preset/preset-recipes.ts
|
|
12
|
+
var presetUi = { react: true, kit: true, tailwind: true };
|
|
13
|
+
var presetRecipes = {
|
|
14
|
+
tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
15
|
+
next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
16
|
+
expo: { react: true, expo: true, schemas: true, forms: true },
|
|
17
|
+
backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
|
|
18
|
+
react: { react: true, tailwind: true, schemas: true },
|
|
19
|
+
desktop: { console: true },
|
|
20
|
+
library: { schemas: true },
|
|
21
|
+
ui: presetUi,
|
|
22
|
+
kit: presetUi,
|
|
23
|
+
cli: { console: true }
|
|
24
|
+
};
|
|
25
|
+
var presetTemplates = {
|
|
26
|
+
monorepo: {
|
|
27
|
+
"apps/web": presetRecipes.tanstack,
|
|
28
|
+
"apps/server": presetRecipes.backend,
|
|
29
|
+
"packages/*": presetRecipes.library,
|
|
30
|
+
scripts: presetRecipes.cli
|
|
31
|
+
}
|
|
32
|
+
};
|
|
7
33
|
|
|
8
34
|
// src/preset/preset.constants.ts
|
|
9
35
|
var presetExtensions = "*.{ts,tsx,mts,cts}";
|
|
@@ -331,7 +357,9 @@ var presetBuiltinFileRoles = [
|
|
|
331
357
|
"test",
|
|
332
358
|
"spec"
|
|
333
359
|
];
|
|
334
|
-
var
|
|
360
|
+
var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
|
|
361
|
+
var presetOutputFolders = ["dist", "build", "coverage", ".output"];
|
|
362
|
+
var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
|
|
335
363
|
var presetIllustrationPalette = [
|
|
336
364
|
"text-color",
|
|
337
365
|
"bg-color",
|
|
@@ -478,8 +506,8 @@ var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateSta
|
|
|
478
506
|
var presetTestFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
|
|
479
507
|
var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
|
|
480
508
|
var presetDetectFrameworks = [
|
|
481
|
-
["tanstack", ["@tanstack/react-start"]],
|
|
482
509
|
["next", ["next", "vinext"]],
|
|
510
|
+
["tanstack", ["@tanstack/react-start"]],
|
|
483
511
|
["expo", ["expo"]]
|
|
484
512
|
];
|
|
485
513
|
var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-apps/cli"];
|
|
@@ -499,7 +527,7 @@ var presetRootImports = { "framer-motion": "Use motion/react." };
|
|
|
499
527
|
var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
|
|
500
528
|
|
|
501
529
|
// src/preset/preset.utils.ts
|
|
502
|
-
import { readFileSync } from "node:fs";
|
|
530
|
+
import { readFileSync, statSync } from "node:fs";
|
|
503
531
|
import { resolve } from "node:path";
|
|
504
532
|
class PresetError extends Error {
|
|
505
533
|
}
|
|
@@ -589,6 +617,357 @@ function presetRuntime(project, settings, framework, rule) {
|
|
|
589
617
|
];
|
|
590
618
|
});
|
|
591
619
|
}
|
|
620
|
+
function presetManifest(root, directory) {
|
|
621
|
+
try {
|
|
622
|
+
return JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
|
|
623
|
+
} catch {
|
|
624
|
+
return {};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function presetIsDirectory(path) {
|
|
628
|
+
try {
|
|
629
|
+
return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
630
|
+
} catch {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function presetIsFile(path) {
|
|
635
|
+
try {
|
|
636
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
637
|
+
} catch {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/preset/preset-detect.ts
|
|
643
|
+
function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
644
|
+
if (!presetIsDirectory(resolve2(root, base))) {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix.join(base, name));
|
|
648
|
+
}
|
|
649
|
+
var presetNamedSkip = ["node_modules"];
|
|
650
|
+
function presetSource(root, directory, configured) {
|
|
651
|
+
if (configured !== undefined) {
|
|
652
|
+
return posix.join(directory, configured);
|
|
653
|
+
}
|
|
654
|
+
return existsSync(resolve2(root, directory, "src")) ? posix.join(directory, "src") : directory;
|
|
655
|
+
}
|
|
656
|
+
function presetRegistryFile(root, source, configured) {
|
|
657
|
+
if (configured === false) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const file = typeof configured === "string" ? configured : configured?.file;
|
|
661
|
+
if (file !== undefined) {
|
|
662
|
+
return posix.normalize(file);
|
|
663
|
+
}
|
|
664
|
+
return presetRegistryFiles.map((path) => posix.join(source, path)).find((path) => existsSync(resolve2(root, path)));
|
|
665
|
+
}
|
|
666
|
+
function presetDomains(root, source) {
|
|
667
|
+
return presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path)).filter((name) => existsSync(resolve2(root, source, "services", name, `${name}.service.ts`)));
|
|
668
|
+
}
|
|
669
|
+
function presetRecipe(root, directory, app, configured) {
|
|
670
|
+
const manifest = presetManifest(root, directory);
|
|
671
|
+
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
672
|
+
const has = (names) => names.some((name) => dependencies.has(name));
|
|
673
|
+
const source = presetSource(root, directory, configured);
|
|
674
|
+
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
675
|
+
const services = resolve2(root, source, "services");
|
|
676
|
+
const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
|
|
677
|
+
const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
|
|
678
|
+
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
|
|
679
|
+
if (framework) {
|
|
680
|
+
return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
|
|
681
|
+
}
|
|
682
|
+
if (has(presetDetectDesktop)) {
|
|
683
|
+
return presetRecipes.desktop;
|
|
684
|
+
}
|
|
685
|
+
if (presetDomains(root, source).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
|
|
686
|
+
return presetRecipes.backend;
|
|
687
|
+
}
|
|
688
|
+
if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
|
|
689
|
+
return app ? presetRecipes.react : presetRecipes.ui;
|
|
690
|
+
}
|
|
691
|
+
return presetRecipes.library;
|
|
692
|
+
}
|
|
693
|
+
function presetMergeObject(base, top, depth) {
|
|
694
|
+
const merged = { ...base };
|
|
695
|
+
for (const [key, value] of Object.entries(top)) {
|
|
696
|
+
const previous = Reflect.get(merged, key);
|
|
697
|
+
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
698
|
+
if (nested && depth > 0) {
|
|
699
|
+
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
700
|
+
} else if (value !== true || !(previous instanceof Object)) {
|
|
701
|
+
Reflect.set(merged, key, value);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return merged;
|
|
705
|
+
}
|
|
706
|
+
function presetMerge(base, top) {
|
|
707
|
+
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
708
|
+
return top === true && base instanceof Object ? base : top;
|
|
709
|
+
}
|
|
710
|
+
return presetMergeObject(base, top, 2);
|
|
711
|
+
}
|
|
712
|
+
function presetImportTargets(value) {
|
|
713
|
+
if (typeof value === "string") {
|
|
714
|
+
return [value];
|
|
715
|
+
}
|
|
716
|
+
if (Array.isArray(value)) {
|
|
717
|
+
return value.flatMap(presetImportTargets);
|
|
718
|
+
}
|
|
719
|
+
return value instanceof Object ? Object.values(value).flatMap(presetImportTargets) : [];
|
|
720
|
+
}
|
|
721
|
+
function presetSpecifiers(manifest, target, file = false) {
|
|
722
|
+
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
723
|
+
const path = posix.normalize(mapped);
|
|
724
|
+
const parts = path.split("*");
|
|
725
|
+
const keyParts = key.split("*");
|
|
726
|
+
if (parts.length === 1 && keyParts.length === 1) {
|
|
727
|
+
return path.replace(/\.[cm]?[jt]sx?$/, "") === target ? [key] : [];
|
|
728
|
+
}
|
|
729
|
+
if (parts.length !== 2 || keyParts.length !== 2) {
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
const [before, after] = parts;
|
|
733
|
+
const [head, suffix] = keyParts;
|
|
734
|
+
const prefix = before.replace(/\/$/, "");
|
|
735
|
+
if (target !== prefix && !target.startsWith(`${prefix}/`)) {
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
const rest = target.slice(prefix.length).replace(/^\//, "");
|
|
739
|
+
const tail = after && rest.endsWith(after) ? rest.slice(0, -after.length) : rest;
|
|
740
|
+
return [`${head}${tail}${file ? suffix : ""}`.replace(/\/$/, "")];
|
|
741
|
+
}));
|
|
742
|
+
return [...new Set(specifiers)];
|
|
743
|
+
}
|
|
744
|
+
function presetOverlaps(left, right) {
|
|
745
|
+
const [leftBase] = left.split("*");
|
|
746
|
+
const [rightBase] = right.split("*");
|
|
747
|
+
return leftBase.startsWith(rightBase) || rightBase.startsWith(leftBase);
|
|
748
|
+
}
|
|
749
|
+
function presetDeny(imports) {
|
|
750
|
+
return { deny: { imports } };
|
|
751
|
+
}
|
|
752
|
+
function presetFilled(conventions, exists) {
|
|
753
|
+
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
754
|
+
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
755
|
+
return exists(path) && !empty;
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
function presetBackendConventions(root, key, value, denied) {
|
|
759
|
+
const manifest = presetManifest(root, key);
|
|
760
|
+
const source = presetSource(root, key, value.source);
|
|
761
|
+
const local = posix.relative(key, source) || ".";
|
|
762
|
+
const at = (path) => local === "." ? path : posix.join(local, path);
|
|
763
|
+
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
764
|
+
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
765
|
+
const registryFile = presetRegistryFile(root, source, configured);
|
|
766
|
+
const registry = registryFile === undefined ? undefined : posix.relative(key, registryFile);
|
|
767
|
+
const services = presetSpecifiers(manifest, at("services"));
|
|
768
|
+
const message = "Use the service API, not another service’s operations.";
|
|
769
|
+
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
770
|
+
const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.[cm]?[jt]sx?$/, ""), true).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
|
|
771
|
+
const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
|
|
772
|
+
const serviceImports = { ...registryImports, ...operations };
|
|
773
|
+
if (manifest.name) {
|
|
774
|
+
Object.assign(serviceImports, entries);
|
|
775
|
+
}
|
|
776
|
+
const conventions = {
|
|
777
|
+
[at("services")]: presetDeny(serviceImports),
|
|
778
|
+
[at("core")]: presetDeny(operations),
|
|
779
|
+
[at("rpc")]: presetDeny(registryImports),
|
|
780
|
+
[at("routes")]: presetDeny(registryImports),
|
|
781
|
+
[at("cli")]: {
|
|
782
|
+
naming: { files: presetCliFiles },
|
|
783
|
+
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
for (const domain of presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path))) {
|
|
787
|
+
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
788
|
+
[`${prefix}/${pattern}`, false],
|
|
789
|
+
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
790
|
+
]));
|
|
791
|
+
conventions[at(`services/${domain}`)] = presetDeny(Object.fromEntries(imports));
|
|
792
|
+
}
|
|
793
|
+
for (const tests of ["tests", "test"]) {
|
|
794
|
+
if (exists(posix.join(tests, "kits"))) {
|
|
795
|
+
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
796
|
+
conventions[posix.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const queue = ["."];
|
|
800
|
+
while (queue.length > 0) {
|
|
801
|
+
const folder = queue.shift() ?? ".";
|
|
802
|
+
for (const child of presetDirectories(root, posix.join(key, folder)).map((path) => posix.relative(key, path))) {
|
|
803
|
+
if (posix.basename(child) === "migrations") {
|
|
804
|
+
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
805
|
+
} else if (child.split("/").length < 8) {
|
|
806
|
+
queue.push(child);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return presetFilled(conventions, exists);
|
|
811
|
+
}
|
|
812
|
+
function presetWebConventions(root, key, value, servers) {
|
|
813
|
+
const source = posix.relative(key, presetSource(root, key, value.source)) || ".";
|
|
814
|
+
const at = (path) => source === "." ? path : posix.join(source, path);
|
|
815
|
+
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
816
|
+
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
817
|
+
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
818
|
+
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
819
|
+
const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
|
|
820
|
+
const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
|
|
821
|
+
const appImports = Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."]));
|
|
822
|
+
if (servers.length > 0) {
|
|
823
|
+
appImports["**/services/*.service"] = "Import the client or rsc service file, not a server service.";
|
|
824
|
+
}
|
|
825
|
+
const conventions = {
|
|
826
|
+
".": presetDeny(appImports),
|
|
827
|
+
[at("components")]: presetDeny(entries),
|
|
828
|
+
[at("routes")]: presetDeny(entries),
|
|
829
|
+
[at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
|
|
830
|
+
};
|
|
831
|
+
return presetFilled(conventions, exists);
|
|
832
|
+
}
|
|
833
|
+
function presetWorkspacePatterns(root, manifest) {
|
|
834
|
+
const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
|
|
835
|
+
const file = resolve2(root, "pnpm-workspace.yaml");
|
|
836
|
+
if (!existsSync(file)) {
|
|
837
|
+
return listed;
|
|
838
|
+
}
|
|
839
|
+
const patterns = [...listed];
|
|
840
|
+
let inside = false;
|
|
841
|
+
for (const line of readFileSync2(file, "utf8").split(`
|
|
842
|
+
`)) {
|
|
843
|
+
const flow = /^packages\s*:\s*\[(.*)\]/.exec(line);
|
|
844
|
+
const item = /^\s*-\s*(["']?)([^"'#\s]+)\1\s*(?:#.*)?$/.exec(line);
|
|
845
|
+
if (flow) {
|
|
846
|
+
patterns.push(...flow[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean));
|
|
847
|
+
inside = false;
|
|
848
|
+
} else if (/^packages\s*:/.test(line)) {
|
|
849
|
+
inside = true;
|
|
850
|
+
} else if (inside && item) {
|
|
851
|
+
patterns.push(item[2]);
|
|
852
|
+
} else if (/^\S/.test(line)) {
|
|
853
|
+
inside = false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return patterns;
|
|
857
|
+
}
|
|
858
|
+
function presetWorkspaces(root, patterns) {
|
|
859
|
+
const clean = (pattern) => posix.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
860
|
+
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
861
|
+
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
862
|
+
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
863
|
+
const candidates = new Set;
|
|
864
|
+
for (const pattern of include) {
|
|
865
|
+
const segments = pattern.split("/");
|
|
866
|
+
const literal = segments.findIndex((segment) => /[*?{[]/.test(segment));
|
|
867
|
+
if (literal === -1) {
|
|
868
|
+
candidates.add(pattern);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const depth = segments.includes("**") ? 8 : segments.length - literal;
|
|
872
|
+
let level = [segments.slice(0, literal).join("/") || "."];
|
|
873
|
+
for (let step = 0;step < depth; step++) {
|
|
874
|
+
level = level.flatMap((folder) => presetDirectories(root, folder, presetNamedSkip));
|
|
875
|
+
level.forEach((folder) => candidates.add(folder));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync(resolve2(root, path, "package.json"))).toSorted();
|
|
879
|
+
}
|
|
880
|
+
function presetContains(key, path) {
|
|
881
|
+
if (key === "." || key === path) {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
const pattern = key.split("/");
|
|
885
|
+
const segments = path.split("/");
|
|
886
|
+
return pattern.length <= segments.length && pattern.every((segment, index) => segment === "*" || segment === segments[index]);
|
|
887
|
+
}
|
|
888
|
+
function presetOwner(folders, path, owns) {
|
|
889
|
+
const depth = (key) => key === "." ? 0 : key.split("/").length;
|
|
890
|
+
return Object.entries(folders).filter(([key, value]) => presetContains(key, path) && owns(value)).toSorted(([left], [right]) => depth(left) - depth(right) || Number(right.includes("*")) - Number(left.includes("*"))).at(-1)?.[1];
|
|
891
|
+
}
|
|
892
|
+
function presetGroupOff(folders, path, group) {
|
|
893
|
+
const owner = presetOwner(folders, path, (value) => value instanceof Object && (group in value));
|
|
894
|
+
return owner instanceof Object && Object(owner)[group] === false;
|
|
895
|
+
}
|
|
896
|
+
function presetDetect(root, folders) {
|
|
897
|
+
const manifest = presetManifest(root, ".");
|
|
898
|
+
const configuredSource = (key) => {
|
|
899
|
+
const value = folders[key];
|
|
900
|
+
return value instanceof Object ? value.source : undefined;
|
|
901
|
+
};
|
|
902
|
+
const patterns = presetWorkspacePatterns(root, manifest);
|
|
903
|
+
const directories = presetWorkspaces(root, patterns);
|
|
904
|
+
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configuredSource(directory))]));
|
|
905
|
+
const rootRecipe = presetRecipe(root, ".", true, configuredSource("."));
|
|
906
|
+
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
907
|
+
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configuredSource(".")) !== ".";
|
|
908
|
+
if (patterns.length === 0 ? single : rootApp) {
|
|
909
|
+
detected["."] = rootRecipe;
|
|
910
|
+
}
|
|
911
|
+
if (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
|
|
912
|
+
detected.scripts = presetRecipes.cli;
|
|
913
|
+
}
|
|
914
|
+
const keys = Object.keys(folders);
|
|
915
|
+
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix.dirname(key) === user.slice(0, -2)));
|
|
916
|
+
const merged = { ".": presetDeny(presetRootImports) };
|
|
917
|
+
for (const [key, value] of Object.entries(detected)) {
|
|
918
|
+
if (!covered(key)) {
|
|
919
|
+
merged[key] = presetMerge(merged[key], value);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const [key, value] of Object.entries(folders)) {
|
|
923
|
+
merged[key] = key in merged ? presetMerge(merged[key], value) : value;
|
|
924
|
+
}
|
|
925
|
+
const length = (key) => key.split("/").length;
|
|
926
|
+
const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
|
|
927
|
+
const units = Object.entries(merged).flatMap(([key, value]) => {
|
|
928
|
+
if (key.endsWith("/*")) {
|
|
929
|
+
return presetDirectories(root, key.slice(0, -2), presetNamedSkip).filter((directory) => !(directory in merged)).map((directory) => [directory, value]);
|
|
930
|
+
}
|
|
931
|
+
if (key.includes("*") || nested(key)) {
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
|
|
935
|
+
return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
|
|
936
|
+
});
|
|
937
|
+
const denied = new Set(Object.values(folders).flatMap((folder) => folder instanceof Object && folder.deny instanceof Object ? Object.entries(folder.deny.imports ?? {}).flatMap(([pattern, message]) => typeof message === "string" ? [pattern] : []) : []));
|
|
938
|
+
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
939
|
+
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
940
|
+
const add = (key, folder) => {
|
|
941
|
+
if (presetOwner(folders, key, () => true) === false) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const kept = folder instanceof Object ? Object.fromEntries(Object.entries(folder).filter(([group]) => !presetGroupOff(folders, key, group))) : folder;
|
|
945
|
+
if (kept instanceof Object && Object.keys(kept).length === 0) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
merged[key] = key in merged ? presetMerge(kept, merged[key]) : kept;
|
|
949
|
+
};
|
|
950
|
+
for (const [key, value] of units) {
|
|
951
|
+
if (!(value instanceof Object)) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (value.backend) {
|
|
955
|
+
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
956
|
+
add(path === "." ? key : posix.join(key, path), folder);
|
|
957
|
+
}
|
|
958
|
+
} else if (value.tanstack || value.next) {
|
|
959
|
+
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
960
|
+
add(path === "." ? key : posix.join(key, path), folder);
|
|
961
|
+
}
|
|
962
|
+
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
963
|
+
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
964
|
+
const message = presetPackageImports["../../apps/**"];
|
|
965
|
+
const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
|
|
966
|
+
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return merged;
|
|
970
|
+
}
|
|
592
971
|
|
|
593
972
|
// src/preset/groups/backend.ts
|
|
594
973
|
var backendGroup = (project, settings) => {
|
|
@@ -606,9 +985,9 @@ var backendGroup = (project, settings) => {
|
|
|
606
985
|
}
|
|
607
986
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
608
987
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
609
|
-
const file = configured.file === undefined ? undefined :
|
|
988
|
+
const file = configured.file === undefined ? undefined : posix2.normalize(configured.file);
|
|
610
989
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
611
|
-
const found =
|
|
990
|
+
const found = presetRegistryFile(project.root, unit.source, undefined);
|
|
612
991
|
return found ? [{ file: found, unit }] : [];
|
|
613
992
|
}) : [{ file, unit: backendUnits.find((unit) => unit.key === "." || file.startsWith(`${unit.key}/`)) }];
|
|
614
993
|
for (const registry of registries) {
|
|
@@ -616,8 +995,8 @@ var backendGroup = (project, settings) => {
|
|
|
616
995
|
throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
|
|
617
996
|
}
|
|
618
997
|
const options = {
|
|
619
|
-
registry:
|
|
620
|
-
services:
|
|
998
|
+
registry: resolve3(project.root, registry.file),
|
|
999
|
+
services: resolve3(project.root, registry.unit.source, "services"),
|
|
621
1000
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
622
1001
|
};
|
|
623
1002
|
if (configured.object !== undefined) {
|
|
@@ -725,7 +1104,7 @@ var formsGroup = (project, settings) => [
|
|
|
725
1104
|
];
|
|
726
1105
|
|
|
727
1106
|
// src/preset/groups/imports.ts
|
|
728
|
-
import { resolve as
|
|
1107
|
+
import { resolve as resolve4 } from "node:path";
|
|
729
1108
|
function importsPackageNames(project, kinds) {
|
|
730
1109
|
return project.units.flatMap((unit) => {
|
|
731
1110
|
const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
|
|
@@ -763,7 +1142,7 @@ var importsGroup = (project, settings) => {
|
|
|
763
1142
|
if (!settings.boundaries) {
|
|
764
1143
|
return overrides;
|
|
765
1144
|
}
|
|
766
|
-
const absolute = (paths) => paths.map((path) =>
|
|
1145
|
+
const absolute = (paths) => paths.map((path) => resolve4(project.root, path));
|
|
767
1146
|
overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
|
|
768
1147
|
"arch/boundaries-imports": [
|
|
769
1148
|
"error",
|
|
@@ -784,7 +1163,7 @@ var importsGroup = (project, settings) => {
|
|
|
784
1163
|
};
|
|
785
1164
|
|
|
786
1165
|
// src/preset/groups/layout.ts
|
|
787
|
-
import { posix as
|
|
1166
|
+
import { posix as posix3 } from "node:path";
|
|
788
1167
|
var layoutGroup = (project, settings) => {
|
|
789
1168
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
790
1169
|
const folderOperations = presetScopes(operations, "services/*/{actions,queries}/*.ts");
|
|
@@ -792,10 +1171,11 @@ var layoutGroup = (project, settings) => {
|
|
|
792
1171
|
const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
|
|
793
1172
|
const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
|
|
794
1173
|
const frontendEntries = [...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")];
|
|
795
|
-
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path ===
|
|
1174
|
+
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix3.join(path, "components")));
|
|
796
1175
|
return [
|
|
797
1176
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
798
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1177
|
+
const folders = folder === "services" && operations.some((source) => posix3.join(source, "services") === path);
|
|
1178
|
+
const registries = folder === "services" ? project.registries : [];
|
|
799
1179
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
800
1180
|
"arch/files-restricted": [
|
|
801
1181
|
"error",
|
|
@@ -803,25 +1183,25 @@ var layoutGroup = (project, settings) => {
|
|
|
803
1183
|
message: mode === "flat" ? "This folder uses a flat layout; subfolders are forbidden." : "This folder uses exactly one owning domain directory; loose and deeper files are forbidden."
|
|
804
1184
|
}
|
|
805
1185
|
]
|
|
806
|
-
}, folders ? [`${path}/*/{actions,queries}/*.ts`] : []);
|
|
1186
|
+
}, [...folders ? [`${path}/*/{actions,queries}/*.ts`] : [], ...registries]);
|
|
807
1187
|
if (mode !== "domain") {
|
|
808
1188
|
return [restricted];
|
|
809
1189
|
}
|
|
810
1190
|
const prefix = { singularize: folder === "components" ? "trailing-s" : "none", separators: [".", "-"] };
|
|
811
1191
|
const naming = folder === "hooks" ? { ...prefix, prefixes: ["use-"] } : prefix;
|
|
812
|
-
const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] })];
|
|
1192
|
+
const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] }, registries)];
|
|
813
1193
|
if (!settings.strict || folder !== "services") {
|
|
814
1194
|
return domain;
|
|
815
1195
|
}
|
|
816
|
-
const strict = presetOverride([`${path}
|
|
1196
|
+
const strict = presetOverride([`${path}/*/**/${presetExtensions}`], {
|
|
817
1197
|
"arch/naming-file": [
|
|
818
1198
|
"error",
|
|
819
1199
|
{
|
|
820
|
-
pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*[.](?:${roles.join("|")})[.]ts$`,
|
|
1200
|
+
pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
|
|
821
1201
|
message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
|
|
822
1202
|
}
|
|
823
1203
|
]
|
|
824
|
-
}, ["**/*-{action,query}.*.ts"]);
|
|
1204
|
+
}, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
|
|
825
1205
|
return [...domain, strict];
|
|
826
1206
|
}),
|
|
827
1207
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
@@ -839,7 +1219,8 @@ var layoutGroup = (project, settings) => {
|
|
|
839
1219
|
"**/*.rsc.tsx",
|
|
840
1220
|
...presetScopes(nextWeb, "services/**/*.action.ts"),
|
|
841
1221
|
...conceptFiles,
|
|
842
|
-
...project.colocated
|
|
1222
|
+
...project.colocated,
|
|
1223
|
+
...testFiles
|
|
843
1224
|
]),
|
|
844
1225
|
presetOverride(services, {
|
|
845
1226
|
"arch/backend-service-functions": "error",
|
|
@@ -851,7 +1232,7 @@ var layoutGroup = (project, settings) => {
|
|
|
851
1232
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
852
1233
|
}, testFiles)),
|
|
853
1234
|
presetOverride([...presetScopes(app, "services/**/*-{action,query}.*.ts"), ...presetScopes(web, "services/**/*-{action,query}.*.rsc.tsx")], { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
854
|
-
presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, message: operationMessage }] }, testFiles),
|
|
1235
|
+
presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
855
1236
|
presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), { "arch/files-domain-constants": "error" }, [...utilities, ...testFiles]),
|
|
856
1237
|
presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), { "arch/files-domain-constants": ["error", { data: true }] }, [
|
|
857
1238
|
"**/*.constants.ts",
|
|
@@ -878,6 +1259,10 @@ var limitsGroup = (project, settings) => [
|
|
|
878
1259
|
|
|
879
1260
|
// src/preset/groups/naming.ts
|
|
880
1261
|
var namingOperationMessage = "Use domain-action.name.ts or domain-query.name.ts.";
|
|
1262
|
+
var namingFrontendPrefix = { ...presetExportPrefix, scope: "functions", services: true, allow: "^\\*$" };
|
|
1263
|
+
function namingOperationPrefix(role) {
|
|
1264
|
+
return { ...presetExportPrefix, roles: [role], scope: "functions", allow: "^\\*$" };
|
|
1265
|
+
}
|
|
881
1266
|
function namingOperation(role, file) {
|
|
882
1267
|
return { mode: "template", file, export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+", camelCase: true };
|
|
883
1268
|
}
|
|
@@ -923,7 +1308,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
923
1308
|
add("files", [`**/${presetExtensions}`], presetNamingRules);
|
|
924
1309
|
add("files", presetScopes(app, "types/**/*.ts"), { "arch/naming-file": ["error", { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$", message: "Use domain.types.ts filenames." }] }, ["**/*.constants.ts"]);
|
|
925
1310
|
add("exports", presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/naming-exports": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]);
|
|
926
|
-
add("exports", frontendEntries, { "arch/naming-exports": ["error",
|
|
1311
|
+
add("exports", frontendEntries, { "arch/naming-exports": ["error", namingFrontendPrefix] }, testFiles);
|
|
927
1312
|
add("exports", presetScopes(web, "services/**/*.constants.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "declarations", allow: "^\\*$" }] }, testFiles);
|
|
928
1313
|
add("exports", presetScopes(app, "**/*.utils.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", allow: "^\\*$" }] }, ui);
|
|
929
1314
|
add("files", hooks, {
|
|
@@ -942,14 +1327,14 @@ var namingGroup = (project, settings, owner) => {
|
|
|
942
1327
|
const files = [...presetScopes(flatApp, `services/**/*-${role}.*.ts`), ...presetScopes(web, `services/**/*-${role}.*.rsc.tsx`)];
|
|
943
1328
|
add("files", files, {
|
|
944
1329
|
"arch/naming-file": ["error", { pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`, message: namingOperationMessage }]
|
|
945
|
-
});
|
|
1330
|
+
}, testFiles);
|
|
946
1331
|
add("exports", files, {
|
|
947
1332
|
"arch/naming-exports": [
|
|
948
1333
|
"error",
|
|
949
|
-
|
|
1334
|
+
namingOperationPrefix(role),
|
|
950
1335
|
namingOperation(role, `{domain}-${role}.{name}.ts`)
|
|
951
1336
|
]
|
|
952
|
-
});
|
|
1337
|
+
}, testFiles);
|
|
953
1338
|
}
|
|
954
1339
|
for (const [role, folder, separator] of [["action", "actions", "."], ["query", "queries", "-"]]) {
|
|
955
1340
|
const files = presetScopes(folderApp, `services/*/${folder}/*.ts`);
|
|
@@ -961,13 +1346,13 @@ var namingGroup = (project, settings, owner) => {
|
|
|
961
1346
|
message: "Use domain-action.name.ts in actions/ and domain-query-name.ts in queries/."
|
|
962
1347
|
}
|
|
963
1348
|
]
|
|
964
|
-
});
|
|
1349
|
+
}, testFiles);
|
|
965
1350
|
add("exports", files, {
|
|
966
1351
|
"arch/naming-exports": [
|
|
967
1352
|
"error",
|
|
968
1353
|
{ mode: "template", file: `{domain}-${role}${separator}{name}.ts`, export: `make{Domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+" }
|
|
969
1354
|
]
|
|
970
|
-
});
|
|
1355
|
+
}, testFiles);
|
|
971
1356
|
}
|
|
972
1357
|
for (const role of ["action", "query"]) {
|
|
973
1358
|
for (const suffix of ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"]) {
|
|
@@ -980,11 +1365,11 @@ var namingGroup = (project, settings, owner) => {
|
|
|
980
1365
|
message: "Frontend operations retain a client, server or rsc runtime suffix."
|
|
981
1366
|
}
|
|
982
1367
|
]
|
|
983
|
-
});
|
|
984
|
-
add("exports", files, { "arch/naming-exports": ["error", namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] });
|
|
1368
|
+
}, testFiles);
|
|
1369
|
+
add("exports", files, { "arch/naming-exports": ["error", namingOperationPrefix(role), namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] }, testFiles);
|
|
985
1370
|
}
|
|
986
1371
|
}
|
|
987
|
-
add("exports", presetScopes(web, "services/**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}"]);
|
|
1372
|
+
add("exports", presetScopes(web, "services/**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", namingFrontendPrefix, { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}", ...testFiles]);
|
|
988
1373
|
if (typeof exportsPattern === "string") {
|
|
989
1374
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
|
|
990
1375
|
}
|
|
@@ -1209,8 +1594,12 @@ function optionsPatternsTest(patterns, value) {
|
|
|
1209
1594
|
});
|
|
1210
1595
|
}
|
|
1211
1596
|
function optionsGlob(pattern) {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1597
|
+
const negated = [];
|
|
1598
|
+
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
1599
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
|
|
1600
|
+
return `\x02${negated.length - 1}\x03`;
|
|
1601
|
+
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
1602
|
+
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
1214
1603
|
}
|
|
1215
1604
|
function optionsPatternLabel(pattern) {
|
|
1216
1605
|
return pattern === undefined ? "" : [pattern].flat().filter((source) => source !== "").join(" | ");
|
|
@@ -1260,6 +1649,7 @@ var testsGroup = (project, settings) => {
|
|
|
1260
1649
|
}
|
|
1261
1650
|
return [
|
|
1262
1651
|
presetOverride(project.testFiles, rules),
|
|
1652
|
+
presetOverride(presetScopes(project.units.map((unit) => unit.source), "**/*.{test,spec}.{ts,tsx,mts,cts}"), { "arch/files-restricted": ["error", { message: "Tests live in a tests/ folder, not next to the code." }] }, ["**/tests/**", "**/test/**"]),
|
|
1263
1653
|
presetOverride(presetScopes(project.backendRoots, "tests/**/*.ts"), backendRules),
|
|
1264
1654
|
...allow.map((entry) => presetOverride(entry.files, { "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis) }))
|
|
1265
1655
|
];
|
|
@@ -1322,254 +1712,149 @@ var typesGroup = (project, settings) => {
|
|
|
1322
1712
|
};
|
|
1323
1713
|
|
|
1324
1714
|
// src/preset/preset-folders.ts
|
|
1325
|
-
import { existsSync as existsSync3, readdirSync as
|
|
1326
|
-
import { posix as
|
|
1327
|
-
|
|
1328
|
-
// src/preset/preset-detect.ts
|
|
1329
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1330
|
-
import { posix as posix3, resolve as resolve4 } from "node:path";
|
|
1331
|
-
|
|
1332
|
-
// src/preset/preset-recipes.ts
|
|
1333
|
-
var presetUi = { react: true, kit: true, tailwind: true };
|
|
1334
|
-
var presetRecipes = {
|
|
1335
|
-
tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
1336
|
-
next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
|
|
1337
|
-
expo: { react: true, expo: true, schemas: true, forms: true },
|
|
1338
|
-
backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
|
|
1339
|
-
react: { react: true, tailwind: true, schemas: true },
|
|
1340
|
-
desktop: { console: true },
|
|
1341
|
-
library: { schemas: true },
|
|
1342
|
-
ui: presetUi,
|
|
1343
|
-
kit: presetUi,
|
|
1344
|
-
cli: { console: true }
|
|
1345
|
-
};
|
|
1346
|
-
var presetTemplates = {
|
|
1347
|
-
monorepo: {
|
|
1348
|
-
"apps/web": presetRecipes.tanstack,
|
|
1349
|
-
"apps/server": presetRecipes.backend,
|
|
1350
|
-
"packages/*": presetRecipes.library,
|
|
1351
|
-
scripts: presetRecipes.cli
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1715
|
+
import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
1716
|
+
import { posix as posix6, resolve as resolve7 } from "node:path";
|
|
1354
1717
|
|
|
1355
|
-
// src/preset/preset-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1718
|
+
// src/preset/preset-stylesheet.ts
|
|
1719
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
1720
|
+
import { posix as posix4, resolve as resolve5 } from "node:path";
|
|
1721
|
+
function presetTailwindEntry(css) {
|
|
1722
|
+
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1723
|
+
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
1724
|
+
}
|
|
1725
|
+
function presetStylesheet(root, directory) {
|
|
1726
|
+
const found = [];
|
|
1727
|
+
const queue = [directory];
|
|
1728
|
+
const visited = new Set;
|
|
1729
|
+
while (queue.length > 0) {
|
|
1730
|
+
const folder = queue.shift() ?? directory;
|
|
1731
|
+
const real = realpathSync(resolve5(root, folder));
|
|
1732
|
+
if (visited.has(real)) {
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
visited.add(real);
|
|
1736
|
+
for (const name of readdirSync2(resolve5(root, folder))) {
|
|
1737
|
+
const path = posix4.join(folder, name);
|
|
1738
|
+
const full = resolve5(root, path);
|
|
1739
|
+
const depth = posix4.relative(directory, path).split("/").length;
|
|
1740
|
+
if (presetIsDirectory(full)) {
|
|
1741
|
+
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1742
|
+
queue.push(path);
|
|
1743
|
+
}
|
|
1744
|
+
} else if (name.endsWith(".css") && presetIsFile(full) && presetTailwindEntry(readFileSync3(full, "utf8"))) {
|
|
1745
|
+
found.push(path);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1361
1748
|
}
|
|
1749
|
+
const rank = (path) => {
|
|
1750
|
+
const index = presetStylesheetNames.indexOf(posix4.basename(path, ".css"));
|
|
1751
|
+
return index === -1 ? presetStylesheetNames.length : index;
|
|
1752
|
+
};
|
|
1753
|
+
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1362
1754
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1755
|
+
|
|
1756
|
+
// src/preset/preset-units.ts
|
|
1757
|
+
import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
1758
|
+
import { posix as posix5, resolve as resolve6 } from "node:path";
|
|
1759
|
+
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1760
|
+
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1761
|
+
function presetUnitSource(root, key, kind, source) {
|
|
1762
|
+
if (source !== undefined) {
|
|
1763
|
+
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1764
|
+
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1765
|
+
}
|
|
1766
|
+
return posix5.join(key, source);
|
|
1366
1767
|
}
|
|
1367
|
-
return
|
|
1768
|
+
return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix5.join(key, "src");
|
|
1368
1769
|
}
|
|
1369
|
-
function
|
|
1370
|
-
return
|
|
1770
|
+
function presetSets(folder, name) {
|
|
1771
|
+
return folder[name] !== undefined && folder[name] !== false;
|
|
1371
1772
|
}
|
|
1372
|
-
function
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const source = presetSource(root, directory);
|
|
1377
|
-
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
1378
|
-
const services = resolve4(root, source, "services");
|
|
1379
|
-
const entries = existsSync2(services) ? readdirSync(services, { withFileTypes: true }) : [];
|
|
1380
|
-
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > entries.filter((entry) => entry.isDirectory()).length;
|
|
1381
|
-
if (framework) {
|
|
1382
|
-
return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
|
|
1383
|
-
}
|
|
1384
|
-
if (has(presetDetectDesktop)) {
|
|
1385
|
-
return presetRecipes.desktop;
|
|
1773
|
+
function presetKind(key, enabled) {
|
|
1774
|
+
const frameworks = presetFrameworks.filter(enabled);
|
|
1775
|
+
if (frameworks.length > 1) {
|
|
1776
|
+
throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
|
|
1386
1777
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1778
|
+
const [framework] = frameworks;
|
|
1779
|
+
if (frameworks.length > 0 && enabled("backend")) {
|
|
1780
|
+
throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
|
|
1389
1781
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1782
|
+
let kind = "library";
|
|
1783
|
+
if (frameworks.length > 0) {
|
|
1784
|
+
kind = "web";
|
|
1785
|
+
} else if (enabled("backend")) {
|
|
1786
|
+
kind = "backend";
|
|
1787
|
+
} else if (enabled("console")) {
|
|
1788
|
+
kind = "console";
|
|
1392
1789
|
}
|
|
1393
|
-
return
|
|
1790
|
+
return { kind, framework };
|
|
1394
1791
|
}
|
|
1395
|
-
function
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const previous = merged[key];
|
|
1399
|
-
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
1400
|
-
if (nested && depth > 0) {
|
|
1401
|
-
merged[key] = presetMergeObject(previous, value, depth - 1);
|
|
1402
|
-
} else if (value !== true || !(previous instanceof Object)) {
|
|
1403
|
-
merged[key] = value;
|
|
1404
|
-
}
|
|
1792
|
+
function presetHasManifest(root, key) {
|
|
1793
|
+
if (key === ".") {
|
|
1794
|
+
return false;
|
|
1405
1795
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
function presetMerge(base, top) {
|
|
1409
|
-
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
1410
|
-
return top === true && base instanceof Object ? base : top;
|
|
1796
|
+
if (!key.endsWith("/*")) {
|
|
1797
|
+
return existsSync2(resolve6(root, key, "package.json"));
|
|
1411
1798
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
function presetSpecifiers(manifest, target) {
|
|
1415
|
-
return Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => {
|
|
1416
|
-
const prefix = typeof value === "string" && value.endsWith("/*") && key.endsWith("/*") ? posix3.normalize(value.slice(0, -2)) : undefined;
|
|
1417
|
-
if (prefix === undefined || target !== prefix && !target.startsWith(`${prefix}/`)) {
|
|
1418
|
-
return [];
|
|
1419
|
-
}
|
|
1420
|
-
return [`${key.slice(0, -2)}${target.slice(prefix.length)}`];
|
|
1421
|
-
});
|
|
1799
|
+
const base = resolve6(root, key.slice(0, -2));
|
|
1800
|
+
return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
|
|
1422
1801
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
const
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
...registryImports,
|
|
1439
|
-
...operations,
|
|
1440
|
-
...manifest.name ? entries : {}
|
|
1441
|
-
}),
|
|
1442
|
-
[at("core")]: presetDeny(operations),
|
|
1443
|
-
[at("rpc")]: presetDeny(registryImports),
|
|
1444
|
-
[at("routes")]: presetDeny(registryImports),
|
|
1445
|
-
[at("cli")]: {
|
|
1446
|
-
naming: { files: presetCliFiles },
|
|
1447
|
-
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
1450
|
-
for (const tests of ["tests", "test"]) {
|
|
1451
|
-
if (exists(posix3.join(tests, "kits"))) {
|
|
1452
|
-
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
1453
|
-
conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
const queue = ["."];
|
|
1457
|
-
while (queue.length > 0) {
|
|
1458
|
-
const folder = queue.shift() ?? ".";
|
|
1459
|
-
for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
|
|
1460
|
-
if (posix3.basename(child) === "migrations") {
|
|
1461
|
-
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
1462
|
-
} else if (child.split("/").length < 6 && !presetDetectSkip.includes(posix3.basename(child))) {
|
|
1463
|
-
queue.push(child);
|
|
1802
|
+
function presetUnit(root, key, scope) {
|
|
1803
|
+
const { effective } = scope;
|
|
1804
|
+
const wildcard = scope.parent && scope.parent.depth === scope.depth && scope.parent.key.includes("*") ? scope.parent.folder : {};
|
|
1805
|
+
const folder = { ...wildcard, ...scope.folder };
|
|
1806
|
+
const own = presetKindGroups.some((name) => presetSets(folder, name)) || presetHasManifest(root, key);
|
|
1807
|
+
const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
|
|
1808
|
+
const layout = {};
|
|
1809
|
+
const settings = effective.layout;
|
|
1810
|
+
if (settings) {
|
|
1811
|
+
const services = settings.services ?? (kind === "console" ? false : "domain");
|
|
1812
|
+
const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
|
|
1813
|
+
const modes = { services, hooks, components: settings.components ?? false };
|
|
1814
|
+
for (const [name, mode] of Object.entries(modes)) {
|
|
1815
|
+
if (mode === "flat" || mode === "domain") {
|
|
1816
|
+
layout[name] = mode;
|
|
1464
1817
|
}
|
|
1465
1818
|
}
|
|
1466
1819
|
}
|
|
1467
|
-
return
|
|
1468
|
-
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
1469
|
-
return exists(path) && !empty;
|
|
1470
|
-
}));
|
|
1471
|
-
}
|
|
1472
|
-
function presetWebConventions(root, key, servers) {
|
|
1473
|
-
const source = posix3.relative(key, presetSource(root, key)) || ".";
|
|
1474
|
-
const at = (path) => source === "." ? path : posix3.join(source, path);
|
|
1475
|
-
const exists = (path) => existsSync2(resolve4(root, key, path));
|
|
1476
|
-
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
1477
|
-
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
1478
|
-
const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
|
|
1479
|
-
const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
|
|
1480
|
-
const conventions = {
|
|
1481
|
-
".": presetDeny({
|
|
1482
|
-
...Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."])),
|
|
1483
|
-
...servers.length > 0 ? { "**/services/*.service": "Import the client or rsc service file, not a server service." } : {}
|
|
1484
|
-
}),
|
|
1485
|
-
[at("components")]: presetDeny(entries),
|
|
1486
|
-
[at("routes")]: presetDeny(entries),
|
|
1487
|
-
[at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
|
|
1488
|
-
};
|
|
1489
|
-
return Object.fromEntries(Object.entries(conventions).filter(([path]) => path === "." || exists(path)));
|
|
1820
|
+
return { key, scope, kind, framework, source: presetUnitSource(root, key, kind, folder.source), layout };
|
|
1490
1821
|
}
|
|
1491
|
-
function
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
detected.scripts = presetRecipes.cli;
|
|
1504
|
-
}
|
|
1505
|
-
const keys = Object.keys(folders);
|
|
1506
|
-
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix3.dirname(key) === user.slice(0, -2)));
|
|
1507
|
-
const merged = { ".": presetDeny(presetRootImports) };
|
|
1508
|
-
for (const [key, value] of Object.entries(detected)) {
|
|
1509
|
-
if (!covered(key)) {
|
|
1510
|
-
merged[key] = presetMerge(merged[key], value);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
for (const [key, value] of Object.entries(folders)) {
|
|
1514
|
-
merged[key] = key in merged ? presetMerge(merged[key], value) : value;
|
|
1515
|
-
}
|
|
1516
|
-
const units = Object.entries(merged).filter(([key]) => !key.includes("*"));
|
|
1517
|
-
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
1518
|
-
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
1519
|
-
const add = (key, folder) => {
|
|
1520
|
-
merged[key] = key in merged ? presetMerge(folder, merged[key]) : folder;
|
|
1521
|
-
};
|
|
1522
|
-
for (const [key, value] of units) {
|
|
1523
|
-
if (!(value instanceof Object)) {
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
if (value.backend) {
|
|
1527
|
-
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, presetManifest(root, key)))) {
|
|
1528
|
-
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1529
|
-
}
|
|
1530
|
-
} else if (value.tanstack || value.next) {
|
|
1531
|
-
for (const [path, folder] of Object.entries(presetWebConventions(root, key, servers))) {
|
|
1532
|
-
add(path === "." ? key : posix3.join(key, path), folder);
|
|
1533
|
-
}
|
|
1534
|
-
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
1535
|
-
const own = Object.keys(presetManifest(root, key).imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**"));
|
|
1536
|
-
const message = presetPackageImports["../../apps/**"];
|
|
1537
|
-
const appImports = internals.filter((specifier) => !own.includes(specifier)).map((specifier) => [specifier, message]);
|
|
1538
|
-
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
1822
|
+
function presetUnits(root, scopes, ignored) {
|
|
1823
|
+
const [rootScope] = scopes;
|
|
1824
|
+
const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
|
|
1825
|
+
const candidates = [];
|
|
1826
|
+
for (const scope of scopes) {
|
|
1827
|
+
const manifest = presetHasManifest(root, scope.key);
|
|
1828
|
+
const own = presetKindGroups.some((name) => presetSets(scope.folder, name)) || manifest;
|
|
1829
|
+
const top = scope.parent?.key === "." && (rootKind === "library" || own);
|
|
1830
|
+
const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
|
|
1831
|
+
const workspace = scope.parent !== null && scope.parent.key !== "." && manifest;
|
|
1832
|
+
if (top || sibling || workspace) {
|
|
1833
|
+
candidates.push(scope);
|
|
1539
1834
|
}
|
|
1540
1835
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
if (child.isDirectory() && !child.name.startsWith(".") && !presetDetectSkip.includes(child.name) && path.split("/").length < 8) {
|
|
1551
|
-
queue.push(path);
|
|
1552
|
-
} else if (child.isFile() && child.name.endsWith(".css") && /@import\s+["']tailwindcss|@tailwind\s/.test(readFileSync2(resolve4(root, path), "utf8"))) {
|
|
1553
|
-
found.push(path);
|
|
1554
|
-
}
|
|
1836
|
+
const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
|
|
1837
|
+
const explicit = new Set(unitScopes.map((scope) => scope.key));
|
|
1838
|
+
const rootSource = presetUnitSource(root, ".", rootKind, rootScope.folder.source);
|
|
1839
|
+
const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
1840
|
+
const rootOwnsSource = rootSource !== "." && !unitScopes.some((scope) => overlaps(scope.key.replace(/\/\*$/, ""), rootSource));
|
|
1841
|
+
const rootUnit = rootKind !== "library" || rootOwnsSource || unitScopes.every((scope) => presetUnit(root, scope.key, scope).kind === "console");
|
|
1842
|
+
return [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
|
|
1843
|
+
if (!scope.key.endsWith("/*")) {
|
|
1844
|
+
return [presetUnit(root, scope.key, scope)];
|
|
1555
1845
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
return index === -1 ? presetStylesheetNames.length : index;
|
|
1560
|
-
};
|
|
1561
|
-
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1846
|
+
const base = scope.key.slice(0, -2);
|
|
1847
|
+
return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix5.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
|
|
1848
|
+
});
|
|
1562
1849
|
}
|
|
1563
1850
|
|
|
1564
1851
|
// src/preset/preset-folders.ts
|
|
1565
|
-
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1566
|
-
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1567
1852
|
function presetNormalizeKey(key) {
|
|
1568
1853
|
const portable = key.replaceAll("\\", "/");
|
|
1569
|
-
if (!portable.trim() ||
|
|
1854
|
+
if (!portable.trim() || posix6.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1570
1855
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1571
1856
|
}
|
|
1572
|
-
const normalized =
|
|
1857
|
+
const normalized = posix6.normalize(portable).replace(/\/$/, "");
|
|
1573
1858
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1574
1859
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1575
1860
|
}
|
|
@@ -1607,7 +1892,14 @@ function presetGroupValue(name, value, inherited, severity) {
|
|
|
1607
1892
|
if (unknown) {
|
|
1608
1893
|
throw new PresetError(`Unknown option \`${name}.${unknown}\`.`);
|
|
1609
1894
|
}
|
|
1610
|
-
|
|
1895
|
+
const plain = (option) => option instanceof Object && !Array.isArray(option);
|
|
1896
|
+
const merged = Object.fromEntries(Object.entries(changes).map(([key, option]) => {
|
|
1897
|
+
const stored = Object(base)[key];
|
|
1898
|
+
const field = presetShorthandOptions.find(([group, setting]) => group === name && setting === key)?.[2];
|
|
1899
|
+
const previous = typeof stored === "string" && field !== undefined && plain(option) ? { [field]: stored } : stored;
|
|
1900
|
+
return [key, plain(previous) && plain(option) ? { ...Object(previous), ...Object(option) } : option];
|
|
1901
|
+
}));
|
|
1902
|
+
return { ...base, ...merged, level: severity ?? base.level };
|
|
1611
1903
|
}
|
|
1612
1904
|
function presetDenyEntries(inherited, changes) {
|
|
1613
1905
|
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).flatMap(([key, message]) => {
|
|
@@ -1653,55 +1945,6 @@ function presetFolder(key, value) {
|
|
|
1653
1945
|
}
|
|
1654
1946
|
return folder;
|
|
1655
1947
|
}
|
|
1656
|
-
function presetUnitSource(root, key, kind, source) {
|
|
1657
|
-
if (source !== undefined) {
|
|
1658
|
-
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1659
|
-
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1660
|
-
}
|
|
1661
|
-
return posix4.join(key, source);
|
|
1662
|
-
}
|
|
1663
|
-
return kind === "console" || key === "." && !existsSync3(resolve5(root, "src")) ? key : posix4.join(key, "src");
|
|
1664
|
-
}
|
|
1665
|
-
function presetSets(folder, name) {
|
|
1666
|
-
return folder[name] !== undefined && folder[name] !== false;
|
|
1667
|
-
}
|
|
1668
|
-
function presetKind(key, enabled) {
|
|
1669
|
-
const frameworks = presetFrameworks.filter(enabled);
|
|
1670
|
-
if (frameworks.length > 1) {
|
|
1671
|
-
throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
|
|
1672
|
-
}
|
|
1673
|
-
const [framework] = frameworks;
|
|
1674
|
-
if (frameworks.length > 0 && enabled("backend")) {
|
|
1675
|
-
throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
|
|
1676
|
-
}
|
|
1677
|
-
let kind = "library";
|
|
1678
|
-
if (frameworks.length > 0) {
|
|
1679
|
-
kind = "web";
|
|
1680
|
-
} else if (enabled("backend")) {
|
|
1681
|
-
kind = "backend";
|
|
1682
|
-
} else if (enabled("console")) {
|
|
1683
|
-
kind = "console";
|
|
1684
|
-
}
|
|
1685
|
-
return { kind, framework };
|
|
1686
|
-
}
|
|
1687
|
-
function presetUnit(root, key, scope) {
|
|
1688
|
-
const { folder, effective } = scope;
|
|
1689
|
-
const own = presetKindGroups.some((name) => presetSets(folder, name));
|
|
1690
|
-
const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
|
|
1691
|
-
const layout = {};
|
|
1692
|
-
const settings = effective.layout;
|
|
1693
|
-
if (settings) {
|
|
1694
|
-
const services = settings.services ?? (kind === "console" ? false : "domain");
|
|
1695
|
-
const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
|
|
1696
|
-
const modes = { services, hooks, components: settings.components ?? false };
|
|
1697
|
-
for (const [name, mode] of Object.entries(modes)) {
|
|
1698
|
-
if (mode === "flat" || mode === "domain") {
|
|
1699
|
-
layout[name] = mode;
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return { key, scope, kind, framework, source: presetUnitSource(root, key, kind, scope.folder.source), layout };
|
|
1704
|
-
}
|
|
1705
1948
|
function presetScopesTree(root, folders) {
|
|
1706
1949
|
const entries = Object.entries(folders).map(([key, value]) => [presetNormalizeKey(key), value]);
|
|
1707
1950
|
const keys = entries.map(([key]) => key);
|
|
@@ -1711,7 +1954,7 @@ function presetScopesTree(root, folders) {
|
|
|
1711
1954
|
}
|
|
1712
1955
|
for (const key of keys) {
|
|
1713
1956
|
const base = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
1714
|
-
if (!existsSync3(
|
|
1957
|
+
if (!existsSync3(resolve7(root, base))) {
|
|
1715
1958
|
throw new PresetError(`Folder \`${key}\` does not exist.`);
|
|
1716
1959
|
}
|
|
1717
1960
|
}
|
|
@@ -1743,36 +1986,22 @@ function presetScopesTree(root, folders) {
|
|
|
1743
1986
|
return scopes;
|
|
1744
1987
|
}
|
|
1745
1988
|
function presetProject(config) {
|
|
1746
|
-
const given =
|
|
1747
|
-
const root = existsSync3(given) ?
|
|
1748
|
-
const
|
|
1989
|
+
const given = resolve7(config.root ?? process.cwd());
|
|
1990
|
+
const root = existsSync3(given) ? realpathSync2(given) : given;
|
|
1991
|
+
const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
|
|
1992
|
+
const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
|
|
1993
|
+
if (twice) {
|
|
1994
|
+
throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
|
|
1995
|
+
}
|
|
1996
|
+
const folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
|
|
1749
1997
|
const scopes = presetScopesTree(root, folders);
|
|
1750
1998
|
const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
|
|
1751
1999
|
const live = scopes.filter((scope) => !ignored.has(scope.key));
|
|
1752
|
-
const [rootScope] = scopes;
|
|
1753
|
-
const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
|
|
1754
2000
|
const dead = live.find((scope) => scope.parent && ignored.has(scope.parent.key) && scope.parent.depth < scope.depth);
|
|
1755
2001
|
if (dead) {
|
|
1756
2002
|
throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
|
|
1757
2003
|
}
|
|
1758
|
-
const
|
|
1759
|
-
for (const scope of scopes) {
|
|
1760
|
-
const top = scope.parent?.key === "." && (rootKind === "library" || presetKindGroups.some((name) => presetSets(scope.folder, name)));
|
|
1761
|
-
const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
|
|
1762
|
-
if (top || sibling) {
|
|
1763
|
-
candidates.push(scope);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
|
|
1767
|
-
const explicit = new Set(unitScopes.map((scope) => scope.key));
|
|
1768
|
-
const rootUnit = rootKind !== "library" || unitScopes.length === 0;
|
|
1769
|
-
const units = [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
|
|
1770
|
-
if (!scope.key.endsWith("/*")) {
|
|
1771
|
-
return [presetUnit(root, scope.key, scope)];
|
|
1772
|
-
}
|
|
1773
|
-
const base = scope.key.slice(0, -2);
|
|
1774
|
-
return readdirSync2(resolve5(root, base)).filter((name) => !name.startsWith(".") && statSync2(resolve5(root, base, name)).isDirectory()).map((name) => posix4.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
|
|
1775
|
-
});
|
|
2004
|
+
const units = presetUnits(root, scopes, ignored);
|
|
1776
2005
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
1777
2006
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
1778
2007
|
if (misplaced) {
|
|
@@ -1804,9 +2033,9 @@ function presetProject(config) {
|
|
|
1804
2033
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
1805
2034
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
1806
2035
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
1807
|
-
const rpcClients = client === undefined ? webUnits.map((unit) =>
|
|
2036
|
+
const rpcClients = client === undefined ? webUnits.map((unit) => posix6.join(unit.source, "services", unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
|
|
1808
2037
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
1809
|
-
const uiRoots = [...new Set([...web.map((path) =>
|
|
2038
|
+
const uiRoots = [...new Set([...web.map((path) => posix6.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
|
|
1810
2039
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
1811
2040
|
const stylesheet = (unit) => {
|
|
1812
2041
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -1814,7 +2043,7 @@ function presetProject(config) {
|
|
|
1814
2043
|
return;
|
|
1815
2044
|
}
|
|
1816
2045
|
const css = tailwind.css ?? presetStylesheet(root, unit.key);
|
|
1817
|
-
return css === undefined ? undefined :
|
|
2046
|
+
return css === undefined ? undefined : resolve7(root, css);
|
|
1818
2047
|
};
|
|
1819
2048
|
const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
|
|
1820
2049
|
const css = stylesheet(unit);
|
|
@@ -1848,7 +2077,7 @@ function presetProject(config) {
|
|
|
1848
2077
|
return [key];
|
|
1849
2078
|
}
|
|
1850
2079
|
const base = key.slice(0, -2);
|
|
1851
|
-
return
|
|
2080
|
+
return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix6.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
|
|
1852
2081
|
});
|
|
1853
2082
|
return {
|
|
1854
2083
|
root,
|
|
@@ -1857,11 +2086,11 @@ function presetProject(config) {
|
|
|
1857
2086
|
suffixes,
|
|
1858
2087
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
1859
2088
|
orpc: {
|
|
1860
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2089
|
+
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix6.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
|
|
1861
2090
|
composers: listOf(orpcScopes.map((settings) => settings.composers)),
|
|
1862
2091
|
client: client ?? ""
|
|
1863
2092
|
},
|
|
1864
|
-
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path:
|
|
2093
|
+
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: posix6.join(unit.source, folder), folder, mode }))),
|
|
1865
2094
|
rpcClients,
|
|
1866
2095
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
1867
2096
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -1898,7 +2127,9 @@ function presetProject(config) {
|
|
|
1898
2127
|
publicEntrypoints,
|
|
1899
2128
|
baseImports: presetBaseImports,
|
|
1900
2129
|
css: { ...webCss, ...kitCss },
|
|
1901
|
-
|
|
2130
|
+
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry) ?? []),
|
|
2131
|
+
ignores: skipped.map((key) => `${key}/**`),
|
|
2132
|
+
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
1902
2133
|
};
|
|
1903
2134
|
}
|
|
1904
2135
|
|
|
@@ -2048,10 +2279,7 @@ function preset(config) {
|
|
|
2048
2279
|
plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
|
|
2049
2280
|
ignorePatterns: [
|
|
2050
2281
|
"**/node_modules/**",
|
|
2051
|
-
|
|
2052
|
-
"**/build/**",
|
|
2053
|
-
"**/coverage/**",
|
|
2054
|
-
"**/.output/**",
|
|
2282
|
+
...project.outputs,
|
|
2055
2283
|
...project.nextWeb.length > 0 ? ["**/.next/**", "**/next-env.d.ts"] : [],
|
|
2056
2284
|
...project.expoWeb.length > 0 ? ["**/.expo/**", "**/expo-env.d.ts"] : [],
|
|
2057
2285
|
...project.generated,
|