oc-template-preact-compiler 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/index.js +11 -0
- package/lib/compile.js +56 -0
- package/lib/higherOrderServerTemplate.js +28 -0
- package/lib/htmlTemplate.js +26 -0
- package/lib/oc-app.d.ts +72 -0
- package/lib/preactOCProviderTemplate.js +57 -0
- package/lib/to-publish/cssModulesPlugin.js +81 -0
- package/lib/to-publish/viteServer.js +75 -0
- package/lib/to-publish/viteView.js +134 -0
- package/lib/verifyConfig.js +211 -0
- package/package.json +60 -0
- package/scaffold/src/.eslintrc.json +3 -0
- package/scaffold/src/package.json +42 -0
- package/scaffold/src/public/logo.png +0 -0
- package/scaffold/src/src/App.test.tsx +28 -0
- package/scaffold/src/src/App.tsx +51 -0
- package/scaffold/src/src/oc-app.d.ts +2 -0
- package/scaffold/src/src/server.ts +34 -0
- package/scaffold/src/src/setupTests.js +1 -0
- package/scaffold/src/src/styles.css +30 -0
- package/scaffold/src/src/types.ts +15 -0
- package/scaffold/src/tsconfig.json +27 -0
- package/scaffold/src/vite.config.ts +9 -0
- package/utils/.eslintrc.js +13 -0
- package/utils/useData.tsx +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 OpenComponents community
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/index.js
ADDED
package/lib/compile.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const createCompile = require('oc-generic-template-compiler').createCompile;
|
|
4
|
+
const compileStatics = require('oc-statics-compiler');
|
|
5
|
+
const getInfo = require('oc-template-preact').getInfo;
|
|
6
|
+
const { preact } = require('@preact/preset-vite');
|
|
7
|
+
|
|
8
|
+
const viteView = require('./to-publish/viteView');
|
|
9
|
+
const viteServer = require('./to-publish/viteServer');
|
|
10
|
+
const verifyTypeScriptSetup = require('./verifyConfig');
|
|
11
|
+
const higherOrderServerTemplate = require('./higherOrderServerTemplate');
|
|
12
|
+
const preactOCProviderTemplate = require('./preactOCProviderTemplate');
|
|
13
|
+
const htmlTemplate = require('./htmlTemplate');
|
|
14
|
+
|
|
15
|
+
const compiler = createCompile({
|
|
16
|
+
compileView: (options, cb) =>
|
|
17
|
+
viteView(
|
|
18
|
+
{
|
|
19
|
+
...options,
|
|
20
|
+
plugins: [preact()],
|
|
21
|
+
viewWrapper: ({ viewPath }) => preactOCProviderTemplate({ viewPath }),
|
|
22
|
+
htmlTemplate,
|
|
23
|
+
externals: getInfo().externals
|
|
24
|
+
},
|
|
25
|
+
cb
|
|
26
|
+
),
|
|
27
|
+
compileServer: (options, cb) =>
|
|
28
|
+
viteServer({ ...options, serverWrapper: higherOrderServerTemplate }, cb),
|
|
29
|
+
compileStatics,
|
|
30
|
+
getInfo
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const hasTsExtension = (file) => !!file.match(/\.tsx?$/);
|
|
34
|
+
|
|
35
|
+
// OPTIONS
|
|
36
|
+
// =======
|
|
37
|
+
// componentPath
|
|
38
|
+
// componentPackage,
|
|
39
|
+
// logger,
|
|
40
|
+
// minify
|
|
41
|
+
// ocPackage
|
|
42
|
+
// publishPath
|
|
43
|
+
// verbose,
|
|
44
|
+
// watch,
|
|
45
|
+
// production
|
|
46
|
+
module.exports = function compile(options, callback) {
|
|
47
|
+
const viewFileName = options.componentPackage.oc.files.template.src;
|
|
48
|
+
const serverFileName = options.componentPackage.oc.files.data;
|
|
49
|
+
const usingTypescript = hasTsExtension(viewFileName) || hasTsExtension(serverFileName);
|
|
50
|
+
|
|
51
|
+
if (usingTypescript) {
|
|
52
|
+
verifyTypeScriptSetup(options.componentPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return compiler(options, callback);
|
|
56
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const removeExtension = (path) => path.replace(/\.(j|t)sx?$/, '');
|
|
2
|
+
|
|
3
|
+
const higherOrderServerTemplate = ({ serverPath, componentName, componentVersion }) => `
|
|
4
|
+
import { data as dataProvider } from '${removeExtension(serverPath)}';
|
|
5
|
+
|
|
6
|
+
export const data = (context : any, callback : (error: any, data?: any) => void) => {
|
|
7
|
+
dataProvider(context, (error: any, model: any) => {
|
|
8
|
+
if (error) {
|
|
9
|
+
return callback(error);
|
|
10
|
+
}
|
|
11
|
+
const props = Object.assign({}, model, {
|
|
12
|
+
_staticPath: context.staticPath,
|
|
13
|
+
_baseUrl: context.baseUrl,
|
|
14
|
+
_componentName: "${componentName}",
|
|
15
|
+
_componentVersion: "${componentVersion}"
|
|
16
|
+
});
|
|
17
|
+
const srcPathHasProtocol = context.staticPath.indexOf("http") === 0;
|
|
18
|
+
const srcPath = srcPathHasProtocol ? context.staticPath : ("https:" + context.staticPath);
|
|
19
|
+
return callback(null, Object.assign({}, {
|
|
20
|
+
preactComponent: {
|
|
21
|
+
props
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
module.exports = higherOrderServerTemplate;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const viewTemplate = ({ templateId, css, bundle, hash }) => `function(model){
|
|
2
|
+
var modelHTML = model.__html ? model.__html : '';
|
|
3
|
+
var staticPath = model.preactComponent.props._staticPath;
|
|
4
|
+
var props = JSON.stringify(model.preactComponent.props);
|
|
5
|
+
window.oc = window.oc || {};
|
|
6
|
+
window.oc.__typescriptReactTemplate = window.oc.__typescriptReactTemplate || { count: 0 };
|
|
7
|
+
oc.preactComponents = oc.preactComponents || {};
|
|
8
|
+
oc.preactComponents['${hash}'] = oc.preactComponents['${hash}'] || (${bundle});
|
|
9
|
+
var count = window.oc.__typescriptReactTemplate.count;
|
|
10
|
+
var templateId = "${templateId}-" + count;
|
|
11
|
+
window.oc.__typescriptReactTemplate.count++;
|
|
12
|
+
return '<div id="' + templateId + '" class="${templateId}">' + modelHTML + '</div>' +
|
|
13
|
+
'${css ? '<style>' + css + '</style>' : ''}' +
|
|
14
|
+
'<script>' +
|
|
15
|
+
'window.oc = window.oc || {};' +
|
|
16
|
+
'oc.cmd = oc.cmd || [];' +
|
|
17
|
+
'oc.cmd.push(function(oc){' +
|
|
18
|
+
'${css ? "oc.events.fire(\\'oc:cssDidMount\\', \\'" + css + "\\');" : ''}' +
|
|
19
|
+
'var targetNode = document.getElementById("' + templateId + '");' +
|
|
20
|
+
'targetNode.setAttribute("id","");' +
|
|
21
|
+
'oc.preactComponents["${hash}"](' + props + ', targetNode);' +
|
|
22
|
+
'});' +
|
|
23
|
+
'</script>'
|
|
24
|
+
}`;
|
|
25
|
+
|
|
26
|
+
module.exports = viewTemplate;
|
package/lib/oc-app.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
interface OC {
|
|
2
|
+
conf: {
|
|
3
|
+
templates: Array<{
|
|
4
|
+
type: string;
|
|
5
|
+
externals: string[];
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
cmd: {
|
|
9
|
+
push: (cb: (oc: OC) => void) => void;
|
|
10
|
+
};
|
|
11
|
+
events: {
|
|
12
|
+
on: (eventName: string, fn: (...data: any[]) => void) => void;
|
|
13
|
+
off: (eventName: string, fn?: (...data: any[]) => void) => void;
|
|
14
|
+
fire: (eventName: string, data?: any) => void;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
};
|
|
17
|
+
renderNestedComponent: (ocElement: HTMLElement, cb: () => void) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
interface Window {
|
|
22
|
+
oc: OC;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
namespace JSX {
|
|
26
|
+
interface IntrinsicElements {
|
|
27
|
+
'oc-component': React.DetailedHTMLProps<
|
|
28
|
+
React.HTMLAttributes<HTMLElement> & { href: string },
|
|
29
|
+
HTMLElement
|
|
30
|
+
>;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AcceptLanguage {
|
|
36
|
+
code: string;
|
|
37
|
+
script?: any;
|
|
38
|
+
region: string;
|
|
39
|
+
quality: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Env {
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Plugins {}
|
|
47
|
+
|
|
48
|
+
export interface External {
|
|
49
|
+
global: string;
|
|
50
|
+
url: string;
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Template {
|
|
55
|
+
type: string;
|
|
56
|
+
version: string;
|
|
57
|
+
externals: External[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Context<T = any, E = Env> {
|
|
61
|
+
acceptLanguage: AcceptLanguage[];
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
env: E;
|
|
64
|
+
params: T;
|
|
65
|
+
plugins: Plugins;
|
|
66
|
+
requestHeaders: Record<string, string>;
|
|
67
|
+
requestIp: string;
|
|
68
|
+
setEmptyResponse: () => void;
|
|
69
|
+
setHeader: (header: string, value: string) => void;
|
|
70
|
+
staticPath: string;
|
|
71
|
+
templates: Template[];
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const removeExtension = (path) => path.replace(/\.(t|j)sx?$/, '');
|
|
2
|
+
|
|
3
|
+
const preactOCProviderTemplate = ({ viewPath }) => `
|
|
4
|
+
import { render } from 'preact';
|
|
5
|
+
import { useEffect } from 'preact/hooks';
|
|
6
|
+
import View from '${removeExtension(viewPath)}';
|
|
7
|
+
import { DataProvider } from 'oc-template-typescript-react-compiler/utils/useData'
|
|
8
|
+
|
|
9
|
+
function OCProvider(props: any): any {
|
|
10
|
+
const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = props;
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
(window as any).oc.events.fire('oc:componentDidMount', rest);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
function getData(providerProps: any, parameters: any, cb: (error: any, parameters?: any, props?: any) => void) {
|
|
17
|
+
return (window as any).oc.getData({
|
|
18
|
+
name: providerProps._componentName,
|
|
19
|
+
version: providerProps._componentVersion,
|
|
20
|
+
baseUrl: providerProps._baseUrl,
|
|
21
|
+
parameters
|
|
22
|
+
}, (err: any, data: any) => {
|
|
23
|
+
if (err) {
|
|
24
|
+
return cb(err);
|
|
25
|
+
}
|
|
26
|
+
cb(null, rest, data.preactComponent.props);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSetting(providerProps: any, setting: string) {
|
|
31
|
+
const settingHash = {
|
|
32
|
+
name: providerProps._componentName,
|
|
33
|
+
version: providerProps._componentVersion,
|
|
34
|
+
baseUrl: providerProps._baseUrl,
|
|
35
|
+
staticPath: providerProps._staticPath
|
|
36
|
+
};
|
|
37
|
+
return (settingHash as any)[setting];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
(rest as any).getData = (parameters: any, cb: (error: any, parameters?: any, props?: any) => void) => getData(props, parameters, cb);
|
|
41
|
+
(rest as any).getSetting = (setting: string) => getSetting(props, setting);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<DataProvider value={{...rest}}>
|
|
45
|
+
<View { ...rest } />
|
|
46
|
+
</DataProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderer(props, element) {
|
|
51
|
+
render(<OCProvider {...props} />, element);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default renderer;
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
module.exports = preactOCProviderTemplate;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { transformSync } = require('@babel/core');
|
|
2
|
+
|
|
3
|
+
const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`;
|
|
4
|
+
const importRE =
|
|
5
|
+
/import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss|styl|stylus|postcss)(\?[\S]*)?('|")/;
|
|
6
|
+
const jsRE = /\.(js|mjs|ts|jsx|tsx)/;
|
|
7
|
+
const cssLangRE = new RegExp(cssLangs);
|
|
8
|
+
const cssModuleRE = new RegExp(`\\.module${cssLangs}`);
|
|
9
|
+
|
|
10
|
+
function autoCSSModulePlugin() {
|
|
11
|
+
return () => {
|
|
12
|
+
return {
|
|
13
|
+
visitor: {
|
|
14
|
+
ImportDeclaration: (path) => {
|
|
15
|
+
const { node } = path;
|
|
16
|
+
if (!node) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// 如果不是module css的那么就通过 转化为 module.styl来模块化css
|
|
20
|
+
if (
|
|
21
|
+
node.specifiers &&
|
|
22
|
+
node.specifiers.length > 0 &&
|
|
23
|
+
cssLangRE.test(node.source.value) &&
|
|
24
|
+
!cssModuleRE.test(node.source.value)
|
|
25
|
+
) {
|
|
26
|
+
const cssFile = node.source.value;
|
|
27
|
+
const extension = cssFile.split('.').pop() ?? 'css';
|
|
28
|
+
node.source.value =
|
|
29
|
+
cssFile + (cssFile.indexOf('?') > -1 ? '&' : '?') + `.module.${extension}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transform(code, { sourceMap, file }) {
|
|
38
|
+
const parsePlugins = ['jsx'];
|
|
39
|
+
if (/\.tsx?$/.test(file)) {
|
|
40
|
+
parsePlugins.push('typescript');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = transformSync(code, {
|
|
44
|
+
configFile: false,
|
|
45
|
+
parserOpts: {
|
|
46
|
+
sourceType: 'module',
|
|
47
|
+
allowAwaitOutsideFunction: true,
|
|
48
|
+
plugins: parsePlugins
|
|
49
|
+
},
|
|
50
|
+
sourceMaps: true,
|
|
51
|
+
sourceFileName: file,
|
|
52
|
+
inputSourceMap: sourceMap,
|
|
53
|
+
plugins: [autoCSSModulePlugin()]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
code: result.code,
|
|
58
|
+
map: result.map
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function viteTransformCSSModulesPlugin() {
|
|
63
|
+
const name = 'vite-plugin-transform-css-modules';
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
transform(code, id) {
|
|
67
|
+
if (jsRE.test(id) && importRE.test(code)) {
|
|
68
|
+
const result = transform(code, {
|
|
69
|
+
file: id,
|
|
70
|
+
sourceMap: this.getCombinedSourcemap()
|
|
71
|
+
});
|
|
72
|
+
if (result) {
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = viteTransformCSSModulesPlugin;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const vite = require('vite');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { callbackify } = require('util');
|
|
5
|
+
const coreModules = require('builtin-modules');
|
|
6
|
+
const hashBuilder = require('oc-hash-builder');
|
|
7
|
+
|
|
8
|
+
const nodeModuleMatcher = /^[a-z@][a-z\-/0-9.]+$/i;
|
|
9
|
+
|
|
10
|
+
async function compileServer(options) {
|
|
11
|
+
const componentPath = options.componentPath;
|
|
12
|
+
const serverFileName = options.componentPackage.oc.files.data;
|
|
13
|
+
let serverPath = path.join(options.componentPath, serverFileName);
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
serverPath = serverPath.split('\\').join('\\\\');
|
|
16
|
+
}
|
|
17
|
+
const publishFileName = options.publishFileName || 'server.js';
|
|
18
|
+
const publishPath = options.publishPath;
|
|
19
|
+
const dependencies = options.componentPackage.dependencies || {};
|
|
20
|
+
const componentName = options.componentPackage.name;
|
|
21
|
+
const componentVersion = options.componentPackage.version;
|
|
22
|
+
const production = !!options.production;
|
|
23
|
+
|
|
24
|
+
const higherOrderServerContent = options.serverWrapper({
|
|
25
|
+
serverPath,
|
|
26
|
+
componentName,
|
|
27
|
+
componentVersion
|
|
28
|
+
});
|
|
29
|
+
const tempFolder = path.join(publishPath, 'temp');
|
|
30
|
+
const higherOrderServerPath = path.join(tempFolder, '__oc_higherOrderServer.ts');
|
|
31
|
+
const externals = [...Object.keys(dependencies), ...coreModules];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await fs.outputFile(higherOrderServerPath, higherOrderServerContent);
|
|
35
|
+
|
|
36
|
+
const result = await vite.build({
|
|
37
|
+
appType: 'custom',
|
|
38
|
+
root: componentPath,
|
|
39
|
+
mode: production ? 'production' : 'development',
|
|
40
|
+
plugins: options.plugins,
|
|
41
|
+
logLevel: options.verbose ? 'info' : 'silent',
|
|
42
|
+
build: {
|
|
43
|
+
lib: { entry: higherOrderServerPath, formats: ['cjs'] },
|
|
44
|
+
write: false,
|
|
45
|
+
minify: production,
|
|
46
|
+
rollupOptions: {
|
|
47
|
+
external: (id) => {
|
|
48
|
+
if (nodeModuleMatcher.test(id)) {
|
|
49
|
+
if (!externals.includes(id)) {
|
|
50
|
+
throw new Error(`Missing dependencies from package.json => ${id}`);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const out = Array.isArray(result) ? result[0] : result;
|
|
60
|
+
const bundle = out.output[0].code;
|
|
61
|
+
|
|
62
|
+
await fs.ensureDir(publishPath);
|
|
63
|
+
await fs.writeFile(path.join(publishPath, publishFileName), bundle);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
type: 'node.js',
|
|
67
|
+
hashKey: hashBuilder.fromString(bundle),
|
|
68
|
+
src: publishFileName
|
|
69
|
+
};
|
|
70
|
+
} finally {
|
|
71
|
+
await fs.remove(tempFolder);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = callbackify(compileServer);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const vite = require('vite');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const EnvironmentPlugin = require('vite-plugin-environment').default;
|
|
5
|
+
const hashBuilder = require('oc-hash-builder');
|
|
6
|
+
const ocViewWrapper = require('oc-view-wrapper');
|
|
7
|
+
const { callbackify } = require('util');
|
|
8
|
+
const cssModules = require('./cssModulesPlugin');
|
|
9
|
+
|
|
10
|
+
const clientName = 'clientBundle';
|
|
11
|
+
|
|
12
|
+
const partition = (array, predicate) => {
|
|
13
|
+
const matches = [];
|
|
14
|
+
const rest = [];
|
|
15
|
+
for (const element of array) {
|
|
16
|
+
if (predicate(element)) {
|
|
17
|
+
matches.push(element);
|
|
18
|
+
} else {
|
|
19
|
+
rest.push(element);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [matches, rest];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function compileView(options) {
|
|
26
|
+
function processRelativePath(relativePath) {
|
|
27
|
+
let pathStr = path.join(options.componentPath, relativePath);
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
return pathStr.split('\\').join('\\\\');
|
|
30
|
+
}
|
|
31
|
+
return pathStr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const staticFiles = options.componentPackage.oc.files.static;
|
|
35
|
+
const staticFolder = Array.isArray(staticFiles) ? staticFiles[0] : staticFiles;
|
|
36
|
+
const viewFileName = options.componentPackage.oc.files.template.src;
|
|
37
|
+
const componentPath = options.componentPath;
|
|
38
|
+
const viewPath = processRelativePath(viewFileName);
|
|
39
|
+
|
|
40
|
+
const publishPath = options.publishPath;
|
|
41
|
+
const tempPath = path.join(publishPath, 'temp');
|
|
42
|
+
const publishFileName = options.publishFileName || 'template.js';
|
|
43
|
+
const componentPackage = options.componentPackage;
|
|
44
|
+
const externals = options.externals || [];
|
|
45
|
+
const production = !!options.production;
|
|
46
|
+
const viewExtension = viewFileName.match(/\.\w{1,5}$/)?.[0] ?? '.js';
|
|
47
|
+
|
|
48
|
+
const viewWrapperContent = options.viewWrapper({ viewPath });
|
|
49
|
+
const viewWrapperName = `_viewWrapperEntry${viewExtension}`;
|
|
50
|
+
const viewWrapperPAth = path.join(tempPath, viewWrapperName);
|
|
51
|
+
|
|
52
|
+
await fs.outputFile(viewWrapperPAth, viewWrapperContent);
|
|
53
|
+
|
|
54
|
+
const globals = externals.reduce((externals, dep) => {
|
|
55
|
+
externals[dep.name] = dep.global;
|
|
56
|
+
return externals;
|
|
57
|
+
}, {});
|
|
58
|
+
|
|
59
|
+
const result = await vite.build({
|
|
60
|
+
appType: 'custom',
|
|
61
|
+
root: componentPath,
|
|
62
|
+
mode: production ? 'production' : 'development',
|
|
63
|
+
plugins: [...options.plugins, EnvironmentPlugin(['NODE_ENV']), cssModules()],
|
|
64
|
+
logLevel: 'silent',
|
|
65
|
+
build: {
|
|
66
|
+
sourcemap: !production,
|
|
67
|
+
lib: { entry: viewWrapperPAth, formats: ['iife'], name: clientName },
|
|
68
|
+
write: false,
|
|
69
|
+
minify: production,
|
|
70
|
+
rollupOptions: {
|
|
71
|
+
external: Object.keys(globals),
|
|
72
|
+
output: {
|
|
73
|
+
globals
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const out = Array.isArray(result) ? result[0] : result;
|
|
79
|
+
const bundle = out.output.find((x) => x.facadeModuleId.endsWith(viewWrapperName)).code;
|
|
80
|
+
const [cssAssets, otherAssets] = partition(
|
|
81
|
+
out.output.filter((x) => x.type === 'asset'),
|
|
82
|
+
(x) => x.fileName.endsWith('.css')
|
|
83
|
+
);
|
|
84
|
+
const cssStyles = cssAssets.map((x) => x.source.replace(/\n/g, '') ?? '').join(' ');
|
|
85
|
+
const bundleHash = hashBuilder.fromString(bundle);
|
|
86
|
+
const wrappedBundle = `(function() {
|
|
87
|
+
${bundle}
|
|
88
|
+
|
|
89
|
+
return ${clientName};
|
|
90
|
+
})()`;
|
|
91
|
+
|
|
92
|
+
const shortTemplateType = options.componentPackage.oc.files.template.type
|
|
93
|
+
.replace('oc-template-', '')
|
|
94
|
+
.replace(/-/, '');
|
|
95
|
+
const templateId = `oc-${shortTemplateType}Root-${componentPackage.name}`;
|
|
96
|
+
const templateString = options.htmlTemplate({
|
|
97
|
+
templateId,
|
|
98
|
+
css: cssStyles,
|
|
99
|
+
externals,
|
|
100
|
+
bundle: wrappedBundle,
|
|
101
|
+
hash: bundleHash
|
|
102
|
+
});
|
|
103
|
+
const templateStringCompressed = production
|
|
104
|
+
? templateString.replace(/\s+/g, ' ')
|
|
105
|
+
: templateString;
|
|
106
|
+
const hash = hashBuilder.fromString(templateStringCompressed);
|
|
107
|
+
const view = ocViewWrapper(hash, templateStringCompressed);
|
|
108
|
+
|
|
109
|
+
await fs.unlink(viewWrapperPAth);
|
|
110
|
+
await fs.mkdir(publishPath, { recursive: true });
|
|
111
|
+
await fs.writeFile(path.join(publishPath, publishFileName), view);
|
|
112
|
+
if (staticFolder) {
|
|
113
|
+
for (const asset of otherAssets) {
|
|
114
|
+
// asset.fileName could have paths like assets/file.js
|
|
115
|
+
// so we need to create those extra directories
|
|
116
|
+
await fs.ensureFile(path.join(publishPath, staticFolder, asset.fileName));
|
|
117
|
+
await fs.writeFile(
|
|
118
|
+
path.join(publishPath, staticFolder, asset.fileName),
|
|
119
|
+
asset.source,
|
|
120
|
+
'utf-8'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
template: {
|
|
127
|
+
type: options.componentPackage.oc.files.template.type,
|
|
128
|
+
hashKey: hash,
|
|
129
|
+
src: publishFileName
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = callbackify(compileView);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const resolve = require('resolve');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const immer = require('immer').produce;
|
|
7
|
+
|
|
8
|
+
function writeJson(fileName, object) {
|
|
9
|
+
fs.writeFileSync(fileName, JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function verifyTypeScriptSetup(componentPath) {
|
|
13
|
+
const paths = {
|
|
14
|
+
appTsConfig: path.resolve(componentPath, 'tsconfig.json'),
|
|
15
|
+
yarnLockFile: path.resolve(componentPath, 'yarn.lock'),
|
|
16
|
+
appNodeModules: path.resolve(componentPath, 'node_modules'),
|
|
17
|
+
appTypeDeclarations: path.resolve(componentPath, 'src', 'oc-app.d.ts')
|
|
18
|
+
};
|
|
19
|
+
let firstTimeSetup = false;
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(paths.appTsConfig)) {
|
|
22
|
+
writeJson(paths.appTsConfig, {});
|
|
23
|
+
firstTimeSetup = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isYarn = fs.existsSync(paths.yarnLockFile);
|
|
27
|
+
|
|
28
|
+
// Ensure typescript is installed
|
|
29
|
+
let ts;
|
|
30
|
+
try {
|
|
31
|
+
ts = require(resolve.sync('typescript', {
|
|
32
|
+
basedir: paths.appNodeModules
|
|
33
|
+
}));
|
|
34
|
+
} catch (_) {
|
|
35
|
+
console.error(
|
|
36
|
+
chalk.bold.red(
|
|
37
|
+
`It looks like you're trying to use TypeScript but do not have ${chalk.bold(
|
|
38
|
+
'typescript'
|
|
39
|
+
)} installed.`
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
console.error(
|
|
43
|
+
chalk.bold(
|
|
44
|
+
'Please install',
|
|
45
|
+
chalk.cyan.bold('typescript'),
|
|
46
|
+
'by running',
|
|
47
|
+
chalk.cyan.bold(isYarn ? 'yarn add typescript' : 'npm install typescript') + '.'
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
console.error();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const compilerOptions = {
|
|
55
|
+
// These are suggested values and will be set when not present in the
|
|
56
|
+
// tsconfig.json
|
|
57
|
+
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
|
|
58
|
+
target: {
|
|
59
|
+
parsedValue: ts.ScriptTarget.ES5,
|
|
60
|
+
suggested: 'es5'
|
|
61
|
+
},
|
|
62
|
+
lib: { suggested: ['dom', 'dom.iterable', 'esnext'] },
|
|
63
|
+
allowJs: { suggested: true },
|
|
64
|
+
skipLibCheck: { suggested: true },
|
|
65
|
+
esModuleInterop: { suggested: true },
|
|
66
|
+
allowSyntheticDefaultImports: { suggested: true },
|
|
67
|
+
strict: { suggested: true },
|
|
68
|
+
forceConsistentCasingInFileNames: { suggested: true },
|
|
69
|
+
// TODO: Enable for v4.0 (#6936)
|
|
70
|
+
// noFallthroughCasesInSwitch: { suggested: true },
|
|
71
|
+
|
|
72
|
+
// These values are required and cannot be changed by the user
|
|
73
|
+
// Keep this in sync with the rollup config
|
|
74
|
+
module: {
|
|
75
|
+
parsedValue: ts.ModuleKind.ESNext,
|
|
76
|
+
value: 'esnext',
|
|
77
|
+
reason: 'for import() and import/export'
|
|
78
|
+
},
|
|
79
|
+
moduleResolution: {
|
|
80
|
+
parsedValue: ts.ModuleResolutionKind.NodeJs,
|
|
81
|
+
value: 'node',
|
|
82
|
+
reason: 'to match rollup resolution'
|
|
83
|
+
},
|
|
84
|
+
resolveJsonModule: { value: true, reason: 'to match rollup loader' },
|
|
85
|
+
isolatedModules: { value: true, reason: 'implementation limitation' },
|
|
86
|
+
jsx: {
|
|
87
|
+
parsedValue: ts.JsxEmit.ReactJSX,
|
|
88
|
+
suggested: 'react-jsx'
|
|
89
|
+
},
|
|
90
|
+
paths: { value: undefined, reason: 'aliased imports are not supported' }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const formatDiagnosticHost = {
|
|
94
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
95
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
96
|
+
getNewLine: () => os.EOL
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const messages = [];
|
|
100
|
+
let appTsConfig;
|
|
101
|
+
let parsedTsConfig;
|
|
102
|
+
let parsedCompilerOptions;
|
|
103
|
+
try {
|
|
104
|
+
const { config: readTsConfig, error } = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile);
|
|
105
|
+
|
|
106
|
+
if (error) {
|
|
107
|
+
throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
appTsConfig = readTsConfig;
|
|
111
|
+
|
|
112
|
+
// Get TS to parse and resolve any "extends"
|
|
113
|
+
// Calling this function also mutates the tsconfig above,
|
|
114
|
+
// adding in "include" and "exclude", but the compilerOptions remain untouched
|
|
115
|
+
let result;
|
|
116
|
+
parsedTsConfig = immer(readTsConfig, (config) => {
|
|
117
|
+
result = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(paths.appTsConfig));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (result.errors && result.errors.length) {
|
|
121
|
+
throw new Error(ts.formatDiagnostic(result.errors[0], formatDiagnosticHost));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
parsedCompilerOptions = result.options;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e && e.name === 'SyntaxError') {
|
|
127
|
+
console.error(
|
|
128
|
+
chalk.red.bold(
|
|
129
|
+
'Could not parse',
|
|
130
|
+
chalk.cyan('tsconfig.json') + '.',
|
|
131
|
+
'Please make sure it contains syntactically correct JSON.'
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(e && e.message ? `${e.message}` : '');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (appTsConfig.compilerOptions == null) {
|
|
141
|
+
appTsConfig.compilerOptions = {};
|
|
142
|
+
firstTimeSetup = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const option of Object.keys(compilerOptions)) {
|
|
146
|
+
const { parsedValue, value, suggested, reason } = compilerOptions[option];
|
|
147
|
+
|
|
148
|
+
const valueToCheck = parsedValue === undefined ? value : parsedValue;
|
|
149
|
+
const coloredOption = chalk.cyan('compilerOptions.' + option);
|
|
150
|
+
|
|
151
|
+
if (suggested != null) {
|
|
152
|
+
if (parsedCompilerOptions[option] === undefined) {
|
|
153
|
+
appTsConfig.compilerOptions[option] = suggested;
|
|
154
|
+
messages.push(
|
|
155
|
+
`${coloredOption} to be ${chalk.bold('suggested')} value: ${chalk.cyan.bold(
|
|
156
|
+
suggested
|
|
157
|
+
)} (this can be changed)`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
} else if (parsedCompilerOptions[option] !== valueToCheck) {
|
|
161
|
+
appTsConfig.compilerOptions[option] = value;
|
|
162
|
+
messages.push(
|
|
163
|
+
`${coloredOption} ${chalk.bold(valueToCheck == null ? 'must not' : 'must')} be ${valueToCheck == null ? 'set' : chalk.cyan.bold(value)
|
|
164
|
+
}` + (reason != null ? ` (${reason})` : '')
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// tsconfig will have the merged "include" and "exclude" by this point
|
|
170
|
+
if (parsedTsConfig.include == null) {
|
|
171
|
+
appTsConfig.include = ['src'];
|
|
172
|
+
messages.push(`${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (parsedTsConfig.exclude == null) {
|
|
176
|
+
appTsConfig.exclude = ['src/_package'];
|
|
177
|
+
messages.push(`${chalk.cyan('exclude')} should have ${chalk.cyan.bold('src/_package')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (messages.length > 0) {
|
|
181
|
+
if (firstTimeSetup) {
|
|
182
|
+
console.log(
|
|
183
|
+
chalk.bold('Your', chalk.cyan('tsconfig.json'), 'has been populated with default values.')
|
|
184
|
+
);
|
|
185
|
+
console.log();
|
|
186
|
+
} else {
|
|
187
|
+
console.warn(
|
|
188
|
+
chalk.bold(
|
|
189
|
+
'The following changes are being made to your',
|
|
190
|
+
chalk.cyan('tsconfig.json'),
|
|
191
|
+
'file:'
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
messages.forEach((message) => {
|
|
195
|
+
console.warn(' - ' + message);
|
|
196
|
+
});
|
|
197
|
+
console.warn();
|
|
198
|
+
}
|
|
199
|
+
writeJson(paths.appTsConfig, appTsConfig);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Reference `oc-template-preact-compiler` types
|
|
203
|
+
if (!fs.existsSync(paths.appTypeDeclarations)) {
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
paths.appTypeDeclarations,
|
|
206
|
+
`/// <reference types="oc-template-preact" />${os.EOL}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = verifyTypeScriptSetup;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oc-template-preact-compiler",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/guestlinelabs/oc-template-preact.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/guestlinelabs/oc-template-preact/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/guestlinelabs/oc-template-preact#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"oc",
|
|
16
|
+
"opencomponents",
|
|
17
|
+
"preact",
|
|
18
|
+
"oc-template"
|
|
19
|
+
],
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Ricardo Devis Agullo",
|
|
22
|
+
"email": "mkredz@gmail.com"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"types": "./lib/oc-app.d.ts",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@preact/preset-vite": "^2.5.0",
|
|
28
|
+
"chalk": "^3.0.0",
|
|
29
|
+
"fs-extra": "^10.0.0",
|
|
30
|
+
"immer": "^9.0.6",
|
|
31
|
+
"oc-external-dependencies-handler": "1.1.0",
|
|
32
|
+
"oc-generic-template-compiler": "2.1.0",
|
|
33
|
+
"oc-hash-builder": "1.0.2",
|
|
34
|
+
"oc-statics-compiler": "2.0.10",
|
|
35
|
+
"oc-template-preact": "0.0.1",
|
|
36
|
+
"oc-view-wrapper": "1.0.3",
|
|
37
|
+
"preact": "^10.13.2",
|
|
38
|
+
"resolve": "^1.20.0",
|
|
39
|
+
"vite": "^4.2.1",
|
|
40
|
+
"vite-plugin-environment": "^1.1.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"preact": "^10"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"index.js",
|
|
47
|
+
"lib",
|
|
48
|
+
"utils",
|
|
49
|
+
"scaffold",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^18.0.28",
|
|
55
|
+
"@types/react-dom": "^18.0.11",
|
|
56
|
+
"lodash": "^4.17.4",
|
|
57
|
+
"prettier": "^2.3.2",
|
|
58
|
+
"prettier-config-guestline": "2.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "base-component-react",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint": "eslint --max-warnings 0 --ext .js,.jsx,.ts,.tsx src",
|
|
7
|
+
"build": "tsc --noEmit && oc package .",
|
|
8
|
+
"test": "vitest"
|
|
9
|
+
},
|
|
10
|
+
"oc": {
|
|
11
|
+
"files": {
|
|
12
|
+
"data": "src/server.ts",
|
|
13
|
+
"template": {
|
|
14
|
+
"src": "src/App.tsx",
|
|
15
|
+
"type": "oc-template-preact"
|
|
16
|
+
},
|
|
17
|
+
"static": [
|
|
18
|
+
"public"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"parameters": {
|
|
22
|
+
"userId": {
|
|
23
|
+
"default": 0,
|
|
24
|
+
"description": "The user id from the user database",
|
|
25
|
+
"example": 0,
|
|
26
|
+
"mandatory": true,
|
|
27
|
+
"type": "number"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@testing-library/react": "^14.0.0",
|
|
34
|
+
"@testing-library/user-event": "^14.4.3",
|
|
35
|
+
"jsdom": "^21.1.1",
|
|
36
|
+
"oc-template-preact": "0.0.1",
|
|
37
|
+
"oc-template-preact-compiler": "0.0.1",
|
|
38
|
+
"preact": "^10.13.2",
|
|
39
|
+
"typescript": "5.0.2",
|
|
40
|
+
"vitest": "^0.29.7"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, afterEach, beforeEach, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { DataProvider } from 'oc-template-preact-compiler/utils/useData';
|
|
4
|
+
|
|
5
|
+
import App from './App';
|
|
6
|
+
|
|
7
|
+
const getData = vi.fn();
|
|
8
|
+
|
|
9
|
+
describe('App - Page', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
window.oc = { events: { on: vi.fn(), fire: vi.fn() } } as any;
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('Gets more data when clicking the button', () => {
|
|
18
|
+
getData.mockImplementationOnce(() => Promise.resolve({}));
|
|
19
|
+
render(
|
|
20
|
+
<DataProvider value={{ firstName: 'firstName', lastName: 'lastName', userId: 0, getData }}>
|
|
21
|
+
<App firstName="firstName" lastName="lastName" userId={0} />
|
|
22
|
+
</DataProvider>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const extraInfoButton = screen.getByText(/Get extra information/i);
|
|
26
|
+
expect(extraInfoButton).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState } from 'preact/hooks';
|
|
2
|
+
import { useData } from 'oc-template-preact-compiler/utils/useData';
|
|
3
|
+
import styles from './styles.css';
|
|
4
|
+
import type { AdditionalData, ClientProps } from './types';
|
|
5
|
+
|
|
6
|
+
interface AppProps extends ClientProps {
|
|
7
|
+
getMoreData?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const App = () => {
|
|
11
|
+
const { firstName, lastName, userId, getData, getSetting } = useData<AppProps>();
|
|
12
|
+
const staticPath = getSetting('staticPath');
|
|
13
|
+
const [additionalData, setAdditionalData] = useState<AdditionalData | null>(null);
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
|
|
16
|
+
const fetchMoreData = async () => {
|
|
17
|
+
setError('');
|
|
18
|
+
try {
|
|
19
|
+
const data = await getData({ userId, getMoreData: true });
|
|
20
|
+
setAdditionalData(data);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
setError(String(err));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
return <div>Something wrong happened!</div>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={styles.container}>
|
|
32
|
+
<img width="50" height="50" src={`${staticPath}/public/logo.png`} alt="Logo" />
|
|
33
|
+
<h1 style={{ margin: '0 0 20px 0' }}>
|
|
34
|
+
Hello, <span style={{ textDecoration: 'underline' }}>{firstName}</span> {lastName}
|
|
35
|
+
</h1>
|
|
36
|
+
{additionalData && (
|
|
37
|
+
<div className={styles.info}>
|
|
38
|
+
<div className={styles.block}>Age: {additionalData.age}</div>
|
|
39
|
+
<div className={styles.block}>
|
|
40
|
+
Hobbies: {additionalData.hobbies.map((x) => x.toLowerCase()).join(', ')}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
<button className={styles.button} onClick={fetchMoreData}>
|
|
45
|
+
Get extra information
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default App;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Context } from 'oc-template-preact-compiler';
|
|
2
|
+
import { AdditionalData, ClientProps, OcParameters } from './types';
|
|
3
|
+
|
|
4
|
+
const database = [
|
|
5
|
+
{ name: 'John Doe', age: 34, hobbies: ['Swimming', 'Basketball'] },
|
|
6
|
+
{ name: 'Jane Doe', age: 35, hobbies: ['Running', 'Rugby'] }
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
async function getUser(userId: number) {
|
|
10
|
+
return database[userId];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function data(
|
|
14
|
+
context: Context<OcParameters>,
|
|
15
|
+
callback: (error: any, data: ClientProps | AdditionalData) => void
|
|
16
|
+
) {
|
|
17
|
+
const { userId } = context.params;
|
|
18
|
+
const user = await getUser(userId);
|
|
19
|
+
const shouldGetMoreData = context.params.getMoreData;
|
|
20
|
+
const [firstName, lastName] = user.name.split(/\s+/);
|
|
21
|
+
|
|
22
|
+
if (shouldGetMoreData) {
|
|
23
|
+
return callback(null, {
|
|
24
|
+
age: user.age,
|
|
25
|
+
hobbies: user.hobbies
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return callback(null, {
|
|
30
|
+
userId,
|
|
31
|
+
firstName,
|
|
32
|
+
lastName
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
background-color: #3b246c;
|
|
3
|
+
color: #fff;
|
|
4
|
+
font-family: sans-serif;
|
|
5
|
+
padding: 40px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.button {
|
|
9
|
+
background-color: #a97613;
|
|
10
|
+
border: none;
|
|
11
|
+
padding: 15px 32px;
|
|
12
|
+
text-align: center;
|
|
13
|
+
font-size: 16px;
|
|
14
|
+
text-decoration: none;
|
|
15
|
+
display: inline-block;
|
|
16
|
+
color: inherit;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.info {
|
|
21
|
+
margin-bottom: 20px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.block {
|
|
25
|
+
margin: 6px 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.button:hover {
|
|
29
|
+
background-color: #c79535;
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface OcParameters {
|
|
2
|
+
userId: number;
|
|
3
|
+
getMoreData?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface AdditionalData {
|
|
7
|
+
hobbies: string[];
|
|
8
|
+
age: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ClientProps extends Partial<AdditionalData> {
|
|
12
|
+
userId: number;
|
|
13
|
+
firstName: string;
|
|
14
|
+
lastName: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// These are suggested values and will be set when not present in the
|
|
4
|
+
// tsconfig.json
|
|
5
|
+
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
|
|
6
|
+
"target": "es5",
|
|
7
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
|
|
16
|
+
// These values are required and cannot be changed by the user
|
|
17
|
+
// Keep this in sync with the rollup config
|
|
18
|
+
"module": "esnext",
|
|
19
|
+
"moduleResolution": "node",
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"jsx": "react-jsx",
|
|
23
|
+
"jsxImportSource": "preact"
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"],
|
|
26
|
+
"exclude": ["src/_package"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createContext } from 'preact';
|
|
2
|
+
import { useCallback, useContext } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
const DataContext = createContext<any>({});
|
|
5
|
+
|
|
6
|
+
type Data<T = any> = T & {
|
|
7
|
+
getData(parameters: T, cb: (err: Error | null, data: any) => void): void;
|
|
8
|
+
getSetting(setting: 'name' | 'version' | 'baseUrl' | 'staticPath'): string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type PromiseData<T = any> = T & {
|
|
12
|
+
getData<O = any>(parameters?: Partial<T>): Promise<O>;
|
|
13
|
+
getSetting(setting: 'name' | 'version' | 'baseUrl' | 'staticPath'): string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DataProvider = ({ children, ...props }: any) => {
|
|
17
|
+
// @ts-ignore children props
|
|
18
|
+
return <DataContext.Provider value={props}>{children}</DataContext.Provider>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function useData<T = any>(): PromiseData<T> {
|
|
22
|
+
const {
|
|
23
|
+
value: { getData, ...rest }
|
|
24
|
+
}: { value: Data<T> } = useContext(DataContext);
|
|
25
|
+
const asyncGetData = useCallback(<O = any, I = any>(data: I) => {
|
|
26
|
+
return new Promise<O>((resolve, reject) => {
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
getData({ ...rest, ...data }, (err, newData) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
reject(err);
|
|
31
|
+
} else {
|
|
32
|
+
resolve(newData);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
return { getData: asyncGetData, ...rest };
|
|
40
|
+
}
|