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

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