code-foundry 1.20.1 → 1.22.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/.github/CONTRIBUTING.md +5 -2
- package/.github/code-foundry.yml +1 -0
- package/.github/release-please-foundry.json +56 -0
- package/.github/workflows/ci.yml +61 -0
- package/.github/workflows/cloudflare-delivery.yml +9 -0
- package/.github/workflows/cloudflare-deploy.yml +8 -1
- package/.github/workflows/opencode-security_self-ci.yml +1 -1
- package/.github/workflows/qualified-foundry-publish.yml +6 -7
- package/.github/workflows/release.yml +58 -10
- package/.github/workflows/release_self-ci.yml +211 -8
- package/.github/workflows/test.yml +75 -0
- package/.github/workflows/validation-no-codeql.yml +7 -0
- package/.github/workflows/validation.yml +7 -0
- package/.gitignore +1 -1
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +60 -0
- package/README.md +20 -17
- package/docs/CONFIGURATION.md +182 -152
- package/docs/EXTENSIONS.md +28 -7
- package/docs/INITIALIZATION.md +13 -9
- package/docs/PERFORMANCE.md +67 -59
- package/docs/PUBLISHING.md +39 -14
- package/docs/README.md +37 -22
- package/docs/RELEASES.md +18 -9
- package/docs/WORKFLOWS.md +11 -0
- package/docs/agent-validation.md +6 -5
- package/docs/cloudflare-delivery.md +20 -8
- package/docs/consumer-qualification.md +11 -9
- package/docs/fleet-release-eligibility.md +14 -15
- package/docs/fleet-rollouts.md +3 -3
- package/docs/merge-queues.md +21 -21
- package/docs/product-quality.md +9 -10
- package/docs/qualified-publication.md +166 -93
- package/docs/release-integrity.md +7 -6
- package/docs/required-capabilities.md +42 -15
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +6 -1
- package/src/commands/qualified-publication.mjs +23 -10
- package/src/commands/release-integrity.mjs +9 -0
- package/src/commands/sync.mjs +29 -24
- package/src/lib/product-quality.mjs +220 -16
- package/src/runtime-core.mjs +12 -8
- package/src/runtime.mjs +1 -1
- package/src/templates/gitignore +1 -1
|
@@ -81,21 +81,226 @@ function decode(value) {
|
|
|
81
81
|
})
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Find an HTML comment terminator, including the parser's comment-end-bang
|
|
86
|
+
* form. An unterminated comment is treated as extending to the end of input.
|
|
87
|
+
* @param {string} html
|
|
88
|
+
* @param {number} start
|
|
89
|
+
* @returns {{ start: number, length: number } | null}
|
|
90
|
+
*/
|
|
91
|
+
function findCommentEnd(html, start) {
|
|
92
|
+
const normal = html.indexOf('-->', start + 4)
|
|
93
|
+
const bang = html.indexOf('--!>', start + 4)
|
|
94
|
+
if (normal === -1 && bang === -1) return null
|
|
95
|
+
if (bang !== -1 && (normal === -1 || bang < normal)) return { start: bang, length: 4 }
|
|
96
|
+
return { start: normal, length: 3 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove comments without leaving an unmatched comment opener that can expose
|
|
101
|
+
* markup to the lightweight tag reader.
|
|
102
|
+
* @param {string} html
|
|
103
|
+
*/
|
|
104
|
+
function stripHtmlComments(html) {
|
|
105
|
+
let clean = ''
|
|
106
|
+
let cursor = 0
|
|
107
|
+
while (cursor < html.length) {
|
|
108
|
+
const start = html.indexOf('<!--', cursor)
|
|
109
|
+
if (start === -1) return clean + html.slice(cursor)
|
|
110
|
+
clean += html.slice(cursor, start)
|
|
111
|
+
const end = findCommentEnd(html, start)
|
|
112
|
+
if (!end) return clean
|
|
113
|
+
cursor = end.start + end.length
|
|
114
|
+
}
|
|
115
|
+
return clean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {string} html @param {number} start */
|
|
119
|
+
function findTagEnd(html, start) {
|
|
120
|
+
let quote = ''
|
|
121
|
+
for (let index = start + 1; index < html.length; index += 1) {
|
|
122
|
+
const character = html[index]
|
|
123
|
+
if (quote) {
|
|
124
|
+
if (character === quote) quote = ''
|
|
125
|
+
} else if (character === '"' || character === "'") {
|
|
126
|
+
quote = character
|
|
127
|
+
} else if (character === '>') {
|
|
128
|
+
return index
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return -1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @param {string | undefined} character */
|
|
135
|
+
function isTagBoundary(character) {
|
|
136
|
+
return (
|
|
137
|
+
character === undefined || character === '>' || character === '/' || character.trim() === ''
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Locate a named HTML tag while accepting parser-tolerated end-tag junk such
|
|
143
|
+
* as `</script\\t\\n data>`.
|
|
144
|
+
* @param {string} html
|
|
145
|
+
* @param {string} name
|
|
146
|
+
* @param {number} from
|
|
147
|
+
* @param {boolean} closing
|
|
148
|
+
* @returns {{ start: number, end: number } | null}
|
|
149
|
+
*/
|
|
150
|
+
function findNamedTag(html, name, from, closing) {
|
|
151
|
+
const lowerName = name.toLowerCase()
|
|
152
|
+
for (let start = html.indexOf('<', from); start !== -1; start = html.indexOf('<', start + 1)) {
|
|
153
|
+
let nameStart = start + 1
|
|
154
|
+
if (closing) {
|
|
155
|
+
if (html[nameStart] !== '/') continue
|
|
156
|
+
nameStart += 1
|
|
157
|
+
} else if (html[nameStart] === '/' || html[nameStart] === '!' || html[nameStart] === '?') {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (html.slice(nameStart, nameStart + name.length).toLowerCase() !== lowerName) continue
|
|
161
|
+
if (!isTagBoundary(html[nameStart + name.length])) continue
|
|
162
|
+
return { start, end: findTagEnd(html, start) }
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const RAW_TEXT_TAGS = [
|
|
168
|
+
'script',
|
|
169
|
+
'style',
|
|
170
|
+
'textarea',
|
|
171
|
+
'template',
|
|
172
|
+
'xmp',
|
|
173
|
+
'iframe',
|
|
174
|
+
'noembed',
|
|
175
|
+
'noframes',
|
|
176
|
+
'noscript',
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} html
|
|
181
|
+
* @param {number} from
|
|
182
|
+
* @returns {{ start: number, end: number, name: string } | null}
|
|
183
|
+
*/
|
|
184
|
+
function findNextRawTextTag(html, from) {
|
|
185
|
+
let next = null
|
|
186
|
+
for (const name of RAW_TEXT_TAGS) {
|
|
187
|
+
const candidate = findNamedTag(html, name, from, false)
|
|
188
|
+
if (candidate && (!next || candidate.start < next.start)) {
|
|
189
|
+
next = { ...candidate, name }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return next
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Remove comments and raw-text contents in document order. Comment markers
|
|
197
|
+
* inside raw-text elements are data, not document comments.
|
|
198
|
+
* @param {string} html
|
|
199
|
+
*/
|
|
200
|
+
function stripCommentsAndRawText(html) {
|
|
201
|
+
let clean = ''
|
|
202
|
+
let cursor = 0
|
|
203
|
+
while (cursor < html.length) {
|
|
204
|
+
const commentStart = html.indexOf('<!--', cursor)
|
|
205
|
+
const rawText = findNextRawTextTag(html, cursor)
|
|
206
|
+
if (commentStart === -1 && !rawText) return clean + html.slice(cursor)
|
|
207
|
+
if (commentStart !== -1 && (!rawText || commentStart < rawText.start)) {
|
|
208
|
+
clean += html.slice(cursor, commentStart)
|
|
209
|
+
const commentEnd = findCommentEnd(html, commentStart)
|
|
210
|
+
if (!commentEnd) return clean
|
|
211
|
+
cursor = commentEnd.start + commentEnd.length
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
if (!rawText) return clean + html.slice(cursor)
|
|
215
|
+
clean += html.slice(cursor, rawText.start)
|
|
216
|
+
if (rawText.end === -1) return clean
|
|
217
|
+
clean += html.slice(rawText.start, rawText.end + 1)
|
|
218
|
+
const close = findNamedTag(html, rawText.name, rawText.end + 1, true)
|
|
219
|
+
if (!close || close.end === -1) return clean
|
|
220
|
+
cursor = close.end + 1
|
|
221
|
+
}
|
|
222
|
+
return clean
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Remove angle-bracket markup from title text. A dangling `<` is discarded
|
|
227
|
+
* through EOF instead of being left as a potentially active tag prefix.
|
|
228
|
+
* @param {string} value
|
|
229
|
+
*/
|
|
230
|
+
function stripAngleBracketMarkup(value) {
|
|
231
|
+
let clean = ''
|
|
232
|
+
let cursor = 0
|
|
233
|
+
while (cursor < value.length) {
|
|
234
|
+
const start = value.indexOf('<', cursor)
|
|
235
|
+
if (start === -1) return clean + value.slice(cursor)
|
|
236
|
+
clean += value.slice(cursor, start)
|
|
237
|
+
const end = value.indexOf('>', start + 1)
|
|
238
|
+
if (end === -1) return clean
|
|
239
|
+
cursor = end + 1
|
|
240
|
+
}
|
|
241
|
+
return clean
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Rewrite title bodies without relying on a multi-character HTML-filtering
|
|
246
|
+
* regexp. Malformed title elements are truncated so their contents cannot be
|
|
247
|
+
* interpreted as page metadata.
|
|
248
|
+
* @param {string} html
|
|
249
|
+
*/
|
|
250
|
+
function rewriteTitleMarkup(html) {
|
|
251
|
+
let clean = ''
|
|
252
|
+
let cursor = 0
|
|
253
|
+
while (cursor < html.length) {
|
|
254
|
+
const open = findNamedTag(html, 'title', cursor, false)
|
|
255
|
+
if (!open) return clean + html.slice(cursor)
|
|
256
|
+
clean += html.slice(cursor, open.start)
|
|
257
|
+
if (open.end === -1) return clean
|
|
258
|
+
clean += html.slice(open.start, open.end + 1)
|
|
259
|
+
const close = findNamedTag(html, 'title', open.end + 1, true)
|
|
260
|
+
if (!close || close.end === -1) return clean
|
|
261
|
+
clean += stripAngleBracketMarkup(html.slice(open.end + 1, close.start))
|
|
262
|
+
clean += html.slice(close.start, close.end + 1)
|
|
263
|
+
cursor = close.end + 1
|
|
264
|
+
}
|
|
265
|
+
return clean
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Return script bodies while respecting comments and parser-tolerated script
|
|
270
|
+
* end tags. An unclosed script consumes the remainder of the document.
|
|
271
|
+
* @param {string} html
|
|
272
|
+
*/
|
|
273
|
+
function readInlineScripts(html) {
|
|
274
|
+
/** @type {string[]} */
|
|
275
|
+
const scripts = []
|
|
276
|
+
let cursor = 0
|
|
277
|
+
while (cursor < html.length) {
|
|
278
|
+
const script = findNamedTag(html, 'script', cursor, false)
|
|
279
|
+
const commentStart = html.indexOf('<!--', cursor)
|
|
280
|
+
if (commentStart !== -1 && (!script || commentStart < script.start)) {
|
|
281
|
+
const commentEnd = findCommentEnd(html, commentStart)
|
|
282
|
+
if (!commentEnd) return scripts
|
|
283
|
+
cursor = commentEnd.start + commentEnd.length
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
if (!script || script.end === -1) return scripts
|
|
287
|
+
const contentStart = script.end + 1
|
|
288
|
+
const candidateClose = findNamedTag(html, 'script', contentStart, true)
|
|
289
|
+
const close = candidateClose?.end === -1 ? null : candidateClose
|
|
290
|
+
const contentEnd = close?.start ?? html.length
|
|
291
|
+
scripts.push(html.slice(contentStart, contentEnd))
|
|
292
|
+
if (!close || close.end === -1) return scripts
|
|
293
|
+
cursor = close.end + 1
|
|
294
|
+
}
|
|
295
|
+
return scripts
|
|
296
|
+
}
|
|
297
|
+
|
|
84
298
|
/** Conservative generated-HTML tag reader, not a DOM implementation.
|
|
85
299
|
* Raw-text HTML elements and comments cannot create fake metadata or links.
|
|
86
300
|
* @param {string} html
|
|
87
301
|
*/
|
|
88
302
|
export function readTags(html) {
|
|
89
|
-
const clean = html
|
|
90
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
91
|
-
.replace(
|
|
92
|
-
/(<(script|style|textarea|template|xmp|iframe|noembed|noframes|noscript)\b(?:"[^"]*"|'[^']*'|[^'">])*>)[\s\S]*?<\/\2\s*>/gi,
|
|
93
|
-
'$1'
|
|
94
|
-
)
|
|
95
|
-
.replace(
|
|
96
|
-
/(<title\b(?:"[^"]*"|'[^']*'|[^'">])*>)([\s\S]*?)<\/title\s*>/gi,
|
|
97
|
-
(_, open, content) => `${open}${content.replace(/<[^>]*>/g, '')}</title>`
|
|
98
|
-
)
|
|
303
|
+
const clean = rewriteTitleMarkup(stripCommentsAndRawText(html))
|
|
99
304
|
/** @type {Array<{ name: string, attrs: Record<string,string> }>} */
|
|
100
305
|
const tags = []
|
|
101
306
|
for (const match of clean.matchAll(/<([a-z][a-z0-9:-]*)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi)) {
|
|
@@ -245,11 +450,10 @@ export function checkStaticSite(root, profile) {
|
|
|
245
450
|
requireValue(lstatSync(file).isFile(), 'Budget assets must be regular files')
|
|
246
451
|
return sum + statSync(file).size
|
|
247
452
|
}, 0)
|
|
248
|
-
const inlineBytes =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
].reduce((sum, match) => sum + Buffer.byteLength(match[1]), 0)
|
|
453
|
+
const inlineBytes = readInlineScripts(readFileSync(page.file, 'utf8')).reduce(
|
|
454
|
+
(sum, script) => sum + Buffer.byteLength(script),
|
|
455
|
+
0
|
|
456
|
+
)
|
|
253
457
|
const result = {
|
|
254
458
|
route: path,
|
|
255
459
|
htmlBytes: statSync(page.file).size,
|
|
@@ -267,7 +471,7 @@ export function checkStaticSite(root, profile) {
|
|
|
267
471
|
if (profile.sitemap) {
|
|
268
472
|
const sitemap = readFileSync(ownedPath(dist, profile.sitemap), 'utf8')
|
|
269
473
|
requireValue(!/<!DOCTYPE|<!ENTITY/i.test(sitemap), 'Sitemap entities are not supported')
|
|
270
|
-
const sitemapWithoutComments = sitemap
|
|
474
|
+
const sitemapWithoutComments = stripHtmlComments(sitemap)
|
|
271
475
|
const locations = new Set(
|
|
272
476
|
[
|
|
273
477
|
...sitemapWithoutComments.matchAll(
|
package/src/runtime-core.mjs
CHANGED
|
@@ -98,7 +98,8 @@ function selectedPerformanceCommands() {
|
|
|
98
98
|
const name = ['performance:check', 'perf:check'].find((candidate) => hasScript(candidate))
|
|
99
99
|
if (name) {
|
|
100
100
|
const [manager, args] = packageCommand(['run', name])
|
|
101
|
-
|
|
101
|
+
if (!manager) throw new Error(`Cannot run ${name}: select a supported package_manager.`)
|
|
102
|
+
return [{ source: `package-script:${name}`, argv: [manager, ...args] }]
|
|
102
103
|
}
|
|
103
104
|
return performanceCommands().map((argv) => ({ source: 'configuration', argv }))
|
|
104
105
|
}
|
|
@@ -600,15 +601,18 @@ function ci(task) {
|
|
|
600
601
|
|
|
601
602
|
if (hasLanguage('rust') && hasRootRustProject()) {
|
|
602
603
|
if (task === 'unit') {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (
|
|
606
|
-
|
|
604
|
+
// Batch exact prior targets through one Cargo graph.
|
|
605
|
+
const args = ['test']
|
|
606
|
+
if (existsSync(resolve(root, 'src/lib.rs'))) args.push('--lib')
|
|
607
|
+
if (existsSync(resolve(root, 'src/main.rs'))) args.push('--bin', packageName())
|
|
608
|
+
run('cargo', args)
|
|
607
609
|
} else {
|
|
608
|
-
|
|
610
|
+
// Keep --test selection narrow; --tests includes unit targets.
|
|
611
|
+
const targets = rustTests.flatMap((file) => {
|
|
609
612
|
const match = file.match(/^tests\/(.+)\.rs$/)
|
|
610
|
-
|
|
611
|
-
}
|
|
613
|
+
return match && !match[1].includes('/') ? ['--test', match[1]] : []
|
|
614
|
+
})
|
|
615
|
+
if (targets.length > 0) run('cargo', ['test', ...targets])
|
|
612
616
|
}
|
|
613
617
|
}
|
|
614
618
|
}
|
package/src/runtime.mjs
CHANGED
|
@@ -81,7 +81,7 @@ function sourceSha(root) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* Stable public runtime; the
|
|
84
|
+
* Stable public runtime; the native executor stays separate from policy and evidence.
|
|
85
85
|
* Tests can substitute an executor without installing consumer dependencies.
|
|
86
86
|
* @param {string[]} args @param {string} [root] @param {string} [entry]
|
|
87
87
|
* @returns {number}
|