proscenium 0.1.0.alpha3-x86_64-darwin → 0.1.0.alpha4-x86_64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +65 -10
- data/app/components/react_component.rb +10 -3
- data/bin/esbuild +0 -0
- data/bin/lightningcss +0 -0
- data/lib/proscenium/compilers/esbuild/compile_error.js +2 -0
- data/lib/proscenium/compilers/esbuild/css/postcss.js +67 -0
- data/lib/proscenium/compilers/esbuild/css_plugin.js +110 -10
- data/lib/proscenium/compilers/esbuild/resolve_plugin.js +42 -43
- data/lib/proscenium/compilers/esbuild/setup_plugin.js +16 -7
- data/lib/proscenium/compilers/esbuild.js +38 -5
- data/lib/proscenium/css_module.rb +48 -6
- data/lib/proscenium/helper.rb +3 -1
- data/lib/proscenium/middleware/esbuild.rb +3 -1
- data/lib/proscenium/middleware/{parcel_css.rb → lightningcss.rb} +3 -3
- data/lib/proscenium/middleware.rb +4 -19
- data/lib/proscenium/phlex.rb +36 -0
- data/lib/proscenium/railtie.rb +2 -10
- data/lib/proscenium/runtime/auto_reload.js +3 -3
- data/lib/proscenium/side_load.rb +14 -40
- data/lib/proscenium/utils.js +8 -0
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium/view_component.rb +5 -1
- data/lib/proscenium.rb +1 -0
- metadata +22 -8
- data/bin/parcel_css +0 -0
- data/lib/proscenium/runtime/component_manager/index.js +0 -27
- data/lib/proscenium/runtime/component_manager/render_component.jsx +0 -36
- data/lib/proscenium/runtime/import_css.js +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7479881d2ace4e2930975f02eee4c5085f2355e06d35d689ca419ec0a4eb6a0
|
4
|
+
data.tar.gz: 216ac3339281bc6c7b526ba25d6f228fe01b855d14071a8b74eddf8d8ae94354
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28c0ea45615d6eaa21bb5d761df7eab863f4b7ce81ea747514b521ebf23bb2ad1baa9a8a3172a423a21da0b11d58f0a0104c2ddb18660d765ac9b8827c36c319
|
7
|
+
data.tar.gz: 1289a1eeeabf08d25f7183fd5d300fe0ac60162784c25ba719039cd3bbe91739541f64d3690d98c75a288bfc660339d601b56ee60aa1bfa21ac149557538b878
|
data/README.md
CHANGED
@@ -1,12 +1,29 @@
|
|
1
|
-
# Proscenium
|
1
|
+
# Proscenium - Modern Client-Side Tooling for Rails
|
2
2
|
|
3
|
-
-
|
3
|
+
Proscenium treats your client-side code as first class citizens of your Rails app, and assumes a
|
4
|
+
"fast by default" internet. It compiles your JS, JSX and CSS in real time, and on demand, with no
|
5
|
+
configuration at all!
|
6
|
+
|
7
|
+
- Zero configuration.
|
8
|
+
- NO JavaScript rumtime needed - just the browser!
|
9
|
+
- Real-time compilation.
|
10
|
+
- No additional process or server - Just run Rails!
|
11
|
+
- Serve assets from anywhere within your Rails root (/app, /config, /lib).
|
4
12
|
- Automatically side load JS/CSS for your layouts and views.
|
5
|
-
- Import JS and CSS from node_modules, URL, local (relative, absolute)
|
6
|
-
-
|
7
|
-
- Import Map
|
8
|
-
- CSS Modules
|
9
|
-
-
|
13
|
+
- Import JS(X) and CSS from node_modules, URL, local (relative, absolute).
|
14
|
+
- Optional bundling of JS(X) and CSS.
|
15
|
+
- Import Map support for JS and CSS.
|
16
|
+
- CSS Modules.
|
17
|
+
- CSS Custom Media Queries.
|
18
|
+
- CSS mixins.
|
19
|
+
- Minification.
|
20
|
+
- Auto reload after changes (development only).
|
21
|
+
|
22
|
+
## !! EXPERIMENTAL SOFTWARE !!
|
23
|
+
|
24
|
+
While my goal is to use Proscenium in production, I strongly recommended that you **DO NOT** use
|
25
|
+
this in production apps! Right now, this is a play thing, and should only be used for
|
26
|
+
development/testing.
|
10
27
|
|
11
28
|
## Installation
|
12
29
|
|
@@ -34,6 +51,7 @@ Using the examples above...
|
|
34
51
|
- `app/views/users/index.js` => `https://yourapp.com/app/views/users/index.js`
|
35
52
|
- `app/views/layouts/application.css` => `https://yourapp.com/app/views/layouts/application.css`
|
36
53
|
- `lib/utils.js` => `https://yourapp.com/lib/utils.js`
|
54
|
+
- `config/properties.css` => `https://yourapp.com/config/properties.css`
|
37
55
|
|
38
56
|
## Importing
|
39
57
|
|
@@ -166,10 +184,28 @@ to `false`.
|
|
166
184
|
|
167
185
|
## CSS Modules
|
168
186
|
|
169
|
-
|
187
|
+
Give any CSS file a `.module.css` extension, and Proscenium will load it as a CSS Module...
|
188
|
+
|
189
|
+
```css
|
190
|
+
.header {
|
191
|
+
background-color: #00f;
|
192
|
+
}
|
193
|
+
```
|
170
194
|
|
171
|
-
|
172
|
-
|
195
|
+
The above produces:
|
196
|
+
|
197
|
+
```css
|
198
|
+
.header5564cdbb {
|
199
|
+
background-color: #00f;
|
200
|
+
}
|
201
|
+
```
|
202
|
+
|
203
|
+
Importing a CSS file from JS will automatically append the stylesheet to the document's head. The
|
204
|
+
results of the import will be an object of CSS modules.
|
205
|
+
|
206
|
+
```js
|
207
|
+
import styles from './styles.module.css'
|
208
|
+
```
|
173
209
|
|
174
210
|
## Auto Reload
|
175
211
|
|
@@ -191,6 +227,25 @@ You can disable auto reload by setting the `config.proscenium.auto_reload` confi
|
|
191
227
|
|
192
228
|
Proscenium supports [custom media queries](https://css-tricks.com/can-we-have-custom-media-queries-please/) as per the [spec](https://www.w3.org/TR/mediaqueries-5/#custom-mq). However, because of the way they are parsed, they cannot be imported using `@import`. So if you define your custom media queries in `/config/custom_media_queries.css`, Proscenium will automatically inject them into your CSS, so you can use them anywhere.
|
193
229
|
|
230
|
+
## CSS Mixins
|
231
|
+
|
232
|
+
CSS mixins are supported using the `@mixin` at-rule. Simply define your mixins in any number of files ending in `.mixin.css`, and using the `@define-mixin` at-rule...
|
233
|
+
|
234
|
+
```css
|
235
|
+
// /lib/text.mixin.css
|
236
|
+
@define-mixin bigText {
|
237
|
+
font-size: 50px;
|
238
|
+
}
|
239
|
+
```
|
240
|
+
|
241
|
+
```css
|
242
|
+
// /app/views/layouts/application.css
|
243
|
+
p {
|
244
|
+
@mixin bigText;
|
245
|
+
color: red;
|
246
|
+
}
|
247
|
+
```
|
248
|
+
|
194
249
|
## How It Works
|
195
250
|
|
196
251
|
Proscenium provides a Rails middleware that proxies requests for your frontend code. By default, it will simply search for a file of the same name in your Rails root. For example, a request for '/app/views/layouts/application.js' or '/lib/hooks.js' will return that exact file relative to your Rails root.
|
@@ -1,14 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ReactComponent < ApplicationComponent
|
4
|
-
attr_accessor :props
|
4
|
+
attr_accessor :props, :lazy
|
5
5
|
|
6
|
-
|
6
|
+
# @param props: [Hash]
|
7
|
+
# @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
|
8
|
+
def initialize(props: {}, lazy: true)
|
7
9
|
@props = props
|
10
|
+
@lazy = lazy
|
11
|
+
|
8
12
|
super
|
9
13
|
end
|
10
14
|
|
11
15
|
def call
|
12
|
-
tag.div
|
16
|
+
tag.div class: ['componentManagedByProscenium', css_module(:component)],
|
17
|
+
data: { component: { path: virtual_path, props: props, lazy: lazy } } do
|
18
|
+
tag.div content
|
19
|
+
end
|
13
20
|
end
|
14
21
|
end
|
data/bin/esbuild
CHANGED
Binary file
|
data/bin/lightningcss
ADDED
Binary file
|
@@ -74,11 +74,13 @@ export default function () {
|
|
74
74
|
padding: 10px 0 0 20px;
|
75
75
|
}
|
76
76
|
.lineText {
|
77
|
+
display: block;
|
77
78
|
white-space: pre-wrap;
|
78
79
|
}
|
79
80
|
.lineCursor {
|
80
81
|
white-space: pre;
|
81
82
|
color: blueviolet;
|
83
|
+
display: block;
|
82
84
|
}
|
83
85
|
</style>
|
84
86
|
<div class="window">
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { expandGlob } from 'std/fs/mod.ts'
|
2
|
+
import postcss, { CssSyntaxError } from 'postcss'
|
3
|
+
|
4
|
+
export default async (root, path) => {
|
5
|
+
let tmpFile
|
6
|
+
let contents
|
7
|
+
|
8
|
+
const mixinFiles = []
|
9
|
+
for await (const file of expandGlob(`lib/**/*.mixin.css`, { root })) {
|
10
|
+
mixinFiles.push(file.path)
|
11
|
+
}
|
12
|
+
|
13
|
+
// Only process mixins with PostCSS if there are any 'lib/**/*.mixin.css' files.
|
14
|
+
if (mixinFiles.length > 0) {
|
15
|
+
tmpFile = await Deno.makeTempFile()
|
16
|
+
contents = await Deno.readTextFile(path)
|
17
|
+
|
18
|
+
const result = await postcss([mixinsPlugin({ mixinFiles })]).process(contents, { from: path })
|
19
|
+
contents = result.css
|
20
|
+
}
|
21
|
+
|
22
|
+
return [tmpFile, contents]
|
23
|
+
}
|
24
|
+
|
25
|
+
const mixinsPlugin = (opts = {}) => {
|
26
|
+
return {
|
27
|
+
postcssPlugin: 'mixins',
|
28
|
+
|
29
|
+
prepare() {
|
30
|
+
const mixins = {}
|
31
|
+
|
32
|
+
return {
|
33
|
+
async Once(_, helpers) {
|
34
|
+
for (const path of opts.mixinFiles) {
|
35
|
+
const content = await Deno.readTextFile(path)
|
36
|
+
const root = helpers.parse(content, { from: path })
|
37
|
+
|
38
|
+
root.walkAtRules('define-mixin', atrule => {
|
39
|
+
mixins[atrule.params] = atrule
|
40
|
+
})
|
41
|
+
}
|
42
|
+
},
|
43
|
+
|
44
|
+
AtRule: {
|
45
|
+
mixin: (rule, helpers) => {
|
46
|
+
const mixin = mixins[rule.params]
|
47
|
+
|
48
|
+
if (!mixin) {
|
49
|
+
throw rule.error(`Undefined mixin '${rule.params}'`)
|
50
|
+
}
|
51
|
+
|
52
|
+
const proxy = new helpers.Root()
|
53
|
+
for (let i = 0; i < mixin.nodes.length; i++) {
|
54
|
+
const node = mixin.nodes[i].clone()
|
55
|
+
delete node.raws.before
|
56
|
+
proxy.append(node)
|
57
|
+
}
|
58
|
+
|
59
|
+
rule.parent.insertBefore(rule, proxy)
|
60
|
+
|
61
|
+
if (rule.parent) rule.remove()
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
@@ -1,11 +1,16 @@
|
|
1
1
|
import { crypto } from 'std/crypto/mod.ts'
|
2
|
-
import { join, resolve, dirname, fromFileUrl } from 'std/path/mod.ts'
|
2
|
+
import { join, resolve, dirname, basename, fromFileUrl } from 'std/path/mod.ts'
|
3
3
|
|
4
|
+
import { fileExists } from '../../utils.js'
|
5
|
+
import postcss from './css/postcss.js'
|
4
6
|
import setup from './setup_plugin.js'
|
5
7
|
|
6
8
|
export default setup('css', async build => {
|
7
9
|
const cwd = build.initialOptions.absWorkingDir
|
8
|
-
const
|
10
|
+
const lightningcssBin = resolve(
|
11
|
+
dirname(fromFileUrl(import.meta.url)),
|
12
|
+
'../../../../bin/lightningcss'
|
13
|
+
)
|
9
14
|
|
10
15
|
let customMedia
|
11
16
|
try {
|
@@ -20,24 +25,55 @@ export default setup('css', async build => {
|
|
20
25
|
filter: /\.css$/,
|
21
26
|
namespace: 'file',
|
22
27
|
async callback(args) {
|
23
|
-
|
28
|
+
const hash = await digest(args.path.slice(cwd.length))
|
24
29
|
const isCssModule = args.path.endsWith('.module.css')
|
25
|
-
let cmd = [parcelBin, '--nesting', '--targets', '>= 0.25%']
|
26
30
|
|
31
|
+
// If path is a CSS module, imported from JS, and a side-loaded ViewComponent stylesheet,
|
32
|
+
// simply return a JS proxy of the class names. The stylesheet itself will have already been
|
33
|
+
// side loaded. This avoids compiling the CSS all over again.
|
34
|
+
if (isCssModule && args.pluginData?.importedFromJs && (await isViewComponent(args.path))) {
|
35
|
+
return {
|
36
|
+
resolveDir: cwd,
|
37
|
+
loader: 'js',
|
38
|
+
contents: cssModulesProxyTemplate(hash)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
let cmd = [
|
43
|
+
lightningcssBin,
|
44
|
+
'--nesting',
|
45
|
+
'--error-recovery',
|
46
|
+
args.pluginData?.importedFromJs && '--minify',
|
47
|
+
'--targets',
|
48
|
+
'>= 0.25%'
|
49
|
+
].filter(Boolean)
|
50
|
+
|
51
|
+
// This will process the CSS with Postcss only if it needs to.
|
52
|
+
let [tmpFile, contents] = await postcss(cwd, args.path)
|
53
|
+
|
54
|
+
// As custom media are defined in their own file, we have to append the file contents to our
|
55
|
+
// stylesheet, so that the custom media can be used.
|
27
56
|
if (customMedia) {
|
28
57
|
cmd.push('--custom-media')
|
29
58
|
|
30
|
-
|
31
|
-
|
59
|
+
if (!tmpFile && !contents) {
|
60
|
+
tmpFile = await Deno.makeTempFile()
|
61
|
+
contents = await Deno.readTextFile(args.path)
|
62
|
+
}
|
63
|
+
|
64
|
+
contents += customMedia
|
65
|
+
}
|
66
|
+
|
67
|
+
if (tmpFile && contents) {
|
68
|
+
await Deno.writeTextFile(tmpFile, contents)
|
32
69
|
}
|
33
70
|
|
34
71
|
if (isCssModule) {
|
35
|
-
const hash = await digest(args.path.slice(cwd.length))
|
36
72
|
cmd = cmd.concat(['--css-modules', '--css-modules-pattern', `[local]${hash}`])
|
37
73
|
}
|
38
74
|
|
39
75
|
const p = Deno.run({
|
40
|
-
cmd: [...cmd, path],
|
76
|
+
cmd: [...cmd, tmpFile || args.path],
|
41
77
|
stdout: 'piped',
|
42
78
|
stderr: 'piped'
|
43
79
|
})
|
@@ -51,9 +87,48 @@ export default setup('css', async build => {
|
|
51
87
|
// here.
|
52
88
|
p.close()
|
53
89
|
|
90
|
+
// Success!
|
54
91
|
if (code === 0) {
|
55
|
-
|
56
|
-
|
92
|
+
let contents = new TextDecoder().decode(rawOutput)
|
93
|
+
if (isCssModule) {
|
94
|
+
contents = JSON.parse(contents)
|
95
|
+
}
|
96
|
+
|
97
|
+
// If stylesheet is imported from JS, then we return JS code that appends the stylesheet
|
98
|
+
// in a <style> in the <head> of the page, and if the stylesheet is a CSS module, it
|
99
|
+
// exports a plain object of class names.
|
100
|
+
if (args.pluginData?.importedFromJs) {
|
101
|
+
const code = isCssModule ? contents.code : contents
|
102
|
+
const mod = [
|
103
|
+
`let e = document.querySelector('#_${hash}');`,
|
104
|
+
'if (!e) {',
|
105
|
+
"e = document.createElement('style');",
|
106
|
+
`e.id = '_${hash}';`,
|
107
|
+
'document.head.appendChild(e);',
|
108
|
+
`e.appendChild(document.createTextNode(\`${code}\`));`,
|
109
|
+
'}'
|
110
|
+
]
|
111
|
+
|
112
|
+
if (isCssModule) {
|
113
|
+
const classes = {}
|
114
|
+
for (const key in contents.exports) {
|
115
|
+
if (Object.hasOwnProperty.call(contents.exports, key)) {
|
116
|
+
classes[key] = contents.exports[key].name
|
117
|
+
}
|
118
|
+
}
|
119
|
+
mod.push(`export default ${JSON.stringify(classes)};`)
|
120
|
+
}
|
121
|
+
|
122
|
+
// We are importing from JS, so return the entire result from LightningCSS via the js
|
123
|
+
// loader.
|
124
|
+
return {
|
125
|
+
resolveDir: cwd,
|
126
|
+
loader: 'js',
|
127
|
+
contents: mod.join('')
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
return { loader: 'css', contents: isCssModule ? contents.code : contents }
|
57
132
|
} else {
|
58
133
|
const errorString = new TextDecoder().decode(rawError)
|
59
134
|
throw errorString
|
@@ -74,3 +149,28 @@ async function digest(value) {
|
|
74
149
|
|
75
150
|
return hexCodes.slice(0, 8)
|
76
151
|
}
|
152
|
+
|
153
|
+
async function isViewComponent(path) {
|
154
|
+
const fileName = basename(path)
|
155
|
+
const dirName = dirname(path)
|
156
|
+
|
157
|
+
return (
|
158
|
+
(fileName === 'component.module.css' && (await fileExists(join(dirName, 'component.rb')))) ||
|
159
|
+
(fileName.endsWith('_component.module.css') &&
|
160
|
+
(await fileExists(join(dirName, fileName.replace(/\.module\.css$/, '.rb')))))
|
161
|
+
)
|
162
|
+
}
|
163
|
+
|
164
|
+
function cssModulesProxyTemplate(hash) {
|
165
|
+
return [
|
166
|
+
`export default new Proxy( {}, {`,
|
167
|
+
` get(target, prop, receiver) {`,
|
168
|
+
` if (prop in target || typeof prop === 'symbol') {`,
|
169
|
+
` return Reflect.get(target, prop, receiver)`,
|
170
|
+
` } else {`,
|
171
|
+
` return prop + '${hash}'`,
|
172
|
+
` }`,
|
173
|
+
` }`,
|
174
|
+
`})`
|
175
|
+
].join('')
|
176
|
+
}
|
@@ -3,13 +3,13 @@ import { resolve as resolveFromImportMap } from 'import-maps/resolve'
|
|
3
3
|
|
4
4
|
import setup from './setup_plugin.js'
|
5
5
|
|
6
|
-
const baseURL = new URL('file://')
|
7
6
|
const importKinds = ['import-statement', 'dynamic-import', 'require-call', 'import-rule']
|
8
7
|
|
9
8
|
export default setup('resolve', (build, options) => {
|
10
9
|
const { runtimeDir, importMap } = options
|
11
10
|
const cwd = build.initialOptions.absWorkingDir
|
12
11
|
const runtimeCwdAlias = `${cwd}/proscenium-runtime`
|
12
|
+
let bundled = false
|
13
13
|
|
14
14
|
return [
|
15
15
|
{
|
@@ -29,18 +29,22 @@ export default setup('resolve', (build, options) => {
|
|
29
29
|
}
|
30
30
|
|
31
31
|
// Proscenium runtime
|
32
|
-
if (args.path.startsWith('@proscenium/')) {
|
33
|
-
|
32
|
+
// if (args.path.startsWith('@proscenium/')) {
|
33
|
+
// const result = { suffix: args.suffix }
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
result.path = `${args.path.replace(/^@proscenium/, '/proscenium-runtime')}/index.js`
|
39
|
-
result.external = true
|
40
|
-
}
|
35
|
+
// if (args.queryParams?.has('bundle-all')) {
|
36
|
+
// bundled = true
|
37
|
+
// }
|
41
38
|
|
42
|
-
|
43
|
-
}
|
39
|
+
// if (bundled || args.queryParams?.has('bundle')) {
|
40
|
+
// result.path = join(runtimeDir, `${args.path.replace(/^@proscenium/, '')}/index.js`)
|
41
|
+
// } else {
|
42
|
+
// result.path = `${args.path.replace(/^@proscenium/, '/proscenium-runtime')}/index.js`
|
43
|
+
// result.external = true
|
44
|
+
// }
|
45
|
+
|
46
|
+
// return result
|
47
|
+
// }
|
44
48
|
|
45
49
|
if (args.path.startsWith(runtimeCwdAlias)) {
|
46
50
|
return { path: join(runtimeDir, args.path.slice(runtimeCwdAlias.length)) }
|
@@ -51,32 +55,6 @@ export default setup('resolve', (build, options) => {
|
|
51
55
|
return await unbundleImport(args)
|
52
56
|
}
|
53
57
|
}
|
54
|
-
},
|
55
|
-
|
56
|
-
{
|
57
|
-
type: 'onLoad',
|
58
|
-
filter: /.*/,
|
59
|
-
namespace: 'importStylesheet',
|
60
|
-
callback(args) {
|
61
|
-
const result = {
|
62
|
-
resolveDir: cwd,
|
63
|
-
loader: 'js'
|
64
|
-
}
|
65
|
-
|
66
|
-
if (args.path.endsWith('.module.css')) {
|
67
|
-
result.contents = `
|
68
|
-
import { importCssModule } from '/proscenium-runtime/import_css.js'
|
69
|
-
export default await importCssModule('${args.path}')
|
70
|
-
`
|
71
|
-
} else {
|
72
|
-
result.contents = `
|
73
|
-
import { appendStylesheet } from '/proscenium-runtime/import_css.js'
|
74
|
-
appendStylesheet('${args.path}')
|
75
|
-
`
|
76
|
-
}
|
77
|
-
|
78
|
-
return result
|
79
|
-
}
|
80
58
|
}
|
81
59
|
]
|
82
60
|
|
@@ -90,7 +68,9 @@ export default setup('resolve', (build, options) => {
|
|
90
68
|
const result = { path: params.path, suffix: params.suffix }
|
91
69
|
|
92
70
|
if (importMap) {
|
71
|
+
const baseURL = new URL(params.importer.slice(cwd.length), 'file://')
|
93
72
|
const { matched, resolvedImport } = resolveFromImportMap(params.path, importMap, baseURL)
|
73
|
+
|
94
74
|
if (matched) {
|
95
75
|
if (resolvedImport.protocol === 'file:') {
|
96
76
|
params.path = resolvedImport.pathname
|
@@ -108,24 +88,39 @@ export default setup('resolve', (build, options) => {
|
|
108
88
|
// Resolve the path using esbuild's internal resolution. This allows us to import node packages
|
109
89
|
// and extension-less paths without custom code, as esbuild with resolve them for us.
|
110
90
|
const resolveResult = await build.resolve(result.path, {
|
111
|
-
resolveDir
|
91
|
+
// If path is a bare module (node_modules), and resolveDir is the Proscenium runtime dir, then
|
92
|
+
// use `cwd` as the `resolveDir`, otherwise pass it through as is. This ensures that nested
|
93
|
+
// node_modules are resolved correctly.
|
94
|
+
resolveDir:
|
95
|
+
isBareModule(result.path) && params.resolveDir.startsWith(runtimeDir)
|
96
|
+
? cwd
|
97
|
+
: params.resolveDir,
|
112
98
|
pluginData: {
|
113
99
|
// We use this property later on, as we should ignore this resolution call.
|
114
100
|
isResolvingPath: true
|
115
101
|
}
|
116
102
|
})
|
117
103
|
|
118
|
-
if
|
119
|
-
|
104
|
+
// Simple return the resolved result if we have an error. Usually happens when module is not
|
105
|
+
// found.
|
106
|
+
if (resolveResult.errors.length > 0) return resolveResult
|
107
|
+
|
108
|
+
// If 'bundle-all' queryParam is defined, return the resolveResult.
|
109
|
+
if (bundled || params.queryParams?.has('bundle-all')) {
|
110
|
+
bundled = true
|
111
|
+
return { ...resolveResult, suffix: '?bundle-all' }
|
120
112
|
}
|
121
113
|
|
122
|
-
// If bundle queryParam is defined, return the resolveResult.
|
114
|
+
// If 'bundle' queryParam is defined, return the resolveResult.
|
123
115
|
if (params.queryParams?.has('bundle')) {
|
124
|
-
return { ...resolveResult, suffix:
|
116
|
+
return { ...resolveResult, suffix: '?bundle' }
|
125
117
|
}
|
126
118
|
|
127
119
|
if (resolveResult.path.startsWith(runtimeDir)) {
|
128
120
|
result.path = '/proscenium-runtime' + resolveResult.path.slice(runtimeDir.length)
|
121
|
+
} else if (!resolveResult.path.startsWith(cwd)) {
|
122
|
+
// If resolved path does not start with cwd, then it is most likely linked, so bundle it.
|
123
|
+
return { ...resolveResult, suffix: '?bundle' }
|
129
124
|
} else {
|
130
125
|
result.path = resolveResult.path.slice(cwd.length)
|
131
126
|
}
|
@@ -138,7 +133,7 @@ export default setup('resolve', (build, options) => {
|
|
138
133
|
/\.jsx?$/.test(params.importer)
|
139
134
|
) {
|
140
135
|
// We're importing a CSS file from JS(X).
|
141
|
-
|
136
|
+
return { ...resolveResult, pluginData: { importedFromJs: true } }
|
142
137
|
} else {
|
143
138
|
result.external = true
|
144
139
|
}
|
@@ -146,3 +141,7 @@ export default setup('resolve', (build, options) => {
|
|
146
141
|
return result
|
147
142
|
}
|
148
143
|
})
|
144
|
+
|
145
|
+
function isBareModule(mod) {
|
146
|
+
return !mod.startsWith('/') && !mod.startsWith('.')
|
147
|
+
}
|
@@ -9,13 +9,22 @@ export default (pluginName, pluginFn) => {
|
|
9
9
|
build.onResolve({ filter, namespace }, async params => {
|
10
10
|
if (params.pluginData?.isResolvingPath) return
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
let results
|
13
|
+
|
14
|
+
if (options.debug) {
|
15
|
+
console.debug()
|
16
|
+
console.group(`plugin(${pluginName}):onResolve`, { filter, namespace })
|
17
|
+
console.debug('params:', params)
|
18
|
+
|
19
|
+
try {
|
20
|
+
results = await callback(params)
|
21
|
+
console.debug('results:', results)
|
22
|
+
} finally {
|
23
|
+
console.groupEnd()
|
24
|
+
}
|
25
|
+
} else {
|
26
|
+
results = await callback(params)
|
27
|
+
}
|
19
28
|
|
20
29
|
return results
|
21
30
|
})
|
@@ -11,14 +11,38 @@ import resolvePlugin from './esbuild/resolve_plugin.js'
|
|
11
11
|
import ArgumentError from './esbuild/argument_error.js'
|
12
12
|
import throwCompileError from './esbuild/compile_error.js'
|
13
13
|
|
14
|
+
/**
|
15
|
+
* Compile the given paths, outputting the result to stdout. This is designed to be called as a CLI:
|
16
|
+
*
|
17
|
+
* Example with Deno run (dev and test):
|
18
|
+
* deno run -A lib/proscenium/compilers/esbuild.js --root ./test/internal lib/foo.js
|
19
|
+
* Example with Deno compiled binary:
|
20
|
+
* bin/esbuild lib/proscenium/compilers/esbuild.js --root ./test/internal lib/foo.js
|
21
|
+
*
|
22
|
+
* USAGE:
|
23
|
+
* esbuild [OPTIONS] <PATHS_ARG>...
|
24
|
+
*
|
25
|
+
* ARGS:
|
26
|
+
* <PATHS_ARG>... One or more file paths to compile.
|
27
|
+
*
|
28
|
+
* OPTIONS:
|
29
|
+
* --root
|
30
|
+
* Relative or absolute path to the root or current working directory when compilation will
|
31
|
+
* take place.
|
32
|
+
* --import-map
|
33
|
+
* Path to an import map, relative to the <root>.
|
34
|
+
* --write
|
35
|
+
* Write output to the filesystem according to esbuild logic.
|
36
|
+
* --debug
|
37
|
+
* Debug output,
|
38
|
+
*/
|
14
39
|
if (import.meta.main) {
|
15
40
|
!Deno.env.get('RAILS_ENV') && Deno.env.set('RAILS_ENV', 'development')
|
16
41
|
|
17
42
|
const { _: paths, ...options } = parseArgs(Deno.args, {
|
18
|
-
string: ['root', '
|
43
|
+
string: ['root', 'import-map'],
|
19
44
|
boolean: ['write', 'debug'],
|
20
45
|
alias: {
|
21
|
-
'runtime-dir': 'runtimeDir',
|
22
46
|
'import-map': 'importMap'
|
23
47
|
}
|
24
48
|
})
|
@@ -34,10 +58,12 @@ if (import.meta.main) {
|
|
34
58
|
}
|
35
59
|
|
36
60
|
async function main(paths = [], options = {}) {
|
37
|
-
const {
|
61
|
+
const { write, debug } = { write: false, ...options }
|
38
62
|
|
39
63
|
if (!Array.isArray(paths) || paths.length < 1) throw new ArgumentError('pathsRequired')
|
40
|
-
if (!root) throw new ArgumentError('rootRequired')
|
64
|
+
if (!options.root) throw new ArgumentError('rootRequired')
|
65
|
+
|
66
|
+
const root = resolve(options.root)
|
41
67
|
|
42
68
|
// Make sure that `root` is a valid directory.
|
43
69
|
try {
|
@@ -84,7 +110,6 @@ async function main(paths = [], options = {}) {
|
|
84
110
|
absWorkingDir: root,
|
85
111
|
logLevel: 'silent',
|
86
112
|
logLimit: 1,
|
87
|
-
sourcemap: isTest ? false : isProd ? 'linked' : 'inline',
|
88
113
|
outdir: 'public/assets',
|
89
114
|
outbase: './',
|
90
115
|
format: 'esm',
|
@@ -97,10 +122,18 @@ async function main(paths = [], options = {}) {
|
|
97
122
|
write
|
98
123
|
}
|
99
124
|
|
125
|
+
if (!debug) {
|
126
|
+
params.sourcemap = isTest ? false : isProd ? 'linked' : 'inline'
|
127
|
+
}
|
128
|
+
|
100
129
|
let result
|
101
130
|
try {
|
102
131
|
result = await build(params)
|
103
132
|
} catch (error) {
|
133
|
+
if (debug) {
|
134
|
+
throw error
|
135
|
+
}
|
136
|
+
|
104
137
|
return { ...error.errors[0] }
|
105
138
|
} finally {
|
106
139
|
stop()
|
@@ -1,22 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Proscenium::CssModule
|
4
|
+
class NotFound < StandardError
|
5
|
+
def initialize(pathname)
|
6
|
+
@pathname = pathname
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"Stylesheet is required, but does not exist: #{@pathname}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
4
15
|
def initialize(path)
|
5
|
-
@path =
|
16
|
+
@path = path
|
17
|
+
@css_module_path = "#{path}.module.css"
|
18
|
+
end
|
6
19
|
|
7
|
-
|
20
|
+
# Parses the given `content` for CSS modules names ('class' attributes beginning with '@'), and
|
21
|
+
# returns the content with said CSS Modules replaced with the compiled class names.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
# <div class="@my_css_module_name"></div>
|
25
|
+
def compile_class_names(content)
|
26
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(content)
|
27
|
+
|
28
|
+
return content if (modules = doc.css('[class*="@"]')).empty?
|
29
|
+
|
30
|
+
modules.each do |ele|
|
31
|
+
classes = ele.classes.map { |cls| cls.starts_with?('@') ? class_names!(cls[1..]) : cls }
|
32
|
+
ele['class'] = classes.join(' ')
|
33
|
+
end
|
8
34
|
|
9
|
-
|
35
|
+
doc.to_html.html_safe
|
10
36
|
end
|
11
37
|
|
12
|
-
#
|
38
|
+
# @returns [Array] of class names generated from the given CSS module `names`.
|
13
39
|
def class_names(*names)
|
14
|
-
|
40
|
+
side_load_css_module
|
41
|
+
names.flatten.compact.map { |name| "#{name.to_s.camelize(:lower)}#{hash}" }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Like #class_names, but requires that the stylesheet exists.
|
45
|
+
#
|
46
|
+
# @raises Proscenium::CssModule::NotFound if stylesheet does not exists.
|
47
|
+
def class_names!(...)
|
48
|
+
raise NotFound, @css_module_path unless Rails.root.join(@css_module_path).exist?
|
49
|
+
|
50
|
+
class_names(...)
|
15
51
|
end
|
16
52
|
|
17
53
|
private
|
18
54
|
|
19
55
|
def hash
|
20
|
-
@hash ||= Digest::SHA1.hexdigest("/#{@
|
56
|
+
@hash ||= Digest::SHA1.hexdigest("/#{@css_module_path}")[..7]
|
57
|
+
end
|
58
|
+
|
59
|
+
def side_load_css_module
|
60
|
+
return unless Rails.application.config.proscenium.side_load
|
61
|
+
|
62
|
+
Proscenium::SideLoad.append "#{@path}.module", :css
|
21
63
|
end
|
22
64
|
end
|
data/lib/proscenium/helper.rb
CHANGED
@@ -11,7 +11,9 @@ module Proscenium
|
|
11
11
|
def side_load_stylesheets
|
12
12
|
return unless Proscenium::Current.loaded
|
13
13
|
|
14
|
-
|
14
|
+
Proscenium::Current.loaded[:css].map do |sheet|
|
15
|
+
stylesheet_link_tag(sheet, id: "_#{Digest::SHA1.hexdigest("/#{sheet}")[..7]}")
|
16
|
+
end.join("\n").html_safe
|
15
17
|
end
|
16
18
|
|
17
19
|
def side_load_javascripts(**options)
|
@@ -31,7 +31,9 @@ module Proscenium
|
|
31
31
|
|
32
32
|
def cli
|
33
33
|
if ENV['PROSCENIUM_TEST']
|
34
|
-
|
34
|
+
[
|
35
|
+
'deno run -q --import-map import_map.json -A', 'lib/proscenium/compilers/esbuild.js'
|
36
|
+
].join(' ')
|
35
37
|
else
|
36
38
|
Gem.bin_path 'proscenium', 'esbuild'
|
37
39
|
end
|
@@ -4,9 +4,9 @@ require 'oj'
|
|
4
4
|
|
5
5
|
module Proscenium
|
6
6
|
class Middleware
|
7
|
-
class
|
7
|
+
class Lightningcss < Base
|
8
8
|
def attempt
|
9
|
-
benchmark :
|
9
|
+
benchmark :lightningcss do
|
10
10
|
with_custom_media { |path| build path }
|
11
11
|
end
|
12
12
|
end
|
@@ -41,7 +41,7 @@ module Proscenium
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def cli
|
44
|
-
Gem.bin_path 'proscenium', '
|
44
|
+
Gem.bin_path 'proscenium', 'lightningcss'
|
45
45
|
end
|
46
46
|
|
47
47
|
def cli_options
|
@@ -9,15 +9,8 @@ module Proscenium
|
|
9
9
|
|
10
10
|
autoload :Base
|
11
11
|
autoload :Esbuild
|
12
|
-
autoload :ParcelCss
|
13
12
|
autoload :Runtime
|
14
13
|
|
15
|
-
MIDDLEWARE_CLASSES = {
|
16
|
-
esbuild: Esbuild,
|
17
|
-
parcelcss: ParcelCss,
|
18
|
-
runtime: Runtime
|
19
|
-
}.freeze
|
20
|
-
|
21
14
|
def initialize(app)
|
22
15
|
@app = app
|
23
16
|
end
|
@@ -38,23 +31,15 @@ module Proscenium
|
|
38
31
|
def attempt(request)
|
39
32
|
return unless (type = find_type(request))
|
40
33
|
|
41
|
-
file_handler.attempt(request.env) ||
|
34
|
+
file_handler.attempt(request.env) || type.attempt(request)
|
42
35
|
end
|
43
36
|
|
44
37
|
# Returns the type of file being requested using Rails.application.config.proscenium.glob_types.
|
45
38
|
def find_type(request)
|
46
|
-
|
47
|
-
|
48
|
-
path = Rails.root.join(request.path[1..])
|
49
|
-
|
50
|
-
type, = glob_types.find do |_, globs|
|
51
|
-
# TODO: Look for the precompiled file in public/assets first
|
52
|
-
# globs.any? { |glob| Rails.public_path.join('assets').glob(glob).any?(path) }
|
53
|
-
|
54
|
-
globs.any? { |glob| Rails.root.glob(glob).any?(path) }
|
55
|
-
end
|
39
|
+
path = Pathname.new(request.path)
|
56
40
|
|
57
|
-
|
41
|
+
return Runtime if path.fnmatch?(glob_types[:runtime], File::FNM_EXTGLOB)
|
42
|
+
return Esbuild if path.fnmatch?(glob_types[:esbuild], File::FNM_EXTGLOB)
|
58
43
|
end
|
59
44
|
|
60
45
|
def file_handler
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Phlex < ::Phlex::View
|
5
|
+
module Sideload
|
6
|
+
def template(...)
|
7
|
+
Proscenium::SideLoad.append self.class.path if Rails.application.config.proscenium.side_load
|
8
|
+
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :path
|
15
|
+
|
16
|
+
def inherited(child)
|
17
|
+
path = caller_locations(1, 1)[0].path
|
18
|
+
child.path = path.delete_prefix(::Rails.root.to_s).delete_suffix('.rb')[1..]
|
19
|
+
|
20
|
+
child.prepend Sideload
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def css_module(name)
|
27
|
+
cssm.class_names!(name).join ' '
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def cssm
|
33
|
+
@cssm ||= Proscenium::CssModule.new(self.class.path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/proscenium/railtie.rb
CHANGED
@@ -12,16 +12,8 @@ module Proscenium
|
|
12
12
|
#
|
13
13
|
# See https://doc.deno.land/https://deno.land/std@0.145.0/path/mod.ts/~/globToRegExp
|
14
14
|
DEFAULT_GLOB_TYPES = {
|
15
|
-
esbuild:
|
16
|
-
|
17
|
-
'lib/**/*.{js,jsx,css}',
|
18
|
-
'app/**/*.{js,jsx,css}'
|
19
|
-
]
|
20
|
-
# parcelcss: [
|
21
|
-
# 'config/**/*.css',
|
22
|
-
# 'lib/**/*.css',
|
23
|
-
# 'app/**/*.css'
|
24
|
-
# ]
|
15
|
+
esbuild: '/{config,app,lib,node_modules}/**.{js,mjs,jsx,css}',
|
16
|
+
runtime: '/proscenium-runtime/**.{js,jsx}'
|
25
17
|
}.freeze
|
26
18
|
|
27
19
|
class << self
|
@@ -7,16 +7,16 @@ export default socketPath => {
|
|
7
7
|
|
8
8
|
consumer.subscriptions.create('Proscenium::ReloadChannel', {
|
9
9
|
received: debounce(() => {
|
10
|
-
console.log('Proscenium
|
10
|
+
console.log('[Proscenium] Files changed; reloading...')
|
11
11
|
location.reload()
|
12
12
|
}, 200),
|
13
13
|
|
14
14
|
connected() {
|
15
|
-
console.log('Proscenium
|
15
|
+
console.log('[Proscenium] Auto-reload websocket connected')
|
16
16
|
},
|
17
17
|
|
18
18
|
disconnected() {
|
19
|
-
console.log('Proscenium
|
19
|
+
console.log('[Proscenium] Auto-reload websocket disconnected')
|
20
20
|
}
|
21
21
|
})
|
22
22
|
}
|
data/lib/proscenium/side_load.rb
CHANGED
@@ -5,17 +5,6 @@ module Proscenium
|
|
5
5
|
DEFAULT_EXTENSIONS = %i[js css].freeze
|
6
6
|
EXTENSIONS = %i[js css].freeze
|
7
7
|
|
8
|
-
class NotFound < StandardError
|
9
|
-
def initialize(pathname)
|
10
|
-
@pathname = pathname
|
11
|
-
super
|
12
|
-
end
|
13
|
-
|
14
|
-
def message
|
15
|
-
"#{@pathname} does not exist"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
8
|
module_function
|
20
9
|
|
21
10
|
# Side load the given asset `path`, by appending it to `Proscenium::Current.loaded`, which is a
|
@@ -46,40 +35,25 @@ module Proscenium
|
|
46
35
|
end
|
47
36
|
end
|
48
37
|
|
49
|
-
# Like #append, but only accepts a single `path` argument, which must be a Pathname. Raises
|
50
|
-
# `NotFound` if path does not exist,
|
51
|
-
def append!(pathname)
|
52
|
-
Proscenium::Current.loaded ||= EXTENSIONS.to_h { |e| [e, Set[]] }
|
53
|
-
|
54
|
-
unless pathname.is_a?(Pathname)
|
55
|
-
raise ArgumentError, "Argument `pathname` (#{pathname}) must be a Pathname"
|
56
|
-
end
|
57
|
-
|
58
|
-
ext = pathname.extname.sub('.', '').to_sym
|
59
|
-
path = pathname.relative_path_from(Rails.root).to_s
|
60
|
-
|
61
|
-
raise ArgumentError, "unsupported extension: #{ext}" unless EXTENSIONS.include?(ext)
|
62
|
-
|
63
|
-
return if Proscenium::Current.loaded[ext].include?(path)
|
64
|
-
|
65
|
-
raise NotFound, path unless pathname.exist?
|
66
|
-
|
67
|
-
Proscenium::Current.loaded[ext] << path
|
68
|
-
|
69
|
-
Rails.logger.debug "[Proscenium] Side loaded /#{path}"
|
70
|
-
end
|
71
|
-
|
72
38
|
module Monkey
|
73
39
|
module TemplateRenderer
|
74
40
|
private
|
75
41
|
|
76
42
|
def render_template(view, template, layout_name, locals)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
43
|
+
layout = find_layout(layout_name, locals.keys, [formats.first])
|
44
|
+
renderable = template.instance_variable_get(:@renderable)
|
45
|
+
|
46
|
+
if template.is_a?(ActionView::Template::Renderable) &&
|
47
|
+
renderable.class < ::ViewComponent::Base && renderable.class.format == :html
|
48
|
+
# Side load controller rendered ViewComponent
|
49
|
+
Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
|
50
|
+
Proscenium::SideLoad.append "app/views/#{renderable.virtual_path}"
|
51
|
+
elsif template.respond_to?(:virtual_path) &&
|
52
|
+
template.respond_to?(:type) && template.type == :html
|
53
|
+
# Side load regular view template.
|
54
|
+
Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
|
55
|
+
|
56
|
+
# Try side loading the variant template
|
83
57
|
if template.respond_to?(:variant) && template.variant
|
84
58
|
Proscenium::SideLoad.append "app/views/#{template.virtual_path}+#{template.variant}"
|
85
59
|
end
|
data/lib/proscenium/version.rb
CHANGED
@@ -5,12 +5,16 @@ module Proscenium::ViewComponent
|
|
5
5
|
|
6
6
|
autoload :TagBuilder
|
7
7
|
|
8
|
+
def render_in(...)
|
9
|
+
cssm.compile_class_names(super)
|
10
|
+
end
|
11
|
+
|
8
12
|
def before_render
|
9
13
|
side_load_assets unless self.class < ReactComponent
|
10
14
|
end
|
11
15
|
|
12
16
|
def css_module(name)
|
13
|
-
cssm.class_names(name
|
17
|
+
cssm.class_names!(name).join ' '
|
14
18
|
end
|
15
19
|
|
16
20
|
private
|
data/lib/proscenium.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: proscenium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.
|
4
|
+
version: 0.1.0.alpha4
|
5
5
|
platform: x86_64-darwin
|
6
6
|
authors:
|
7
7
|
- Joel Moss
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actioncable
|
@@ -64,6 +64,20 @@ dependencies:
|
|
64
64
|
- - "~>"
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '3.0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: nokogiri
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '1.13'
|
74
|
+
type: :runtime
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '1.13'
|
67
81
|
- !ruby/object:Gem::Dependency
|
68
82
|
name: oj
|
69
83
|
requirement: !ruby/object:Gem::Requirement
|
@@ -117,7 +131,7 @@ email:
|
|
117
131
|
- joel@developwithstyle.com
|
118
132
|
executables:
|
119
133
|
- esbuild
|
120
|
-
-
|
134
|
+
- lightningcss
|
121
135
|
extensions: []
|
122
136
|
extra_rdoc_files: []
|
123
137
|
files:
|
@@ -129,7 +143,7 @@ files:
|
|
129
143
|
- app/components/application_component.rb
|
130
144
|
- app/components/react_component.rb
|
131
145
|
- bin/esbuild
|
132
|
-
- bin/
|
146
|
+
- bin/lightningcss
|
133
147
|
- config/routes.rb
|
134
148
|
- lib/proscenium.rb
|
135
149
|
- lib/proscenium/compiler.js
|
@@ -137,6 +151,7 @@ files:
|
|
137
151
|
- lib/proscenium/compilers/esbuild.js
|
138
152
|
- lib/proscenium/compilers/esbuild/argument_error.js
|
139
153
|
- lib/proscenium/compilers/esbuild/compile_error.js
|
154
|
+
- lib/proscenium/compilers/esbuild/css/postcss.js
|
140
155
|
- lib/proscenium/compilers/esbuild/css_plugin.js
|
141
156
|
- lib/proscenium/compilers/esbuild/env_plugin.js
|
142
157
|
- lib/proscenium/compilers/esbuild/http_bundle_plugin.js
|
@@ -151,18 +166,17 @@ files:
|
|
151
166
|
- lib/proscenium/middleware.rb
|
152
167
|
- lib/proscenium/middleware/base.rb
|
153
168
|
- lib/proscenium/middleware/esbuild.rb
|
154
|
-
- lib/proscenium/middleware/
|
169
|
+
- lib/proscenium/middleware/lightningcss.rb
|
155
170
|
- lib/proscenium/middleware/runtime.rb
|
156
171
|
- lib/proscenium/middleware/static.rb
|
172
|
+
- lib/proscenium/phlex.rb
|
157
173
|
- lib/proscenium/precompile.rb
|
158
174
|
- lib/proscenium/railtie.rb
|
159
175
|
- lib/proscenium/runtime/auto_reload.js
|
160
|
-
- lib/proscenium/runtime/component_manager/index.js
|
161
|
-
- lib/proscenium/runtime/component_manager/render_component.jsx
|
162
|
-
- lib/proscenium/runtime/import_css.js
|
163
176
|
- lib/proscenium/runtime/react_shim/index.js
|
164
177
|
- lib/proscenium/runtime/react_shim/package.json
|
165
178
|
- lib/proscenium/side_load.rb
|
179
|
+
- lib/proscenium/utils.js
|
166
180
|
- lib/proscenium/version.rb
|
167
181
|
- lib/proscenium/view_component.rb
|
168
182
|
- lib/proscenium/view_component/tag_builder.rb
|
data/bin/parcel_css
DELETED
Binary file
|
@@ -1,27 +0,0 @@
|
|
1
|
-
/* eslint-disable no-console */
|
2
|
-
|
3
|
-
export async function init() {
|
4
|
-
const elements = document.querySelectorAll('[data-component]')
|
5
|
-
|
6
|
-
if (elements.length < 1) return
|
7
|
-
|
8
|
-
const { default: renderComponent } = await import(`./render_component`)
|
9
|
-
|
10
|
-
Array.from(elements, ele => {
|
11
|
-
const data = JSON.parse(ele.getAttribute('data-component'))
|
12
|
-
|
13
|
-
let isVisible = false
|
14
|
-
const observer = new IntersectionObserver(entries => {
|
15
|
-
entries.forEach(entry => {
|
16
|
-
if (!isVisible && entry.isIntersecting) {
|
17
|
-
isVisible = true
|
18
|
-
observer.unobserve(ele)
|
19
|
-
|
20
|
-
renderComponent(ele, data)
|
21
|
-
}
|
22
|
-
})
|
23
|
-
})
|
24
|
-
|
25
|
-
observer.observe(ele)
|
26
|
-
})
|
27
|
-
}
|
@@ -1,36 +0,0 @@
|
|
1
|
-
/* eslint-disable no-console */
|
2
|
-
|
3
|
-
import { RAILS_ENV } from 'env'
|
4
|
-
|
5
|
-
// We don't use JSX, as doing so would auto-inject React. We don't want to do this, as React is lazy
|
6
|
-
// loaded only when needed.
|
7
|
-
export default async function (ele, data) {
|
8
|
-
const { createElement, useEffect, lazy, Suspense } = await import('react')
|
9
|
-
const { createRoot } = await import('react-dom/client')
|
10
|
-
|
11
|
-
const component = lazy(() => import(`/app/components${data.path}.jsx`))
|
12
|
-
const contentLoader = data.contentLoader && ele.firstElementChild
|
13
|
-
|
14
|
-
const Fallback = ({ contentLoader }) => {
|
15
|
-
useEffect(() => {
|
16
|
-
contentLoader && contentLoader.remove()
|
17
|
-
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
18
|
-
|
19
|
-
if (!contentLoader) return null
|
20
|
-
|
21
|
-
return (
|
22
|
-
<div
|
23
|
-
style={{ height: '100%' }}
|
24
|
-
dangerouslySetInnerHTML={{ __html: contentLoader.outerHTML }}
|
25
|
-
></div>
|
26
|
-
)
|
27
|
-
}
|
28
|
-
|
29
|
-
createRoot(ele).render(
|
30
|
-
<Suspense fallback={<Fallback contentLoader={contentLoader} />}>
|
31
|
-
{createElement(component, data.props)}
|
32
|
-
</Suspense>
|
33
|
-
)
|
34
|
-
|
35
|
-
RAILS_ENV === 'development' && console.debug(`[REACT]`, `Rendered ${data.path.slice(1)}`)
|
36
|
-
}
|
@@ -1,46 +0,0 @@
|
|
1
|
-
async function digest(value) {
|
2
|
-
value = new TextEncoder().encode(value)
|
3
|
-
const view = new DataView(await crypto.subtle.digest('SHA-1', value))
|
4
|
-
|
5
|
-
let hexCodes = ''
|
6
|
-
for (let index = 0; index < view.byteLength; index += 4) {
|
7
|
-
hexCodes += view.getUint32(index).toString(16).padStart(8, '0')
|
8
|
-
}
|
9
|
-
|
10
|
-
return hexCodes.slice(0, 8)
|
11
|
-
}
|
12
|
-
|
13
|
-
const proxyCache = {}
|
14
|
-
|
15
|
-
export async function importCssModule(path) {
|
16
|
-
appendStylesheet(path)
|
17
|
-
|
18
|
-
if (Object.keys(proxyCache).includes(path)) {
|
19
|
-
return proxyCache[path]
|
20
|
-
}
|
21
|
-
|
22
|
-
const hashValue = await digest(path)
|
23
|
-
return (proxyCache[path] = new Proxy(
|
24
|
-
{},
|
25
|
-
{
|
26
|
-
get(target, prop, receiver) {
|
27
|
-
if (prop in target || typeof prop === 'symbol') {
|
28
|
-
return Reflect.get(target, prop, receiver)
|
29
|
-
} else {
|
30
|
-
return `${prop}${hashValue}`
|
31
|
-
}
|
32
|
-
}
|
33
|
-
}
|
34
|
-
))
|
35
|
-
}
|
36
|
-
|
37
|
-
export function appendStylesheet(path) {
|
38
|
-
// Make sure we only load the stylesheet once.
|
39
|
-
if (document.head.querySelector(`link[rel=stylesheet][href='${path}']`)) return
|
40
|
-
|
41
|
-
const ele = document.createElement('link')
|
42
|
-
ele.setAttribute('rel', 'stylesheet')
|
43
|
-
ele.setAttribute('media', 'all')
|
44
|
-
ele.setAttribute('href', path)
|
45
|
-
document.head.appendChild(ele)
|
46
|
-
}
|