proscenium 0.1.0.alpha1-x86_64-darwin

Sign up to get free protection for your applications and to get access to all the features.
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