proscenium 0.1.0.alpha1-x86_64-linux

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/README.md +91 -0
  4. data/lib/proscenium/cli/argument_error.js +24 -0
  5. data/lib/proscenium/cli/builders/index.js +1 -0
  6. data/lib/proscenium/cli/builders/javascript.js +45 -0
  7. data/lib/proscenium/cli/builders/react.js +60 -0
  8. data/lib/proscenium/cli/builders/solid.js +46 -0
  9. data/lib/proscenium/cli/esbuild/env_plugin.js +21 -0
  10. data/lib/proscenium/cli/esbuild/resolve_plugin.js +136 -0
  11. data/lib/proscenium/cli/esbuild/solidjs_plugin.js +23 -0
  12. data/lib/proscenium/cli/js_builder.js +194 -0
  13. data/lib/proscenium/cli/solid.js +15 -0
  14. data/lib/proscenium/cli/utils.js +93 -0
  15. data/lib/proscenium/compiler.js +84 -0
  16. data/lib/proscenium/compilers/esbuild/argument_error.js +22 -0
  17. data/lib/proscenium/compilers/esbuild/env_plugin.js +21 -0
  18. data/lib/proscenium/compilers/esbuild/resolve_plugin.js +145 -0
  19. data/lib/proscenium/compilers/esbuild/setup_plugin.js +35 -0
  20. data/lib/proscenium/compilers/esbuild.bench.js +9 -0
  21. data/lib/proscenium/compilers/esbuild.js +82 -0
  22. data/lib/proscenium/css_module.rb +22 -0
  23. data/lib/proscenium/current.rb +9 -0
  24. data/lib/proscenium/helper.rb +32 -0
  25. data/lib/proscenium/link_to_helper.rb +49 -0
  26. data/lib/proscenium/middleware/base.rb +94 -0
  27. data/lib/proscenium/middleware/esbuild.rb +27 -0
  28. data/lib/proscenium/middleware/parcel_css.rb +37 -0
  29. data/lib/proscenium/middleware/runtime.rb +22 -0
  30. data/lib/proscenium/middleware/static.rb +14 -0
  31. data/lib/proscenium/middleware.rb +66 -0
  32. data/lib/proscenium/precompile.rb +31 -0
  33. data/lib/proscenium/railtie.rb +116 -0
  34. data/lib/proscenium/runtime/auto_reload.js +22 -0
  35. data/lib/proscenium/runtime/component_manager/index.js +27 -0
  36. data/lib/proscenium/runtime/component_manager/render_component.js +40 -0
  37. data/lib/proscenium/runtime/import_css.js +46 -0
  38. data/lib/proscenium/runtime/react_shim/index.js +1 -0
  39. data/lib/proscenium/runtime/react_shim/package.json +5 -0
  40. data/lib/proscenium/side_load.rb +96 -0
  41. data/lib/proscenium/version.rb +5 -0
  42. data/lib/proscenium/view_component/tag_builder.rb +23 -0
  43. data/lib/proscenium/view_component.rb +38 -0
  44. data/lib/proscenium.rb +18 -0
  45. data/lib/tasks/assets.rake +19 -0
  46. metadata +179 -0
