klenod-plugin-javascript 0.0.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.
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "native"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "native"
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ magnus = "0.8.2"
12
+ swc_core = { version = "73.0.0", features = ["common", "ecma_ast", "ecma_codegen", "ecma_parser", "ecma_parser_typescript", "ecma_transforms_react", "ecma_transforms_typescript", "ecma_visit"] }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("klenod/plugin/javascript/native")
@@ -0,0 +1,418 @@
1
+ use magnus::{function, prelude::*, Error, RArray, RHash, Ruby, TryConvert};
2
+ use swc_core::{
3
+ common::{
4
+ comments::NoopComments, sync::Lrc, FileName, Globals, Mark, SourceMap, Span, GLOBALS,
5
+ },
6
+ ecma::{
7
+ ast::{
8
+ Callee, ExportAll, ExportNamedSpecifier, ExportSpecifier, Expr, ImportDecl, Lit,
9
+ ModuleDecl, ModuleItem, Pass, Program,
10
+ },
11
+ codegen::{text_writer::JsWriter, Emitter},
12
+ parser::{lexer::Lexer, EsSyntax, Parser, StringInput, Syntax, TsSyntax},
13
+ transforms::{react, typescript::strip},
14
+ visit::{Visit, VisitWith},
15
+ },
16
+ };
17
+
18
+ #[derive(Clone)]
19
+ struct ImportRecord {
20
+ specifier: String,
21
+ kind: &'static str,
22
+ start_offset: usize,
23
+ end_offset: usize,
24
+ attributes: Vec<(String, String)>,
25
+ loc: String,
26
+ }
27
+
28
+ struct ImportVisitor<'a> {
29
+ source_map: Lrc<SourceMap>,
30
+ source_start: u32,
31
+ filename: &'a str,
32
+ records: Vec<ImportRecord>,
33
+ }
34
+
35
+ impl<'a> ImportVisitor<'a> {
36
+ fn push(
37
+ &mut self,
38
+ specifier: &str,
39
+ kind: &'static str,
40
+ span: Span,
41
+ attributes: Vec<(String, String)>,
42
+ ) {
43
+ let lo = span.lo.0.saturating_sub(self.source_start) as usize;
44
+ let hi = span.hi.0.saturating_sub(self.source_start) as usize;
45
+
46
+ self.records.push(ImportRecord {
47
+ specifier: specifier.to_string(),
48
+ kind,
49
+ start_offset: lo + 1,
50
+ end_offset: hi.saturating_sub(1),
51
+ attributes,
52
+ loc: self.loc(span),
53
+ });
54
+ }
55
+
56
+ fn loc(&self, span: Span) -> String {
57
+ let loc = self.source_map.lookup_char_pos(span.lo);
58
+ format!("{}:{}:{}", self.filename, loc.line, loc.col_display + 1)
59
+ }
60
+ }
61
+
62
+ impl Visit for ImportVisitor<'_> {
63
+ fn visit_module_item(&mut self, item: &ModuleItem) {
64
+ match item {
65
+ ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
66
+ self.push(
67
+ import.src.value.to_string_lossy().as_ref(),
68
+ "javascript_import",
69
+ import.src.span,
70
+ import_attributes(import),
71
+ );
72
+ }
73
+ ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
74
+ if let Some(src) = &export.src {
75
+ self.push(
76
+ src.value.to_string_lossy().as_ref(),
77
+ "javascript_export",
78
+ src.span,
79
+ Vec::new(),
80
+ );
81
+ }
82
+
83
+ for specifier in &export.specifiers {
84
+ if let ExportSpecifier::Named(ExportNamedSpecifier {
85
+ is_type_only: true, ..
86
+ }) = specifier
87
+ {
88
+ continue;
89
+ }
90
+ }
91
+ }
92
+ ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll { src, .. })) => {
93
+ self.push(
94
+ src.value.to_string_lossy().as_ref(),
95
+ "javascript_export",
96
+ src.span,
97
+ Vec::new(),
98
+ );
99
+ }
100
+ _ => {
101
+ item.visit_children_with(self);
102
+ }
103
+ }
104
+ }
105
+
106
+ fn visit_call_expr(&mut self, call: &swc_core::ecma::ast::CallExpr) {
107
+ if matches!(call.callee, Callee::Import(_)) {
108
+ if let Some(first_arg) = call.args.first() {
109
+ if let Expr::Lit(Lit::Str(string)) = &*first_arg.expr {
110
+ self.push(
111
+ string.value.to_string_lossy().as_ref(),
112
+ "javascript_dynamic_import",
113
+ string.span,
114
+ Vec::new(),
115
+ );
116
+ return;
117
+ }
118
+ }
119
+ }
120
+
121
+ call.visit_children_with(self);
122
+ }
123
+ }
124
+
125
+ fn parse_native(ruby: &Ruby, source: String, filename: String) -> Result<RArray, Error> {
126
+ let parsed = parse_module(ruby, source, filename.clone(), SourceKind::JavaScript)?;
127
+ records_to_array(
128
+ ruby,
129
+ import_records(
130
+ parsed.source_map,
131
+ parsed.source_start,
132
+ &filename,
133
+ &parsed.program,
134
+ ),
135
+ )
136
+ }
137
+
138
+ fn transform_native(
139
+ ruby: &Ruby,
140
+ source: String,
141
+ filename: String,
142
+ source_kind: String,
143
+ options: RHash,
144
+ ) -> Result<RHash, Error> {
145
+ let source_kind = SourceKind::from_string(ruby, &source_kind)?;
146
+ let minify = hash_fetch_bool(options, "minify", false)?;
147
+ let jsx_runtime_namespace =
148
+ hash_fetch_string(options, "jsx_runtime_namespace", "__klenod_jsx")?;
149
+ let transformed_source = transform_source(
150
+ ruby,
151
+ source,
152
+ filename.clone(),
153
+ source_kind,
154
+ minify,
155
+ &jsx_runtime_namespace,
156
+ )?;
157
+ let parsed = parse_module(
158
+ ruby,
159
+ transformed_source.clone(),
160
+ filename.clone(),
161
+ SourceKind::JavaScript,
162
+ )?;
163
+ let hash = ruby.hash_new();
164
+ hash.aset("code", transformed_source)?;
165
+ hash.aset(
166
+ "imports",
167
+ records_to_array(
168
+ ruby,
169
+ import_records(
170
+ parsed.source_map,
171
+ parsed.source_start,
172
+ &filename,
173
+ &parsed.program,
174
+ ),
175
+ )?,
176
+ )?;
177
+ Ok(hash)
178
+ }
179
+
180
+ #[derive(Clone, Copy)]
181
+ enum SourceKind {
182
+ JavaScript,
183
+ TypeScript,
184
+ JavaScriptJsx,
185
+ TypeScriptJsx,
186
+ }
187
+
188
+ impl SourceKind {
189
+ fn from_string(ruby: &Ruby, source_kind: &str) -> Result<Self, Error> {
190
+ match source_kind {
191
+ "javascript" => Ok(Self::JavaScript),
192
+ "typescript" => Ok(Self::TypeScript),
193
+ "javascript_jsx" => Ok(Self::JavaScriptJsx),
194
+ "typescript_jsx" => Ok(Self::TypeScriptJsx),
195
+ _ => Err(Error::new(
196
+ ruby.exception_arg_error(),
197
+ format!("unknown JavaScript source kind: {}", source_kind),
198
+ )),
199
+ }
200
+ }
201
+ }
202
+
203
+ struct ParsedProgram {
204
+ source_map: Lrc<SourceMap>,
205
+ source_start: u32,
206
+ program: Program,
207
+ }
208
+
209
+ fn parse_module(
210
+ ruby: &Ruby,
211
+ source: String,
212
+ filename: String,
213
+ source_kind: SourceKind,
214
+ ) -> Result<ParsedProgram, Error> {
215
+ let source_map: Lrc<SourceMap> = Default::default();
216
+ let source_file = source_map.new_source_file(FileName::Custom(filename.clone()).into(), source);
217
+ let lexer = Lexer::new(
218
+ syntax_for(source_kind),
219
+ Default::default(),
220
+ StringInput::from(&*source_file),
221
+ None,
222
+ );
223
+ let mut parser = Parser::new_from(lexer);
224
+ let module = parser.parse_module().map_err(|error| {
225
+ Error::new(
226
+ ruby.exception_syntax_error(),
227
+ format!("{}: {}", filename, error.kind().msg()),
228
+ )
229
+ })?;
230
+ Ok(ParsedProgram {
231
+ source_map,
232
+ source_start: source_file.start_pos.0,
233
+ program: Program::Module(module),
234
+ })
235
+ }
236
+
237
+ fn syntax_for(source_kind: SourceKind) -> Syntax {
238
+ match source_kind {
239
+ SourceKind::JavaScript => Syntax::Es(EsSyntax {
240
+ import_attributes: true,
241
+ ..Default::default()
242
+ }),
243
+ SourceKind::JavaScriptJsx => Syntax::Es(EsSyntax {
244
+ import_attributes: true,
245
+ jsx: true,
246
+ ..Default::default()
247
+ }),
248
+ SourceKind::TypeScript => Syntax::Typescript(TsSyntax {
249
+ tsx: false,
250
+ decorators: true,
251
+ ..Default::default()
252
+ }),
253
+ SourceKind::TypeScriptJsx => Syntax::Typescript(TsSyntax {
254
+ tsx: true,
255
+ decorators: true,
256
+ ..Default::default()
257
+ }),
258
+ }
259
+ }
260
+
261
+ fn import_records(
262
+ source_map: Lrc<SourceMap>,
263
+ source_start: u32,
264
+ filename: &str,
265
+ program: &Program,
266
+ ) -> Vec<ImportRecord> {
267
+ let mut visitor = ImportVisitor {
268
+ source_map,
269
+ source_start,
270
+ filename,
271
+ records: Vec::new(),
272
+ };
273
+ program.visit_with(&mut visitor);
274
+
275
+ visitor.records
276
+ }
277
+
278
+ fn import_attributes(import: &ImportDecl) -> Vec<(String, String)> {
279
+ import
280
+ .with
281
+ .as_ref()
282
+ .and_then(|with| with.as_import_with())
283
+ .map(|with| {
284
+ with.values
285
+ .iter()
286
+ .map(|item| {
287
+ (
288
+ item.key.sym.to_string(),
289
+ item.value.value.to_string_lossy().to_string(),
290
+ )
291
+ })
292
+ .collect()
293
+ })
294
+ .unwrap_or_default()
295
+ }
296
+
297
+ fn records_to_array(ruby: &Ruby, records: Vec<ImportRecord>) -> Result<RArray, Error> {
298
+ let array = ruby.ary_new();
299
+ for record in records {
300
+ let hash = ruby.hash_new();
301
+ hash.aset("specifier", record.specifier)?;
302
+ hash.aset("kind", record.kind)?;
303
+ hash.aset("start_offset", record.start_offset)?;
304
+ hash.aset("end_offset", record.end_offset)?;
305
+ let attributes = ruby.hash_new();
306
+ for (key, value) in record.attributes {
307
+ attributes.aset(key, value)?;
308
+ }
309
+ hash.aset("attributes", attributes)?;
310
+ hash.aset("loc", record.loc)?;
311
+ array.push(hash)?;
312
+ }
313
+ Ok(array)
314
+ }
315
+
316
+ fn transform_source(
317
+ ruby: &Ruby,
318
+ source: String,
319
+ filename: String,
320
+ source_kind: SourceKind,
321
+ minify: bool,
322
+ jsx_runtime_namespace: &str,
323
+ ) -> Result<String, Error> {
324
+ if !source_kind.needs_transform() && !minify {
325
+ return Ok(source);
326
+ }
327
+
328
+ let parsed = parse_module(ruby, source, filename, source_kind)?;
329
+ let source_map = parsed.source_map;
330
+ let mut program = parsed.program;
331
+
332
+ GLOBALS.set(&Globals::default(), || {
333
+ let unresolved_mark = Mark::new();
334
+ let top_level_mark = Mark::new();
335
+ if source_kind.is_typescript() {
336
+ strip(unresolved_mark, top_level_mark).process(&mut program);
337
+ }
338
+ if source_kind.is_jsx() {
339
+ react::jsx(
340
+ source_map.clone(),
341
+ None::<NoopComments>,
342
+ react::Options {
343
+ pragma: Some(format!("{}.h", jsx_runtime_namespace).into()),
344
+ pragma_frag: Some(format!("{}.Fragment", jsx_runtime_namespace).into()),
345
+ ..Default::default()
346
+ },
347
+ top_level_mark,
348
+ unresolved_mark,
349
+ )
350
+ .process(&mut program);
351
+ }
352
+ });
353
+
354
+ let mut bytes = Vec::new();
355
+ {
356
+ let writer = JsWriter::new(source_map.clone(), "\n", &mut bytes, None);
357
+ let mut emitter = Emitter {
358
+ cfg: swc_core::ecma::codegen::Config::default().with_minify(minify),
359
+ cm: source_map,
360
+ comments: None,
361
+ wr: writer,
362
+ };
363
+ emitter.emit_program(&program).map_err(|error| {
364
+ Error::new(
365
+ ruby.exception_runtime_error(),
366
+ format!("failed to emit JavaScript from TypeScript: {}", error),
367
+ )
368
+ })?;
369
+ }
370
+
371
+ String::from_utf8(bytes).map_err(|error| {
372
+ Error::new(
373
+ ruby.exception_runtime_error(),
374
+ format!("SWC generated invalid UTF-8: {}", error),
375
+ )
376
+ })
377
+ }
378
+
379
+ fn hash_fetch_bool(hash: RHash, key: &str, default: bool) -> Result<bool, Error> {
380
+ match hash.get(key) {
381
+ Some(value) => bool::try_convert(value),
382
+ None => Ok(default),
383
+ }
384
+ }
385
+
386
+ fn hash_fetch_string(hash: RHash, key: &str, default: &str) -> Result<String, Error> {
387
+ match hash.get(key) {
388
+ Some(value) => Option::<String>::try_convert(value)
389
+ .map(|value| value.unwrap_or_else(|| default.to_string())),
390
+ None => Ok(default.to_string()),
391
+ }
392
+ }
393
+
394
+ impl SourceKind {
395
+ fn needs_transform(self) -> bool {
396
+ self.is_typescript() || self.is_jsx()
397
+ }
398
+
399
+ fn is_typescript(self) -> bool {
400
+ matches!(self, SourceKind::TypeScript | SourceKind::TypeScriptJsx)
401
+ }
402
+
403
+ fn is_jsx(self) -> bool {
404
+ matches!(self, SourceKind::JavaScriptJsx | SourceKind::TypeScriptJsx)
405
+ }
406
+ }
407
+
408
+ #[magnus::init]
409
+ fn init(ruby: &Ruby) -> Result<(), Error> {
410
+ let klenod = ruby.define_module("Klenod")?;
411
+ let build = klenod.define_module("Build")?;
412
+ let plugins = build.define_module("Plugins")?;
413
+ let javascript_plugin = plugins.define_module("JavaScriptPlugin")?;
414
+ let native = javascript_plugin.define_module("Native")?;
415
+ native.define_singleton_method("parse_native", function!(parse_native, 2))?;
416
+ native.define_singleton_method("transform_native", function!(transform_native, 4))?;
417
+ Ok(())
418
+ }