oxlint-plugin-arch 0.12.0 → 0.12.2
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 +36 -0
- package/dist/index.js +60 -15
- 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 +617 -364
- package/dist/preset/preset.utils.d.ts +5 -0
- package/dist/types/boundaries.types.d.ts +1 -0
- package/dist/types/preset.types.d.ts +10 -1
- package/dist/types/schemas.types.d.ts +1 -0
- 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 posix3, 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 as posix2, 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",
|
|
@@ -436,7 +464,7 @@ var presetGroupOptions = {
|
|
|
436
464
|
deny: { level: "error", imports: {}, calls: {} },
|
|
437
465
|
externals: { level: "error", packages: presetExternals }
|
|
438
466
|
};
|
|
439
|
-
var presetFolderKeys = ["severity", "source", "rules"];
|
|
467
|
+
var presetFolderKeys = ["severity", "source", "services", "rules"];
|
|
440
468
|
var presetA11yRules = [
|
|
441
469
|
"alt-text",
|
|
442
470
|
"anchor-has-content",
|
|
@@ -478,15 +506,15 @@ 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"];
|
|
486
514
|
var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
|
|
487
515
|
var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
|
|
488
516
|
var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
|
|
489
|
-
var presetRegistryFiles = ["
|
|
517
|
+
var presetRegistryFiles = ["core/core.services.ts", "service.ts"];
|
|
490
518
|
var presetRegistrySkip = ["brand", "database", "db", "math", "security", "tools"];
|
|
491
519
|
var presetServerEntries = ["rpc", "server", "security", "services"];
|
|
492
520
|
var presetOperationImports = ["*/*-action.*", "*/*-query.*", "*/actions/*", "*/queries/*"];
|
|
@@ -499,13 +527,16 @@ 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";
|
|
503
|
-
import { resolve } from "node:path";
|
|
530
|
+
import { readFileSync, statSync } from "node:fs";
|
|
531
|
+
import { posix, resolve } from "node:path";
|
|
504
532
|
class PresetError extends Error {
|
|
505
533
|
}
|
|
506
534
|
function presetScopes(roots, suffix = `**/${presetExtensions}`) {
|
|
507
535
|
return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
|
|
508
536
|
}
|
|
537
|
+
function presetServiceScopes(folders, sources, suffix = `**/${presetExtensions}`) {
|
|
538
|
+
return presetScopes(sources.map((source) => folders[source] ?? posix.join(source, "services")), suffix);
|
|
539
|
+
}
|
|
509
540
|
function presetOverride(files, rules, excludeFiles = []) {
|
|
510
541
|
return {
|
|
511
542
|
files,
|
|
@@ -583,12 +614,376 @@ function presetRuntime(project, settings, framework, rule) {
|
|
|
583
614
|
options.rpc = project.rpcClients.map((path) => resolve(project.root, path));
|
|
584
615
|
}
|
|
585
616
|
return [
|
|
586
|
-
presetOverride([...
|
|
617
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, [unit.source], "**/*.{ts,tsx}"), ...presetScopes([unit.source], "lib/**/*.{ts,tsx}")], {
|
|
587
618
|
[rule]: ["error", options]
|
|
588
619
|
})
|
|
589
620
|
];
|
|
590
621
|
});
|
|
591
622
|
}
|
|
623
|
+
function presetManifest(root, directory) {
|
|
624
|
+
try {
|
|
625
|
+
return JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
|
|
626
|
+
} catch {
|
|
627
|
+
return {};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function presetIsDirectory(path) {
|
|
631
|
+
try {
|
|
632
|
+
return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
633
|
+
} catch {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function presetServicesFolder(root, source, configured, fallback = true) {
|
|
638
|
+
if (configured !== undefined) {
|
|
639
|
+
if (!configured.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
640
|
+
throw new PresetError(`services \`${configured}\` must be a literal relative path.`);
|
|
641
|
+
}
|
|
642
|
+
return posix.join(source, configured);
|
|
643
|
+
}
|
|
644
|
+
const lib = fallback && !presetIsDirectory(resolve(root, source, "services")) && presetIsDirectory(resolve(root, source, "lib"));
|
|
645
|
+
return posix.join(source, lib ? "lib" : "services");
|
|
646
|
+
}
|
|
647
|
+
function presetIsFile(path) {
|
|
648
|
+
try {
|
|
649
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/preset/preset-detect.ts
|
|
656
|
+
function presetDirectories(root, base, skip = presetDetectSkip) {
|
|
657
|
+
if (!presetIsDirectory(resolve2(root, base))) {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix2.join(base, name));
|
|
661
|
+
}
|
|
662
|
+
var presetNamedSkip = ["node_modules"];
|
|
663
|
+
function presetSource(root, directory, configured) {
|
|
664
|
+
if (configured !== undefined) {
|
|
665
|
+
return posix2.join(directory, configured);
|
|
666
|
+
}
|
|
667
|
+
return existsSync(resolve2(root, directory, "src")) ? posix2.join(directory, "src") : directory;
|
|
668
|
+
}
|
|
669
|
+
function presetRegistryFile(root, source, configured, services) {
|
|
670
|
+
if (configured === false) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const file = typeof configured === "string" ? configured : configured?.file;
|
|
674
|
+
if (file !== undefined) {
|
|
675
|
+
return posix2.normalize(file);
|
|
676
|
+
}
|
|
677
|
+
return [posix2.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix2.join(source, path))].find((path) => existsSync(resolve2(root, path)));
|
|
678
|
+
}
|
|
679
|
+
function presetDomains(root, services) {
|
|
680
|
+
return presetDirectories(root, services, presetNamedSkip).map((path) => posix2.basename(path)).filter((name) => existsSync(resolve2(root, services, name, `${name}.service.ts`)));
|
|
681
|
+
}
|
|
682
|
+
function presetRecipe(root, directory, app, folder) {
|
|
683
|
+
const manifest = presetManifest(root, directory);
|
|
684
|
+
const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
|
|
685
|
+
const has = (names) => names.some((name) => dependencies.has(name));
|
|
686
|
+
const source = presetSource(root, directory, folder.source);
|
|
687
|
+
const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
|
|
688
|
+
const servicesFolder = presetServicesFolder(root, source, folder.services, app);
|
|
689
|
+
const services = resolve2(root, servicesFolder);
|
|
690
|
+
const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
|
|
691
|
+
const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
|
|
692
|
+
const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
|
|
693
|
+
if (framework) {
|
|
694
|
+
return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
|
|
695
|
+
}
|
|
696
|
+
if (has(presetDetectDesktop)) {
|
|
697
|
+
return presetRecipes.desktop;
|
|
698
|
+
}
|
|
699
|
+
if (presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
|
|
700
|
+
return presetRecipes.backend;
|
|
701
|
+
}
|
|
702
|
+
if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
|
|
703
|
+
return app ? presetRecipes.react : presetRecipes.ui;
|
|
704
|
+
}
|
|
705
|
+
return presetRecipes.library;
|
|
706
|
+
}
|
|
707
|
+
function presetMergeObject(base, top, depth) {
|
|
708
|
+
const merged = { ...base };
|
|
709
|
+
for (const [key, value] of Object.entries(top)) {
|
|
710
|
+
const previous = Reflect.get(merged, key);
|
|
711
|
+
const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
|
|
712
|
+
if (nested && depth > 0) {
|
|
713
|
+
Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
|
|
714
|
+
} else if (value !== true || !(previous instanceof Object)) {
|
|
715
|
+
Reflect.set(merged, key, value);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return merged;
|
|
719
|
+
}
|
|
720
|
+
function presetMerge(base, top) {
|
|
721
|
+
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
722
|
+
return top === true && base instanceof Object ? base : top;
|
|
723
|
+
}
|
|
724
|
+
return presetMergeObject(base, top, 2);
|
|
725
|
+
}
|
|
726
|
+
function presetImportTargets(value) {
|
|
727
|
+
if (typeof value === "string") {
|
|
728
|
+
return [value];
|
|
729
|
+
}
|
|
730
|
+
if (Array.isArray(value)) {
|
|
731
|
+
return value.flatMap(presetImportTargets);
|
|
732
|
+
}
|
|
733
|
+
return value instanceof Object ? Object.values(value).flatMap(presetImportTargets) : [];
|
|
734
|
+
}
|
|
735
|
+
function presetSpecifiers(manifest, target, file = false) {
|
|
736
|
+
const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
|
|
737
|
+
const path = posix2.normalize(mapped);
|
|
738
|
+
const parts = path.split("*");
|
|
739
|
+
const keyParts = key.split("*");
|
|
740
|
+
if (parts.length === 1 && keyParts.length === 1) {
|
|
741
|
+
return path.replace(/\.[cm]?[jt]sx?$/, "") === target ? [key] : [];
|
|
742
|
+
}
|
|
743
|
+
if (parts.length !== 2 || keyParts.length !== 2) {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
const [before, after] = parts;
|
|
747
|
+
const [head, suffix] = keyParts;
|
|
748
|
+
const prefix = before.replace(/\/$/, "");
|
|
749
|
+
if (target !== prefix && !target.startsWith(`${prefix}/`)) {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
const rest = target.slice(prefix.length).replace(/^\//, "");
|
|
753
|
+
const tail = after && rest.endsWith(after) ? rest.slice(0, -after.length) : rest;
|
|
754
|
+
return [`${head}${tail}${file ? suffix : ""}`.replace(/\/$/, "")];
|
|
755
|
+
}));
|
|
756
|
+
return [...new Set(specifiers)];
|
|
757
|
+
}
|
|
758
|
+
function presetOverlaps(left, right) {
|
|
759
|
+
const [leftBase] = left.split("*");
|
|
760
|
+
const [rightBase] = right.split("*");
|
|
761
|
+
return leftBase.startsWith(rightBase) || rightBase.startsWith(leftBase);
|
|
762
|
+
}
|
|
763
|
+
function presetDeny(imports) {
|
|
764
|
+
return { deny: { imports } };
|
|
765
|
+
}
|
|
766
|
+
function presetFilled(conventions, exists) {
|
|
767
|
+
return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
|
|
768
|
+
const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
|
|
769
|
+
return exists(path) && !empty;
|
|
770
|
+
}));
|
|
771
|
+
}
|
|
772
|
+
function presetBackendConventions(root, key, value, denied) {
|
|
773
|
+
const manifest = presetManifest(root, key);
|
|
774
|
+
const source = presetSource(root, key, value.source);
|
|
775
|
+
const local = posix2.relative(key, source) || ".";
|
|
776
|
+
const at = (path) => local === "." ? path : posix2.join(local, path);
|
|
777
|
+
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
778
|
+
const configured = value.backend instanceof Object ? value.backend.registry : undefined;
|
|
779
|
+
const servicesFolder = presetServicesFolder(root, source, value.services);
|
|
780
|
+
const servicesKey = posix2.relative(key, servicesFolder);
|
|
781
|
+
const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
|
|
782
|
+
const registry = registryFile === undefined ? undefined : posix2.relative(key, registryFile);
|
|
783
|
+
const services = presetSpecifiers(manifest, servicesKey);
|
|
784
|
+
const message = "Use the service API, not another service’s operations.";
|
|
785
|
+
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
|
|
786
|
+
const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.[cm]?[jt]sx?$/, ""), true).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
|
|
787
|
+
const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
|
|
788
|
+
const serviceImports = { ...registryImports, ...operations };
|
|
789
|
+
if (manifest.name) {
|
|
790
|
+
Object.assign(serviceImports, entries);
|
|
791
|
+
}
|
|
792
|
+
const conventions = {
|
|
793
|
+
[servicesKey]: presetDeny(serviceImports),
|
|
794
|
+
[at("core")]: presetDeny(operations),
|
|
795
|
+
[at("rpc")]: presetDeny(registryImports),
|
|
796
|
+
[at("routes")]: presetDeny(registryImports),
|
|
797
|
+
[at("cli")]: {
|
|
798
|
+
naming: { files: presetCliFiles },
|
|
799
|
+
...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix2.basename(path))) {
|
|
803
|
+
const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
|
|
804
|
+
[`${prefix}/${pattern}`, false],
|
|
805
|
+
[`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
|
|
806
|
+
]));
|
|
807
|
+
conventions[posix2.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
|
|
808
|
+
}
|
|
809
|
+
for (const tests of ["tests", "test"]) {
|
|
810
|
+
if (exists(posix2.join(tests, "kits"))) {
|
|
811
|
+
conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
|
|
812
|
+
conventions[posix2.join(tests, "kits")] = { naming: { files: presetKitFiles } };
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const queue = ["."];
|
|
816
|
+
while (queue.length > 0) {
|
|
817
|
+
const folder = queue.shift() ?? ".";
|
|
818
|
+
for (const child of presetDirectories(root, posix2.join(key, folder)).map((path) => posix2.relative(key, path))) {
|
|
819
|
+
if (posix2.basename(child) === "migrations") {
|
|
820
|
+
conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
|
|
821
|
+
} else if (child.split("/").length < 8) {
|
|
822
|
+
queue.push(child);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return presetFilled(conventions, exists);
|
|
827
|
+
}
|
|
828
|
+
function presetWebConventions(root, key, value, servers) {
|
|
829
|
+
const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
|
|
830
|
+
const at = (path) => source === "." ? path : posix2.join(source, path);
|
|
831
|
+
const exists = (path) => existsSync(resolve2(root, key, path));
|
|
832
|
+
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
833
|
+
const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
834
|
+
const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
|
|
835
|
+
const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
|
|
836
|
+
const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
|
|
837
|
+
const appImports = Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."]));
|
|
838
|
+
if (servers.length > 0) {
|
|
839
|
+
appImports["**/services/*.service"] = "Import the client or rsc service file, not a server service.";
|
|
840
|
+
}
|
|
841
|
+
const conventions = {
|
|
842
|
+
".": presetDeny(appImports),
|
|
843
|
+
[at("components")]: presetDeny(entries),
|
|
844
|
+
[at("routes")]: presetDeny(entries),
|
|
845
|
+
[at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
|
|
846
|
+
};
|
|
847
|
+
return presetFilled(conventions, exists);
|
|
848
|
+
}
|
|
849
|
+
function presetWorkspacePatterns(root, manifest) {
|
|
850
|
+
const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
|
|
851
|
+
const file = resolve2(root, "pnpm-workspace.yaml");
|
|
852
|
+
if (!existsSync(file)) {
|
|
853
|
+
return listed;
|
|
854
|
+
}
|
|
855
|
+
const patterns = [...listed];
|
|
856
|
+
let inside = false;
|
|
857
|
+
for (const line of readFileSync2(file, "utf8").split(`
|
|
858
|
+
`)) {
|
|
859
|
+
const flow = /^packages\s*:\s*\[(.*)\]/.exec(line);
|
|
860
|
+
const item = /^\s*-\s*(["']?)([^"'#\s]+)\1\s*(?:#.*)?$/.exec(line);
|
|
861
|
+
if (flow) {
|
|
862
|
+
patterns.push(...flow[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean));
|
|
863
|
+
inside = false;
|
|
864
|
+
} else if (/^packages\s*:/.test(line)) {
|
|
865
|
+
inside = true;
|
|
866
|
+
} else if (inside && item) {
|
|
867
|
+
patterns.push(item[2]);
|
|
868
|
+
} else if (/^\S/.test(line)) {
|
|
869
|
+
inside = false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return patterns;
|
|
873
|
+
}
|
|
874
|
+
function presetWorkspaces(root, patterns) {
|
|
875
|
+
const clean = (pattern) => posix2.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
|
|
876
|
+
const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
|
|
877
|
+
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
|
|
878
|
+
const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
|
|
879
|
+
const candidates = new Set;
|
|
880
|
+
for (const pattern of include) {
|
|
881
|
+
const segments = pattern.split("/");
|
|
882
|
+
const literal = segments.findIndex((segment) => /[*?{[]/.test(segment));
|
|
883
|
+
if (literal === -1) {
|
|
884
|
+
candidates.add(pattern);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const depth = segments.includes("**") ? 8 : segments.length - literal;
|
|
888
|
+
let level = [segments.slice(0, literal).join("/") || "."];
|
|
889
|
+
for (let step = 0;step < depth; step++) {
|
|
890
|
+
level = level.flatMap((folder) => presetDirectories(root, folder, presetNamedSkip));
|
|
891
|
+
level.forEach((folder) => candidates.add(folder));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync(resolve2(root, path, "package.json"))).toSorted();
|
|
895
|
+
}
|
|
896
|
+
function presetContains(key, path) {
|
|
897
|
+
if (key === "." || key === path) {
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const pattern = key.split("/");
|
|
901
|
+
const segments = path.split("/");
|
|
902
|
+
return pattern.length <= segments.length && pattern.every((segment, index) => segment === "*" || segment === segments[index]);
|
|
903
|
+
}
|
|
904
|
+
function presetOwner(folders, path, owns) {
|
|
905
|
+
const depth = (key) => key === "." ? 0 : key.split("/").length;
|
|
906
|
+
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];
|
|
907
|
+
}
|
|
908
|
+
function presetGroupOff(folders, path, group) {
|
|
909
|
+
const owner = presetOwner(folders, path, (value) => value instanceof Object && (group in value));
|
|
910
|
+
return owner instanceof Object && Object(owner)[group] === false;
|
|
911
|
+
}
|
|
912
|
+
function presetDetect(root, folders) {
|
|
913
|
+
const manifest = presetManifest(root, ".");
|
|
914
|
+
const configured = (key) => {
|
|
915
|
+
const value = folders[key];
|
|
916
|
+
return value instanceof Object ? value : {};
|
|
917
|
+
};
|
|
918
|
+
const patterns = presetWorkspacePatterns(root, manifest);
|
|
919
|
+
const directories = presetWorkspaces(root, patterns);
|
|
920
|
+
const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configured(directory))]));
|
|
921
|
+
const rootRecipe = presetRecipe(root, ".", true, configured("."));
|
|
922
|
+
const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
|
|
923
|
+
const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
|
|
924
|
+
if (patterns.length === 0 ? single : rootApp) {
|
|
925
|
+
detected["."] = rootRecipe;
|
|
926
|
+
}
|
|
927
|
+
if (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
|
|
928
|
+
detected.scripts = presetRecipes.cli;
|
|
929
|
+
}
|
|
930
|
+
const keys = Object.keys(folders);
|
|
931
|
+
const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix2.dirname(key) === user.slice(0, -2)));
|
|
932
|
+
const merged = { ".": presetDeny(presetRootImports) };
|
|
933
|
+
for (const [key, value] of Object.entries(detected)) {
|
|
934
|
+
if (!covered(key)) {
|
|
935
|
+
merged[key] = presetMerge(merged[key], value);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const [key, value] of Object.entries(folders)) {
|
|
939
|
+
merged[key] = key in merged ? presetMerge(merged[key], value) : value;
|
|
940
|
+
}
|
|
941
|
+
const length = (key) => key.split("/").length;
|
|
942
|
+
const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
|
|
943
|
+
const units = Object.entries(merged).flatMap(([key, value]) => {
|
|
944
|
+
if (key.endsWith("/*")) {
|
|
945
|
+
return presetDirectories(root, key.slice(0, -2), presetNamedSkip).filter((directory) => !(directory in merged)).map((directory) => [directory, value]);
|
|
946
|
+
}
|
|
947
|
+
if (key.includes("*") || nested(key)) {
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
|
|
951
|
+
return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
|
|
952
|
+
});
|
|
953
|
+
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] : []) : []));
|
|
954
|
+
const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
|
|
955
|
+
const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
|
|
956
|
+
const add = (key, folder) => {
|
|
957
|
+
if (presetOwner(folders, key, () => true) === false) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const kept = folder instanceof Object ? Object.fromEntries(Object.entries(folder).filter(([group]) => !presetGroupOff(folders, key, group))) : folder;
|
|
961
|
+
if (kept instanceof Object && Object.keys(kept).length === 0) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
merged[key] = key in merged ? presetMerge(kept, merged[key]) : kept;
|
|
965
|
+
};
|
|
966
|
+
for (const [key, value] of units) {
|
|
967
|
+
if (!(value instanceof Object)) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (value.backend) {
|
|
971
|
+
for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
|
|
972
|
+
add(path === "." ? key : posix2.join(key, path), folder);
|
|
973
|
+
}
|
|
974
|
+
} else if (value.tanstack || value.next) {
|
|
975
|
+
for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
|
|
976
|
+
add(path === "." ? key : posix2.join(key, path), folder);
|
|
977
|
+
}
|
|
978
|
+
} else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
|
|
979
|
+
const own = Object.keys(presetManifest(root, key).imports ?? {});
|
|
980
|
+
const message = presetPackageImports["../../apps/**"];
|
|
981
|
+
const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
|
|
982
|
+
add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return merged;
|
|
986
|
+
}
|
|
592
987
|
|
|
593
988
|
// src/preset/groups/backend.ts
|
|
594
989
|
var backendGroup = (project, settings) => {
|
|
@@ -606,9 +1001,9 @@ var backendGroup = (project, settings) => {
|
|
|
606
1001
|
}
|
|
607
1002
|
const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
|
|
608
1003
|
const backendUnits = project.units.filter((unit) => unit.kind === "backend");
|
|
609
|
-
const file = configured.file === undefined ? undefined :
|
|
1004
|
+
const file = configured.file === undefined ? undefined : posix3.normalize(configured.file);
|
|
610
1005
|
const registries = file === undefined ? backendUnits.flatMap((unit) => {
|
|
611
|
-
const found =
|
|
1006
|
+
const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
|
|
612
1007
|
return found ? [{ file: found, unit }] : [];
|
|
613
1008
|
}) : [{ file, unit: backendUnits.find((unit) => unit.key === "." || file.startsWith(`${unit.key}/`)) }];
|
|
614
1009
|
for (const registry of registries) {
|
|
@@ -616,8 +1011,8 @@ var backendGroup = (project, settings) => {
|
|
|
616
1011
|
throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
|
|
617
1012
|
}
|
|
618
1013
|
const options = {
|
|
619
|
-
registry:
|
|
620
|
-
services:
|
|
1014
|
+
registry: resolve3(project.root, registry.file),
|
|
1015
|
+
services: resolve3(project.root, registry.unit.services),
|
|
621
1016
|
skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
|
|
622
1017
|
};
|
|
623
1018
|
if (configured.object !== undefined) {
|
|
@@ -635,7 +1030,7 @@ var basicsGroup = (project, settings) => {
|
|
|
635
1030
|
return [
|
|
636
1031
|
...settings.trivial ? [
|
|
637
1032
|
presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
|
|
638
|
-
presetOverride([...
|
|
1033
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")], { "arch/code-trivial-functions": ["error", { ...trivial, services: true }] }, ui)
|
|
639
1034
|
] : [],
|
|
640
1035
|
...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
|
|
641
1036
|
...settings.switch ? [presetOverride(project.backendFiles, { "arch/code-switch": ["error", { message: "Use ts-pattern in backend application code." }] })] : [],
|
|
@@ -666,8 +1061,8 @@ var denyGroup = (project, settings) => {
|
|
|
666
1061
|
continue;
|
|
667
1062
|
}
|
|
668
1063
|
const files = [
|
|
669
|
-
...
|
|
670
|
-
...
|
|
1064
|
+
...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
|
|
1065
|
+
...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
|
|
671
1066
|
];
|
|
672
1067
|
const reads = Object.fromEntries(writes.map((name) => [name, "Query files only read; move writes to an action."]));
|
|
673
1068
|
overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
|
|
@@ -725,7 +1120,7 @@ var formsGroup = (project, settings) => [
|
|
|
725
1120
|
];
|
|
726
1121
|
|
|
727
1122
|
// src/preset/groups/imports.ts
|
|
728
|
-
import { resolve as
|
|
1123
|
+
import { resolve as resolve4 } from "node:path";
|
|
729
1124
|
function importsPackageNames(project, kinds) {
|
|
730
1125
|
return project.units.flatMap((unit) => {
|
|
731
1126
|
const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
|
|
@@ -763,7 +1158,7 @@ var importsGroup = (project, settings) => {
|
|
|
763
1158
|
if (!settings.boundaries) {
|
|
764
1159
|
return overrides;
|
|
765
1160
|
}
|
|
766
|
-
const absolute = (paths) => paths.map((path) =>
|
|
1161
|
+
const absolute = (paths) => paths.map((path) => resolve4(project.root, path));
|
|
767
1162
|
overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
|
|
768
1163
|
"arch/boundaries-imports": [
|
|
769
1164
|
"error",
|
|
@@ -772,6 +1167,7 @@ var importsGroup = (project, settings) => {
|
|
|
772
1167
|
backend: absolute(project.backend),
|
|
773
1168
|
packages: absolute(project.packages),
|
|
774
1169
|
roles: project.suffixes,
|
|
1170
|
+
services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
|
|
775
1171
|
portable: true,
|
|
776
1172
|
aliases: presetAliases(project.root, settings.aliases),
|
|
777
1173
|
appPackages: importsPackageNames(project, ["web", "backend"]),
|
|
@@ -784,18 +1180,19 @@ var importsGroup = (project, settings) => {
|
|
|
784
1180
|
};
|
|
785
1181
|
|
|
786
1182
|
// src/preset/groups/layout.ts
|
|
787
|
-
import { posix as
|
|
1183
|
+
import { posix as posix4 } from "node:path";
|
|
788
1184
|
var layoutGroup = (project, settings) => {
|
|
789
1185
|
const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
|
|
790
|
-
const folderOperations =
|
|
1186
|
+
const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
|
|
791
1187
|
const operationMessage = "Action/query files export exactly one named operation. Keep helpers used only by this operation private inside its function; do not export them. Move a helper to the module's .utils.ts only when it is genuinely shared, not merely to satisfy lint.";
|
|
792
1188
|
const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
|
|
793
1189
|
const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
|
|
794
|
-
const frontendEntries = [...
|
|
795
|
-
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path ===
|
|
1190
|
+
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
1191
|
+
const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix4.join(path, "components")));
|
|
796
1192
|
return [
|
|
797
1193
|
...layouts.flatMap(({ path, folder, mode }) => {
|
|
798
|
-
const folders = folder === "services" && operations.some((source) =>
|
|
1194
|
+
const folders = folder === "services" && operations.some((source) => posix4.join(source, "services") === path);
|
|
1195
|
+
const registries = folder === "services" ? project.registries : [];
|
|
799
1196
|
const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
|
|
800
1197
|
"arch/files-restricted": [
|
|
801
1198
|
"error",
|
|
@@ -803,31 +1200,31 @@ var layoutGroup = (project, settings) => {
|
|
|
803
1200
|
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
1201
|
}
|
|
805
1202
|
]
|
|
806
|
-
}, folders ? [`${path}/*/{actions,queries}/*.ts`] : []);
|
|
1203
|
+
}, [...folders ? [`${path}/*/{actions,queries}/*.ts`] : [], ...registries]);
|
|
807
1204
|
if (mode !== "domain") {
|
|
808
1205
|
return [restricted];
|
|
809
1206
|
}
|
|
810
1207
|
const prefix = { singularize: folder === "components" ? "trailing-s" : "none", separators: [".", "-"] };
|
|
811
1208
|
const naming = folder === "hooks" ? { ...prefix, prefixes: ["use-"] } : prefix;
|
|
812
|
-
const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] })];
|
|
1209
|
+
const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] }, registries)];
|
|
813
1210
|
if (!settings.strict || folder !== "services") {
|
|
814
1211
|
return domain;
|
|
815
1212
|
}
|
|
816
|
-
const strict = presetOverride([`${path}
|
|
1213
|
+
const strict = presetOverride([`${path}/*/**/${presetExtensions}`], {
|
|
817
1214
|
"arch/naming-file": [
|
|
818
1215
|
"error",
|
|
819
1216
|
{
|
|
820
|
-
pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*[.](?:${roles.join("|")})[.]ts$`,
|
|
1217
|
+
pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
|
|
821
1218
|
message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
|
|
822
1219
|
}
|
|
823
1220
|
]
|
|
824
|
-
}, ["**/*-{action,query}.*.ts"]);
|
|
1221
|
+
}, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
|
|
825
1222
|
return [...domain, strict];
|
|
826
1223
|
}),
|
|
827
1224
|
presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
|
|
828
1225
|
presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], { "arch/files-restricted": ["error", { message: "Components use TSX; hooks use .ts files." }] }, ui),
|
|
829
1226
|
presetOverride(presetScopes(web, "**/use-*.{ts,tsx}"), { "arch/files-restricted": ["error", { message: "Hooks belong in src/hooks." }] }, [...presetScopes(web, "hooks/**"), ...ui]),
|
|
830
|
-
presetOverride(
|
|
1227
|
+
presetOverride(presetServiceScopes(project.serviceFolders, web, "**/*.{ts,tsx}"), {
|
|
831
1228
|
"arch/files-restricted": [
|
|
832
1229
|
"error",
|
|
833
1230
|
{
|
|
@@ -837,31 +1234,32 @@ var layoutGroup = (project, settings) => {
|
|
|
837
1234
|
}, [
|
|
838
1235
|
"**/*.{client,server,rsc,utils,constants}.ts",
|
|
839
1236
|
"**/*.rsc.tsx",
|
|
840
|
-
...
|
|
1237
|
+
...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
|
|
841
1238
|
...conceptFiles,
|
|
842
|
-
...project.colocated
|
|
1239
|
+
...project.colocated,
|
|
1240
|
+
...testFiles
|
|
843
1241
|
]),
|
|
844
1242
|
presetOverride(services, {
|
|
845
1243
|
"arch/backend-service-functions": "error",
|
|
846
1244
|
"arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
|
|
847
1245
|
}, [...utilities, ...testFiles, ...conceptFiles]),
|
|
848
1246
|
presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
|
|
849
|
-
...conceptFiles.map((pattern) => presetOverride(
|
|
1247
|
+
...conceptFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
|
|
850
1248
|
"arch/backend-service-functions": "off",
|
|
851
1249
|
"arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
|
|
852
1250
|
}, testFiles)),
|
|
853
|
-
presetOverride([...
|
|
854
|
-
presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, message: operationMessage }] }, testFiles),
|
|
855
|
-
presetOverride(
|
|
856
|
-
presetOverride(
|
|
1251
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, app, "**/*-{action,query}.*.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*-{action,query}.*.rsc.tsx")], { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
1252
|
+
presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
|
|
1253
|
+
presetOverride(presetServiceScopes(project.serviceFolders, serviceRoots, "**/*.ts"), { "arch/files-domain-constants": "error" }, [...utilities, ...testFiles]),
|
|
1254
|
+
presetOverride(presetServiceScopes(project.serviceFolders, web, "**/*.{ts,tsx}"), { "arch/files-domain-constants": ["error", { data: true }] }, [
|
|
857
1255
|
"**/*.constants.ts",
|
|
858
1256
|
...testFiles
|
|
859
1257
|
]),
|
|
860
1258
|
presetOverride(frontendEntries, { "arch/files-domain-constants": ["error", { data: true, services: true }] }, testFiles),
|
|
861
|
-
presetOverride([...
|
|
1259
|
+
presetOverride([...presetServiceScopes(project.serviceFolders, serviceRoots, "**/*-{action,query}.*.ts"), ...folderOperations, ...frontendEntries], { "arch/code-object-params": ["error", { max: 2 }] }, testFiles),
|
|
862
1260
|
presetOverride(frontendEntries, { "arch/code-object-params": ["error", { max: 2, services: true }] }, testFiles),
|
|
863
|
-
presetOverride(
|
|
864
|
-
presetOverride(
|
|
1261
|
+
presetOverride(presetServiceScopes(project.serviceFolders, serviceRoots, "**/*.service.ts"), { "arch/files-exports": ["error", { names: settings.service, stem: "before-first-dot", types: false }] }, testFiles),
|
|
1262
|
+
presetOverride(presetServiceScopes(project.serviceFolders, app.filter((path) => !operations.includes(path)), "**/{actions,queries}/**/*.ts"), {
|
|
865
1263
|
"arch/files-restricted": ["error", { message: "Use domain-action.name.ts or domain-query.name.ts." }]
|
|
866
1264
|
})
|
|
867
1265
|
];
|
|
@@ -878,6 +1276,10 @@ var limitsGroup = (project, settings) => [
|
|
|
878
1276
|
|
|
879
1277
|
// src/preset/groups/naming.ts
|
|
880
1278
|
var namingOperationMessage = "Use domain-action.name.ts or domain-query.name.ts.";
|
|
1279
|
+
var namingFrontendPrefix = { ...presetExportPrefix, scope: "functions", services: true, allow: "^\\*$" };
|
|
1280
|
+
function namingOperationPrefix(role) {
|
|
1281
|
+
return { ...presetExportPrefix, roles: [role], scope: "functions", allow: "^\\*$" };
|
|
1282
|
+
}
|
|
881
1283
|
function namingOperation(role, file) {
|
|
882
1284
|
return { mode: "template", file, export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+", camelCase: true };
|
|
883
1285
|
}
|
|
@@ -885,7 +1287,7 @@ var namingGroup = (project, settings, owner) => {
|
|
|
885
1287
|
const { web, serviceRoots, app, ui, testFiles, components, hooks, operations } = project;
|
|
886
1288
|
const flatApp = app.filter((path) => !operations.includes(path));
|
|
887
1289
|
const folderApp = app.filter((path) => operations.includes(path));
|
|
888
|
-
const frontendEntries = [...
|
|
1290
|
+
const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
|
|
889
1291
|
const ungroupedWeb = web.filter((path) => !project.layouts.some((scope) => scope.path === `${path}/components`));
|
|
890
1292
|
const overrides = [];
|
|
891
1293
|
const { files: filesPattern, exports: exportsPattern, unique } = settings;
|
|
@@ -923,8 +1325,8 @@ var namingGroup = (project, settings, owner) => {
|
|
|
923
1325
|
add("files", [`**/${presetExtensions}`], presetNamingRules);
|
|
924
1326
|
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
1327
|
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",
|
|
927
|
-
add("exports",
|
|
1328
|
+
add("exports", frontendEntries, { "arch/naming-exports": ["error", namingFrontendPrefix] }, testFiles);
|
|
1329
|
+
add("exports", presetServiceScopes(project.serviceFolders, web, "**/*.constants.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "declarations", allow: "^\\*$" }] }, testFiles);
|
|
928
1330
|
add("exports", presetScopes(app, "**/*.utils.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", allow: "^\\*$" }] }, ui);
|
|
929
1331
|
add("files", hooks, {
|
|
930
1332
|
"arch/naming-file": [
|
|
@@ -939,20 +1341,20 @@ var namingGroup = (project, settings, owner) => {
|
|
|
939
1341
|
add("exports", components, { "arch/files-components-only": ["error", { filename: true, types: true, roles: ["illustration"] }] }, [...ui, ...testFiles]);
|
|
940
1342
|
add("folders", presetScopes(ungroupedWeb, "components/**/*.tsx"), { "arch/naming-folder": ["error", { singularize: "trailing-s", separators: ["-"] }] }, [...ui, ...testFiles]);
|
|
941
1343
|
for (const role of ["action", "query"]) {
|
|
942
|
-
const files = [...
|
|
1344
|
+
const files = [...presetServiceScopes(project.serviceFolders, flatApp, `**/*-${role}.*.ts`), ...presetServiceScopes(project.serviceFolders, web, `**/*-${role}.*.rsc.tsx`)];
|
|
943
1345
|
add("files", files, {
|
|
944
1346
|
"arch/naming-file": ["error", { pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`, message: namingOperationMessage }]
|
|
945
|
-
});
|
|
1347
|
+
}, testFiles);
|
|
946
1348
|
add("exports", files, {
|
|
947
1349
|
"arch/naming-exports": [
|
|
948
1350
|
"error",
|
|
949
|
-
|
|
1351
|
+
namingOperationPrefix(role),
|
|
950
1352
|
namingOperation(role, `{domain}-${role}.{name}.ts`)
|
|
951
1353
|
]
|
|
952
|
-
});
|
|
1354
|
+
}, testFiles);
|
|
953
1355
|
}
|
|
954
1356
|
for (const [role, folder, separator] of [["action", "actions", "."], ["query", "queries", "-"]]) {
|
|
955
|
-
const files =
|
|
1357
|
+
const files = presetServiceScopes(project.serviceFolders, folderApp, `*/${folder}/*.ts`);
|
|
956
1358
|
add("files", files, {
|
|
957
1359
|
"arch/naming-file": [
|
|
958
1360
|
"error",
|
|
@@ -961,17 +1363,17 @@ var namingGroup = (project, settings, owner) => {
|
|
|
961
1363
|
message: "Use domain-action.name.ts in actions/ and domain-query-name.ts in queries/."
|
|
962
1364
|
}
|
|
963
1365
|
]
|
|
964
|
-
});
|
|
1366
|
+
}, testFiles);
|
|
965
1367
|
add("exports", files, {
|
|
966
1368
|
"arch/naming-exports": [
|
|
967
1369
|
"error",
|
|
968
1370
|
{ mode: "template", file: `{domain}-${role}${separator}{name}.ts`, export: `make{Domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+" }
|
|
969
1371
|
]
|
|
970
|
-
});
|
|
1372
|
+
}, testFiles);
|
|
971
1373
|
}
|
|
972
1374
|
for (const role of ["action", "query"]) {
|
|
973
1375
|
for (const suffix of ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"]) {
|
|
974
|
-
const files =
|
|
1376
|
+
const files = presetServiceScopes(project.serviceFolders, web, `**/*-${role}.*.${suffix}`);
|
|
975
1377
|
add("files", files, {
|
|
976
1378
|
"arch/naming-file": [
|
|
977
1379
|
"error",
|
|
@@ -980,11 +1382,11 @@ var namingGroup = (project, settings, owner) => {
|
|
|
980
1382
|
message: "Frontend operations retain a client, server or rsc runtime suffix."
|
|
981
1383
|
}
|
|
982
1384
|
]
|
|
983
|
-
});
|
|
984
|
-
add("exports", files, { "arch/naming-exports": ["error", namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] });
|
|
1385
|
+
}, testFiles);
|
|
1386
|
+
add("exports", files, { "arch/naming-exports": ["error", namingOperationPrefix(role), namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] }, testFiles);
|
|
985
1387
|
}
|
|
986
1388
|
}
|
|
987
|
-
add("exports",
|
|
1389
|
+
add("exports", presetServiceScopes(project.serviceFolders, web, "**/*.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
1390
|
if (typeof exportsPattern === "string") {
|
|
989
1391
|
overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
|
|
990
1392
|
}
|
|
@@ -1118,7 +1520,7 @@ var shadcnGroup = (project) => {
|
|
|
1118
1520
|
|
|
1119
1521
|
// src/preset/groups/sql.ts
|
|
1120
1522
|
var sqlGroup = (project, settings) => [
|
|
1121
|
-
presetOverride(
|
|
1523
|
+
presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
|
|
1122
1524
|
"arch/backend-sql-like": [
|
|
1123
1525
|
"error",
|
|
1124
1526
|
{ methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
|
|
@@ -1179,7 +1581,7 @@ var tanstackGroup = (project, settings) => {
|
|
|
1179
1581
|
patterns: project.baseImports
|
|
1180
1582
|
}
|
|
1181
1583
|
]
|
|
1182
|
-
},
|
|
1584
|
+
}, presetServiceScopes(project.serviceFolders, tanstackWeb, "**/*.rsc.{ts,tsx}")),
|
|
1183
1585
|
...presetRuntime(project, settings, "tanstack", "arch/boundaries-tanstack")
|
|
1184
1586
|
];
|
|
1185
1587
|
};
|
|
@@ -1209,8 +1611,12 @@ function optionsPatternsTest(patterns, value) {
|
|
|
1209
1611
|
});
|
|
1210
1612
|
}
|
|
1211
1613
|
function optionsGlob(pattern) {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1614
|
+
const negated = [];
|
|
1615
|
+
const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
|
|
1616
|
+
negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
|
|
1617
|
+
return `\x02${negated.length - 1}\x03`;
|
|
1618
|
+
}).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
|
|
1619
|
+
return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
|
|
1214
1620
|
}
|
|
1215
1621
|
function optionsPatternLabel(pattern) {
|
|
1216
1622
|
return pattern === undefined ? "" : [pattern].flat().filter((source) => source !== "").join(" | ");
|
|
@@ -1260,6 +1666,7 @@ var testsGroup = (project, settings) => {
|
|
|
1260
1666
|
}
|
|
1261
1667
|
return [
|
|
1262
1668
|
presetOverride(project.testFiles, rules),
|
|
1669
|
+
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
1670
|
presetOverride(presetScopes(project.backendRoots, "tests/**/*.ts"), backendRules),
|
|
1264
1671
|
...allow.map((entry) => presetOverride(entry.files, { "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis) }))
|
|
1265
1672
|
];
|
|
@@ -1322,254 +1729,151 @@ var typesGroup = (project, settings) => {
|
|
|
1322
1729
|
};
|
|
1323
1730
|
|
|
1324
1731
|
// 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";
|
|
1732
|
+
import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
1733
|
+
import { posix as posix7, resolve as resolve7 } from "node:path";
|
|
1331
1734
|
|
|
1332
|
-
// src/preset/preset-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1735
|
+
// src/preset/preset-stylesheet.ts
|
|
1736
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
1737
|
+
import { posix as posix5, resolve as resolve5 } from "node:path";
|
|
1738
|
+
function presetTailwindEntry(css) {
|
|
1739
|
+
const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
1740
|
+
return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
|
|
1741
|
+
}
|
|
1742
|
+
function presetStylesheet(root, directory) {
|
|
1743
|
+
const found = [];
|
|
1744
|
+
const queue = [directory];
|
|
1745
|
+
const visited = new Set;
|
|
1746
|
+
while (queue.length > 0) {
|
|
1747
|
+
const folder = queue.shift() ?? directory;
|
|
1748
|
+
const real = realpathSync(resolve5(root, folder));
|
|
1749
|
+
if (visited.has(real)) {
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
visited.add(real);
|
|
1753
|
+
for (const name of readdirSync2(resolve5(root, folder))) {
|
|
1754
|
+
const path = posix5.join(folder, name);
|
|
1755
|
+
const full = resolve5(root, path);
|
|
1756
|
+
const depth = posix5.relative(directory, path).split("/").length;
|
|
1757
|
+
if (presetIsDirectory(full)) {
|
|
1758
|
+
if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
|
|
1759
|
+
queue.push(path);
|
|
1760
|
+
}
|
|
1761
|
+
} else if (name.endsWith(".css") && presetIsFile(full) && presetTailwindEntry(readFileSync3(full, "utf8"))) {
|
|
1762
|
+
found.push(path);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1361
1765
|
}
|
|
1766
|
+
const rank = (path) => {
|
|
1767
|
+
const index = presetStylesheetNames.indexOf(posix5.basename(path, ".css"));
|
|
1768
|
+
return index === -1 ? presetStylesheetNames.length : index;
|
|
1769
|
+
};
|
|
1770
|
+
return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
|
|
1362
1771
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1772
|
+
|
|
1773
|
+
// src/preset/preset-units.ts
|
|
1774
|
+
import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
1775
|
+
import { posix as posix6, resolve as resolve6 } from "node:path";
|
|
1776
|
+
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1777
|
+
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1778
|
+
function presetUnitSource(root, key, kind, source) {
|
|
1779
|
+
if (source !== undefined) {
|
|
1780
|
+
if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
|
|
1781
|
+
throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
|
|
1782
|
+
}
|
|
1783
|
+
return posix6.join(key, source);
|
|
1366
1784
|
}
|
|
1367
|
-
return
|
|
1785
|
+
return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix6.join(key, "src");
|
|
1368
1786
|
}
|
|
1369
|
-
function
|
|
1370
|
-
return
|
|
1787
|
+
function presetSets(folder, name) {
|
|
1788
|
+
return folder[name] !== undefined && folder[name] !== false;
|
|
1371
1789
|
}
|
|
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;
|
|
1790
|
+
function presetKind(key, enabled) {
|
|
1791
|
+
const frameworks = presetFrameworks.filter(enabled);
|
|
1792
|
+
if (frameworks.length > 1) {
|
|
1793
|
+
throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
|
|
1386
1794
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1795
|
+
const [framework] = frameworks;
|
|
1796
|
+
if (frameworks.length > 0 && enabled("backend")) {
|
|
1797
|
+
throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
|
|
1389
1798
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1799
|
+
let kind = "library";
|
|
1800
|
+
if (frameworks.length > 0) {
|
|
1801
|
+
kind = "web";
|
|
1802
|
+
} else if (enabled("backend")) {
|
|
1803
|
+
kind = "backend";
|
|
1804
|
+
} else if (enabled("console")) {
|
|
1805
|
+
kind = "console";
|
|
1392
1806
|
}
|
|
1393
|
-
return
|
|
1807
|
+
return { kind, framework };
|
|
1394
1808
|
}
|
|
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
|
-
}
|
|
1809
|
+
function presetHasManifest(root, key) {
|
|
1810
|
+
if (key === ".") {
|
|
1811
|
+
return false;
|
|
1405
1812
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
function presetMerge(base, top) {
|
|
1409
|
-
if (!(base instanceof Object) || !(top instanceof Object)) {
|
|
1410
|
-
return top === true && base instanceof Object ? base : top;
|
|
1813
|
+
if (!key.endsWith("/*")) {
|
|
1814
|
+
return existsSync2(resolve6(root, key, "package.json"));
|
|
1411
1815
|
}
|
|
1412
|
-
|
|
1816
|
+
const base = resolve6(root, key.slice(0, -2));
|
|
1817
|
+
return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
|
|
1413
1818
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const at = (path) => local === "." ? path : posix3.join(local, path);
|
|
1430
|
-
const exists = (path) => existsSync2(resolve4(root, key, path));
|
|
1431
|
-
const registry = presetRegistryFiles.map(at).find(exists);
|
|
1432
|
-
const services = presetSpecifiers(manifest, at("services"));
|
|
1433
|
-
const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, "Use the service API, not another service’s operations."])));
|
|
1434
|
-
const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.ts$/, "")).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
|
|
1435
|
-
const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
|
|
1436
|
-
const conventions = {
|
|
1437
|
-
[at("services")]: presetDeny({
|
|
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);
|
|
1819
|
+
function presetUnit(root, key, scope) {
|
|
1820
|
+
const { effective } = scope;
|
|
1821
|
+
const wildcard = scope.parent && scope.parent.depth === scope.depth && scope.parent.key.includes("*") ? scope.parent.folder : {};
|
|
1822
|
+
const folder = { ...wildcard, ...scope.folder };
|
|
1823
|
+
const own = presetKindGroups.some((name) => presetSets(folder, name)) || presetHasManifest(root, key);
|
|
1824
|
+
const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
|
|
1825
|
+
const layout = {};
|
|
1826
|
+
const settings = effective.layout;
|
|
1827
|
+
if (settings) {
|
|
1828
|
+
const services2 = settings.services ?? (kind === "console" ? false : "domain");
|
|
1829
|
+
const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
|
|
1830
|
+
const modes = { services: services2, hooks, components: settings.components ?? false };
|
|
1831
|
+
for (const [name, mode] of Object.entries(modes)) {
|
|
1832
|
+
if (mode === "flat" || mode === "domain") {
|
|
1833
|
+
layout[name] = mode;
|
|
1464
1834
|
}
|
|
1465
1835
|
}
|
|
1466
1836
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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)));
|
|
1837
|
+
const source = presetUnitSource(root, key, kind, folder.source);
|
|
1838
|
+
const services = presetServicesFolder(root, source, folder.services, kind === "web" || kind === "backend");
|
|
1839
|
+
return { key, scope, kind, framework, source, services, layout };
|
|
1490
1840
|
}
|
|
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) }));
|
|
1841
|
+
function presetUnits(root, scopes, ignored) {
|
|
1842
|
+
const [rootScope] = scopes;
|
|
1843
|
+
const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
|
|
1844
|
+
const candidates = [];
|
|
1845
|
+
for (const scope of scopes) {
|
|
1846
|
+
const manifest = presetHasManifest(root, scope.key);
|
|
1847
|
+
const own = presetKindGroups.some((name) => presetSets(scope.folder, name)) || manifest;
|
|
1848
|
+
const top = scope.parent?.key === "." && (rootKind === "library" || own);
|
|
1849
|
+
const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
|
|
1850
|
+
const workspace = scope.parent !== null && scope.parent.key !== "." && manifest;
|
|
1851
|
+
if (top || sibling || workspace) {
|
|
1852
|
+
candidates.push(scope);
|
|
1539
1853
|
}
|
|
1540
1854
|
}
|
|
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
|
-
}
|
|
1855
|
+
const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
|
|
1856
|
+
const explicit = new Set(unitScopes.map((scope) => scope.key));
|
|
1857
|
+
const rootSource = presetUnitSource(root, ".", rootKind, rootScope.folder.source);
|
|
1858
|
+
const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
1859
|
+
const rootOwnsSource = rootSource !== "." && !unitScopes.some((scope) => overlaps(scope.key.replace(/\/\*$/, ""), rootSource));
|
|
1860
|
+
const rootUnit = rootKind !== "library" || rootOwnsSource || unitScopes.every((scope) => presetUnit(root, scope.key, scope).kind === "console");
|
|
1861
|
+
return [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
|
|
1862
|
+
if (!scope.key.endsWith("/*")) {
|
|
1863
|
+
return [presetUnit(root, scope.key, scope)];
|
|
1555
1864
|
}
|
|
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);
|
|
1865
|
+
const base = scope.key.slice(0, -2);
|
|
1866
|
+
return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix6.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
|
|
1867
|
+
});
|
|
1562
1868
|
}
|
|
1563
1869
|
|
|
1564
1870
|
// src/preset/preset-folders.ts
|
|
1565
|
-
var presetFrameworks = ["tanstack", "next", "expo"];
|
|
1566
|
-
var presetKindGroups = [...presetFrameworks, "backend", "console"];
|
|
1567
1871
|
function presetNormalizeKey(key) {
|
|
1568
1872
|
const portable = key.replaceAll("\\", "/");
|
|
1569
|
-
if (!portable.trim() ||
|
|
1873
|
+
if (!portable.trim() || posix7.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
|
|
1570
1874
|
throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
|
|
1571
1875
|
}
|
|
1572
|
-
const normalized =
|
|
1876
|
+
const normalized = posix7.normalize(portable).replace(/\/$/, "");
|
|
1573
1877
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
1574
1878
|
throw new PresetError(`Folder keys must stay inside the project: ${key}`);
|
|
1575
1879
|
}
|
|
@@ -1607,7 +1911,14 @@ function presetGroupValue(name, value, inherited, severity) {
|
|
|
1607
1911
|
if (unknown) {
|
|
1608
1912
|
throw new PresetError(`Unknown option \`${name}.${unknown}\`.`);
|
|
1609
1913
|
}
|
|
1610
|
-
|
|
1914
|
+
const plain = (option) => option instanceof Object && !Array.isArray(option);
|
|
1915
|
+
const merged = Object.fromEntries(Object.entries(changes).map(([key, option]) => {
|
|
1916
|
+
const stored = Object(base)[key];
|
|
1917
|
+
const field = presetShorthandOptions.find(([group, setting]) => group === name && setting === key)?.[2];
|
|
1918
|
+
const previous = typeof stored === "string" && field !== undefined && plain(option) ? { [field]: stored } : stored;
|
|
1919
|
+
return [key, plain(previous) && plain(option) ? { ...Object(previous), ...Object(option) } : option];
|
|
1920
|
+
}));
|
|
1921
|
+
return { ...base, ...merged, level: severity ?? base.level };
|
|
1611
1922
|
}
|
|
1612
1923
|
function presetDenyEntries(inherited, changes) {
|
|
1613
1924
|
return Object.fromEntries(Object.entries({ ...inherited, ...changes }).flatMap(([key, message]) => {
|
|
@@ -1653,55 +1964,6 @@ function presetFolder(key, value) {
|
|
|
1653
1964
|
}
|
|
1654
1965
|
return folder;
|
|
1655
1966
|
}
|
|
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
1967
|
function presetScopesTree(root, folders) {
|
|
1706
1968
|
const entries = Object.entries(folders).map(([key, value]) => [presetNormalizeKey(key), value]);
|
|
1707
1969
|
const keys = entries.map(([key]) => key);
|
|
@@ -1711,7 +1973,7 @@ function presetScopesTree(root, folders) {
|
|
|
1711
1973
|
}
|
|
1712
1974
|
for (const key of keys) {
|
|
1713
1975
|
const base = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
1714
|
-
if (!existsSync3(
|
|
1976
|
+
if (!existsSync3(resolve7(root, base))) {
|
|
1715
1977
|
throw new PresetError(`Folder \`${key}\` does not exist.`);
|
|
1716
1978
|
}
|
|
1717
1979
|
}
|
|
@@ -1743,38 +2005,28 @@ function presetScopesTree(root, folders) {
|
|
|
1743
2005
|
return scopes;
|
|
1744
2006
|
}
|
|
1745
2007
|
function presetProject(config) {
|
|
1746
|
-
const given =
|
|
1747
|
-
const root = existsSync3(given) ?
|
|
1748
|
-
const
|
|
2008
|
+
const given = resolve7(config.root ?? process.cwd());
|
|
2009
|
+
const root = existsSync3(given) ? realpathSync2(given) : given;
|
|
2010
|
+
const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
|
|
2011
|
+
const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
|
|
2012
|
+
if (twice) {
|
|
2013
|
+
throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
|
|
2014
|
+
}
|
|
2015
|
+
const folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
|
|
1749
2016
|
const scopes = presetScopesTree(root, folders);
|
|
1750
2017
|
const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
|
|
1751
2018
|
const live = scopes.filter((scope) => !ignored.has(scope.key));
|
|
1752
|
-
const [rootScope] = scopes;
|
|
1753
|
-
const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
|
|
1754
2019
|
const dead = live.find((scope) => scope.parent && ignored.has(scope.parent.key) && scope.parent.depth < scope.depth);
|
|
1755
2020
|
if (dead) {
|
|
1756
2021
|
throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
|
|
1757
2022
|
}
|
|
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
|
-
});
|
|
2023
|
+
const units = presetUnits(root, scopes, ignored);
|
|
1776
2024
|
const unitKeys = new Set(units.map((unit) => unit.scope.key));
|
|
1777
2025
|
const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
|
|
2026
|
+
const servicesKey = live.find((scope) => !unitKeys.has(scope.key) && scope.folder.services !== undefined);
|
|
2027
|
+
if (servicesKey) {
|
|
2028
|
+
throw new PresetError(`Set services on an app or package folder, not on \`${servicesKey.key}\`.`);
|
|
2029
|
+
}
|
|
1778
2030
|
if (misplaced) {
|
|
1779
2031
|
throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
|
|
1780
2032
|
}
|
|
@@ -1791,6 +2043,7 @@ function presetProject(config) {
|
|
|
1791
2043
|
const libraryUnits = of("library");
|
|
1792
2044
|
const packageSources = libraryUnits.map((unit) => unit.source);
|
|
1793
2045
|
const app = [...web, ...backend, ...packageSources];
|
|
2046
|
+
const serviceFolders = Object.fromEntries(units.map((unit) => [unit.source, unit.services]));
|
|
1794
2047
|
const appFiles = presetScopes(app);
|
|
1795
2048
|
const webFiles = presetScopes(web);
|
|
1796
2049
|
const effective = live.map((scope) => scope.effective);
|
|
@@ -1804,9 +2057,9 @@ function presetProject(config) {
|
|
|
1804
2057
|
const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
|
|
1805
2058
|
const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
|
|
1806
2059
|
const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
|
|
1807
|
-
const rpcClients = client === undefined ? webUnits.map((unit) =>
|
|
2060
|
+
const rpcClients = client === undefined ? webUnits.map((unit) => posix7.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
|
|
1808
2061
|
const kitUnits = units.filter((unit) => unit.scope.effective.kit);
|
|
1809
|
-
const uiRoots = [...new Set([...web.map((path) =>
|
|
2062
|
+
const uiRoots = [...new Set([...web.map((path) => posix7.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
|
|
1810
2063
|
const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
|
|
1811
2064
|
const stylesheet = (unit) => {
|
|
1812
2065
|
const tailwind = unit.scope.effective.tailwind;
|
|
@@ -1814,7 +2067,7 @@ function presetProject(config) {
|
|
|
1814
2067
|
return;
|
|
1815
2068
|
}
|
|
1816
2069
|
const css = tailwind.css ?? presetStylesheet(root, unit.key);
|
|
1817
|
-
return css === undefined ? undefined :
|
|
2070
|
+
return css === undefined ? undefined : resolve7(root, css);
|
|
1818
2071
|
};
|
|
1819
2072
|
const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
|
|
1820
2073
|
const css = stylesheet(unit);
|
|
@@ -1848,20 +2101,21 @@ function presetProject(config) {
|
|
|
1848
2101
|
return [key];
|
|
1849
2102
|
}
|
|
1850
2103
|
const base = key.slice(0, -2);
|
|
1851
|
-
return
|
|
2104
|
+
return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
|
|
1852
2105
|
});
|
|
1853
2106
|
return {
|
|
1854
2107
|
root,
|
|
2108
|
+
serviceFolders,
|
|
1855
2109
|
scopes: live,
|
|
1856
2110
|
units,
|
|
1857
2111
|
suffixes,
|
|
1858
2112
|
colocated: listOf(effective.map((groups) => groups.types?.colocated)),
|
|
1859
2113
|
orpc: {
|
|
1860
|
-
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) =>
|
|
2114
|
+
public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix7.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
|
|
1861
2115
|
composers: listOf(orpcScopes.map((settings) => settings.composers)),
|
|
1862
2116
|
client: client ?? ""
|
|
1863
2117
|
},
|
|
1864
|
-
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path:
|
|
2118
|
+
layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix7.join(unit.source, folder), folder, mode }))),
|
|
1865
2119
|
rpcClients,
|
|
1866
2120
|
operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
|
|
1867
2121
|
packages: libraryUnits.map((unit) => unit.key),
|
|
@@ -1884,7 +2138,7 @@ function presetProject(config) {
|
|
|
1884
2138
|
...presetScopes(packageSources, `hooks/**/${presetExtensions}`)
|
|
1885
2139
|
],
|
|
1886
2140
|
backendFiles: presetScopes(backend),
|
|
1887
|
-
services:
|
|
2141
|
+
services: presetServiceScopes(serviceFolders, app),
|
|
1888
2142
|
utilities: ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"],
|
|
1889
2143
|
types: presetScopes(app, "types/**"),
|
|
1890
2144
|
uiRoots,
|
|
@@ -1898,7 +2152,9 @@ function presetProject(config) {
|
|
|
1898
2152
|
publicEntrypoints,
|
|
1899
2153
|
baseImports: presetBaseImports,
|
|
1900
2154
|
css: { ...webCss, ...kitCss },
|
|
1901
|
-
|
|
2155
|
+
registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
|
|
2156
|
+
ignores: skipped.map((key) => `${key}/**`),
|
|
2157
|
+
outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
|
|
1902
2158
|
};
|
|
1903
2159
|
}
|
|
1904
2160
|
|
|
@@ -2048,10 +2304,7 @@ function preset(config) {
|
|
|
2048
2304
|
plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
|
|
2049
2305
|
ignorePatterns: [
|
|
2050
2306
|
"**/node_modules/**",
|
|
2051
|
-
|
|
2052
|
-
"**/build/**",
|
|
2053
|
-
"**/coverage/**",
|
|
2054
|
-
"**/.output/**",
|
|
2307
|
+
...project.outputs,
|
|
2055
2308
|
...project.nextWeb.length > 0 ? ["**/.next/**", "**/next-env.d.ts"] : [],
|
|
2056
2309
|
...project.expoWeb.length > 0 ? ["**/.expo/**", "**/expo-env.d.ts"] : [],
|
|
2057
2310
|
...project.generated,
|