presolve_compiler/
production_module_emitter.rs1use std::collections::BTreeMap;
4use std::fmt::Write as _;
5
6use sha2::{Digest, Sha256};
7
8use crate::{ProductionChunkGraph, ProductionChunkId, ProductionChunkKind};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ProductionModuleRecord {
12 pub chunk_id: ProductionChunkId,
13 pub filename: String,
14 pub source: String,
15 pub exports: Vec<String>,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ProductionModuleLayout {
20 pub eager: ProductionModuleRecord,
21 pub shared: Vec<ProductionModuleRecord>,
22 pub roots: Vec<ProductionModuleRecord>,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum ProductionModuleValidationError {
27 DynamicCode,
28 InvalidExport,
29 NonCanonicalImportOrder,
30 SourceComment,
31}
32
33#[must_use]
40pub fn emit_production_modules(graph: &ProductionChunkGraph) -> ProductionModuleLayout {
41 let eager_chunk = graph
42 .chunks
43 .iter()
44 .find(|chunk| chunk.kind == ProductionChunkKind::Eager)
45 .expect("validated graph contains one eager chunk");
46 let eager = module(eager_chunk.id.clone(), "boot", Vec::new());
47 let shared_by_id = graph
48 .chunks
49 .iter()
50 .filter(|chunk| chunk.kind == ProductionChunkKind::Shared)
51 .map(|chunk| {
52 (
53 chunk.id.clone(),
54 module(chunk.id.clone(), "shared", vec![eager.filename.clone()]),
55 )
56 })
57 .collect::<BTreeMap<_, _>>();
58 let shared = shared_by_id.values().cloned().collect::<Vec<_>>();
59 let mut roots = graph
60 .chunks
61 .iter()
62 .filter(|chunk| chunk.kind == ProductionChunkKind::Root)
63 .map(|chunk| {
64 let mut imports = graph
65 .dependencies
66 .iter()
67 .filter(|edge| edge.dependent_chunk_id == chunk.id)
68 .filter_map(|edge| shared_by_id.get(&edge.dependency_chunk_id))
69 .map(|shared| shared.filename.clone())
70 .collect::<Vec<_>>();
71 imports.push(eager.filename.clone());
72 module(chunk.id.clone(), "root", imports)
73 })
74 .collect::<Vec<_>>();
75 roots.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
76 ProductionModuleLayout {
77 eager,
78 shared,
79 roots,
80 }
81}
82
83#[must_use]
85pub fn validate_production_module(
86 module: &ProductionModuleRecord,
87) -> Vec<ProductionModuleValidationError> {
88 let mut errors = Vec::new();
89 if module.source.contains("eval(")
90 || module.source.contains("Function(")
91 || module.source.contains("import(")
92 {
93 errors.push(ProductionModuleValidationError::DynamicCode);
94 }
95 if module.source.contains("//") || module.source.contains("/*") {
96 errors.push(ProductionModuleValidationError::SourceComment);
97 }
98 if module.exports
99 != vec![
100 "productionChunkId".to_string(),
101 "registerProductionChunk".to_string(),
102 ]
103 {
104 errors.push(ProductionModuleValidationError::InvalidExport);
105 }
106 let imports = module
107 .source
108 .lines()
109 .filter_map(|line| {
110 line.strip_prefix("import{registerProductionChunk as a}from\"./")
111 .and_then(|line| line.strip_suffix("\";"))
112 })
113 .collect::<Vec<_>>();
114 if imports.windows(2).any(|pair| pair[0] > pair[1]) {
115 errors.push(ProductionModuleValidationError::NonCanonicalImportOrder);
116 }
117 errors
118}
119
120fn module(
121 chunk_id: ProductionChunkId,
122 prefix: &str,
123 mut imports: Vec<String>,
124) -> ProductionModuleRecord {
125 imports.sort();
126 imports.dedup();
127 let mut import_text = String::new();
128 for filename in &imports {
129 write!(
130 import_text,
131 "import{{registerProductionChunk as a}}from\"./{filename}\";"
132 )
133 .expect("writing to a string should not fail");
134 }
135 let source = format!(
136 "{import_text}export const productionChunkId=\"{chunk_id}\";export function registerProductionChunk(){{return productionChunkId;}}\n"
137 );
138 let filename = format!("{prefix}.{}.js", short_hash(&source));
139 ProductionModuleRecord {
140 chunk_id,
141 filename,
142 source,
143 exports: vec![
144 "productionChunkId".to_string(),
145 "registerProductionChunk".to_string(),
146 ],
147 }
148}
149
150fn short_hash(source: &str) -> String {
151 format!("{:x}", Sha256::digest(source.as_bytes()))[..16].to_string()
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::{
158 extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionRootChunkInput,
159 SharedChunkCandidatePlan,
160 };
161
162 #[test]
163 fn k9_emits_deterministic_syntax_safe_modules() {
164 let graph = extract_production_chunk_graph(
165 &SharedChunkCandidatePlan {
166 candidates: Vec::new(),
167 rejections: Vec::new(),
168 },
169 &[ProductionRootChunkInput {
170 activation_root_id: "root".to_string(),
171 root_kind: "interaction".to_string(),
172 programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
173 b"return",
174 )],
175 }],
176 )
177 .expect("graph")
178 .0;
179 let first = emit_production_modules(&graph);
180 let second = emit_production_modules(&graph);
181 assert_eq!(first, second);
182 assert!(first.eager.filename.starts_with("boot."));
183 assert!(first
184 .roots
185 .iter()
186 .all(|module| validate_production_module(module).is_empty()));
187 assert!(validate_production_module(&first.eager).is_empty());
188 }
189}