@cplieger/deadset-ts 8.0.0-dev.14 → 8.0.0-dev.16
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/dist/src/alias-chain.js +2 -2
- package/dist/src/analysis.js +43 -38
- package/dist/src/commands/read.js +6 -7
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +4 -2
- package/dist/src/contract/config.generated.js +1 -1
- package/dist/src/contract/contract.generated.js +120 -2
- package/dist/src/dependencies.js +81 -49
- package/dist/src/entries/build-scripts.js +222 -0
- package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
- package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
- package/dist/src/entries/conventions.js +739 -0
- package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
- package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
- package/dist/src/entries/run-directories.js +77 -0
- package/dist/src/entries/sources.js +144 -0
- package/dist/src/entries/static-module.js +669 -0
- package/dist/src/entries/test-runners.js +684 -0
- package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
- package/dist/src/extensions.js +37 -0
- package/dist/src/facts.js +38 -0
- package/dist/src/file-facts.js +1 -1
- package/dist/src/findings/emitters.js +5 -8
- package/dist/src/findings/self-check.js +36 -20
- package/dist/src/findings-pass.js +5 -6
- package/dist/src/framework-lifecycle.js +1 -1
- package/dist/src/fs/manifest.js +15 -20
- package/dist/src/generated-file.js +172 -29
- package/dist/src/graph.js +2 -5
- package/dist/src/index.js +1 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/inventory/commonjs.js +507 -0
- package/dist/src/inventory/enumerate.js +167 -0
- package/dist/src/inventory/syntax.js +236 -0
- package/dist/src/inventory.js +305 -705
- package/dist/src/javascript-file.js +26 -11
- package/dist/src/load/classify.js +28 -9
- package/dist/src/load/diagnostics.js +8 -1
- package/dist/src/load/discover.js +16 -8
- package/dist/src/load/package-scope.js +56 -0
- package/dist/src/load/plan.js +2 -2
- package/dist/src/load/synthesize.js +5 -5
- package/dist/src/load/workspace-manifests.js +1 -1
- package/dist/src/matrix.js +6 -13
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/paths.js +16 -0
- package/dist/src/platform-calls.js +78 -0
- package/dist/src/platform-symbols.js +140 -0
- package/dist/src/position.js +41 -0
- package/dist/src/ref.js +11 -20
- package/dist/src/references.js +281 -50
- package/dist/src/resolve.js +1 -1
- package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
- package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
- package/dist/src/{manifest.js → roots/manifest.js} +4 -4
- package/dist/src/roots/published-reach.js +412 -0
- package/dist/src/roots/root-set.js +489 -0
- package/dist/src/roots/unseen-loads.js +208 -0
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/session.js +3 -0
- package/dist/src/source-files.js +5 -9
- package/dist/src/suppress-inline.js +33 -81
- package/dist/src/suppress.js +30 -32
- package/dist/src/test-support.js +4 -2
- package/dist/src/verbs/analyze.js +13 -1
- package/dist/src/verbs/describe.js +5 -3
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +2 -2
- package/src/analysis.ts +77 -53
- package/src/automatic-types.ts +1 -1
- package/src/commands/read.ts +6 -8
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +5 -1
- package/src/contract/config.generated.ts +1 -1
- package/src/contract/contract.generated.ts +139 -2
- package/src/dependencies.ts +98 -50
- package/src/entries/build-scripts.ts +292 -0
- package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
- package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
- package/src/{conventions.ts → entries/conventions.ts} +347 -522
- package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
- package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
- package/src/entries/run-directories.ts +108 -0
- package/src/entries/sources.ts +266 -0
- package/src/entries/static-module.ts +857 -0
- package/src/entries/test-runners.ts +868 -0
- package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
- package/src/exempt.ts +3 -2
- package/src/extensions.ts +45 -0
- package/src/facts.ts +44 -0
- package/src/file-facts.ts +2 -2
- package/src/findings/emitters.ts +14 -10
- package/src/findings/self-check.ts +41 -22
- package/src/findings/visibility-narrowing.ts +1 -1
- package/src/findings-pass.ts +5 -12
- package/src/framework-lifecycle.ts +1 -1
- package/src/fs/manifest.ts +25 -21
- package/src/generated-file.ts +240 -36
- package/src/graph.ts +3 -6
- package/src/index.ts +1 -1
- package/src/injection-container.ts +1 -1
- package/src/inventory/commonjs.ts +696 -0
- package/src/inventory/enumerate.ts +203 -0
- package/src/inventory/syntax.ts +311 -0
- package/src/inventory.ts +424 -878
- package/src/javascript-file.ts +29 -12
- package/src/load/classify.ts +49 -13
- package/src/load/diagnostics.ts +11 -1
- package/src/load/discover.ts +28 -12
- package/src/load/package-scope.ts +74 -0
- package/src/load/plan.ts +11 -2
- package/src/load/synthesize.ts +6 -6
- package/src/load/test-globals.ts +1 -1
- package/src/load/workspace-manifests.ts +1 -1
- package/src/load/workspace-run.ts +1 -1
- package/src/matrix.ts +7 -14
- package/src/outside-tests.ts +1 -1
- package/src/paths.ts +18 -0
- package/src/platform-calls.ts +119 -0
- package/src/platform-symbols.ts +205 -0
- package/src/position.ts +42 -0
- package/src/ref.ts +11 -20
- package/src/references.ts +383 -73
- package/src/report.ts +1 -1
- package/src/resolve.ts +1 -1
- package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
- package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
- package/src/{manifest.ts → roots/manifest.ts} +5 -5
- package/src/roots/published-reach.ts +481 -0
- package/src/{roots.ts → roots/root-set.ts} +203 -630
- package/src/roots/unseen-loads.ts +307 -0
- package/src/serialization-contract.ts +1 -1
- package/src/session.ts +17 -1
- package/src/source-files.ts +5 -10
- package/src/suppress-inline.ts +53 -89
- package/src/suppress.ts +48 -33
- package/src/test-support.ts +8 -3
- package/src/verbs/analyze.ts +17 -4
- package/src/verbs/describe.ts +5 -2
- package/src/verbs/explain.ts +1 -1
- package/dist/src/build-scripts.js +0 -418
- package/dist/src/conventions.js +0 -907
- package/dist/src/entry-points.js +0 -513
- package/dist/src/roots.js +0 -865
- package/dist/src/unseen-loads.js +0 -100
- package/src/build-scripts.ts +0 -536
- package/src/entry-points.ts +0 -661
- package/src/unseen-loads.ts +0 -134
package/src/dependencies.ts
CHANGED
|
@@ -31,17 +31,18 @@ import {
|
|
|
31
31
|
} from "@typescript/native/ast";
|
|
32
32
|
import { automaticTypes, type AutomaticTypes } from "./automatic-types.ts";
|
|
33
33
|
import {
|
|
34
|
-
configurationFiles,
|
|
35
34
|
declaredIn as declaredBeside,
|
|
36
35
|
dependenciesNamed,
|
|
37
36
|
moduleStrings,
|
|
38
37
|
outsideChain,
|
|
38
|
+
type ConfigurationFiles,
|
|
39
39
|
type OutsideModule,
|
|
40
40
|
type ParseFile,
|
|
41
|
-
} from "./configuration-files.ts";
|
|
41
|
+
} from "./entries/configuration-files.ts";
|
|
42
42
|
import type { FindingPosition } from "./finding.ts";
|
|
43
43
|
import type { Host } from "./host.ts";
|
|
44
|
-
import { nodeKey,
|
|
44
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
45
|
+
import { packageScope } from "./load/package-scope.ts";
|
|
45
46
|
import {
|
|
46
47
|
ancestors,
|
|
47
48
|
dirnamePath,
|
|
@@ -54,8 +55,11 @@ import type { DependencySection, Module } from "./ref.ts";
|
|
|
54
55
|
import { UNANSWERED } from "./query.ts";
|
|
55
56
|
import type { Handle, ProjectView } from "./session.ts";
|
|
56
57
|
import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
|
|
57
|
-
import { manifestCommands, nodemonTexts } from "./manifest.ts";
|
|
58
|
-
import {
|
|
58
|
+
import { manifestCommands, nodemonTexts } from "./roots/manifest.ts";
|
|
59
|
+
import { writtenName } from "./platform-calls.ts";
|
|
60
|
+
import { callsPlatform } from "./platform-symbols.ts";
|
|
61
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
62
|
+
import type { Workflows } from "./entries/workflow-steps.ts";
|
|
59
63
|
import {
|
|
60
64
|
pathsNamed,
|
|
61
65
|
preloadedPackages,
|
|
@@ -354,8 +358,9 @@ function bindingsOf(node: Node | undefined): readonly Texted[] {
|
|
|
354
358
|
* Every package one project needs, and which declaration each use is written in.
|
|
355
359
|
* One local walk and one batched resolution per file: a use of an import binding is
|
|
356
360
|
* an identifier resolving to the binding's own symbol, so a local declaration that
|
|
357
|
-
* shadows the name is not one. A string of a configuration file
|
|
358
|
-
* the manifest beside the file
|
|
361
|
+
* shadows the name is not one. A string of a configuration file, as the entry sources
|
|
362
|
+
* read them in `configuration`, uses each dependency the manifest beside the file
|
|
363
|
+
* declares that it spells.
|
|
359
364
|
*/
|
|
360
365
|
export function projectNeeds<Brand>(
|
|
361
366
|
project: ProjectView<Brand>,
|
|
@@ -363,6 +368,8 @@ export function projectNeeds<Brand>(
|
|
|
363
368
|
host: Host,
|
|
364
369
|
targetRoot: string,
|
|
365
370
|
parse: ParseFile,
|
|
371
|
+
configuration: ConfigurationFiles,
|
|
372
|
+
workflows: Workflows,
|
|
366
373
|
): ProjectNeeds {
|
|
367
374
|
const programPackages = new Set<string>();
|
|
368
375
|
for (const name of project.program.getSourceFileNames()) {
|
|
@@ -416,7 +423,6 @@ export function projectNeeds<Brand>(
|
|
|
416
423
|
return names;
|
|
417
424
|
};
|
|
418
425
|
const files = sourceFilesOf(project, targetRoot);
|
|
419
|
-
const configuration = configurationFiles(host, targetRoot, files, parse);
|
|
420
426
|
for (const { file, manifestDir } of configuration.modules) {
|
|
421
427
|
const names = declaredAt(joinPath(manifestDir, MANIFEST));
|
|
422
428
|
for (const { node, text } of moduleStrings(file)) {
|
|
@@ -436,7 +442,7 @@ export function projectNeeds<Brand>(
|
|
|
436
442
|
targetRoot,
|
|
437
443
|
files,
|
|
438
444
|
parse,
|
|
439
|
-
scriptRunFiles(host, targetRoot, files),
|
|
445
|
+
scriptRunFiles(host, targetRoot, files, workflows),
|
|
440
446
|
);
|
|
441
447
|
for (const { file, manifestDirs } of [...configuration.outside, ...ranFiles]) {
|
|
442
448
|
for (const manifestDir of manifestDirs) {
|
|
@@ -459,6 +465,7 @@ function scriptRunFiles(
|
|
|
459
465
|
host: Host,
|
|
460
466
|
targetRoot: string,
|
|
461
467
|
files: SourceFiles,
|
|
468
|
+
workflows: Workflows,
|
|
462
469
|
): readonly { readonly path: string; readonly manifestDir: string }[] {
|
|
463
470
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
464
471
|
const dirs = new Set<string>([root]);
|
|
@@ -471,12 +478,7 @@ function scriptRunFiles(
|
|
|
471
478
|
}
|
|
472
479
|
}
|
|
473
480
|
const named = new Map<string, readonly string[]>([
|
|
474
|
-
[
|
|
475
|
-
root,
|
|
476
|
-
workflowReadings(host, root)
|
|
477
|
-
.flatMap(pathsNamed)
|
|
478
|
-
.map((one) => one.path),
|
|
479
|
-
],
|
|
481
|
+
[root, workflows.readings.flatMap(pathsNamed).map((one) => one.path)],
|
|
480
482
|
]);
|
|
481
483
|
for (const dir of dirs) {
|
|
482
484
|
const read = readDocument(host, joinPath(dir, MANIFEST));
|
|
@@ -521,20 +523,9 @@ function manifestUses(host: Host, dir: string, manifest: Record<string, unknown>
|
|
|
521
523
|
}
|
|
522
524
|
|
|
523
525
|
/** What the target root's workflow steps, git hooks and recipes use, held with their files' whole text. */
|
|
524
|
-
function workflowUses(
|
|
525
|
-
const uses = usesOf(
|
|
526
|
-
return { ...uses, held: [uses.held, ...
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* What the target root's workflow steps, git hooks, hook managers and recipes run, each held
|
|
531
|
-
* with the directory the file writes for it.
|
|
532
|
-
*/
|
|
533
|
-
function workflowReadings(host: Host, targetRoot: string): readonly CommandReading[] {
|
|
534
|
-
return workflowCommands(host, targetRoot).map(({ text, dir, writtenDirectory }) => {
|
|
535
|
-
const reading = readCommands(text, dir);
|
|
536
|
-
return { ...reading, held: [reading.held, writtenDirectory ?? ""].join("\n") };
|
|
537
|
-
});
|
|
526
|
+
function workflowUses(workflows: Workflows): CommandUses {
|
|
527
|
+
const uses = usesOf(workflows.readings);
|
|
528
|
+
return { ...uses, held: [uses.held, ...workflows.texts].join("\n") };
|
|
538
529
|
}
|
|
539
530
|
|
|
540
531
|
/** What some command strings say about the packages they use. */
|
|
@@ -660,8 +651,16 @@ function fileNeeds<Brand>(
|
|
|
660
651
|
if (names.size > 0 || automatic.globals.size > 0) {
|
|
661
652
|
visit(file);
|
|
662
653
|
}
|
|
654
|
+
const loads = runtimeLoads(file);
|
|
655
|
+
const callees = loads.flatMap((load) => load.callees);
|
|
663
656
|
|
|
664
|
-
const asked = [
|
|
657
|
+
const asked = [
|
|
658
|
+
...specifiers,
|
|
659
|
+
...bindings.map(({ node }) => node),
|
|
660
|
+
...candidates,
|
|
661
|
+
...globals,
|
|
662
|
+
...callees,
|
|
663
|
+
];
|
|
665
664
|
const handles: Handle<Brand>[] = asked.map((node) => project.handle(node));
|
|
666
665
|
const resolved = project.symbolsAt(handles);
|
|
667
666
|
|
|
@@ -737,35 +736,83 @@ function fileNeeds<Brand>(
|
|
|
737
736
|
for (const reference of file.typeReferenceDirectives) {
|
|
738
737
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
739
738
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
739
|
+
// A callee the checker leaves unanswered may be a require, so its call holds the
|
|
740
|
+
// dependency it names by live code, as program.33 holds one.
|
|
741
|
+
const calleesAt = unresolvedAt + globals.length;
|
|
742
|
+
const decided = new Map<string, boolean>();
|
|
743
|
+
const requires = new Set<Identifier>();
|
|
744
|
+
const unansweredCallees = new Set<Identifier>();
|
|
745
|
+
callees.forEach((callee, index) => {
|
|
746
|
+
const symbol = resolved[calleesAt + index];
|
|
747
|
+
if (symbol === UNANSWERED) {
|
|
748
|
+
unansweredCallees.add(callee);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
// The decision reads the callee's symbol and its written name, and nothing else of it.
|
|
752
|
+
const key = symbol === undefined ? undefined : `${symbol.id}\u0000${callee.text}`;
|
|
753
|
+
const answer =
|
|
754
|
+
(key === undefined ? undefined : decided.get(key)) ??
|
|
755
|
+
callsPlatform(project, "require", callee, symbol, () => undefined);
|
|
756
|
+
if (key !== undefined) {
|
|
757
|
+
decided.set(key, answer);
|
|
758
|
+
}
|
|
759
|
+
if (answer) {
|
|
760
|
+
requires.add(callee);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
for (const { specifier, named, callees: through } of loads) {
|
|
764
|
+
if (through.length === 0 || through.some((one) => requires.has(one))) {
|
|
743
765
|
use(ownersOf(file, held, specifier), [named]);
|
|
766
|
+
} else if (through.some((one) => unansweredCallees.has(one))) {
|
|
767
|
+
use([PROJECT_OWNER], [named]);
|
|
744
768
|
}
|
|
745
769
|
}
|
|
746
770
|
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
747
771
|
}
|
|
748
772
|
|
|
749
773
|
/**
|
|
750
|
-
*
|
|
751
|
-
*
|
|
774
|
+
* A call of one file that may load or resolve a package at run time: the literal specifier
|
|
775
|
+
* naming it, and the names a `require` would be written with, of which one has to be a
|
|
776
|
+
* `require` for the call to need the package; none for a use where it stands.
|
|
752
777
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
778
|
+
interface RuntimeLoad {
|
|
779
|
+
readonly specifier: Texted;
|
|
780
|
+
readonly named: string;
|
|
781
|
+
readonly callees: readonly Identifier[];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* The calls one file may load or resolve a package with at run time, each of whose first
|
|
786
|
+
* argument is literal text naming a package: a `require()` call, which the compiler lists among
|
|
787
|
+
* a file's imports only in a JavaScript file and only through the bare name; a `.resolve` of
|
|
788
|
+
* another name, which needs it where that name is a `require`; and `import.meta.resolve` or
|
|
789
|
+
* a `.resolve` of the name `require`, whatever it names, a use where it stands.
|
|
790
|
+
*/
|
|
791
|
+
function runtimeLoads(file: SourceFile): readonly RuntimeLoad[] {
|
|
792
|
+
const found: RuntimeLoad[] = [];
|
|
758
793
|
const visit = (node: Node): void => {
|
|
759
|
-
if (isCallExpression(node)
|
|
760
|
-
const callee = node.expression;
|
|
794
|
+
if (isCallExpression(node)) {
|
|
761
795
|
const [argument] = node.arguments;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
callee
|
|
765
|
-
|
|
766
|
-
(
|
|
767
|
-
|
|
768
|
-
|
|
796
|
+
if (argument !== undefined && isLiteralText(argument)) {
|
|
797
|
+
const named = packageOfSpecifier(argument.text);
|
|
798
|
+
const callee = parenthesesRemoved(node.expression);
|
|
799
|
+
const receiver =
|
|
800
|
+
isPropertyAccessExpression(callee) && callee.name.text === "resolve"
|
|
801
|
+
? parenthesesRemoved(callee.expression)
|
|
802
|
+
: undefined;
|
|
803
|
+
const stands =
|
|
804
|
+
receiver !== undefined &&
|
|
805
|
+
((isMetaProperty(receiver) && receiver.name.text === "meta") ||
|
|
806
|
+
(isIdentifier(receiver) && receiver.text === "require"));
|
|
807
|
+
const callees = stands
|
|
808
|
+
? []
|
|
809
|
+
: [
|
|
810
|
+
writtenName(callee),
|
|
811
|
+
receiver === undefined ? undefined : writtenName(receiver),
|
|
812
|
+
].filter((one) => one !== undefined);
|
|
813
|
+
if (named !== undefined && (stands || callees.length > 0)) {
|
|
814
|
+
found.push({ specifier: argument, named, callees });
|
|
815
|
+
}
|
|
769
816
|
}
|
|
770
817
|
}
|
|
771
818
|
node.forEachChild(visit);
|
|
@@ -988,6 +1035,7 @@ function addNeededPeers(
|
|
|
988
1035
|
export function dependenciesOf(
|
|
989
1036
|
host: Host,
|
|
990
1037
|
targetRoot: string,
|
|
1038
|
+
workflows: Workflows,
|
|
991
1039
|
perProject: readonly ProjectNeeds[],
|
|
992
1040
|
candidates: readonly string[],
|
|
993
1041
|
members: readonly string[] = [],
|
|
@@ -1010,7 +1058,7 @@ export function dependenciesOf(
|
|
|
1010
1058
|
unbuilt.add(path);
|
|
1011
1059
|
}
|
|
1012
1060
|
}
|
|
1013
|
-
const workflow = workflowUses(
|
|
1061
|
+
const workflow = workflowUses(workflows);
|
|
1014
1062
|
const declared: DeclaredDependency[] = [];
|
|
1015
1063
|
const ran = new Set<string>();
|
|
1016
1064
|
const peers = new Map<string, readonly string[]>();
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a bundler's JavaScript API builds from, read from the build scripts a
|
|
3
|
+
* manifest's scripts run: the strings an options object passes as the bundler's entries.
|
|
4
|
+
* A script is read without running it, so a value that is not written out, a computed
|
|
5
|
+
* path or an options object built at run time among them, names nothing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isArrowFunction,
|
|
10
|
+
isCallExpression,
|
|
11
|
+
isConstructorDeclaration,
|
|
12
|
+
isFunctionDeclaration,
|
|
13
|
+
isFunctionExpression,
|
|
14
|
+
isGetAccessorDeclaration,
|
|
15
|
+
isIdentifier,
|
|
16
|
+
isMethodDeclaration,
|
|
17
|
+
isNewExpression,
|
|
18
|
+
isSetAccessorDeclaration,
|
|
19
|
+
isVariableDeclaration,
|
|
20
|
+
type CallExpression,
|
|
21
|
+
type Node,
|
|
22
|
+
type SourceFile,
|
|
23
|
+
} from "@typescript/native/ast";
|
|
24
|
+
import { declarationOf, membersNamed, staticModule, type Value } from "./static-module.ts";
|
|
25
|
+
import { filesNamedBy, type ParseFile } from "./configuration-files.ts";
|
|
26
|
+
import type { Host } from "../host.ts";
|
|
27
|
+
import { ancestors, dirnamePath, relativePath, resolvePath } from "../paths.ts";
|
|
28
|
+
import type { SourceFiles } from "../source-files.ts";
|
|
29
|
+
import { bytewise } from "../text/compare.ts";
|
|
30
|
+
import { unwrapped } from "../unwrap.ts";
|
|
31
|
+
import { holdsManifest, inInstalledPackages, manifestAt } from "../fs/manifest.ts";
|
|
32
|
+
import { globExpression } from "../text/glob.ts";
|
|
33
|
+
import { pathsNamed, readCommands } from "../commands/read.ts";
|
|
34
|
+
import { manifestCommands } from "../roots/manifest.ts";
|
|
35
|
+
import { extensionOf, MODULE_EXTENSIONS } from "../extensions.ts";
|
|
36
|
+
|
|
37
|
+
/** One bundler API: the module, the functions it exports that build, and the option naming entries. */
|
|
38
|
+
interface BundlerAPI {
|
|
39
|
+
readonly module: string;
|
|
40
|
+
/** The exported names of the building functions; `default` is the module's own value called. */
|
|
41
|
+
readonly calls: readonly string[];
|
|
42
|
+
/** The option whose strings name the entries, read as `entries` states. */
|
|
43
|
+
readonly option: string;
|
|
44
|
+
/** The option that moves the directory the entries are read against. */
|
|
45
|
+
readonly base: string;
|
|
46
|
+
/** Whether an entry string is a path, glob characters matching, or a module request, only a relative one naming a file. */
|
|
47
|
+
readonly paths: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The bundlers whose build call names its entries: esbuild resolves `entryPoints` against
|
|
52
|
+
* `absWorkingDir` (https://esbuild.github.io/api/#working-directory), webpack `entry`
|
|
53
|
+
* against `context` (https://webpack.js.org/configuration/entry-context/), and Rollup
|
|
54
|
+
* `input` against the working directory, each the manifest's directory when its script runs.
|
|
55
|
+
*/
|
|
56
|
+
const BUNDLERS: readonly BundlerAPI[] = [
|
|
57
|
+
{
|
|
58
|
+
module: "esbuild",
|
|
59
|
+
calls: ["build", "buildSync", "context"],
|
|
60
|
+
option: "entryPoints",
|
|
61
|
+
base: "absWorkingDir",
|
|
62
|
+
paths: true,
|
|
63
|
+
},
|
|
64
|
+
{ module: "webpack", calls: ["default"], option: "entry", base: "context", paths: false },
|
|
65
|
+
{ module: "rollup", calls: ["rollup", "watch"], option: "input", base: "", paths: true },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** One own file a build script's bundler call names as an entry. */
|
|
69
|
+
interface BuildEntry {
|
|
70
|
+
readonly file: SourceFile;
|
|
71
|
+
/** The string that named it. */
|
|
72
|
+
readonly source: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One path an entry value writes. */
|
|
76
|
+
interface EntryPath {
|
|
77
|
+
readonly path: string;
|
|
78
|
+
/** What the script writes for it. */
|
|
79
|
+
readonly text: string;
|
|
80
|
+
/** Whether it is written as neither a relative nor an absolute path. */
|
|
81
|
+
readonly request: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The one value an object writes last for a key, undefined where it writes none or may hide it. */
|
|
85
|
+
function lastMember(object: Value, key: string): Value | undefined {
|
|
86
|
+
return membersNamed(object, key)?.at(-1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The path one entry value writes, read against `dir`: a literal, or a join of the
|
|
91
|
+
* script's own directory and literals, read against that directory.
|
|
92
|
+
*/
|
|
93
|
+
function pathOf(value: Value, dir: string, scriptDir: string): EntryPath | undefined {
|
|
94
|
+
if (value.kind === "text") {
|
|
95
|
+
const { text } = value;
|
|
96
|
+
const request = !text.startsWith("./") && !text.startsWith("../") && !text.startsWith("/");
|
|
97
|
+
return { path: resolvePath(dir, text), text, request };
|
|
98
|
+
}
|
|
99
|
+
if (value.kind === "directory") {
|
|
100
|
+
const joined = value.segments.join("/");
|
|
101
|
+
return { path: resolvePath(scriptDir, joined), text: joined, request: false };
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The entry values one entries option writes: a value, an array of them, an object of them. */
|
|
107
|
+
function entryValues(value: Value): readonly Value[] {
|
|
108
|
+
if (value.kind === "array") {
|
|
109
|
+
return value.elements.flatMap(entryValues);
|
|
110
|
+
}
|
|
111
|
+
if (value.kind === "object") {
|
|
112
|
+
const imported = lastMember(value, "import") ?? lastMember(value, "in");
|
|
113
|
+
if (imported !== undefined) {
|
|
114
|
+
return entryValues(imported);
|
|
115
|
+
}
|
|
116
|
+
return value.members.flatMap((member) => entryValues(member.value));
|
|
117
|
+
}
|
|
118
|
+
return [value];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The own files one entry string names, read as `api` reads it. */
|
|
122
|
+
function entriesNamed(
|
|
123
|
+
files: SourceFiles,
|
|
124
|
+
api: BundlerAPI,
|
|
125
|
+
entry: EntryPath,
|
|
126
|
+
): readonly SourceFile[] {
|
|
127
|
+
if (!api.paths && entry.request) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (api.paths && /[*?[{]/u.test(entry.text)) {
|
|
131
|
+
const expression = globExpression(entry.path);
|
|
132
|
+
return [...files.byName.values()].filter((file) => expression.test(file.fileName));
|
|
133
|
+
}
|
|
134
|
+
const dir = dirnamePath(entry.path);
|
|
135
|
+
return filesNamedBy(files, dir, entry.path.slice(dir.length + 1));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether one node is a function, whose body runs only where something calls it. */
|
|
139
|
+
function isFunction(node: Node): boolean {
|
|
140
|
+
return (
|
|
141
|
+
isFunctionDeclaration(node) ||
|
|
142
|
+
isFunctionExpression(node) ||
|
|
143
|
+
isArrowFunction(node) ||
|
|
144
|
+
isMethodDeclaration(node) ||
|
|
145
|
+
isConstructorDeclaration(node) ||
|
|
146
|
+
isGetAccessorDeclaration(node) ||
|
|
147
|
+
isSetAccessorDeclaration(node)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The calls one build script runs: those of its top level, and those of each function a
|
|
153
|
+
* call it runs calls or passes on, as a callee, an argument or a function called where it
|
|
154
|
+
* is written, a name read through to the function it declares.
|
|
155
|
+
*/
|
|
156
|
+
function runCalls(file: SourceFile, each: (call: CallExpression) => void): void {
|
|
157
|
+
const ran = new Set<Node>();
|
|
158
|
+
const run = (fn: Node): void => {
|
|
159
|
+
if (!ran.has(fn)) {
|
|
160
|
+
ran.add(fn);
|
|
161
|
+
fn.forEachChild(visit);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const runs = (node: Node): void => {
|
|
165
|
+
const at = unwrapped(node);
|
|
166
|
+
const declared = isIdentifier(at) ? declarationOf(at) : undefined;
|
|
167
|
+
const fn =
|
|
168
|
+
declared !== undefined && isVariableDeclaration(declared) && declared.initializer
|
|
169
|
+
? unwrapped(declared.initializer)
|
|
170
|
+
: (declared ?? at);
|
|
171
|
+
if (isFunction(fn)) {
|
|
172
|
+
run(fn);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const visit = (node: Node): void => {
|
|
176
|
+
if (isFunction(node)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
180
|
+
if (isCallExpression(node)) {
|
|
181
|
+
each(node);
|
|
182
|
+
}
|
|
183
|
+
runs(node.expression);
|
|
184
|
+
for (const argument of node.arguments ?? []) {
|
|
185
|
+
runs(argument);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
node.forEachChild(visit);
|
|
189
|
+
};
|
|
190
|
+
file.forEachChild(visit);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The entries every bundler call one build script runs names, read against `dir`. */
|
|
194
|
+
function scriptEntries(file: SourceFile, dir: string, files: SourceFiles): readonly BuildEntry[] {
|
|
195
|
+
const module = staticModule(file);
|
|
196
|
+
const building = new Map<CallExpression, BundlerAPI>();
|
|
197
|
+
for (const api of BUNDLERS) {
|
|
198
|
+
for (const one of module.calls(api.module)) {
|
|
199
|
+
if (api.calls.includes(one.export)) {
|
|
200
|
+
building.set(one.call, api);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const scriptDir = dirnamePath(file.fileName);
|
|
205
|
+
const found: BuildEntry[] = [];
|
|
206
|
+
runCalls(file, (call) => {
|
|
207
|
+
const api = building.get(call);
|
|
208
|
+
if (api === undefined) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const options of module.argument(call)) {
|
|
212
|
+
const entries = lastMember(options, api.option);
|
|
213
|
+
const base = api.base === "" ? undefined : lastMember(options, api.base);
|
|
214
|
+
const against = base === undefined ? { path: dir } : pathOf(base, dir, scriptDir);
|
|
215
|
+
if (entries === undefined || against === undefined) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
for (const value of entryValues(entries)) {
|
|
219
|
+
const named = pathOf(value, against.path, scriptDir);
|
|
220
|
+
if (named === undefined) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const entry of entriesNamed(files, api, named)) {
|
|
224
|
+
found.push({ file: entry, source: named.text });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return found;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The modules one manifest's command strings run or name, by absolute path, each below the target root. */
|
|
233
|
+
function scriptModules(host: Host, root: string, dir: string): readonly string[] {
|
|
234
|
+
const manifest = manifestAt(host, dir);
|
|
235
|
+
const found = new Set<string>();
|
|
236
|
+
for (const { command } of manifest === undefined ? [] : manifestCommands(host, dir, manifest)) {
|
|
237
|
+
for (const { path } of pathsNamed(readCommands(command, dir))) {
|
|
238
|
+
if (
|
|
239
|
+
extensionOf(path, MODULE_EXTENSIONS) !== undefined &&
|
|
240
|
+
relativePath(root, path) !== undefined &&
|
|
241
|
+
!inInstalledPackages(path) &&
|
|
242
|
+
host.kindOf(path) === "file"
|
|
243
|
+
) {
|
|
244
|
+
found.add(path);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...found].sort(bytewise);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Every own file of one project a build script names as a bundler's entry: a module a
|
|
253
|
+
* script of a manifest at or above an own file runs, below the target root, read from
|
|
254
|
+
* the program where it holds the module and parsed alone otherwise, where a parser is given.
|
|
255
|
+
*/
|
|
256
|
+
export function buildScriptEntries(
|
|
257
|
+
host: Host,
|
|
258
|
+
targetRoot: string,
|
|
259
|
+
files: SourceFiles,
|
|
260
|
+
parse: ParseFile | undefined,
|
|
261
|
+
): readonly BuildEntry[] {
|
|
262
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
263
|
+
const dirs = new Set<string>([root]);
|
|
264
|
+
for (const file of files.byName.values()) {
|
|
265
|
+
for (const dir of ancestors(dirnamePath(file.fileName), root)) {
|
|
266
|
+
if (dirs.has(dir)) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
dirs.add(dir);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const found: BuildEntry[] = [];
|
|
273
|
+
for (const dir of [...dirs].sort(bytewise)) {
|
|
274
|
+
if (!holdsManifest(host, dir)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const path of scriptModules(host, root, dir)) {
|
|
278
|
+
let script = files.byName.get(path);
|
|
279
|
+
if (script === undefined && parse !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
script = parse(path, host.readFile(path));
|
|
282
|
+
} catch {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (script !== undefined) {
|
|
287
|
+
found.push(...scriptEntries(script, dir, files));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return found;
|
|
292
|
+
}
|