@wix/origami-transforms 0.1.14 → 0.1.15
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.
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every registration in `files`, by its `companionApp.<Name>` type.
|
|
3
|
+
* @param {Iterable<readonly [string, string]>} files Each extension file's path and source.
|
|
4
|
+
* @returns {Map<string, CompanionRegistration>}
|
|
5
|
+
*/
|
|
6
|
+
export function readCompanionRegistrations(files: Iterable<readonly [string, string]>): Map<string, CompanionRegistration>;
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} CompanionModule
|
|
9
|
+
* @property {CompanionRegistration} registration
|
|
10
|
+
* @property {CompanionImport} import How a page imports it: a component as its entry exports it, a context provider
|
|
11
|
+
* as the entry's default export.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Every `@site/site-code/<name>` a page can import, by its `companionApp` name. A provider's hook is imported from
|
|
15
|
+
* the provider's module.
|
|
16
|
+
* @param {ReadonlyMap<string, CompanionRegistration>} registrations
|
|
17
|
+
* @returns {Map<string, CompanionModule>}
|
|
18
|
+
*/
|
|
19
|
+
export function companionModules(registrations: ReadonlyMap<string, CompanionRegistration>): Map<string, CompanionModule>;
|
|
20
|
+
export function isExtensionFile(path: string): boolean;
|
|
21
|
+
export function importTarget(from: string, specifier: string): string;
|
|
22
|
+
export function entryTarget(registration: CompanionRegistration, srcDir: string): string;
|
|
23
|
+
export function findFile(target: string, exists: (path: string) => boolean): string | undefined;
|
|
24
|
+
export function companionExports(modules: ReadonlyMap<string, CompanionModule>, entry: (registration: CompanionRegistration) => string): Record<string, string>;
|
|
25
|
+
export function readCompanionRegistrationsIn(srcDir: string): Map<string, CompanionRegistration>;
|
|
26
|
+
export type CompanionModule = {
|
|
27
|
+
registration: CompanionRegistration;
|
|
28
|
+
/**
|
|
29
|
+
* How a page imports it: a component as its entry exports it, a context provider
|
|
30
|
+
* as the entry's default export.
|
|
31
|
+
*/
|
|
32
|
+
import: CompanionImport;
|
|
33
|
+
};
|
|
34
|
+
export type CompanionImport = {
|
|
35
|
+
kind: "default";
|
|
36
|
+
} | {
|
|
37
|
+
kind: "named";
|
|
38
|
+
exportName: string;
|
|
39
|
+
};
|
|
40
|
+
export type CompanionComponentRegistration = {
|
|
41
|
+
kind: "component";
|
|
42
|
+
/**
|
|
43
|
+
* The registration's file.
|
|
44
|
+
*/
|
|
45
|
+
path: string;
|
|
46
|
+
/**
|
|
47
|
+
* The client entry's import as written (`./component.tsx?url`), relative to `path`.
|
|
48
|
+
*/
|
|
49
|
+
source: string;
|
|
50
|
+
/**
|
|
51
|
+
* How the entry's component is exported.
|
|
52
|
+
*/
|
|
53
|
+
componentImport: CompanionImport;
|
|
54
|
+
};
|
|
55
|
+
export type CompanionContextRegistration = {
|
|
56
|
+
kind: "contextProvider";
|
|
57
|
+
/**
|
|
58
|
+
* The registration's file.
|
|
59
|
+
*/
|
|
60
|
+
path: string;
|
|
61
|
+
/**
|
|
62
|
+
* `resources.client.url`, relative to the companion app's `src`, as the Wix CLI reads it.
|
|
63
|
+
* The module's default export is the provider.
|
|
64
|
+
*/
|
|
65
|
+
source: string;
|
|
66
|
+
/**
|
|
67
|
+
* `resources.contextSpecifier.hook`, the module's named export that reads the context.
|
|
68
|
+
*/
|
|
69
|
+
hook: string;
|
|
70
|
+
};
|
|
71
|
+
export type CompanionRegistration = CompanionComponentRegistration | CompanionContextRegistration;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-transforms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Babel and PostCSS checks and transforms for Wix origami page files.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/core": "^7.26.0",
|
|
18
|
+
"@babel/parser": "^7.25.0",
|
|
19
|
+
"@babel/types": "^7.25.2",
|
|
18
20
|
"@wix/origami-fonts": "1.0.1",
|
|
19
21
|
"@wix/origami-system-components": "1.9.0",
|
|
20
22
|
"postcss": "^8.5.3",
|
|
@@ -54,9 +56,13 @@
|
|
|
54
56
|
"./wrapper-import": {
|
|
55
57
|
"types": "./dist/wrapperImport.d.ts",
|
|
56
58
|
"default": "./src/wrapperImport.js"
|
|
59
|
+
},
|
|
60
|
+
"./companion-registrations": {
|
|
61
|
+
"types": "./dist/companionRegistrations.d.ts",
|
|
62
|
+
"default": "./src/companionRegistrations.js"
|
|
57
63
|
}
|
|
58
64
|
},
|
|
59
65
|
"type": "module",
|
|
60
66
|
"types": "./dist/index.d.ts",
|
|
61
|
-
"falconPackageHash": "
|
|
67
|
+
"falconPackageHash": "08fa24b6d989c88b62d43cea5a959c50ee2a2212bc6cb279ecf53dc1"
|
|
62
68
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// A companion app's `editorReactComponent` and `contextProvider` registrations, read the same way by the export
|
|
2
|
+
// (origami-site-generator, from the exported files) and by the site's Vite plugin (origami-site-runtime, from disk).
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { posix } from 'node:path'
|
|
5
|
+
import { parse } from '@babel/parser'
|
|
6
|
+
import * as t from '@babel/types'
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ kind: 'default' } | { kind: 'named', exportName: string }} CompanionImport */
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} CompanionComponentRegistration
|
|
11
|
+
* @property {'component'} kind
|
|
12
|
+
* @property {string} path The registration's file.
|
|
13
|
+
* @property {string} source The client entry's import as written (`./component.tsx?url`), relative to `path`.
|
|
14
|
+
* @property {CompanionImport} componentImport How the entry's component is exported.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} CompanionContextRegistration
|
|
18
|
+
* @property {'contextProvider'} kind
|
|
19
|
+
* @property {string} path The registration's file.
|
|
20
|
+
* @property {string} source `resources.client.url`, relative to the companion app's `src`, as the Wix CLI reads it.
|
|
21
|
+
* The module's default export is the provider.
|
|
22
|
+
* @property {string} hook `resources.contextSpecifier.hook`, the module's named export that reads the context.
|
|
23
|
+
*/
|
|
24
|
+
/** @typedef {CompanionComponentRegistration | CompanionContextRegistration} CompanionRegistration */
|
|
25
|
+
|
|
26
|
+
/** @param {string} path */
|
|
27
|
+
export const isExtensionFile = path => /\.extension\.[cm]?[jt]sx?$/.test(path)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every registration in `files`, by its `companionApp.<Name>` type.
|
|
31
|
+
* @param {Iterable<readonly [string, string]>} files Each extension file's path and source.
|
|
32
|
+
* @returns {Map<string, CompanionRegistration>}
|
|
33
|
+
*/
|
|
34
|
+
export function readCompanionRegistrations(files) {
|
|
35
|
+
/** @type {Map<string, CompanionRegistration>} */
|
|
36
|
+
const registrations = new Map()
|
|
37
|
+
for (const [path, source] of files) {
|
|
38
|
+
const ast = parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript'] })
|
|
39
|
+
t.traverseFast(ast, node => {
|
|
40
|
+
if (!t.isCallExpression(node) || !t.isMemberExpression(node.callee)) return
|
|
41
|
+
const isComponent = t.isIdentifier(node.callee.property, { name: 'editorReactComponent' })
|
|
42
|
+
if (!isComponent && !t.isIdentifier(node.callee.property, { name: 'contextProvider' })) return
|
|
43
|
+
const config = node.arguments[0]
|
|
44
|
+
const type = property(config, 'type')
|
|
45
|
+
const resources = property(config, 'resources')
|
|
46
|
+
const registration = isComponent ? component(ast, path, resources) : contextProvider(path, resources)
|
|
47
|
+
if (!t.isStringLiteral(type)) throw new Error(`Unsupported component registration: ${path}`)
|
|
48
|
+
if (!/^companionApp\.[^.]+$/.test(type.value))
|
|
49
|
+
throw new Error(`Unsupported companion component namespace: ${type.value}`)
|
|
50
|
+
if (registrations.has(type.value)) throw new Error(`Duplicate user code component: ${type.value}`)
|
|
51
|
+
registrations.set(type.value, { ...registration, path })
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
return registrations
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {t.File} ast @param {string} path @param {t.Expression | undefined} resources
|
|
59
|
+
* @returns {Omit<CompanionComponentRegistration, 'path'>}
|
|
60
|
+
*/
|
|
61
|
+
const component = (ast, path, resources) => {
|
|
62
|
+
const url = property(property(resources, 'client'), 'componentUrl')
|
|
63
|
+
if (!t.isIdentifier(url)) throw new Error(`Unsupported component registration: ${path}`)
|
|
64
|
+
const declaration = ast.program.body.find(
|
|
65
|
+
statement =>
|
|
66
|
+
t.isImportDeclaration(statement) && statement.specifiers.some(specifier => specifier.local.name === url.name)
|
|
67
|
+
)
|
|
68
|
+
if (!t.isImportDeclaration(declaration)) throw new Error(`Missing component entry: ${path}`)
|
|
69
|
+
const specifier = declaration.specifiers.find(specifier => specifier.local.name === url.name)
|
|
70
|
+
if (!specifier || t.isImportNamespaceSpecifier(specifier))
|
|
71
|
+
throw new Error(`Unsupported component registration: ${path}`)
|
|
72
|
+
return {
|
|
73
|
+
kind: 'component',
|
|
74
|
+
source: declaration.source.value,
|
|
75
|
+
componentImport: t.isImportDefaultSpecifier(specifier)
|
|
76
|
+
? { kind: 'default' }
|
|
77
|
+
: {
|
|
78
|
+
kind: 'named',
|
|
79
|
+
exportName: t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value,
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} path @param {t.Expression | undefined} resources
|
|
86
|
+
* @returns {Omit<CompanionContextRegistration, 'path'>}
|
|
87
|
+
*/
|
|
88
|
+
const contextProvider = (path, resources) => {
|
|
89
|
+
const url = property(property(resources, 'client'), 'url')
|
|
90
|
+
const hook = property(property(resources, 'contextSpecifier'), 'hook')
|
|
91
|
+
if (!t.isStringLiteral(url) || !t.isStringLiteral(hook) || !/^[A-Za-z_$][\w$]*$/.test(hook.value))
|
|
92
|
+
throw new Error(`Unsupported context provider registration: ${path}`)
|
|
93
|
+
return { kind: 'contextProvider', source: url.value, hook: hook.value }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The file a relative import in `from` points at, without its query, before `findFile` fills in its extension.
|
|
98
|
+
* @param {string} from @param {string} specifier
|
|
99
|
+
*/
|
|
100
|
+
export const importTarget = (from, specifier) =>
|
|
101
|
+
posix.normalize(posix.join(posix.dirname(from), specifier.split(/[?#]/)[0]))
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Where a registration's client entry points, before `findFile` fills in its extension.
|
|
105
|
+
* @param {CompanionRegistration} registration @param {string} srcDir The companion app's `src`, in the same form as `path`.
|
|
106
|
+
*/
|
|
107
|
+
export const entryTarget = (registration, srcDir) =>
|
|
108
|
+
registration.kind === 'component'
|
|
109
|
+
? importTarget(registration.path, registration.source)
|
|
110
|
+
: importTarget(posix.join(srcDir, 'index'), registration.source)
|
|
111
|
+
|
|
112
|
+
// The files an import can mean, most likely first: as written, a `.js` import of a TypeScript source, then an added
|
|
113
|
+
// extension or index.
|
|
114
|
+
/** @param {string} target */
|
|
115
|
+
const candidates = target => [
|
|
116
|
+
target,
|
|
117
|
+
...(/\.[cm]?js$/.test(target)
|
|
118
|
+
? [
|
|
119
|
+
target.replace(/\.js$/, '.ts'),
|
|
120
|
+
target.replace(/\.js$/, '.tsx'),
|
|
121
|
+
target.replace(/\.mjs$/, '.mts'),
|
|
122
|
+
target.replace(/\.cjs$/, '.cts'),
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
125
|
+
...['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '/index.ts', '/index.tsx', '/index.js'].map(ext => target + ext),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The file an import target (from `importTarget` or `entryTarget`) means, or undefined when none exists.
|
|
130
|
+
* @param {string} target @param {(path: string) => boolean} exists
|
|
131
|
+
*/
|
|
132
|
+
export const findFile = (target, exists) => candidates(target).find(exists)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @typedef {object} CompanionModule
|
|
136
|
+
* @property {CompanionRegistration} registration
|
|
137
|
+
* @property {CompanionImport} import How a page imports it: a component as its entry exports it, a context provider
|
|
138
|
+
* as the entry's default export.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Every `@site/site-code/<name>` a page can import, by its `companionApp` name. A provider's hook is imported from
|
|
143
|
+
* the provider's module.
|
|
144
|
+
* @param {ReadonlyMap<string, CompanionRegistration>} registrations
|
|
145
|
+
* @returns {Map<string, CompanionModule>}
|
|
146
|
+
*/
|
|
147
|
+
export function companionModules(registrations) {
|
|
148
|
+
/** @type {Map<string, CompanionModule>} */
|
|
149
|
+
const modules = new Map()
|
|
150
|
+
for (const [type, registration] of registrations) {
|
|
151
|
+
const name = type.slice('companionApp.'.length)
|
|
152
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) throw new Error(`Unsupported extension component name: ${name}`)
|
|
153
|
+
modules.set(name, {
|
|
154
|
+
registration,
|
|
155
|
+
import: registration.kind === 'component' ? registration.componentImport : { kind: 'default' },
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
return modules
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The site-code `package.json` `exports` that TypeScript resolves `@site/site-code/<name>` through, one per module.
|
|
163
|
+
* @param {ReadonlyMap<string, CompanionModule>} modules
|
|
164
|
+
* @param {(registration: CompanionRegistration) => string} entry The registration's client entry, relative to the
|
|
165
|
+
* companion app.
|
|
166
|
+
* @returns {Record<string, string>}
|
|
167
|
+
*/
|
|
168
|
+
export const companionExports = (modules, entry) =>
|
|
169
|
+
Object.fromEntries([...modules].map(([name, { registration }]) => [`./${name}`, `./${entry(registration)}`]))
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every registration under a companion app's `src` on disk, with absolute paths; none when it has no `src`.
|
|
173
|
+
* @param {string} srcDir
|
|
174
|
+
*/
|
|
175
|
+
export const readCompanionRegistrationsIn = srcDir =>
|
|
176
|
+
readCompanionRegistrations(
|
|
177
|
+
extensionFiles(srcDir).map(file => /** @type {const} */ ([file, readFileSync(file, 'utf8')]))
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
/** @param {string} directory @returns {string[]} */
|
|
181
|
+
const extensionFiles = directory =>
|
|
182
|
+
existsSync(directory)
|
|
183
|
+
? readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
184
|
+
const path = posix.join(directory, entry.name)
|
|
185
|
+
if (entry.isDirectory()) return entry.name === 'node_modules' ? [] : extensionFiles(path)
|
|
186
|
+
return isExtensionFile(entry.name) ? [path] : []
|
|
187
|
+
})
|
|
188
|
+
: []
|
|
189
|
+
|
|
190
|
+
/** @param {unknown} node @param {string} name @returns {t.Expression | undefined} */
|
|
191
|
+
const property = (node, name) => {
|
|
192
|
+
if (!node || !t.isObjectExpression(/** @type {t.Node} */ (node))) return undefined
|
|
193
|
+
const match = /** @type {t.ObjectExpression} */ (node).properties.find(
|
|
194
|
+
candidate =>
|
|
195
|
+
t.isObjectProperty(candidate) &&
|
|
196
|
+
!candidate.computed &&
|
|
197
|
+
(t.isIdentifier(candidate.key, { name }) || t.isStringLiteral(candidate.key, { value: name }))
|
|
198
|
+
)
|
|
199
|
+
return t.isObjectProperty(match) && t.isExpression(match.value) ? match.value : undefined
|
|
200
|
+
}
|