@transloadit/node 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -4
- package/dist/Transloadit.d.ts +45 -4
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +104 -21
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/assembly-linter.d.ts +123 -0
- package/dist/alphalib/assembly-linter.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.js +1142 -0
- package/dist/alphalib/assembly-linter.js.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.js +326 -0
- package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
- package/dist/alphalib/goldenTemplates.d.ts +52 -0
- package/dist/alphalib/goldenTemplates.d.ts.map +1 -0
- package/dist/alphalib/goldenTemplates.js +46 -0
- package/dist/alphalib/goldenTemplates.js.map +1 -0
- package/dist/alphalib/object.d.ts +20 -0
- package/dist/alphalib/object.d.ts.map +1 -0
- package/dist/alphalib/object.js +23 -0
- package/dist/alphalib/object.js.map +1 -0
- package/dist/alphalib/stepParsing.d.ts +93 -0
- package/dist/alphalib/stepParsing.d.ts.map +1 -0
- package/dist/alphalib/stepParsing.js +1154 -0
- package/dist/alphalib/stepParsing.js.map +1 -0
- package/dist/alphalib/templateMerge.d.ts +4 -0
- package/dist/alphalib/templateMerge.d.ts.map +1 -0
- package/dist/alphalib/templateMerge.js +22 -0
- package/dist/alphalib/templateMerge.js.map +1 -0
- package/dist/cli/commands/assemblies.d.ts +20 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +137 -2
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +19 -19
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
- package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
- package/dist/cli/docs/assemblyLintingExamples.js +10 -0
- package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
- package/dist/cli/helpers.d.ts +11 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +29 -0
- package/dist/cli/helpers.js.map +1 -1
- package/dist/inputFiles.d.ts +41 -0
- package/dist/inputFiles.d.ts.map +1 -0
- package/dist/inputFiles.js +214 -0
- package/dist/inputFiles.js.map +1 -0
- package/dist/lintAssemblyInput.d.ts +10 -0
- package/dist/lintAssemblyInput.d.ts.map +1 -0
- package/dist/lintAssemblyInput.js +73 -0
- package/dist/lintAssemblyInput.js.map +1 -0
- package/dist/lintAssemblyInstructions.d.ts +29 -0
- package/dist/lintAssemblyInstructions.d.ts.map +1 -0
- package/dist/lintAssemblyInstructions.js +33 -0
- package/dist/lintAssemblyInstructions.js.map +1 -0
- package/dist/robots.d.ts +38 -0
- package/dist/robots.d.ts.map +1 -0
- package/dist/robots.js +230 -0
- package/dist/robots.js.map +1 -0
- package/dist/tus.d.ts +5 -1
- package/dist/tus.d.ts.map +1 -1
- package/dist/tus.js +80 -6
- package/dist/tus.js.map +1 -1
- package/package.json +5 -2
- package/src/Transloadit.ts +170 -26
- package/src/alphalib/assembly-linter.lang.en.ts +393 -0
- package/src/alphalib/assembly-linter.ts +1475 -0
- package/src/alphalib/goldenTemplates.ts +53 -0
- package/src/alphalib/object.ts +27 -0
- package/src/alphalib/stepParsing.ts +1465 -0
- package/src/alphalib/templateMerge.ts +32 -0
- package/src/alphalib/typings/json-to-ast.d.ts +34 -0
- package/src/cli/commands/assemblies.ts +161 -2
- package/src/cli/commands/auth.ts +19 -22
- package/src/cli/commands/index.ts +2 -0
- package/src/cli/docs/assemblyLintingExamples.ts +9 -0
- package/src/cli/helpers.ts +50 -0
- package/src/inputFiles.ts +278 -0
- package/src/lintAssemblyInput.ts +89 -0
- package/src/lintAssemblyInstructions.ts +72 -0
- package/src/robots.ts +317 -0
- package/src/tus.ts +91 -5
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { isIP } from 'node:net'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { basename, join } from 'node:path'
|
|
6
|
+
import type { Readable } from 'node:stream'
|
|
7
|
+
import { pipeline } from 'node:stream/promises'
|
|
8
|
+
import got from 'got'
|
|
9
|
+
import type { Input as IntoStreamInput } from 'into-stream'
|
|
10
|
+
import type { CreateAssemblyParams } from './apiTypes.ts'
|
|
11
|
+
|
|
12
|
+
export type InputFile =
|
|
13
|
+
| {
|
|
14
|
+
kind: 'path'
|
|
15
|
+
field: string
|
|
16
|
+
path: string
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
kind: 'base64'
|
|
20
|
+
field: string
|
|
21
|
+
base64: string
|
|
22
|
+
filename: string
|
|
23
|
+
contentType?: string
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
kind: 'url'
|
|
27
|
+
field: string
|
|
28
|
+
url: string
|
|
29
|
+
filename?: string
|
|
30
|
+
contentType?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UploadInput = Readable | IntoStreamInput
|
|
34
|
+
|
|
35
|
+
export type Base64Strategy = 'buffer' | 'tempfile'
|
|
36
|
+
export type UrlStrategy = 'import' | 'download' | 'import-if-present'
|
|
37
|
+
|
|
38
|
+
export type PrepareInputFilesOptions = {
|
|
39
|
+
inputFiles?: InputFile[]
|
|
40
|
+
params?: CreateAssemblyParams
|
|
41
|
+
fields?: Record<string, unknown>
|
|
42
|
+
base64Strategy?: Base64Strategy
|
|
43
|
+
urlStrategy?: UrlStrategy
|
|
44
|
+
maxBase64Bytes?: number
|
|
45
|
+
allowPrivateUrls?: boolean
|
|
46
|
+
tempDir?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PrepareInputFilesResult = {
|
|
50
|
+
params: CreateAssemblyParams
|
|
51
|
+
files: Record<string, string>
|
|
52
|
+
uploads: Record<string, UploadInput>
|
|
53
|
+
cleanup: Array<() => Promise<void>>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
57
|
+
typeof value === 'object' && value !== null
|
|
58
|
+
|
|
59
|
+
const ensureUnique = (field: string, used: Set<string>): void => {
|
|
60
|
+
if (used.has(field)) {
|
|
61
|
+
throw new Error(`Duplicate file field: ${field}`)
|
|
62
|
+
}
|
|
63
|
+
used.add(field)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ensureUniqueStepName = (baseName: string, used: Set<string>): string => {
|
|
67
|
+
let name = baseName
|
|
68
|
+
let counter = 1
|
|
69
|
+
while (used.has(name)) {
|
|
70
|
+
name = `${baseName}_${counter}`
|
|
71
|
+
counter += 1
|
|
72
|
+
}
|
|
73
|
+
used.add(name)
|
|
74
|
+
return name
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const decodeBase64 = (value: string): Buffer => Buffer.from(value, 'base64')
|
|
78
|
+
|
|
79
|
+
const estimateBase64DecodedBytes = (value: string): number => {
|
|
80
|
+
const trimmed = value.trim()
|
|
81
|
+
if (!trimmed) return 0
|
|
82
|
+
let padding = 0
|
|
83
|
+
if (trimmed.endsWith('==')) padding = 2
|
|
84
|
+
else if (trimmed.endsWith('=')) padding = 1
|
|
85
|
+
return Math.floor((trimmed.length * 3) / 4) - padding
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getFilenameFromUrl = (value: string): string | null => {
|
|
89
|
+
try {
|
|
90
|
+
const pathname = new URL(value).pathname
|
|
91
|
+
const base = basename(pathname)
|
|
92
|
+
if (base && base !== '/' && base !== '.') return base
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isHttpImportStep = (value: unknown): value is Record<string, unknown> =>
|
|
100
|
+
isRecord(value) && value.robot === '/http/import'
|
|
101
|
+
|
|
102
|
+
const findImportStepName = (field: string, steps: Record<string, unknown>): string | null => {
|
|
103
|
+
if (isHttpImportStep(steps[field])) return field
|
|
104
|
+
const matches = Object.entries(steps).filter(([, step]) => isHttpImportStep(step))
|
|
105
|
+
if (matches.length === 1) return matches[0]?.[0] ?? null
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const downloadUrlToFile = async (url: string, filePath: string): Promise<void> => {
|
|
110
|
+
await pipeline(got.stream(url), createWriteStream(filePath))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isPrivateIp = (address: string): boolean => {
|
|
114
|
+
if (address === 'localhost') return true
|
|
115
|
+
const family = isIP(address)
|
|
116
|
+
if (family === 4) {
|
|
117
|
+
const parts = address.split('.').map((chunk) => Number(chunk))
|
|
118
|
+
const [a, b] = parts
|
|
119
|
+
if (a === 10) return true
|
|
120
|
+
if (a === 127) return true
|
|
121
|
+
if (a === 0) return true
|
|
122
|
+
if (a === 169 && b === 254) return true
|
|
123
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
124
|
+
if (a === 192 && b === 168) return true
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if (family === 6) {
|
|
128
|
+
const normalized = address.toLowerCase()
|
|
129
|
+
if (normalized === '::1') return true
|
|
130
|
+
if (normalized.startsWith('fe80:')) return true
|
|
131
|
+
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const assertPublicDownloadUrl = (value: string): void => {
|
|
138
|
+
const parsed = new URL(value)
|
|
139
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
140
|
+
throw new Error(`URL downloads are limited to http/https: ${value}`)
|
|
141
|
+
}
|
|
142
|
+
if (isPrivateIp(parsed.hostname)) {
|
|
143
|
+
throw new Error(`URL downloads are limited to public hosts: ${value}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const prepareInputFiles = async (
|
|
148
|
+
options: PrepareInputFilesOptions = {},
|
|
149
|
+
): Promise<PrepareInputFilesResult> => {
|
|
150
|
+
const {
|
|
151
|
+
inputFiles = [],
|
|
152
|
+
params = {},
|
|
153
|
+
fields,
|
|
154
|
+
base64Strategy = 'buffer',
|
|
155
|
+
urlStrategy = 'import',
|
|
156
|
+
maxBase64Bytes,
|
|
157
|
+
allowPrivateUrls = true,
|
|
158
|
+
tempDir,
|
|
159
|
+
} = options
|
|
160
|
+
|
|
161
|
+
let nextParams: CreateAssemblyParams = { ...params }
|
|
162
|
+
const files: Record<string, string> = {}
|
|
163
|
+
const uploads: Record<string, UploadInput> = {}
|
|
164
|
+
const cleanup: Array<() => Promise<void>> = []
|
|
165
|
+
|
|
166
|
+
if (fields && Object.keys(fields).length > 0) {
|
|
167
|
+
nextParams = {
|
|
168
|
+
...nextParams,
|
|
169
|
+
fields: {
|
|
170
|
+
...(isRecord(nextParams.fields) ? nextParams.fields : {}),
|
|
171
|
+
...fields,
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const steps = isRecord(nextParams.steps) ? { ...nextParams.steps } : {}
|
|
177
|
+
const usedSteps = new Set(Object.keys(steps))
|
|
178
|
+
const usedFields = new Set<string>()
|
|
179
|
+
const importUrlsByStep = new Map<string, string[]>()
|
|
180
|
+
const importStepNames = Object.keys(steps).filter((name) => isHttpImportStep(steps[name]))
|
|
181
|
+
const sharedImportStep = importStepNames.length === 1 ? importStepNames[0] : null
|
|
182
|
+
|
|
183
|
+
let tempRoot: string | null = null
|
|
184
|
+
const ensureTempRoot = async (): Promise<string> => {
|
|
185
|
+
if (!tempRoot) {
|
|
186
|
+
const root = await mkdtemp(join(tempDir ?? tmpdir(), 'transloadit-input-'))
|
|
187
|
+
tempRoot = root
|
|
188
|
+
cleanup.push(() => rm(root, { recursive: true, force: true }))
|
|
189
|
+
}
|
|
190
|
+
return tempRoot
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
for (const file of inputFiles) {
|
|
195
|
+
ensureUnique(file.field, usedFields)
|
|
196
|
+
if (file.kind === 'path') {
|
|
197
|
+
files[file.field] = file.path
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if (file.kind === 'base64') {
|
|
201
|
+
if (maxBase64Bytes) {
|
|
202
|
+
const estimated = estimateBase64DecodedBytes(file.base64)
|
|
203
|
+
if (estimated > maxBase64Bytes) {
|
|
204
|
+
throw new Error(`Base64 payload exceeds ${maxBase64Bytes} bytes.`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const buffer = decodeBase64(file.base64)
|
|
208
|
+
if (maxBase64Bytes && buffer.length > maxBase64Bytes) {
|
|
209
|
+
throw new Error(`Base64 payload exceeds ${maxBase64Bytes} bytes.`)
|
|
210
|
+
}
|
|
211
|
+
if (base64Strategy === 'tempfile') {
|
|
212
|
+
const root = await ensureTempRoot()
|
|
213
|
+
const filename = file.filename ? basename(file.filename) : `${file.field}.bin`
|
|
214
|
+
const filePath = join(root, filename)
|
|
215
|
+
await writeFile(filePath, buffer)
|
|
216
|
+
files[file.field] = filePath
|
|
217
|
+
} else {
|
|
218
|
+
uploads[file.field] = buffer
|
|
219
|
+
}
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (file.kind === 'url') {
|
|
223
|
+
const matchedStep = findImportStepName(file.field, steps)
|
|
224
|
+
const targetStep = matchedStep ?? sharedImportStep
|
|
225
|
+
const shouldImport =
|
|
226
|
+
urlStrategy === 'import' || (urlStrategy === 'import-if-present' && targetStep)
|
|
227
|
+
|
|
228
|
+
if (shouldImport) {
|
|
229
|
+
const stepName = targetStep ?? ensureUniqueStepName(file.field, usedSteps)
|
|
230
|
+
const urls = importUrlsByStep.get(stepName) ?? []
|
|
231
|
+
urls.push(file.url)
|
|
232
|
+
importUrlsByStep.set(stepName, urls)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const root = await ensureTempRoot()
|
|
237
|
+
const filename =
|
|
238
|
+
(file.filename ? basename(file.filename) : null) ??
|
|
239
|
+
getFilenameFromUrl(file.url) ??
|
|
240
|
+
`${file.field}.bin`
|
|
241
|
+
const filePath = join(root, filename)
|
|
242
|
+
if (!allowPrivateUrls) {
|
|
243
|
+
assertPublicDownloadUrl(file.url)
|
|
244
|
+
}
|
|
245
|
+
await downloadUrlToFile(file.url, filePath)
|
|
246
|
+
files[file.field] = filePath
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
await Promise.all(cleanup.map((fn) => fn()))
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (Object.keys(steps).length > 0 || importUrlsByStep.size > 0) {
|
|
255
|
+
if (importUrlsByStep.size > 0) {
|
|
256
|
+
for (const [stepName, urls] of importUrlsByStep.entries()) {
|
|
257
|
+
const existing = isRecord(steps[stepName]) ? steps[stepName] : {}
|
|
258
|
+
steps[stepName] = {
|
|
259
|
+
...existing,
|
|
260
|
+
robot: '/http/import',
|
|
261
|
+
url: urls.length === 1 ? urls[0] : urls,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
nextParams = {
|
|
267
|
+
...nextParams,
|
|
268
|
+
steps,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
params: nextParams,
|
|
274
|
+
files,
|
|
275
|
+
uploads,
|
|
276
|
+
cleanup,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getIndentation } from './alphalib/stepParsing.ts'
|
|
2
|
+
import { mergeTemplateContent } from './alphalib/templateMerge.ts'
|
|
3
|
+
import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
|
|
4
|
+
import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_INDENT = ' '
|
|
7
|
+
|
|
8
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
9
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
|
+
|
|
11
|
+
export interface BuildLintInputResult {
|
|
12
|
+
lintContent: string
|
|
13
|
+
wasStepsOnly: boolean
|
|
14
|
+
indent: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const unwrapStepsOnly = (content: string, indent: string): string => {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(content)
|
|
20
|
+
if (isRecord(parsed) && 'steps' in parsed) {
|
|
21
|
+
return JSON.stringify((parsed as { steps?: unknown }).steps ?? {}, null, indent)
|
|
22
|
+
}
|
|
23
|
+
} catch (_err) {
|
|
24
|
+
return content
|
|
25
|
+
}
|
|
26
|
+
return content
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const buildLintInput = (
|
|
30
|
+
assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string,
|
|
31
|
+
template?: TemplateContent | ResponseTemplateContent,
|
|
32
|
+
): BuildLintInputResult => {
|
|
33
|
+
let inputString: string | undefined
|
|
34
|
+
let parsedInput: unknown | undefined
|
|
35
|
+
let parseFailed = false
|
|
36
|
+
let indent = DEFAULT_INDENT
|
|
37
|
+
|
|
38
|
+
if (typeof assemblyInstructions === 'string') {
|
|
39
|
+
inputString = assemblyInstructions
|
|
40
|
+
indent = getIndentation(assemblyInstructions)
|
|
41
|
+
try {
|
|
42
|
+
parsedInput = JSON.parse(assemblyInstructions)
|
|
43
|
+
} catch (_err) {
|
|
44
|
+
parseFailed = true
|
|
45
|
+
}
|
|
46
|
+
} else if (assemblyInstructions != null) {
|
|
47
|
+
parsedInput = assemblyInstructions
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let wasStepsOnly = false
|
|
51
|
+
let instructions: AssemblyInstructionsInput | undefined
|
|
52
|
+
|
|
53
|
+
if (parsedInput !== undefined) {
|
|
54
|
+
if (isRecord(parsedInput)) {
|
|
55
|
+
if ('steps' in parsedInput) {
|
|
56
|
+
instructions = parsedInput as AssemblyInstructionsInput
|
|
57
|
+
} else {
|
|
58
|
+
instructions = { steps: parsedInput as StepsInput }
|
|
59
|
+
wasStepsOnly = true
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
instructions = { steps: parsedInput as StepsInput }
|
|
63
|
+
wasStepsOnly = true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const shouldMergeTemplate = template != null && !parseFailed
|
|
68
|
+
if (shouldMergeTemplate) {
|
|
69
|
+
instructions = mergeTemplateContent(template, instructions)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let lintContent = ''
|
|
73
|
+
if (instructions != null) {
|
|
74
|
+
if (
|
|
75
|
+
typeof assemblyInstructions === 'string' &&
|
|
76
|
+
!wasStepsOnly &&
|
|
77
|
+
!parseFailed &&
|
|
78
|
+
!shouldMergeTemplate
|
|
79
|
+
) {
|
|
80
|
+
lintContent = assemblyInstructions
|
|
81
|
+
} else {
|
|
82
|
+
lintContent = JSON.stringify(instructions, null, indent)
|
|
83
|
+
}
|
|
84
|
+
} else if (inputString != null) {
|
|
85
|
+
lintContent = inputString
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { lintContent, wasStepsOnly, indent }
|
|
89
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HydratedLintIssue } from './alphalib/assembly-linter.lang.en.ts'
|
|
2
|
+
import { hydrateLintIssues } from './alphalib/assembly-linter.lang.en.ts'
|
|
3
|
+
import { applyFix, parseAndLint } from './alphalib/assembly-linter.ts'
|
|
4
|
+
import type { AssemblyInstructionsInput, StepsInput } from './alphalib/types/template.ts'
|
|
5
|
+
import type { ResponseTemplateContent, TemplateContent } from './apiTypes.ts'
|
|
6
|
+
import { buildLintInput, unwrapStepsOnly } from './lintAssemblyInput.ts'
|
|
7
|
+
|
|
8
|
+
export type LintFatalLevel = 'error' | 'warning'
|
|
9
|
+
|
|
10
|
+
export interface LintAssemblyInstructionsInput {
|
|
11
|
+
/**
|
|
12
|
+
* Assembly Instructions as a JSON string, a full instructions object, or a steps-only object.
|
|
13
|
+
*/
|
|
14
|
+
assemblyInstructions?: AssemblyInstructionsInput | StepsInput | string
|
|
15
|
+
/**
|
|
16
|
+
* Optional template content to merge with the provided instructions.
|
|
17
|
+
*/
|
|
18
|
+
template?: TemplateContent | ResponseTemplateContent
|
|
19
|
+
/**
|
|
20
|
+
* Treat issues at this level or above as fatal. Defaults to "error".
|
|
21
|
+
*/
|
|
22
|
+
fatal?: LintFatalLevel
|
|
23
|
+
/**
|
|
24
|
+
* Apply auto-fixes where possible and return the fixed JSON.
|
|
25
|
+
*/
|
|
26
|
+
fix?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LintAssemblyInstructionsResult {
|
|
30
|
+
success: boolean
|
|
31
|
+
issues: HydratedLintIssue[]
|
|
32
|
+
fixedInstructions?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function lintAssemblyInstructions(
|
|
36
|
+
options: LintAssemblyInstructionsInput,
|
|
37
|
+
): Promise<LintAssemblyInstructionsResult> {
|
|
38
|
+
const { assemblyInstructions, template, fix = false, fatal = 'error' } = options
|
|
39
|
+
|
|
40
|
+
if (assemblyInstructions == null && template == null) {
|
|
41
|
+
throw new Error('Provide assemblyInstructions or template content to lint.')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { lintContent, wasStepsOnly, indent } = buildLintInput(assemblyInstructions, template)
|
|
45
|
+
|
|
46
|
+
let issues = await parseAndLint(lintContent)
|
|
47
|
+
let fixedContent = lintContent
|
|
48
|
+
|
|
49
|
+
if (fix) {
|
|
50
|
+
for (const issue of issues) {
|
|
51
|
+
if (!issue.fixId) continue
|
|
52
|
+
// applyFix validates fixData against the fix schema for the fixId.
|
|
53
|
+
fixedContent = applyFix(fixedContent, issue.fixId, issue.fixData as never)
|
|
54
|
+
}
|
|
55
|
+
issues = await parseAndLint(fixedContent)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const describedIssues = hydrateLintIssues(issues)
|
|
59
|
+
const fatalTypes = fatal === 'warning' ? new Set(['warning', 'error']) : new Set(['error'])
|
|
60
|
+
const success = !describedIssues.some((issue) => fatalTypes.has(issue.type))
|
|
61
|
+
|
|
62
|
+
const result: LintAssemblyInstructionsResult = {
|
|
63
|
+
success,
|
|
64
|
+
issues: describedIssues,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fix) {
|
|
68
|
+
result.fixedInstructions = wasStepsOnly ? unwrapStepsOnly(fixedContent, indent) : fixedContent
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
}
|