proscenium 0.5.1-x86_64-darwin → 0.7.0-x86_64-darwin

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -92
  3. data/bin/proscenium +0 -0
  4. data/bin/proscenium.h +109 -0
  5. data/config/routes.rb +0 -3
  6. data/lib/proscenium/css_module/class_names_resolver.rb +66 -0
  7. data/lib/proscenium/css_module/resolver.rb +76 -0
  8. data/lib/proscenium/css_module.rb +18 -39
  9. data/lib/proscenium/esbuild/golib.rb +97 -0
  10. data/lib/proscenium/esbuild.rb +32 -0
  11. data/lib/proscenium/helper.rb +0 -23
  12. data/lib/proscenium/log_subscriber.rb +26 -0
  13. data/lib/proscenium/middleware/base.rb +28 -36
  14. data/lib/proscenium/middleware/esbuild.rb +18 -44
  15. data/lib/proscenium/middleware/url.rb +1 -6
  16. data/lib/proscenium/middleware.rb +12 -16
  17. data/lib/proscenium/phlex/component_concerns.rb +27 -0
  18. data/lib/proscenium/phlex/page.rb +62 -0
  19. data/lib/proscenium/phlex/react_component.rb +52 -8
  20. data/lib/proscenium/phlex/resolve_css_modules.rb +67 -0
  21. data/lib/proscenium/phlex.rb +34 -33
  22. data/lib/proscenium/railtie.rb +41 -67
  23. data/lib/proscenium/side_load/ensure_loaded.rb +25 -0
  24. data/lib/proscenium/side_load/helper.rb +25 -0
  25. data/lib/proscenium/side_load/monkey.rb +48 -0
  26. data/lib/proscenium/side_load.rb +58 -52
  27. data/lib/proscenium/version.rb +1 -1
  28. data/lib/proscenium/view_component/react_component.rb +14 -0
  29. data/lib/proscenium/view_component.rb +28 -18
  30. data/lib/proscenium.rb +79 -2
  31. metadata +35 -72
  32. data/app/channels/proscenium/connection.rb +0 -13
  33. data/app/channels/proscenium/reload_channel.rb +0 -9
  34. data/bin/esbuild +0 -0
  35. data/bin/lightningcss +0 -0
  36. data/lib/proscenium/compiler.js +0 -84
  37. data/lib/proscenium/compilers/esbuild/argument_error.js +0 -24
  38. data/lib/proscenium/compilers/esbuild/compile_error.js +0 -148
  39. data/lib/proscenium/compilers/esbuild/css/postcss.js +0 -67
  40. data/lib/proscenium/compilers/esbuild/css_plugin.js +0 -172
  41. data/lib/proscenium/compilers/esbuild/env_plugin.js +0 -46
  42. data/lib/proscenium/compilers/esbuild/http_bundle_plugin.js +0 -53
  43. data/lib/proscenium/compilers/esbuild/import_map.js +0 -59
  44. data/lib/proscenium/compilers/esbuild/resolve_plugin.js +0 -205
  45. data/lib/proscenium/compilers/esbuild/setup_plugin.js +0 -45
  46. data/lib/proscenium/compilers/esbuild/solidjs_plugin.js +0 -24
  47. data/lib/proscenium/compilers/esbuild.bench.js +0 -14
  48. data/lib/proscenium/compilers/esbuild.js +0 -179
  49. data/lib/proscenium/link_to_helper.rb +0 -40
  50. data/lib/proscenium/middleware/lightningcss.rb +0 -64
  51. data/lib/proscenium/middleware/outside_root.rb +0 -26
  52. data/lib/proscenium/middleware/runtime.rb +0 -22
  53. data/lib/proscenium/middleware/static.rb +0 -14
  54. data/lib/proscenium/phlex/component.rb +0 -9
  55. data/lib/proscenium/precompile.rb +0 -31
  56. data/lib/proscenium/runtime/auto_reload.js +0 -40
  57. data/lib/proscenium/runtime/react_shim/index.js +0 -1
  58. data/lib/proscenium/runtime/react_shim/package.json +0 -5
  59. data/lib/proscenium/utils.js +0 -8
  60. data/lib/tasks/assets.rake +0 -19
