semantifold 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/bin/semantifold.js +6 -0
- package/build/bin/semantifold.d.ts +3 -0
- package/build/bin/semantifold.d.ts.map +1 -0
- package/build/bin/semantifold.js +5 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -1
- package/build/src/artifact-path.d.ts +16 -0
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +29 -3
- package/build/src/backends/c-check.d.ts +7 -0
- package/build/src/backends/c-check.d.ts.map +1 -0
- package/build/src/backends/c-check.js +65 -0
- package/build/src/backends/check-plan.d.ts +96 -0
- package/build/src/backends/check-plan.d.ts.map +1 -0
- package/build/src/backends/check-plan.js +135 -0
- package/build/src/backends/cpp-check.d.ts +7 -0
- package/build/src/backends/cpp-check.d.ts.map +1 -0
- package/build/src/backends/cpp-check.js +59 -0
- package/build/src/backends/csharp-check.d.ts +7 -0
- package/build/src/backends/csharp-check.d.ts.map +1 -0
- package/build/src/backends/csharp-check.js +125 -0
- package/build/src/backends/csharp.d.ts +1 -0
- package/build/src/backends/csharp.d.ts.map +1 -1
- package/build/src/backends/csharp.js +5 -4
- package/build/src/backends/dart-check.d.ts +7 -0
- package/build/src/backends/dart-check.d.ts.map +1 -0
- package/build/src/backends/dart-check.js +103 -0
- package/build/src/backends/dart.d.ts +2 -0
- package/build/src/backends/dart.d.ts.map +1 -1
- package/build/src/backends/dart.js +5 -5
- package/build/src/backends/go-check.d.ts +7 -0
- package/build/src/backends/go-check.d.ts.map +1 -0
- package/build/src/backends/go-check.js +77 -0
- package/build/src/backends/go.d.ts +1 -0
- package/build/src/backends/go.d.ts.map +1 -1
- package/build/src/backends/go.js +3 -3
- package/build/src/backends/java-check.d.ts +21 -0
- package/build/src/backends/java-check.d.ts.map +1 -0
- package/build/src/backends/java-check.js +73 -0
- package/build/src/backends/javascript-check.d.ts +7 -0
- package/build/src/backends/javascript-check.d.ts.map +1 -0
- package/build/src/backends/javascript-check.js +29 -0
- package/build/src/backends/kotlin-check.d.ts +7 -0
- package/build/src/backends/kotlin-check.d.ts.map +1 -0
- package/build/src/backends/kotlin-check.js +31 -0
- package/build/src/backends/php-check.d.ts +7 -0
- package/build/src/backends/php-check.d.ts.map +1 -0
- package/build/src/backends/php-check.js +21 -0
- package/build/src/backends/program.d.ts +7 -0
- package/build/src/backends/program.d.ts.map +1 -1
- package/build/src/backends/program.js +16 -1
- package/build/src/backends/python-check.d.ts +7 -0
- package/build/src/backends/python-check.d.ts.map +1 -0
- package/build/src/backends/python-check.js +28 -0
- package/build/src/backends/ruby-check.d.ts +7 -0
- package/build/src/backends/ruby-check.d.ts.map +1 -0
- package/build/src/backends/ruby-check.js +21 -0
- package/build/src/backends/rust-check.d.ts +7 -0
- package/build/src/backends/rust-check.d.ts.map +1 -0
- package/build/src/backends/rust-check.js +65 -0
- package/build/src/backends/swift-check.d.ts +7 -0
- package/build/src/backends/swift-check.d.ts.map +1 -0
- package/build/src/backends/swift-check.js +77 -0
- package/build/src/backends/typescript-check.d.ts +7 -0
- package/build/src/backends/typescript-check.d.ts.map +1 -0
- package/build/src/backends/typescript-check.js +33 -0
- package/build/src/backends/zig-check.d.ts +7 -0
- package/build/src/backends/zig-check.d.ts.map +1 -0
- package/build/src/backends/zig-check.js +70 -0
- package/build/src/diagnostic.d.ts +13 -1
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +14 -2
- package/build/src/frontends/program.d.ts +6 -0
- package/build/src/frontends/program.d.ts.map +1 -1
- package/build/src/frontends/program.js +9 -1
- package/build/src/language-registry.d.ts +54 -0
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +107 -5
- package/build/src/project-build.d.ts +172 -0
- package/build/src/project-build.d.ts.map +1 -0
- package/build/src/project-build.js +320 -0
- package/build/src/project-cli.d.ts +40 -0
- package/build/src/project-cli.d.ts.map +1 -0
- package/build/src/project-cli.js +145 -0
- package/build/src/project-manifest.d.ts +148 -0
- package/build/src/project-manifest.d.ts.map +1 -0
- package/build/src/project-manifest.js +432 -0
- package/build/src/project-reporter.d.ts +81 -0
- package/build/src/project-reporter.d.ts.map +1 -0
- package/build/src/project-reporter.js +293 -0
- package/build/src/project-snapshot.d.ts +129 -0
- package/build/src/project-snapshot.d.ts.map +1 -0
- package/build/src/project-snapshot.js +362 -0
- package/build/src/project-watch-reporter.d.ts +125 -0
- package/build/src/project-watch-reporter.d.ts.map +1 -0
- package/build/src/project-watch-reporter.js +280 -0
- package/build/src/project-watch.d.ts +133 -0
- package/build/src/project-watch.d.ts.map +1 -0
- package/build/src/project-watch.js +839 -0
- package/build/src/publication.d.ts +218 -0
- package/build/src/publication.d.ts.map +1 -0
- package/build/src/publication.js +1832 -0
- package/build/src/semantic/types.d.ts +613 -0
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +163 -1
- package/build/src/subprocess.d.ts +8 -0
- package/build/src/subprocess.d.ts.map +1 -1
- package/build/src/subprocess.js +50 -9
- package/build/src/target-check.d.ts +52 -0
- package/build/src/target-check.d.ts.map +1 -0
- package/build/src/target-check.js +796 -0
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +3 -3
- package/docs/architecture.md +3 -1
- package/docs/c.md +4 -0
- package/docs/cpp.md +4 -0
- package/docs/dart.md +4 -0
- package/docs/generated-artifact-publication.md +98 -0
- package/docs/kotlin.md +2 -2
- package/docs/language-support.md +29 -3
- package/docs/project-build-cli.md +147 -0
- package/docs/project-watch.md +60 -0
- package/docs/rust.md +4 -0
- package/docs/swift.md +2 -0
- package/docs/testing.md +70 -3
- package/docs/watch-build-pipeline.md +42 -26
- package/docs/zig.md +2 -0
- package/examples/jsdoc-java-watch/expected-output.txt +3 -0
- package/examples/jsdoc-java-watch/semantifold.json +23 -0
- package/examples/jsdoc-java-watch/src/main.js +21 -0
- package/index.js +9 -0
- package/package.json +6 -1
- package/src/artifact-path.js +31 -2
- package/src/backends/c-check.js +80 -0
- package/src/backends/check-plan.js +147 -0
- package/src/backends/cpp-check.js +73 -0
- package/src/backends/csharp-check.js +139 -0
- package/src/backends/csharp.js +4 -3
- package/src/backends/dart-check.js +117 -0
- package/src/backends/dart.js +4 -4
- package/src/backends/go-check.js +89 -0
- package/src/backends/go.js +2 -2
- package/src/backends/java-check.js +77 -0
- package/src/backends/javascript-check.js +33 -0
- package/src/backends/kotlin-check.js +41 -0
- package/src/backends/php-check.js +23 -0
- package/src/backends/program.js +17 -0
- package/src/backends/python-check.js +30 -0
- package/src/backends/ruby-check.js +23 -0
- package/src/backends/rust-check.js +78 -0
- package/src/backends/swift-check.js +89 -0
- package/src/backends/typescript-check.js +37 -0
- package/src/backends/zig-check.js +81 -0
- package/src/diagnostic.js +9 -1
- package/src/frontends/program.js +9 -0
- package/src/language-registry.js +113 -4
- package/src/project-build.js +353 -0
- package/src/project-cli.js +155 -0
- package/src/project-manifest.js +479 -0
- package/src/project-reporter.js +320 -0
- package/src/project-snapshot.js +403 -0
- package/src/project-watch-reporter.js +303 -0
- package/src/project-watch.js +870 -0
- package/src/publication.js +2071 -0
- package/src/semantic/types.js +184 -0
- package/src/subprocess.js +49 -8
- package/src/target-check.js +844 -0
- package/src/toolchains.js +3 -2
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {lstat, readFile, realpath} from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import {
|
|
6
|
+
findPortableArtifactPathConflict,
|
|
7
|
+
findPortableSourcePathConflict,
|
|
8
|
+
isSafeArtifactPath,
|
|
9
|
+
isSafeSourcePath
|
|
10
|
+
} from "./artifact-path.js"
|
|
11
|
+
import {supportsProgramArtifactRole} from "./backends/program.js"
|
|
12
|
+
import {SemantifoldDiagnostic} from "./diagnostic.js"
|
|
13
|
+
import {supportsProgramSourceLanguage} from "./frontends/program.js"
|
|
14
|
+
import {languageRegistry} from "./language-registry.js"
|
|
15
|
+
|
|
16
|
+
const projectIdentityPattern = /^[a-z][a-z0-9-]*$/u
|
|
17
|
+
const moduleIdentityPattern = /^[a-z][a-z0-9_]*(?:[.-][a-z][a-z0-9_]*)*$/u
|
|
18
|
+
const sourceRoles = new Map([
|
|
19
|
+
["text", "textBackend"],
|
|
20
|
+
["binary", "binaryBackend"],
|
|
21
|
+
["application", "applicationBackend"]
|
|
22
|
+
])
|
|
23
|
+
const manifestDefaultApplicationTargets = new Set(["android", "flutter"])
|
|
24
|
+
const projectFields = ["id", "publicationRoot", "schema", "sources", "targets", "version"]
|
|
25
|
+
const sourceFields = ["entry", "id", "language", "path"]
|
|
26
|
+
const targetRequiredFields = ["id", "language", "role", "sourceProjection"]
|
|
27
|
+
const targetOptionalFields = ["buildProjection"]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One validated immutable Semantifold project request.
|
|
31
|
+
*/
|
|
32
|
+
export class SemantifoldProject {
|
|
33
|
+
/**
|
|
34
|
+
* Constructs a trusted normalized project value for the manifest loader.
|
|
35
|
+
* @param {NormalizedProject} value - Validated normalized fields.
|
|
36
|
+
*/
|
|
37
|
+
constructor(value) {
|
|
38
|
+
this.entryModule = value.entryModule
|
|
39
|
+
this.id = value.id
|
|
40
|
+
this.manifestPath = value.manifestPath
|
|
41
|
+
this.manifestText = value.manifestText
|
|
42
|
+
this.projectRoot = value.projectRoot
|
|
43
|
+
this.publicationRoot = value.publicationRoot
|
|
44
|
+
this.sources = Object.freeze(value.sources.map(source => Object.freeze({...source})))
|
|
45
|
+
this.targets = Object.freeze(value.targets.map(target => Object.freeze({...target})))
|
|
46
|
+
this.version = /** @type {const} */ (1)
|
|
47
|
+
Object.freeze(this)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loads and validates the strict versioned project-manifest boundary.
|
|
53
|
+
*/
|
|
54
|
+
export class ProjectManifestLoader {
|
|
55
|
+
/**
|
|
56
|
+
* Loads one project manifest without reading source content.
|
|
57
|
+
* @param {string} [projectPath] - Manifest path, relative to the process working directory or absolute.
|
|
58
|
+
* @returns {Promise<SemantifoldProject>} Immutable normalized project.
|
|
59
|
+
*/
|
|
60
|
+
async load(projectPath = "./semantifold.json") {
|
|
61
|
+
if (typeof projectPath != "string" || projectPath.length == 0 || projectPath.includes("\0")) {
|
|
62
|
+
projectFailure("INVALID_PROJECT_MANIFEST", "project", "Project manifest path must be a non-empty path string.")
|
|
63
|
+
}
|
|
64
|
+
const manifestPath = path.resolve(projectPath)
|
|
65
|
+
const projectRoot = path.dirname(manifestPath)
|
|
66
|
+
|
|
67
|
+
await requireCanonicalProjectRoot(projectRoot)
|
|
68
|
+
await requireRegularFile(manifestPath, "semantifold.json", "PROJECT_MANIFEST_READ_FAILED")
|
|
69
|
+
let bytes
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
bytes = await readFile(manifestPath)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
projectFailure("PROJECT_MANIFEST_READ_FAILED", "project", "Project manifest could not be read.", manifestLocation(), error)
|
|
75
|
+
}
|
|
76
|
+
let manifestText
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
manifestText = new TextDecoder("utf-8", {fatal: true}).decode(bytes)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
projectFailure("INVALID_PROJECT_MANIFEST", "project", "Project manifest must be valid UTF-8.", manifestLocation(), error)
|
|
82
|
+
}
|
|
83
|
+
let candidate
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
candidate = JSON.parse(manifestText)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
projectFailure("INVALID_PROJECT_MANIFEST", "project", "Project manifest must contain valid JSON.", manifestLocation(), error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return normalizeProject(candidate, {manifestPath, manifestText, projectRoot})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates and normalizes one parsed manifest.
|
|
97
|
+
* @param {unknown} candidate - Parsed JSON value.
|
|
98
|
+
* @param {{manifestPath: string, manifestText: string, projectRoot: string}} context - Canonical manifest context.
|
|
99
|
+
* @returns {Promise<SemantifoldProject>} Immutable project value.
|
|
100
|
+
*/
|
|
101
|
+
async function normalizeProject(candidate, context) {
|
|
102
|
+
if (!isPlainObject(candidate) || !hasExactKeys(candidate, projectFields)) {
|
|
103
|
+
projectFailure("INVALID_PROJECT_MANIFEST", "project", "Project manifest requires exactly the version-1 root fields.", manifestLocation())
|
|
104
|
+
}
|
|
105
|
+
const {id, publicationRoot, schema, sources, targets, version} = candidate
|
|
106
|
+
|
|
107
|
+
if (schema != "SemantifoldProject" || version != 1) {
|
|
108
|
+
projectFailure("INVALID_PROJECT_MANIFEST", typeof id == "string" ? id : "project",
|
|
109
|
+
"Project manifest requires schema 'SemantifoldProject' version 1.", manifestLocation())
|
|
110
|
+
}
|
|
111
|
+
if (typeof id != "string" || !projectIdentityPattern.test(id)) {
|
|
112
|
+
projectFailure("INVALID_PROJECT_MANIFEST", "project", "Project identity must be a stable lowercase ID.", manifestLocation())
|
|
113
|
+
}
|
|
114
|
+
if (!isSafeArtifactPath(publicationRoot)) {
|
|
115
|
+
projectFailure("INVALID_PROJECT_PATH", id, "Publication root must be one canonical project-relative path.", manifestLocation())
|
|
116
|
+
}
|
|
117
|
+
if (!isDenseArray(sources) || sources.length == 0) {
|
|
118
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, "Project sources must be a non-empty ordered array.", manifestLocation())
|
|
119
|
+
}
|
|
120
|
+
if (!isDenseArray(targets) || targets.length == 0) {
|
|
121
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, "Project targets must be a non-empty ordered array.", manifestLocation())
|
|
122
|
+
}
|
|
123
|
+
const normalizedPublicationRoot = path.join(context.projectRoot, publicationRoot)
|
|
124
|
+
const sourceIds = new Set()
|
|
125
|
+
const sourcePaths = []
|
|
126
|
+
/** @type {NormalizedProjectSource[]} */
|
|
127
|
+
const normalizedSources = []
|
|
128
|
+
let entryModule
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < sources.length; index += 1) {
|
|
131
|
+
const source = sources[index]
|
|
132
|
+
|
|
133
|
+
if (!isPlainObject(source) || !hasExactKeys(source, sourceFields)) {
|
|
134
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Source ${index} requires exactly path, language, ID, and entry fields.`, manifestLocation())
|
|
135
|
+
}
|
|
136
|
+
if (typeof source.id != "string" || !moduleIdentityPattern.test(source.id) || sourceIds.has(source.id)) {
|
|
137
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Source ${index} has an invalid or duplicate module identity.`, manifestLocation())
|
|
138
|
+
}
|
|
139
|
+
if (!isSafeSourcePath(source.path)) {
|
|
140
|
+
projectFailure("INVALID_PROJECT_PATH", id, `Source '${source.id}' path must be canonical and project-relative.`, manifestLocation())
|
|
141
|
+
}
|
|
142
|
+
if (typeof source.language != "string") {
|
|
143
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Source '${source.id}' requires a registered source language.`, manifestLocation())
|
|
144
|
+
}
|
|
145
|
+
languageRegistry.resolve(source.language, "frontend", manifestLocation())
|
|
146
|
+
if (!supportsProgramSourceLanguage(source.language)) {
|
|
147
|
+
projectFailure("UNSUPPORTED_PROJECT_SOURCE", source.language,
|
|
148
|
+
"Registered frontend does not participate in the explicit multi-file project profile.", manifestLocation())
|
|
149
|
+
}
|
|
150
|
+
if (typeof source.entry != "boolean") {
|
|
151
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Source '${source.id}' entry must be Boolean.`, manifestLocation())
|
|
152
|
+
}
|
|
153
|
+
if (source.entry) {
|
|
154
|
+
if (entryModule !== undefined) {
|
|
155
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, "Project sources must declare exactly one entry module.", manifestLocation())
|
|
156
|
+
}
|
|
157
|
+
entryModule = source.id
|
|
158
|
+
}
|
|
159
|
+
const absolutePath = path.join(context.projectRoot, source.path)
|
|
160
|
+
|
|
161
|
+
if (portablePathsOverlap(source.path, publicationRoot)) {
|
|
162
|
+
projectFailure("PROJECT_SOURCE_PUBLICATION_OVERLAP", id,
|
|
163
|
+
`Source '${source.id}' overlaps the publisher-owned publication root.`, manifestLocation())
|
|
164
|
+
}
|
|
165
|
+
sourceIds.add(source.id)
|
|
166
|
+
sourcePaths.push(source.path)
|
|
167
|
+
normalizedSources.push({
|
|
168
|
+
absolutePath,
|
|
169
|
+
entry: source.entry,
|
|
170
|
+
id: source.id,
|
|
171
|
+
language: /** @type {import("./semantic/types.js").SemanticLanguage} */ (source.language),
|
|
172
|
+
path: source.path
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
if (entryModule === undefined) {
|
|
176
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, "Project sources must declare exactly one entry module.", manifestLocation())
|
|
177
|
+
}
|
|
178
|
+
const sourceConflict = findPortableSourcePathConflict(sourcePaths)
|
|
179
|
+
|
|
180
|
+
if (sourceConflict) {
|
|
181
|
+
projectFailure("PROJECT_SOURCE_ALIAS", id, "Project source paths must be unique under portable comparison.", manifestLocation())
|
|
182
|
+
}
|
|
183
|
+
const sourceLanguages = new Set(normalizedSources.map(({language}) => language))
|
|
184
|
+
|
|
185
|
+
if (sourceLanguages.size != 1) {
|
|
186
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, "A project must use one source-language compatibility profile.", manifestLocation())
|
|
187
|
+
}
|
|
188
|
+
await requireExistingProjectPaths(context.projectRoot, normalizedPublicationRoot, normalizedSources, id)
|
|
189
|
+
const targetIds = new Set()
|
|
190
|
+
/** @type {NormalizedProjectTarget[]} */
|
|
191
|
+
const normalizedTargets = []
|
|
192
|
+
/** @type {string[]} */
|
|
193
|
+
const projections = []
|
|
194
|
+
|
|
195
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
196
|
+
const target = targets[index]
|
|
197
|
+
|
|
198
|
+
if (!isPlainObject(target) || !hasRequiredAndOptionalKeys(target, targetRequiredFields, targetOptionalFields)) {
|
|
199
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Target ${index} contains missing or unknown fields.`, manifestLocation())
|
|
200
|
+
}
|
|
201
|
+
if (typeof target.id != "string" || !projectIdentityPattern.test(target.id) || targetIds.has(target.id)) {
|
|
202
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Target ${index} has an invalid or duplicate identity.`, manifestLocation())
|
|
203
|
+
}
|
|
204
|
+
if (typeof target.language != "string" || typeof target.role != "string" || !sourceRoles.has(target.role)) {
|
|
205
|
+
projectFailure("INVALID_PROJECT_MANIFEST", id, `Target '${target.id}' requires a registered target and explicit role.`, manifestLocation())
|
|
206
|
+
}
|
|
207
|
+
languageRegistry.resolve(target.language, /** @type {"textBackend" | "binaryBackend" | "applicationBackend"} */ (sourceRoles.get(target.role)), manifestLocation())
|
|
208
|
+
if (!supportsProgramArtifactRole(target.language, target.role)) {
|
|
209
|
+
projectFailure("UNSUPPORTED_PROJECT_TARGET", target.language,
|
|
210
|
+
`Registered target does not support '${target.role}' generation for complete semantic programs.`, manifestLocation())
|
|
211
|
+
}
|
|
212
|
+
if (target.role == "application" && !manifestDefaultApplicationTargets.has(target.language)) {
|
|
213
|
+
projectFailure("UNSUPPORTED_PROJECT_TARGET", target.language,
|
|
214
|
+
"Project manifest version 1 cannot express the target's required application configuration.", manifestLocation())
|
|
215
|
+
}
|
|
216
|
+
const buildProjection = target.buildProjection === undefined ? `targets/${target.id}/build` : target.buildProjection
|
|
217
|
+
|
|
218
|
+
if (!isSafeArtifactPath(target.sourceProjection) || !isSafeArtifactPath(buildProjection)) {
|
|
219
|
+
projectFailure("INVALID_PROJECT_PATH", id, `Target '${target.id}' projections must be canonical generation-relative paths.`, manifestLocation())
|
|
220
|
+
}
|
|
221
|
+
targetIds.add(target.id)
|
|
222
|
+
projections.push(target.sourceProjection, buildProjection)
|
|
223
|
+
normalizedTargets.push({
|
|
224
|
+
buildProjection,
|
|
225
|
+
id: target.id,
|
|
226
|
+
language: /** @type {import("./semantic/types.js").BackendLanguage} */ (target.language),
|
|
227
|
+
role: /** @type {import("./semantic/types.js").PublicationTargetRole} */ (target.role),
|
|
228
|
+
sourceProjection: target.sourceProjection
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
if (findPortableArtifactPathConflict(projections)) {
|
|
232
|
+
projectFailure("PROJECT_PROJECTION_COLLISION", id,
|
|
233
|
+
"Target source/build projections must be distinct and non-nested under portable comparison.", manifestLocation())
|
|
234
|
+
}
|
|
235
|
+
if (findPortableArtifactPathConflict([...projections, "manifest.json"])) {
|
|
236
|
+
projectFailure("INVALID_PROJECT_PATH", id,
|
|
237
|
+
"Target source/build projections must not overlap the reserved generation manifest path.", manifestLocation())
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return new SemantifoldProject({
|
|
241
|
+
entryModule,
|
|
242
|
+
id,
|
|
243
|
+
manifestPath: context.manifestPath,
|
|
244
|
+
manifestText: context.manifestText,
|
|
245
|
+
projectRoot: context.projectRoot,
|
|
246
|
+
publicationRoot: normalizedPublicationRoot,
|
|
247
|
+
sources: normalizedSources,
|
|
248
|
+
targets: normalizedTargets
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Requires a canonical real project root.
|
|
254
|
+
* @param {string} projectRoot - Absolute manifest directory.
|
|
255
|
+
* @returns {Promise<void>} Completion.
|
|
256
|
+
*/
|
|
257
|
+
async function requireCanonicalProjectRoot(projectRoot) {
|
|
258
|
+
try {
|
|
259
|
+
const exact = await realpath(projectRoot)
|
|
260
|
+
|
|
261
|
+
if (exact != projectRoot) {
|
|
262
|
+
projectFailure("PROJECT_SYMLINK_TRAVERSAL", "project", "Project root must not be reached through a symbolic link.", manifestLocation())
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof SemantifoldDiagnostic) throw error
|
|
266
|
+
projectFailure("PROJECT_MANIFEST_READ_FAILED", "project", "Project root could not be resolved.", manifestLocation(), error)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validates every source path and every existing publication-root component.
|
|
272
|
+
* @param {string} projectRoot - Canonical project root.
|
|
273
|
+
* @param {string} publicationRoot - Normalized publication root.
|
|
274
|
+
* @param {readonly NormalizedProjectSource[]} sources - Normalized sources.
|
|
275
|
+
* @param {string} projectId - Project identity.
|
|
276
|
+
* @returns {Promise<void>} Completion.
|
|
277
|
+
*/
|
|
278
|
+
async function requireExistingProjectPaths(projectRoot, publicationRoot, sources, projectId) {
|
|
279
|
+
await requireNoSymlinkComponents(projectRoot, publicationRoot, projectId, true)
|
|
280
|
+
const fileIdentities = new Set()
|
|
281
|
+
|
|
282
|
+
for (const source of sources) {
|
|
283
|
+
await requireNoSymlinkComponents(projectRoot, source.absolutePath, projectId, false)
|
|
284
|
+
const status = await requireRegularFile(source.absolutePath, source.path, "PROJECT_SOURCE_READ_FAILED", projectId)
|
|
285
|
+
const identity = `${String(status.dev)}:${String(status.ino)}`
|
|
286
|
+
|
|
287
|
+
if (fileIdentities.has(identity)) {
|
|
288
|
+
projectFailure("PROJECT_SOURCE_ALIAS", projectId, "Project source paths must not alias the same file.", sourceLocation(source.path))
|
|
289
|
+
}
|
|
290
|
+
fileIdentities.add(identity)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Rejects symlinks beneath a canonical project root.
|
|
296
|
+
* @param {string} projectRoot - Canonical project root.
|
|
297
|
+
* @param {string} target - Absolute target.
|
|
298
|
+
* @param {string} projectId - Project identity.
|
|
299
|
+
* @param {boolean} allowMissing - Whether a missing suffix is permitted.
|
|
300
|
+
* @returns {Promise<void>} Completion.
|
|
301
|
+
*/
|
|
302
|
+
async function requireNoSymlinkComponents(projectRoot, target, projectId, allowMissing) {
|
|
303
|
+
const relative = path.relative(projectRoot, target)
|
|
304
|
+
let current = projectRoot
|
|
305
|
+
|
|
306
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
307
|
+
current = path.join(current, part)
|
|
308
|
+
try {
|
|
309
|
+
const status = await lstat(current)
|
|
310
|
+
|
|
311
|
+
if (status.isSymbolicLink()) {
|
|
312
|
+
projectFailure("PROJECT_SYMLINK_TRAVERSAL", projectId, "Project path traversal encountered a symbolic link.", manifestLocation())
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (allowMissing && isErrorCode(error, "ENOENT")) return
|
|
316
|
+
if (error instanceof SemantifoldDiagnostic) throw error
|
|
317
|
+
projectFailure("PROJECT_SOURCE_READ_FAILED", projectId, "Declared project path could not be inspected.", manifestLocation(), error)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Requires one real regular file.
|
|
324
|
+
* @param {string} filename - Absolute file path.
|
|
325
|
+
* @param {string} displayPath - Stable diagnostic filename.
|
|
326
|
+
* @param {string} code - Diagnostic code.
|
|
327
|
+
* @param {string} [projectId] - Project identity.
|
|
328
|
+
* @returns {Promise<import("node:fs").Stats>} File status.
|
|
329
|
+
*/
|
|
330
|
+
async function requireRegularFile(filename, displayPath, code, projectId = "project") {
|
|
331
|
+
try {
|
|
332
|
+
const status = await lstat(filename)
|
|
333
|
+
|
|
334
|
+
if (status.isSymbolicLink()) {
|
|
335
|
+
projectFailure("PROJECT_SYMLINK_TRAVERSAL", projectId, "Declared project file must not be a symbolic link.", sourceLocation(displayPath))
|
|
336
|
+
}
|
|
337
|
+
if (!status.isFile()) projectFailure(code, projectId, "Declared project file must be a regular file.", sourceLocation(displayPath))
|
|
338
|
+
|
|
339
|
+
return status
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof SemantifoldDiagnostic) throw error
|
|
342
|
+
projectFailure(code, projectId, "Declared project file could not be inspected.", sourceLocation(displayPath), error)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks whether two validated POSIX-relative paths overlap portably.
|
|
348
|
+
* @param {string} left - First path.
|
|
349
|
+
* @param {string} right - Second path.
|
|
350
|
+
* @returns {boolean} Whether either path contains the other.
|
|
351
|
+
*/
|
|
352
|
+
function portablePathsOverlap(left, right) {
|
|
353
|
+
const leftKey = left.toLowerCase()
|
|
354
|
+
const rightKey = right.toLowerCase()
|
|
355
|
+
|
|
356
|
+
return leftKey == rightKey || leftKey.startsWith(`${rightKey}/`) || rightKey.startsWith(`${leftKey}/`)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Checks for an ordinary string-keyed object.
|
|
361
|
+
* @param {unknown} value - Candidate value.
|
|
362
|
+
* @returns {value is Record<string, unknown>} Whether the value is a plain object.
|
|
363
|
+
*/
|
|
364
|
+
function isPlainObject(value) {
|
|
365
|
+
return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Checks for a dense array without extra enumerable keys.
|
|
370
|
+
* @param {unknown} value - Candidate value.
|
|
371
|
+
* @returns {value is unknown[]} Whether the value is a dense array.
|
|
372
|
+
*/
|
|
373
|
+
function isDenseArray(value) {
|
|
374
|
+
return Array.isArray(value) && Object.keys(value).every((key, index) => key == String(index))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Checks an object's exact closed key set.
|
|
379
|
+
* @param {Record<string, unknown>} value - Candidate object.
|
|
380
|
+
* @param {readonly string[]} keys - Required exact keys.
|
|
381
|
+
* @returns {boolean} Whether the key set is exact.
|
|
382
|
+
*/
|
|
383
|
+
function hasExactKeys(value, keys) {
|
|
384
|
+
const actual = Object.keys(value).sort()
|
|
385
|
+
const expected = [...keys].sort()
|
|
386
|
+
|
|
387
|
+
return actual.length == expected.length && actual.every((key, index) => key == expected[index])
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks exact required fields plus an optional closed field set.
|
|
392
|
+
* @param {Record<string, unknown>} value - Candidate record.
|
|
393
|
+
* @param {readonly string[]} required - Required fields.
|
|
394
|
+
* @param {readonly string[]} optional - Optional fields.
|
|
395
|
+
* @returns {boolean} Whether the shape is exact.
|
|
396
|
+
*/
|
|
397
|
+
function hasRequiredAndOptionalKeys(value, required, optional) {
|
|
398
|
+
const actual = Object.keys(value)
|
|
399
|
+
|
|
400
|
+
return required.every(key => actual.includes(key)) && actual.every(key => required.includes(key) || optional.includes(key))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Creates the stable manifest boundary location.
|
|
405
|
+
* @returns {import("./semantic/types.js").SourceLocation} Manifest location.
|
|
406
|
+
*/
|
|
407
|
+
function manifestLocation() {
|
|
408
|
+
return sourceLocation("semantifold.json")
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Creates a stable project-relative file location.
|
|
413
|
+
* @param {string} filename - Project-relative filename.
|
|
414
|
+
* @returns {import("./semantic/types.js").SourceLocation} Start-of-file location.
|
|
415
|
+
*/
|
|
416
|
+
function sourceLocation(filename) {
|
|
417
|
+
const point = {column: 1, line: 1, offset: 0}
|
|
418
|
+
|
|
419
|
+
return {end: point, filename, start: point}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Checks one narrowed filesystem error code.
|
|
424
|
+
* @param {unknown} error - Opaque failure.
|
|
425
|
+
* @param {string} code - Expected code.
|
|
426
|
+
* @returns {boolean} Whether the code matches.
|
|
427
|
+
*/
|
|
428
|
+
function isErrorCode(error, code) {
|
|
429
|
+
return error instanceof Error && "code" in error && error.code == code
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Throws one stable located project diagnostic.
|
|
434
|
+
* @param {string} code - Stable diagnostic code.
|
|
435
|
+
* @param {string} language - Project or registration identity.
|
|
436
|
+
* @param {string} message - Stable detail.
|
|
437
|
+
* @param {import("./semantic/types.js").SourceLocation} [location] - Project-relative location.
|
|
438
|
+
* @param {unknown} [error] - Preserved cause.
|
|
439
|
+
* @returns {never} Always throws.
|
|
440
|
+
*/
|
|
441
|
+
function projectFailure(code, language, message, location, error) {
|
|
442
|
+
throw new SemantifoldDiagnostic({
|
|
443
|
+
cause: error instanceof Error ? error : undefined,
|
|
444
|
+
code,
|
|
445
|
+
language,
|
|
446
|
+
location,
|
|
447
|
+
message
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @typedef NormalizedProjectSource
|
|
453
|
+
* @property {string} absolutePath - Canonical project-absolute file path.
|
|
454
|
+
* @property {boolean} entry - Whether this is the sole entry module.
|
|
455
|
+
* @property {string} id - Stable semantic module identity.
|
|
456
|
+
* @property {import("./semantic/types.js").SemanticLanguage} language - Registered source language.
|
|
457
|
+
* @property {string} path - Canonical project-relative file path.
|
|
458
|
+
*/
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @typedef NormalizedProjectTarget
|
|
462
|
+
* @property {string} buildProjection - Generation-relative reserved build projection.
|
|
463
|
+
* @property {string} id - Stable project target identity.
|
|
464
|
+
* @property {import("./semantic/types.js").BackendLanguage} language - Registered target identity.
|
|
465
|
+
* @property {import("./semantic/types.js").PublicationTargetRole} role - Explicit target role.
|
|
466
|
+
* @property {string} sourceProjection - Generation-relative generated-source projection.
|
|
467
|
+
*/
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @typedef NormalizedProject
|
|
471
|
+
* @property {string} entryModule - Sole entry module identity.
|
|
472
|
+
* @property {string} id - Stable project identity.
|
|
473
|
+
* @property {string} manifestPath - Canonical absolute manifest path.
|
|
474
|
+
* @property {string} manifestText - Exact validated manifest text.
|
|
475
|
+
* @property {string} projectRoot - Canonical absolute project root.
|
|
476
|
+
* @property {string} publicationRoot - Canonical absolute publication root.
|
|
477
|
+
* @property {NormalizedProjectSource[]} sources - Ordered normalized sources.
|
|
478
|
+
* @property {NormalizedProjectTarget[]} targets - Ordered normalized targets.
|
|
479
|
+
*/
|