@@ -0,0 +1,194 @@
1
+ import * as esbuild from 'https://deno.land/x/esbuild@v0.14.27/mod.js'
2
+ import { parse } from 'flags/mod.ts'
3
+ import { join } from 'path/mod.ts'
4
+
5
+ import resolvePlugin from './esbuild/resolve_plugin.js'
6
+
7
+ const isProd = Deno.env.get('RAILS_ENV') === 'production'
8
+ const isTest = Deno.env.get('RAILS_ENV') === 'test'
9
+ const template = `
10
+ <style>
11
+ :host {
12
+ position: fixed;
13
+ z-index: 99999;
14
+ top: 0;
15
+ left: 0;
16
+ width: 100%;
17
+ height: 100%;
18
+ overflow-y: scroll;
19
+ margin: 0;
20
+ background: rgba(0, 0, 0, 0.66);
21
+ display: flex;
22
+ align-items: center;
23
+ --monospace: 'SFMono-Regular', Consolas,
24
+ 'Liberation Mono', Menlo, Courier, monospace;
25
+ --red: #ff5555;
26
+ --yellow: #e2aa53;
27
+ --purple: #cfa4ff;
28
+ --cyan: #2dd9da;
29
+ --dim: #c9c9c9;
30
+ }
31
+
32
+ .window {
33
+ font-family: var(--monospace);
34
+ line-height: 1.5;
35
+ width: 800px;
36
+ color: #d8d8d8;
37
+ margin: 30px auto;
38
+ padding: 25px 40px;
39
+ position: relative;
40
+ background: #181818;
41
+ border-radius: 6px 6px 8px 8px;
42
+ box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
43
+ overflow: hidden;
44
+ border-top: 8px solid var(--red);
45
+ }
46
+
47
+ pre {
48
+ font-family: var(--monospace);
49
+ font-size: 16px;
50
+ margin-top: 0;
51
+ margin-bottom: 1em;
52
+ overflow-x: scroll;
53
+ scrollbar-width: none;
54
+ }
55
+
56
+ pre::-webkit-scrollbar {
57
+ display: none;
58
+ }
59
+
60
+ .message {
61
+ line-height: 1.3;
62
+ font-weight: 600;
63
+ white-space: pre-wrap;
64
+ }
65
+
66
+ .message-body {
67
+ color: var(--red);
68
+ }
69
+
70
+ .file {
71
+ color: var(--cyan);
72
+ margin-bottom: 0;
73
+ white-space: pre-wrap;
74
+ word-break: break-all;
75
+ }
76
+
77
+ </style>
78
+ <div class="window">
79
+ <pre class="message"><span class="message-body"></span></pre>
80
+ <pre class="file"></pre>
81
+ </div>
82
+ `
83
+
84
+ export default async (args, { debug }) => {
85
+ const { _: entrypoints, ...flags } = parse(args)
86
+ const [cwd, entrypoint] = validatePaths(entrypoints)
87
+
88
+ const params = {
89
+ entryPoints: [entrypoint],
90
+ absWorkingDir: cwd,
91
+ logLevel: 'error',
92
+ sourcemap: isTest ? false : 'inline',
93
+ write: false,
94
+ format: 'esm',
95
+ // minify: true,
96
+ bundle: true,
97
+ plugins: [resolvePlugin({ debug })]
98
+ }
99
+
100
+ if (entrypoint.endsWith('.jsx')) {
101
+ try {
102
+ const stat = Deno.lstatSync(join(cwd, 'lib/react_shim.js'))
103
+ if (stat.isFile) {
104
+ params.inject = ['./lib/react_shim.js']
105
+ }
106
+ } catch {
107
+ // Safe to swallow as this should only throw if file does not exist.
108
+ }
109
+ }
110
+
111
+ try {
112
+ const result = await esbuild.build(params)
113
+ return result.outputFiles[0].contents
114
+
115
+ // } catch (e) {
116
+ // if (isProd) {
117
+ // return new TextEncoder().encode(`
118
+ // const err = ${JSON.stringify(e.errors[0])}
119
+ // const location = \`\${err.location.file}:\${err.location.line}:\${err.location.column}\`
120
+ // console.error('%s at %O', err.text, location);
121
+ // `)
122
+ // } else {
123
+ // return new TextEncoder().encode(`
124
+ // class ErrorOverlay extends HTMLElement {
125
+ // constructor(err) {
126
+ // super()
127
+
128
+ // this.root = this.attachShadow({ mode: 'open' })
129
+ // this.root.innerHTML = \`${template}\`
130
+ // this.root.querySelector('.message-body').textContent = err.text.trim()
131
+
132
+ // if (err.location) {
133
+ // const location = \`\${err.location.file}:\${err.location.line}:\${err.location.column}\`
134
+ // this.root.querySelector('.file').textContent = location
135
+ // console.error('%s at %O', err.text, location)
136
+ // } else {
137
+ // console.error(err.text)
138
+ // }
139
+
140
+ // throw err
141
+ // }
142
+ // }
143
+
144
+ // customElements.define('error-overlay', ErrorOverlay)
145
+ // document.body.appendChild(new ErrorOverlay(${JSON.stringify(e.errors[0])}))
146
+ // `)
147
+ // }
148
+ } finally {
149
+ esbuild.stop()
150
+ }
151
+ }
152
+
153
+ function validatePaths(paths) {
154
+ const cwd = paths[0]
155
+ const entrypoint = paths[1]
156
+
157
+ if (!cwd || !entrypoint) {
158
+ throw new TypeError(
159
+ 'Current working directory and entrypoint are required as first and second arguments.'
160
+ )
161
+ }
162
+
163
+ try {
164
+ const stat = Deno.lstatSync(cwd)
165
+ if (!stat.isDirectory) {
166
+ throw new TypeError(
167
+ `Current working directory is required as the first argument - received ${cwd}`
168
+ )
169
+ }
170
+ } catch {
171
+ throw new TypeError(
172
+ `A valid working directory is required as the first argument - received ${cwd}`
173
+ )
174
+ }
175
+
176
+ try {
177
+ const stat = Deno.lstatSync(join(cwd, entrypoint))
178
+ if (!stat.isFile) {
179
+ throw new TypeError(`Entrypoint is required as the second argument - received ${entrypoint}`)
180
+ }
181
+ } catch {
182
+ throw new TypeError(
183
+ `A valid entrypoint is required as the second argument - received ${entrypoint}`
184
+ )
185
+ }
186
+
187
+ if (/\.(js|jsx)$/.test(entrypoint) === false) {
188
+ throw new TypeError(
189
+ `Only a JS/JSX entrypoint is supported with this CLI - received ${entrypoint}`
190
+ )
191
+ }
192
+
193
+ return [cwd, entrypoint]
194
+ }
@@ -0,0 +1,15 @@
1
+ import { writeAll } from 'std/streams/mod.ts'
2
+ import { parseArgs } from './utils.js'
3
+
4
+ import builder from './builders/solid.js'
5
+
6
+ if (import.meta.main) {
7
+ await writeAll(Deno.stdout, await main(Deno.args))
8
+ }
9
+
10
+ async function main(args = []) {
11
+ const [cwd, entrypoint, _] = parseArgs(args)
12
+ return await builder(cwd, entrypoint)
13
+ }
14
+
15
+ export default main
@@ -0,0 +1,93 @@
1
+ import { join } from 'std/path/mod.ts'
2
+
3
+ import CliArgumentError from './argument_error.js'
4
+ import { builderNames } from './builders/index.js'
5
+
6
+ export const isTest = () => Deno.env.get('ENVIRONMENT') === 'test'
7
+
8
+ export const debug = (...args) => {
9
+ isTest() && console.log(...args)
10
+ }
11
+
12
+ export const setup = (pluginName, pluginFn) => {
13
+ return (options = {}) => ({
14
+ name: pluginName,
15
+ setup(build) {
16
+ const plugin = pluginFn(build)
17
+
18
+ if (plugin.onResolve) {
19
+ const { callback, ...onResolve } = plugin.onResolve
20
+
21
+ build.onResolve(onResolve, async params => {
22
+ if (params.pluginData?.isResolvingPath) return
23
+
24
+ const results = await callback(params)
25
+
26
+ options.debug && console.debug(`plugin(${pluginName}:onResolve)`, { params, results })
27
+
28
+ return results
29
+ })
30
+ }
31
+
32
+ if (plugin.onLoad) {
33
+ const { callback, ...onLoad } = plugin.onLoad
34
+
35
+ build.onLoad(onLoad, params => {
36
+ const results = callback(params)
37
+
38
+ options.debug && console.debug(`plugin(${pluginName}:onLoad)`, { params, results })
39
+
40
+ return results
41
+ })
42
+ }
43
+ }
44
+ })
45
+ }
46
+
47
+ export const parseArgs = args => {
48
+ let [cwd, entrypoint, builder] = args
49
+
50
+ if (!cwd) {
51
+ throw new CliArgumentError('cwdRequired')
52
+ }
53
+
54
+ if (!entrypoint) {
55
+ throw new CliArgumentError('entrypointRequired')
56
+ }
57
+
58
+ if (!builder) {
59
+ throw new CliArgumentError('builderRequired')
60
+ }
61
+
62
+ try {
63
+ const stat = Deno.lstatSync(cwd)
64
+ if (!stat.isDirectory) {
65
+ throw new CliArgumentError(
66
+ `Current working directory is required as the first argument - received ${cwd}`
67
+ )
68
+ }
69
+ } catch {
70
+ throw new CliArgumentError('cwdUnknown', { cwd })
71
+ }
72
+
73
+ if (/\.(jsx?)|(css)\.map$/.test(entrypoint)) {
74
+ entrypoint = entrypoint.replace(/\.map$/, '')
75
+ }
76
+
77
+ try {
78
+ const stat = Deno.lstatSync(join(cwd, entrypoint))
79
+ if (!stat.isFile) {
80
+ throw new CliArgumentError(
81
+ `Entrypoint is required as the second argument - received ${entrypoint}`
82
+ )
83
+ }
84
+ } catch {
85
+ throw new CliArgumentError('entrypointUnknown', { entrypoint })
86
+ }
87
+
88
+ if (!builderNames.includes(builder)) {
89
+ throw new CliArgumentError('builderUnknown', { builder })
90
+ }
91
+
92
+ return args
93
+ }
@@ -0,0 +1,84 @@
1
+ // Recursively Scans the /app path for any JS/JSX/CSS files, and compiles each one, while also
2
+ // building a manifest (JSON) of all files that are built. The manifest contains a simple mapping of
3
+ // source file => compiled file. The compiled files are appended with the content digest, for
4
+ // caching.
5
+
6
+ import { writeAll } from 'std/streams/mod.ts'
7
+ import { MuxAsyncIterator } from 'std/async/mod.ts'
8
+ import { expandGlob, ensureDir } from 'std/fs/mod.ts'
9
+ import { extname, relative, join, dirname, parse } from 'std/path/mod.ts'
10
+
11
+ import build from './cli.js'
12
+
13
+ const extnameToBuilderMap = {
14
+ '.js': 'javascript'
15
+ }
16
+
17
+ async function main(args = []) {
18
+ const [root, ...paths] = args
19
+ const outDir = join(root, 'public', 'assets')
20
+ const manifest = {}
21
+ const promises = []
22
+ const mux = new MuxAsyncIterator()
23
+
24
+ paths.forEach(path => {
25
+ mux.add(expandGlob(`${path}/**/*.{css,js,jsx}`, { root }))
26
+ })
27
+
28
+ for await (const file of mux) {
29
+ const builder = extnameToBuilderMap[extname(file.path)]
30
+
31
+ if (!builder) {
32
+ console.error('--! Failed to compile %o (unknown builder)', relative(root, file.path))
33
+ continue
34
+ }
35
+
36
+ promises.push(
37
+ compile({ ...file, root, outDir }).then(({ inPath, outPath }) => {
38
+ manifest[inPath] = outPath
39
+ })
40
+ )
41
+ }
42
+
43
+ await Promise.allSettled(promises)
44
+
45
+ return new TextEncoder().encode(JSON.stringify(manifest))
46
+ }
47
+
48
+ function compile({ root, path, outDir }) {
49
+ const entrypoint = relative(root, path)
50
+ const { dir, name, ext } = parse(entrypoint)
51
+ const builder = extnameToBuilderMap[ext]
52
+
53
+ console.log('--- Compiling %o with %s builder...', entrypoint, builder)
54
+
55
+ return build([root, entrypoint, builder])
56
+ .then(src => {
57
+ console.log(2)
58
+ return digest(src)
59
+ })
60
+ .then(({ hash, source }) => {
61
+ const path = join(outDir, dir, `${name}-${hash}${ext}`)
62
+
63
+ return ensureDir(dirname(path))
64
+ .then(() => Deno.writeTextFile(path, new TextDecoder().decode(source)))
65
+ .then(() => ({ inPath: entrypoint, outPath: relative(outDir, path) }))
66
+ })
67
+ }
68
+
69
+ async function digest(source) {
70
+ const view = new DataView(await crypto.subtle.digest('SHA-1', source))
71
+
72
+ let hash = ''
73
+ for (let index = 0; index < view.byteLength; index += 4) {
74
+ hash += view.getUint32(index).toString(16).padStart(8, '0')
75
+ }
76
+
77
+ return { hash, source }
78
+ }
79
+
80
+ export default main
81
+
82
+ if (import.meta.main) {
83
+ await writeAll(Deno.stdout, await main(Deno.args))
84
+ }
@@ -0,0 +1,22 @@
1
+ export default class ArgumentError extends Error {
2
+ static MESSAGES = {
3
+ rootRequired: 'Current working directory is required as --root.',
4
+ pathsRequired: 'One or more file paths or globs are required.',
5
+
6
+ rootUnknown: ({ root }) => `A valid working directory is required - received ${root}`
7
+ }
8
+
9
+ constructor(reason, options) {
10
+ let message = ArgumentError.MESSAGES[reason]
11
+ if (typeof message === 'function') {
12
+ message = message(options)
13
+ }
14
+
15
+ message = `${reason}: ${message}`
16
+
17
+ super(message, options)
18
+
19
+ this.reason = reason
20
+ this.message = message
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ import setup from './setup_plugin.js'
2
+
3
+ export default setup('env', () => {
4
+ return {
5
+ onResolve: {
6
+ filter: /^env$/,
7
+ callback({ path }) {
8
+ return { path, namespace: 'env' }
9
+ }
10
+ },
11
+
12
+ onLoad: {
13
+ filter: /.*/,
14
+ namespace: 'env',
15
+ callback() {
16
+ const env = Deno.env.toObject()
17
+ return { loader: 'json', contents: JSON.stringify(env) }
18
+ }
19
+ }
20
+ }
21
+ })
@@ -0,0 +1,145 @@
1
+ import { join, resolve } from 'std/path/mod.ts'
2
+ import {
3
+ parseFromString as parseImportMap,
4
+ resolve as resolveFromImportMap
5
+ } from 'import-maps/resolve'
6
+
7
+ import setup from './setup_plugin.js'
8
+
9
+ const baseURL = new URL('file://')
10
+ const importKinds = ['import-statement', 'dynamic-import', 'require-call']
11
+
12
+ export default setup('resolve', (build, options) => {
13
+ const { runtimeDir } = options
14
+ const cwd = build.initialOptions.absWorkingDir
15
+ const runtimeCwdAlias = `${cwd}/proscenium-runtime`
16
+ const importMap = readImportMap()
17
+
18
+ return {
19
+ onResolve: {
20
+ filter: /.*/,
21
+ async callback(args) {
22
+ // Remote modules
23
+ if (args.path.startsWith('http://') || args.path.startsWith('https://')) {
24
+ return { external: true }
25
+ }
26
+
27
+ // Proscenium runtime
28
+ if (args.path.startsWith('@proscenium/')) {
29
+ return {
30
+ path: `${args.path.replace(/^@proscenium/, '/proscenium-runtime')}/index.js`,
31
+ external: true
32
+ }
33
+ }
34
+
35
+ if (args.path.startsWith(runtimeCwdAlias)) {
36
+ return { path: join(runtimeDir, args.path.slice(runtimeCwdAlias.length)) }
37
+ }
38
+
39
+ // Everything else is unbundled.
40
+ if (importKinds.includes(args.kind)) {
41
+ return await unbundleImport(args)
42
+ }
43
+ }
44
+ },
45
+
46
+ onLoad: {
47
+ filter: /.*/,
48
+ namespace: 'importStylesheet',
49
+ callback(args) {
50
+ const result = {
51
+ resolveDir: cwd,
52
+ loader: 'js'
53
+ }
54
+
55
+ if (args.path.endsWith('.module.css')) {
56
+ result.contents = `
57
+ import { importCssModule } from '/proscenium-runtime/import_css.js'
58
+ export default await importCssModule('${args.path}')
59
+ `
60
+ } else {
61
+ result.contents = `
62
+ import { appendStylesheet } from '/proscenium-runtime/import_css.js'
63
+ appendStylesheet('${args.path}')
64
+ `
65
+ }
66
+
67
+ return result
68
+ }
69
+ }
70
+ }
71
+
72
+ // Resolve the given `params.path` to a path relative to the Rails root.
73
+ //
74
+ // Examples:
75
+ // 'react' -> '/.../node_modules/react/index.js'
76
+ // './my_module' -> '/.../app/my_module.js'
77
+ // '/app/my_module' -> '/.../app/my_module.js'
78
+ async function unbundleImport(params) {
79
+ const result = { path: params.path }
80
+
81
+ if (importMap) {
82
+ const { matched, resolvedImport } = resolveFromImportMap(params.path, importMap, baseURL)
83
+ if (matched) {
84
+ if (resolvedImport.protocol === 'file:') {
85
+ params.path = resolvedImport.pathname
86
+ } else {
87
+ return { path: resolvedImport.href, external: true }
88
+ }
89
+ }
90
+ }
91
+
92
+ // Absolute path - append to current working dir.
93
+ if (params.path.startsWith('/')) {
94
+ result.path = resolve(cwd, params.path.slice(1))
95
+ }
96
+
97
+ // Resolve the path using esbuild's internal resolution. This allows us to import node packages
98
+ // and extension-less paths without custom code, as esbuild with resolve them for us.
99
+ const resolveResult = await build.resolve(result.path, {
100
+ resolveDir: params.resolveDir,
101
+ pluginData: {
102
+ // We use this property later on, as we should ignore this resolution call.
103
+ isResolvingPath: true
104
+ }
105
+ })
106
+
107
+ if (resolveResult.errors.length > 0) {
108
+ // throw `${resolveResult.errors[0].text} (resolveDir: ${cwd})`
109
+ }
110
+
111
+ result.path = resolveResult.path.slice(cwd.length)
112
+ result.sideEffects = resolveResult.sideEffects
113
+
114
+ if (
115
+ params.path.endsWith('.css') &&
116
+ params.kind === 'import-statement' &&
117
+ /\.jsx?$/.test(params.importer)
118
+ ) {
119
+ // We're importing a CSS file from JS(X).
120
+ result.namespace = 'importStylesheet'
121
+ } else {
122
+ // Requested path is a bare module.
123
+ result.external = true
124
+ }
125
+
126
+ return result
127
+ }
128
+
129
+ function readImportMap() {
130
+ const file = join(cwd, 'config', 'import_map.json')
131
+ let source
132
+
133
+ try {
134
+ source = Deno.readTextFileSync(file)
135
+ } catch {
136
+ return null
137
+ }
138
+
139
+ return parseImportMap(source, baseURL)
140
+ }
141
+ })
142
+
143
+ function isBareModule(path) {
144
+ return !path.startsWith('.') && !path.startsWith('/')
145
+ }
@@ -0,0 +1,35 @@
1
+ export default (pluginName, pluginFn) => {
2
+ return (options = {}) => ({
3
+ name: pluginName,
4
+ setup(build) {
5
+ const plugin = pluginFn(build, options)
6
+
7
+ if (plugin.onResolve) {
8
+ const { callback, ...onResolve } = plugin.onResolve
9
+
10
+ build.onResolve(onResolve, async params => {
11
+ if (params.pluginData?.isResolvingPath) return
12
+
13
+ options.debug && console.debug(`plugin(${pluginName}):onResolve`, params.path, { params })
14
+ const results = await callback(params)
15
+ options.debug &&
16
+ console.debug(`plugin(${pluginName}):onResolve`, params.path, { results })
17
+
18
+ return results
19
+ })
20
+ }
21
+
22
+ if (plugin.onLoad) {
23
+ const { callback, ...onLoad } = plugin.onLoad
24
+
25
+ build.onLoad(onLoad, params => {
26
+ options.debug && console.debug(`plugin(${pluginName}):onLoad`, { params })
27
+ const results = callback(params)
28
+ options.debug && console.debug(`plugin(${pluginName}):onLoad`, { results })
29
+
30
+ return results
31
+ })
32
+ }
33
+ }
34
+ })
35
+ }
@@ -0,0 +1,9 @@
1
+ import { join } from 'std/path/mod.ts'
2
+
3
+ import compile from './esbuild.js'
4
+
5
+ const cwd = join(Deno.cwd(), 'test', 'internal')
6
+
7
+ Deno.bench('esbuild', async () => {
8
+ await compile(cwd, 'lib/foo.js')
9
+ })
@@ -0,0 +1,82 @@
1
+ import { writeAll } from 'std/streams/mod.ts'
2
+ import { parse as parseArgs } from 'std/flags/mod.ts'
3
+ import { expandGlob } from 'std/fs/mod.ts'
4
+ import { join, isGlob, resolve, dirname, fromFileUrl } from 'std/path/mod.ts'
5
+ import { build, stop } from 'esbuild'
6
+
7
+ import envPlugin from './esbuild/env_plugin.js'
8
+ import resolvePlugin from './esbuild/resolve_plugin.js'
9
+ import ArgumentError from './esbuild/argument_error.js'
10
+
11
+ if (import.meta.main) {
12
+ const { _: paths, ...options } = parseArgs(Deno.args, {
13
+ string: ['root', 'runtime-dir'],
14
+ boolean: ['write'],
15
+ alias: { 'runtime-dir': 'runtimeDir' }
16
+ })
17
+ await writeAll(Deno.stdout, await main(paths, options))
18
+ }
19
+
20
+ async function main(paths = [], options = {}) {
21
+ const { root, write } = { write: false, ...options }
22
+
23
+ if (!Array.isArray(paths) || paths.length < 1) throw new ArgumentError('pathsRequired')
24
+ if (!root) throw new ArgumentError('rootRequired')
25
+
26
+ // Make sure that `root` is a valid directory.
27
+ try {
28
+ const stat = Deno.lstatSync(root)
29
+ if (!stat.isDirectory) throw new ArgumentError('rootUnknown', { root })
30
+ } catch {
31
+ throw new ArgumentError('rootUnknown', { root })
32
+ }
33
+
34
+ const isProd = Deno.env.get('RAILS_ENV') === 'production'
35
+
36
+ const entryPoints = new Set()
37
+ for (let i = 0; i < paths.length; i++) {
38
+ const path = paths[i]
39
+
40
+ if (isGlob(path)) {
41
+ for await (const file of expandGlob(path, { root })) {
42
+ file.isFile && entryPoints.add(file.path)
43
+ }
44
+ } else {
45
+ entryPoints.add(join(root, path))
46
+ }
47
+ }
48
+
49
+ const runtimeDir = resolve(dirname(fromFileUrl(import.meta.url)), '../runtime')
50
+
51
+ const params = {
52
+ entryPoints: Array.from(entryPoints),
53
+ absWorkingDir: root,
54
+ logLevel: 'error',
55
+ sourcemap: isProd ? 'linked' : 'inline',
56
+ outdir: 'public/assets',
57
+ outbase: './',
58
+ format: 'esm',
59
+ jsxFactory: 'reactCreateElement',
60
+ jsxFragment: 'ReactFragment',
61
+ minify: isProd,
62
+ bundle: true,
63
+ plugins: [envPlugin(), resolvePlugin({ runtimeDir, debug: false })],
64
+ inject: [join(runtimeDir, 'react_shim/index.js')],
65
+ metafile: write,
66
+ write
67
+ }
68
+
69
+ try {
70
+ const result = await build(params)
71
+
72
+ if (write) {
73
+ return new TextEncoder().encode(JSON.stringify(result))
74
+ } else {
75
+ return result.outputFiles[0].contents
76
+ }
77
+ } finally {
78
+ stop()
79
+ }
80
+ }
81
+
82
+ export default main