@@ -1,172 +0,0 @@
1
- import { crypto } from 'std/crypto/mod.ts'
2
- import { join, dirname, basename } from 'std/path/mod.ts'
3
-
4
- import { fileExists } from '../../utils.js'
5
- import postcss from './css/postcss.js'
6
- import setup from './setup_plugin.js'
7
-
8
- export default setup('css', async (build, options) => {
9
- const cwd = build.initialOptions.absWorkingDir
10
-
11
- let customMedia
12
- try {
13
- customMedia = await Deno.readTextFile(join(cwd, 'config', 'custom_media_queries.css'))
14
- } catch {
15
- // do nothing, as we don't require custom media.
16
- }
17
-
18
- return [
19
- {
20
- type: 'onLoad',
21
- filter: /\.css$/,
22
- namespace: 'file',
23
- async callback(args) {
24
- const hash = await digest(args.path.slice(cwd.length))
25
- const isCssModule = args.path.endsWith('.module.css')
26
-
27
- // If path is a CSS module, imported from JS, and a side-loaded ViewComponent stylesheet,
28
- // simply return a JS proxy of the class names. The stylesheet itself will have already been
29
- // side loaded. This avoids compiling the CSS all over again.
30
- if (isCssModule && args.pluginData?.importedFromJs && (await isViewComponent(args.path))) {
31
- return {
32
- resolveDir: cwd,
33
- loader: 'js',
34
- contents: cssModulesProxyTemplate(hash)
35
- }
36
- }
37
-
38
- let cmd = [
39
- options.lightningcssBin,
40
- '--nesting',
41
- '--error-recovery',
42
- args.pluginData?.importedFromJs && '--minify',
43
- '--targets',
44
- '>= 0.25%'
45
- ].filter(Boolean)
46
-
47
- // This will process the CSS with Postcss only if it needs to.
48
- let [tmpFile, contents] = await postcss(cwd, args.path)
49
-
50
- // As custom media are defined in their own file, we have to append the file contents to our
51
- // stylesheet, so that the custom media can be used.
52
- if (customMedia) {
53
- cmd.push('--custom-media')
54
-
55
- if (!tmpFile && !contents) {
56
- tmpFile = await Deno.makeTempFile()
57
- contents = await Deno.readTextFile(args.path)
58
- }
59
-
60
- contents += customMedia
61
- }
62
-
63
- if (tmpFile && contents) {
64
- await Deno.writeTextFile(tmpFile, contents)
65
- }
66
-
67
- if (isCssModule) {
68
- cmd = cmd.concat(['--css-modules', '--css-modules-pattern', `[local]${hash}`])
69
- }
70
-
71
- const p = Deno.run({
72
- cmd: [...cmd, tmpFile || args.path],
73
- stdout: 'piped',
74
- stderr: 'piped'
75
- })
76
-
77
- const { code } = await p.status()
78
- const rawOutput = await p.output()
79
- const rawError = await p.stderrOutput()
80
-
81
- // Even though Deno docs say that reading the outputs (above) closes their pipes, warnings
82
- // are raised during tests that the child process have not been closed. So we manually close
83
- // here.
84
- p.close()
85
-
86
- // Success!
87
- if (code === 0) {
88
- let contents = new TextDecoder().decode(rawOutput)
89
- if (isCssModule) {
90
- contents = JSON.parse(contents)
91
- }
92
-
93
- // If stylesheet is imported from JS, then we return JS code that appends the stylesheet
94
- // in a <style> in the <head> of the page, and if the stylesheet is a CSS module, it
95
- // exports a plain object of class names.
96
- if (args.pluginData?.importedFromJs) {
97
- const code = isCssModule ? contents.code : contents
98
- const mod = [
99
- `let e = document.querySelector('#_${hash}');`,
100
- 'if (!e) {',
101
- "e = document.createElement('style');",
102
- `e.id = '_${hash}';`,
103
- 'document.head.appendChild(e);',
104
- `e.appendChild(document.createTextNode(\`${code}\`));`,
105
- '}'
106
- ]
107
-
108
- if (isCssModule) {
109
- const classes = {}
110
- for (const key in contents.exports) {
111
- if (Object.hasOwnProperty.call(contents.exports, key)) {
112
- classes[key] = contents.exports[key].name
113
- }
114
- }
115
- mod.push(`export default ${JSON.stringify(classes)};`)
116
- }
117
-
118
- // We are importing from JS, so return the entire result from LightningCSS via the js
119
- // loader.
120
- return {
121
- resolveDir: cwd,
122
- loader: 'js',
123
- contents: mod.join('')
124
- }
125
- }
126
-
127
- return { loader: 'css', contents: isCssModule ? contents.code : contents }
128
- } else {
129
- const errorString = new TextDecoder().decode(rawError)
130
- throw errorString
131
- }
132
- }
133
- }
134
- ]
135
- })
136
-
137
- async function digest(value) {
138
- value = new TextEncoder().encode(value)
139
- const view = new DataView(await crypto.subtle.digest('SHA-1', value))
140
-
141
- let hexCodes = ''
142
- for (let index = 0; index < view.byteLength; index += 4) {
143
- hexCodes += view.getUint32(index).toString(16).padStart(8, '0')
144
- }
145
-
146
- return hexCodes.slice(0, 8)
147
- }
148
-
149
- async function isViewComponent(path) {
150
- const fileName = basename(path)
151
- const dirName = dirname(path)
152
-
153
- return (
154
- (fileName === 'component.module.css' && (await fileExists(join(dirName, 'component.rb')))) ||
155
- (fileName.endsWith('_component.module.css') &&
156
- (await fileExists(join(dirName, fileName.replace(/\.module\.css$/, '.rb')))))
157
- )
158
- }
159
-
160
- function cssModulesProxyTemplate(hash) {
161
- return [
162
- `export default new Proxy( {}, {`,
163
- ` get(target, prop, receiver) {`,
164
- ` if (prop in target || typeof prop === 'symbol') {`,
165
- ` return Reflect.get(target, prop, receiver)`,
166
- ` } else {`,
167
- ` return prop + '${hash}'`,
168
- ` }`,
169
- ` }`,
170
- `})`
171
- ].join('')
172
- }
@@ -1,46 +0,0 @@
1
- import setup from './setup_plugin.js'
2
-
3
- // Export environment variables as named exports only. You can also import from `env:ENV_VAR_NAME`,
4
- // which will return the value of the environment variable as the default export. This allows you to
5
- // safely import a variable regardless of its existence.
6
- export default setup('env', () => {
7
- return [
8
- {
9
- type: 'onResolve',
10
- filter: /^env(:.+)?$/,
11
- callback({ path }) {
12
- return { path, namespace: 'env' }
13
- }
14
- },
15
-
16
- {
17
- type: 'onLoad',
18
- filter: /.*/,
19
- namespace: 'env',
20
- callback({ path }) {
21
- if (path.includes(':')) {
22
- const name = Deno.env.get(path.split(':')[1])
23
-
24
- return {
25
- loader: 'js',
26
- contents: name ? `export default '${name}'` : `export default ${name}`
27
- }
28
- }
29
-
30
- const env = Deno.env.toObject()
31
- const contents = []
32
-
33
- for (const key in env) {
34
- if (Object.hasOwnProperty.call(env, key)) {
35
- contents.push(`export const ${key} = '${env[key]}'`)
36
- }
37
- }
38
-
39
- return {
40
- loader: 'js',
41
- contents: contents.join(';')
42
- }
43
- }
44
- }
45
- ]
46
- })
@@ -1,53 +0,0 @@
1
- import setup from './setup_plugin.js'
2
-
3
- export default setup('httpBundle', () => {
4
- return [
5
- {
6
- type: 'onResolve',
7
- filter: /^https?:\/\//,
8
- callback(args) {
9
- let queryParams, suffix
10
- if (args.path.includes('?')) {
11
- const [path, query] = args.path.split('?')
12
- queryParams = new URLSearchParams(query)
13
- suffix = `?${query}`
14
- args.path = path
15
- }
16
-
17
- if (queryParams?.has('bundle')) {
18
- return { path: args.path, namespace: 'httpBundle', suffix }
19
- } else {
20
- return { external: true }
21
- }
22
- }
23
- },
24
-
25
- // Intercept all import paths inside downloaded files and resolve them against the original URL.
26
- {
27
- type: 'onResolve',
28
- filter: /.*/,
29
- namespace: 'httpBundle',
30
- callback(args) {
31
- return {
32
- path: new URL(args.path, args.importer).toString(),
33
- namespace: 'httpBundle'
34
- }
35
- }
36
- },
37
-
38
- // Download and return the content.
39
- //
40
- // TODO: cache this!
41
- {
42
- type: 'onLoad',
43
- filter: /.*/,
44
- namespace: 'httpBundle',
45
- async callback(args) {
46
- const textResponse = await fetch(args.path)
47
- const contents = await textResponse.text()
48
-
49
- return { contents }
50
- }
51
- }
52
- ]
53
- })
@@ -1,59 +0,0 @@
1
- import { join } from 'std/path/mod.ts'
2
- import { parseFromString } from 'import-maps/resolve'
3
-
4
- const baseURL = new URL('file://')
5
-
6
- class ImportMapError extends Error {
7
- constructor(fileName, ...params) {
8
- super(...params)
9
-
10
- if (Error.captureStackTrace) {
11
- Error.captureStackTrace(this, ImportMapError)
12
- }
13
-
14
- this.name = 'ImportMapError'
15
- this.file = fileName
16
- }
17
- }
18
-
19
- export function readImportMap(fileName, rootDir) {
20
- let importMap
21
-
22
- if (fileName) {
23
- importMap = readFile(fileName, rootDir, true)
24
- } else {
25
- fileName = ['config/import_map.json', 'config/import_map.js'].find(f => {
26
- const result = readFile(f, rootDir)
27
- if (result) {
28
- importMap = result
29
- return true
30
- }
31
- })
32
- }
33
-
34
- return importMap
35
- }
36
-
37
- function readFile(file, rootDir, required = false) {
38
- let contents = null
39
-
40
- try {
41
- contents = Deno.readTextFileSync(join(rootDir, file))
42
- } catch (error) {
43
- if (required) {
44
- throw new ImportMapError(file, error.message, { cause: error })
45
- }
46
- }
47
-
48
- if (contents === null) return null
49
-
50
- try {
51
- if (file.endsWith('.js')) {
52
- contents = JSON.stringify(eval(contents)(Deno.env.get('RAILS_ENV')))
53
- }
54
-
55
- return parseFromString(contents, baseURL)
56
- } catch (error) {
57
- throw new ImportMapError(file, error.message, { cause: error })
58
- }
59
- }
@@ -1,205 +0,0 @@
1
- import { join, resolve } from 'std/path/mod.ts'
2
- import { resolve as resolveFromImportMap } from 'import-maps/resolve'
3
- import { cache } from 'cache'
4
-
5
- import setup from './setup_plugin.js'
6
-
7
- const importKinds = ['import-statement', 'dynamic-import', 'require-call', 'import-rule']
8
-
9
- export default setup('resolve', (build, options) => {
10
- const { runtimeDir, importMap } = options
11
- const cwd = build.initialOptions.absWorkingDir
12
- const runtimeCwdAlias = `${cwd}/proscenium-runtime`
13
- let bundled = false
14
-
15
- const env = Deno.env.get('RAILS_ENV')
16
- const isProd = env === 'production'
17
-
18
- return [
19
- {
20
- // Filters for imports starting with `url:http://` or `url:https://`; returning the path
21
- // without the `url:` prefix, and a namespace of 'url`
22
- type: 'onResolve',
23
- filter: /^url:https?:\/\//,
24
- callback(args) {
25
- return {
26
- path: args.path.slice(4),
27
- namespace: 'url'
28
- }
29
- }
30
- },
31
-
32
- {
33
- type: 'onResolve',
34
- filter: /.*/,
35
- namespace: 'url',
36
- callback(args) {
37
- if (!isBareModule(args.path)) {
38
- return {
39
- path: new URL(args.path, args.importer).toString(),
40
- namespace: 'url'
41
- }
42
- }
43
- }
44
- },
45
-
46
- {
47
- type: 'onLoad',
48
- filter: /.*/,
49
- namespace: 'url',
50
- async callback(args) {
51
- const file = await cache(args.path)
52
- const contents = await Deno.readTextFile(file.path)
53
-
54
- return { contents }
55
- }
56
- },
57
-
58
- {
59
- type: 'onResolve',
60
- filter: /.*/,
61
- async callback(args) {
62
- if (args.path.includes('?')) {
63
- const [path, query] = args.path.split('?')
64
- args.path = path
65
- args.suffix = `?${query}`
66
- args.queryParams = new URLSearchParams(query)
67
- } else if (options.cacheQueryString && options.cacheQueryString !== '') {
68
- args.suffix = `?${options.cacheQueryString}`
69
- }
70
-
71
- // Mark remote modules as external. If not css, then the path is prefixed with "url:", which
72
- // is then handled by the Url Middleware.
73
- if (
74
- !args.importer.endsWith('.css') &&
75
- (args.path.startsWith('http://') || args.path.startsWith('https://'))
76
- ) {
77
- return { path: `/url:${encodeURIComponent(args.path)}`, external: true }
78
- }
79
-
80
- // Rewrite the path to the actual runtime directory.
81
- if (args.path.startsWith(runtimeCwdAlias)) {
82
- return { path: join(runtimeDir, args.path.slice(runtimeCwdAlias.length)) }
83
- }
84
-
85
- // Everything else is unbundled.
86
- if (importKinds.includes(args.kind)) {
87
- return await unbundleImport(args)
88
- }
89
- }
90
- }
91
- ]
92
-
93
- // Resolve the given `params.path` to a path relative to the Rails root.
94
- //
95
- // Examples:
96
- // 'react' -> '/.../node_modules/react/index.js'
97
- // './my_module' -> '/.../app/my_module.js'
98
- // '/app/my_module' -> '/.../app/my_module.js'
99
- async function unbundleImport(params) {
100
- const result = { path: params.path, suffix: params.suffix }
101
-
102
- if (importMap) {
103
- let baseURL
104
- if (params.importer.startsWith('https://') || params.importer.startsWith('http://')) {
105
- baseURL = new URL(params.importer)
106
- } else {
107
- baseURL = new URL(params.importer.slice(cwd.length), 'file://')
108
- }
109
-
110
- const { matched, resolvedImport } = resolveFromImportMap(params.path, importMap, baseURL)
111
-
112
- if (matched) {
113
- if (resolvedImport.protocol === 'file:') {
114
- params.path = resolvedImport.pathname
115
- } else {
116
- if (params.importer.endsWith('.css')) {
117
- return { path: resolvedImport.href, external: true }
118
- }
119
-
120
- return { path: `/url:${encodeURIComponent(resolvedImport.href)}`, external: true }
121
- }
122
- }
123
- }
124
-
125
- // Absolute path - append to current working dir.
126
- if (params.path.startsWith('/')) {
127
- result.path = resolve(cwd, params.path.slice(1))
128
- }
129
-
130
- const resOptions = {
131
- // If path is a bare module (node_modules), and resolveDir is the Proscenium runtime dir, or
132
- // is the current working dir, then use `cwd` as the `resolveDir`, otherwise pass it through
133
- // as is. This ensures that nested node_modules are resolved correctly.
134
- resolveDir:
135
- isBareModule(result.path) &&
136
- (!params.resolveDir.startsWith(cwd) || params.resolveDir.startsWith(runtimeDir))
137
- ? cwd
138
- : params.resolveDir,
139
- kind: params.kind,
140
- pluginData: {
141
- // We use this property later on, as we should ignore this resolution call.
142
- isResolvingPath: true
143
- }
144
- }
145
-
146
- // Resolve the path using esbuild's internal resolution. This allows us to import node packages
147
- // and extension-less paths without custom code, as esbuild with resolve them for us.
148
- const resolveResult = await build.resolve(result.path, resOptions)
149
-
150
- // Simple return the resolved result if we have an error. Usually happens when module is not
151
- // found.
152
- if (resolveResult.errors.length > 0) return resolveResult
153
-
154
- // If 'bundle-all' queryParam is defined, return the resolveResult.
155
- if (bundled || params.queryParams?.has('bundle-all')) {
156
- bundled = true
157
- return { ...resolveResult, suffix: '?bundle-all' }
158
- }
159
-
160
- // If 'bundle' queryParam is defined, return the resolveResult.
161
- if (params.queryParams?.has('bundle')) {
162
- return { ...resolveResult, suffix: '?bundle' }
163
- }
164
-
165
- if (resolveResult.path.startsWith(runtimeDir)) {
166
- result.path = '/proscenium-runtime' + resolveResult.path.slice(runtimeDir.length)
167
- } else if (!resolveResult.path.startsWith(cwd) && !isProd) {
168
- // Resolved path is not in the current working directory. It could be linked to a file outside
169
- // the CWD, or it's just invalid. If not in production, return as an outsideRoot namespaced,
170
- // and externally suffixed path. This lets the Rails Proscenium::Middleware::OutsideRoot
171
- // handle the import.
172
- return {
173
- ...resolveResult,
174
- namespace: 'outsideRoot',
175
- path: `${resolveResult.path}?outsideRoot`,
176
- external: true
177
- }
178
- } else {
179
- result.path = resolveResult.path.slice(cwd.length)
180
- }
181
-
182
- result.sideEffects = resolveResult.sideEffects
183
-
184
- if (
185
- params.path.endsWith('.css') &&
186
- params.kind === 'import-statement' &&
187
- /\.jsx?$/.test(params.importer)
188
- ) {
189
- // We're importing a CSS file from JS(X).
190
- return { ...resolveResult, pluginData: { importedFromJs: true } }
191
- } else {
192
- result.external = true
193
- }
194
-
195
- if (result.suffix && result.suffix !== '') {
196
- result.path = `${result.path}${result.suffix}`
197
- }
198
-
199
- return result
200
- }
201
- })
202
-
203
- function isBareModule(mod) {
204
- return !mod.startsWith('/') && !mod.startsWith('.')
205
- }
@@ -1,45 +0,0 @@
1
- export default (pluginName, pluginFn) => {
2
- return (options = {}) => ({
3
- name: pluginName,
4
- async setup(build) {
5
- const callbacks = await pluginFn(build, options)
6
-
7
- callbacks.forEach(({ type, callback, filter, namespace }) => {
8
- if (type === 'onResolve') {
9
- build.onResolve({ filter, namespace }, async params => {
10
- if (params.pluginData?.isResolvingPath) return
11
-
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
- }
28
-
29
- return results
30
- })
31
- } else if (type === 'onLoad') {
32
- build.onLoad({ filter, namespace }, async params => {
33
- options.debug && console.debug(`plugin(${pluginName}):onLoad`, { params })
34
-
35
- const results = await callback(params)
36
-
37
- options.debug && console.debug(`plugin(${pluginName}):onLoad`, { results })
38
-
39
- return results
40
- })
41
- }
42
- })
43
- }
44
- })
45
- }
@@ -1,24 +0,0 @@
1
- import { basename } from 'std/path/mod.ts'
2
- import { transformAsync } from '@babel/core'
3
- import solid from 'babel-preset-solid'
4
-
5
- import { setup } from './setup_plugin.js'
6
-
7
- export default setup('solidjs', () => {
8
- return [
9
- {
10
- type: 'onLoad',
11
- filter: /\.jsx$/,
12
- async callback(args) {
13
- const source = await Deno.readTextFile(args.path)
14
-
15
- const { code } = await transformAsync(source, {
16
- presets: [solid],
17
- filename: basename(args.path)
18
- })
19
-
20
- return { contents: code, loader: 'js' }
21
- }
22
- }
23
- ]
24
- })
@@ -1,14 +0,0 @@
1
- import { join } from 'std/path/mod.ts'
2
-
3
- import compile from './esbuild.js'
4
-
5
- const root = join(Deno.cwd(), 'test', 'internal')
6
- const lightningcssBin = join(Deno.cwd(), 'bin', 'lightningcss')
7
-
8
- Deno.bench('esbuild js', async () => {
9
- await compile(['lib/foo.js'], { root, lightningcssBin })
10
- })
11
-
12
- Deno.bench('esbuild css', async () => {
13
- await compile(['lib/foo.css'], { root, lightningcssBin })
14
- })