@lowdefy/server-dev 5.6.0 → 6.1.0
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/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +9 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +128 -0
- package/lib/server/createLowdefyContext.test.mjs +117 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/lib/server/log/createLogger.test.mjs +62 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +62 -49
- package/package.original.json +65 -52
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +247 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
import { jest } from '@jest/globals';
|
|
22
|
+
import { projectCaughtError } from '@lowdefy/helpers';
|
|
23
|
+
import { operatorsServer } from '@lowdefy/operators-js';
|
|
24
|
+
|
|
25
|
+
const secret = 'planted-dev-secret';
|
|
26
|
+
process.env.LOWDEFY_SECRET_TEST = secret;
|
|
27
|
+
|
|
28
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({ createApiContext: jest.fn() }));
|
|
29
|
+
jest.unstable_mockModule('../build/appMeta.js', () => ({ default: {} }));
|
|
30
|
+
jest.unstable_mockModule('../build/config.js', () => ({ default: {} }));
|
|
31
|
+
jest.unstable_mockModule('../build/i18n.js', () => ({ default: {} }));
|
|
32
|
+
jest.unstable_mockModule('./log/createHandleError.js', () => ({
|
|
33
|
+
default: jest.fn(() => jest.fn()),
|
|
34
|
+
}));
|
|
35
|
+
jest.unstable_mockModule('./log/createLogger.js', () => ({
|
|
36
|
+
default: jest.fn(() => ({ debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() })),
|
|
37
|
+
}));
|
|
38
|
+
jest.unstable_mockModule('./fileCache.js', () => ({ default: {} }));
|
|
39
|
+
jest.unstable_mockModule('./auth/session.js', () => ({ default: jest.fn(async () => undefined) }));
|
|
40
|
+
jest.unstable_mockModule('./auth/strategies.js', () => ({ default: jest.fn(async () => null) }));
|
|
41
|
+
jest.unstable_mockModule('./log/logRequest.js', () => ({ default: jest.fn() }));
|
|
42
|
+
|
|
43
|
+
// createLowdefyContext statically imports the app build's plugin artifacts,
|
|
44
|
+
// which only exist in a built app (build/** is gitignored). Materialize the
|
|
45
|
+
// minimal set it needs — same fixture strategy as auth/getDevSession.test.mjs
|
|
46
|
+
// — so the import resolves; clean up only what this test created.
|
|
47
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const pluginsDir = path.resolve(dirname, '../../build/plugins');
|
|
49
|
+
const fixtures = [
|
|
50
|
+
{ file: 'agents.js', content: 'export default {};\n' },
|
|
51
|
+
{ file: 'connections.js', content: 'export default {};\n' },
|
|
52
|
+
{
|
|
53
|
+
file: 'notifications.js',
|
|
54
|
+
content: 'export default {};\nexport const interpolateProperties = () => {};\nexport const renderEmail = () => {};\n',
|
|
55
|
+
},
|
|
56
|
+
{ file: path.join('operators', 'server.js'), content: 'export default {};\n' },
|
|
57
|
+
{ file: path.join('operators', 'serverJsMap.js'), content: 'export default {};\n' },
|
|
58
|
+
{ file: 'websockets.js', content: 'export default {};\n' },
|
|
59
|
+
].map(({ file, content }) => ({ absolutePath: path.join(pluginsDir, file), content }));
|
|
60
|
+
|
|
61
|
+
const createdFixtures = [];
|
|
62
|
+
fixtures.forEach(({ absolutePath, content }) => {
|
|
63
|
+
if (!fs.existsSync(absolutePath)) {
|
|
64
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
65
|
+
fs.writeFileSync(absolutePath, content);
|
|
66
|
+
createdFixtures.push(absolutePath);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// A checkout that has built an app already holds real artifacts here, which import plugins this
|
|
71
|
+
// package does not depend on, so the stubs are also mocked over whatever is on disk.
|
|
72
|
+
fixtures.forEach(({ absolutePath }) => {
|
|
73
|
+
jest.unstable_mockModule(absolutePath, () => ({
|
|
74
|
+
default: {},
|
|
75
|
+
interpolateProperties: () => {},
|
|
76
|
+
renderEmail: () => {},
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(() => {
|
|
81
|
+
createdFixtures.forEach((absolutePath) => fs.rmSync(absolutePath, { force: true }));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { default: createLowdefyContext } = await import('./createLowdefyContext.js');
|
|
85
|
+
|
|
86
|
+
function createHonoContext({ path: reqPath = '/api/request/foo' } = {}) {
|
|
87
|
+
return {
|
|
88
|
+
req: {
|
|
89
|
+
header: (name) => (name ? undefined : {}),
|
|
90
|
+
path: reqPath,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
url: `http://localhost${reqPath}`,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('createLowdefyContext sets mode to dev', async () => {
|
|
98
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
99
|
+
expect(context.mode).toEqual('dev');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('createLowdefyContext scrubSecrets redacts a planted secret', async () => {
|
|
103
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
104
|
+
expect(context.scrubSecrets(`token ${secret} end`)).toEqual('token [REDACTED] end');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// controlTry builds the value `_error` reads from context.scrubSecrets, so a dev context has to
|
|
108
|
+
// scrub a caught message the same way the production server does.
|
|
109
|
+
test('_error on a dev context reads a caught message with a planted secret scrubbed', async () => {
|
|
110
|
+
const context = await createLowdefyContext({ c: createHonoContext() });
|
|
111
|
+
const error = projectCaughtError(new Error(`Connect failed with ${secret}.`), {
|
|
112
|
+
scrub: context.scrubSecrets,
|
|
113
|
+
});
|
|
114
|
+
expect(operatorsServer._error({ error, location: 'test', params: 'message' })).toEqual(
|
|
115
|
+
'Connect failed with [REDACTED].'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getPageJitEnrichment } from './jitPageBuilder.js';
|
|
18
|
+
|
|
19
|
+
// A stub of cachedBuildContext. getPageJitEnrichment reads jsMap.client and
|
|
20
|
+
// dynamicIconData off it; production omits buildContext so it defaults to the
|
|
21
|
+
// module-private cachedBuildContext.
|
|
22
|
+
function buildContext({ client = {}, dynamicIconData = {} } = {}) {
|
|
23
|
+
return { jsMap: { client, server: {} }, dynamicIconData };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('getPageJitEnrichment scopes string-form, object-form, and args-nested _js hashes', () => {
|
|
27
|
+
const client = {
|
|
28
|
+
hashString: 'return args.x + 1;',
|
|
29
|
+
hashFn: 'return 2;',
|
|
30
|
+
hashNested: 'return 3;',
|
|
31
|
+
hashUnused: 'return 4;', // present in the map but not referenced by the page
|
|
32
|
+
};
|
|
33
|
+
const pageConfig = {
|
|
34
|
+
id: 'p',
|
|
35
|
+
blocks: [
|
|
36
|
+
{ properties: { value: { _js: 'hashString' } } },
|
|
37
|
+
// object form; the args carry another _js node that must be descended into
|
|
38
|
+
{ properties: { compute: { _js: { fn: 'hashFn', args: { seed: { _js: 'hashNested' } } } } } },
|
|
39
|
+
// referenced but absent from the client map — excluded, no crash
|
|
40
|
+
{ properties: { missing: { _js: 'hashAbsent' } } },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const { jsEntries } = getPageJitEnrichment({
|
|
45
|
+
pageConfig,
|
|
46
|
+
buildContext: buildContext({ client }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(jsEntries).toContain("'hashString'");
|
|
50
|
+
expect(jsEntries).toContain("'hashFn'");
|
|
51
|
+
expect(jsEntries).toContain("'hashNested'");
|
|
52
|
+
expect(jsEntries).not.toContain("'hashUnused'");
|
|
53
|
+
expect(jsEntries).not.toContain("'hashAbsent'");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('getPageJitEnrichment discovers an icon name that appears only inside a _js source', () => {
|
|
57
|
+
const client = {
|
|
58
|
+
// The icon name lives only in the function body, in double quotes — the
|
|
59
|
+
// served pageConfig has this replaced by the hash, so scanning the config
|
|
60
|
+
// alone would miss it. Only FiZap is a real dynamic icon.
|
|
61
|
+
hashIcon: 'return on ? "FiZap" : "FiZapOff";',
|
|
62
|
+
};
|
|
63
|
+
const pageConfig = {
|
|
64
|
+
id: 'p',
|
|
65
|
+
blocks: [{ properties: { icon: { _js: 'hashIcon' } } }],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const { dynamicIcons } = getPageJitEnrichment({
|
|
69
|
+
pageConfig,
|
|
70
|
+
buildContext: buildContext({ client, dynamicIconData: { FiZap: { tag: 'svg' } } }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(dynamicIcons).toEqual({ FiZap: { tag: 'svg' } });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('getPageJitEnrichment scopes icons referenced directly in the page config', () => {
|
|
77
|
+
const pageConfig = { id: 'p', blocks: [{ properties: { icon: 'FiStar' } }] };
|
|
78
|
+
|
|
79
|
+
const { dynamicIcons } = getPageJitEnrichment({
|
|
80
|
+
pageConfig,
|
|
81
|
+
buildContext: buildContext({
|
|
82
|
+
dynamicIconData: { FiStar: { tag: 'svg' }, FiOther: { tag: 'svg' } },
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(dynamicIcons).toEqual({ FiStar: { tag: 'svg' } });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('getPageJitEnrichment returns {} when there is no build context', () => {
|
|
90
|
+
expect(getPageJitEnrichment({ pageConfig: { id: 'p' }, buildContext: null })).toEqual({});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('getPageJitEnrichment omits both fields when the page has no dynamic _js or icons', () => {
|
|
94
|
+
const pageConfig = { id: 'p', blocks: [{ properties: { title: 'Hello' } }] };
|
|
95
|
+
|
|
96
|
+
const enrichment = getPageJitEnrichment({
|
|
97
|
+
pageConfig,
|
|
98
|
+
buildContext: buildContext({
|
|
99
|
+
client: { hashUnused: 'return 1;' },
|
|
100
|
+
dynamicIconData: { FiZap: {} },
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(enrichment).toEqual({ jsEntries: undefined, dynamicIcons: undefined });
|
|
105
|
+
});
|
|
@@ -16,10 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
|
-
import { serializer } from '@lowdefy/helpers';
|
|
20
|
-
import {
|
|
19
|
+
import { serializer, type } from '@lowdefy/helpers';
|
|
20
|
+
import {
|
|
21
|
+
buildPageJit,
|
|
22
|
+
createContext,
|
|
23
|
+
generateClientJsModule,
|
|
24
|
+
hydrateDeferredRecords,
|
|
25
|
+
iconPackages,
|
|
26
|
+
makeId,
|
|
27
|
+
} from '@lowdefy/build/dev';
|
|
21
28
|
|
|
22
|
-
import compileCss from './compileCss.js';
|
|
23
29
|
import createLogger from './log/createLogger.js';
|
|
24
30
|
import PageCache from './pageCache.mjs';
|
|
25
31
|
import readBuildApiArtifacts from './readBuildApiArtifacts.mjs';
|
|
@@ -36,10 +42,10 @@ let cachedRegistry = null;
|
|
|
36
42
|
let cachedBuildContext = null;
|
|
37
43
|
let lastInvalidationMtime = null;
|
|
38
44
|
|
|
39
|
-
// Frozen snapshot of icon imports from the initial build (what's actually in the
|
|
45
|
+
// Frozen snapshot of icon imports from the initial build (what's actually in the client bundle).
|
|
40
46
|
// Module-level so it persists across context resets (skeleton rebuilds update iconImports.json
|
|
41
|
-
// with newly discovered icons, but those aren't in the bundle until
|
|
42
|
-
// Only resets when the server process restarts
|
|
47
|
+
// with newly discovered icons, but those aren't in the bundle until a server restart).
|
|
48
|
+
// Only resets when the server process restarts.
|
|
43
49
|
let bundledIconImports = null;
|
|
44
50
|
|
|
45
51
|
function readJsonFile(filePath) {
|
|
@@ -92,10 +98,10 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
92
98
|
const keyMap = readJsonFile(path.join(buildDirectory, 'keyMap.json')) ?? {};
|
|
93
99
|
const jsMap = readJsonFile(path.join(buildDirectory, 'jsMap.json')) ?? { client: {}, server: {} };
|
|
94
100
|
const connectionIds = readJsonFile(path.join(buildDirectory, 'connectionIds.json')) ?? [];
|
|
101
|
+
const websocketIds = readJsonFile(path.join(buildDirectory, 'websocketIds.json')) ?? [];
|
|
95
102
|
|
|
96
103
|
const customTypesMap = readJsonFile(path.join(buildDirectory, 'customTypesMap.json')) ?? {};
|
|
97
|
-
const customMessagesMap =
|
|
98
|
-
readJsonFile(path.join(buildDirectory, 'customMessagesMap.json')) ?? {};
|
|
104
|
+
const customMessagesMap = readJsonFile(path.join(buildDirectory, 'customMessagesMap.json')) ?? {};
|
|
99
105
|
|
|
100
106
|
cachedBuildContext = createContext({
|
|
101
107
|
customMessagesMap,
|
|
@@ -109,7 +115,7 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
109
115
|
stage: 'dev',
|
|
110
116
|
});
|
|
111
117
|
|
|
112
|
-
// Restore refMap, keyMap, jsMap, and
|
|
118
|
+
// Restore refMap, keyMap, jsMap, connectionIds, and websocketIds from skeleton build
|
|
113
119
|
Object.assign(cachedBuildContext.refMap, refMap);
|
|
114
120
|
Object.assign(cachedBuildContext.keyMap, keyMap);
|
|
115
121
|
cachedBuildContext.jsMap.client = jsMap.client ?? {};
|
|
@@ -117,6 +123,9 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
117
123
|
for (const id of connectionIds) {
|
|
118
124
|
cachedBuildContext.connectionIds.add(id);
|
|
119
125
|
}
|
|
126
|
+
for (const id of websocketIds) {
|
|
127
|
+
cachedBuildContext.websocketIds.add(id);
|
|
128
|
+
}
|
|
120
129
|
|
|
121
130
|
// Load installed packages snapshot from skeleton build for missing-package detection
|
|
122
131
|
const installedPluginPackages =
|
|
@@ -129,15 +138,27 @@ function getBuildContext(buildDirectory, configDirectory) {
|
|
|
129
138
|
Object.assign(cachedBuildContext.modules, modules);
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
// Hydrate the deferred-record registry — module component bodies referenced
|
|
142
|
+
// by '~deferred' placeholders in modules.json live here. readJsonFile runs
|
|
143
|
+
// the marker-restoring reviver, so record-body ~r/~l markers survive.
|
|
144
|
+
const deferredRecords = readJsonFile(path.join(buildDirectory, 'deferredRecords.json'));
|
|
145
|
+
if (deferredRecords) {
|
|
146
|
+
hydrateDeferredRecords(cachedBuildContext, deferredRecords);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Restore app metadata so JIT page builds resolve _app / _build.app against
|
|
150
|
+
// the same metadata the skeleton build computed.
|
|
151
|
+
cachedBuildContext.appMeta = readJsonFile(path.join(buildDirectory, 'appMeta.json')) ?? null;
|
|
152
|
+
|
|
132
153
|
// Restore api endpoint configs so JIT CallAPI validation (validateCallApiRefs in
|
|
133
154
|
// buildPageJit) can resolve endpointIds. Without this the dev context has no
|
|
134
155
|
// components.api and every CallAPI action is flagged as a non-existent endpoint.
|
|
135
156
|
cachedBuildContext.components = { api: readBuildApiArtifacts(buildDirectory) };
|
|
136
157
|
|
|
137
158
|
// Use the frozen icon imports from the initial build for JIT detection.
|
|
138
|
-
// This represents what's actually in the
|
|
159
|
+
// This represents what's actually in the client bundle — not what shallowBuild
|
|
139
160
|
// discovers on subsequent rebuilds (those icons aren't bundled yet).
|
|
140
|
-
// bundledIconImports is module-level and only resets on server restart
|
|
161
|
+
// bundledIconImports is module-level and only resets on server restart.
|
|
141
162
|
if (!bundledIconImports) {
|
|
142
163
|
bundledIconImports = readJsonFile(path.join(buildDirectory, 'iconImports.json')) ?? [];
|
|
143
164
|
}
|
|
@@ -190,7 +211,18 @@ async function buildPageIfNeeded({ pageId, buildDirectory, configDirectory }) {
|
|
|
190
211
|
return result;
|
|
191
212
|
}
|
|
192
213
|
pageCache.markCompiled(pageId);
|
|
193
|
-
|
|
214
|
+
// Touch the candidates file so Vite's CSS pipeline re-runs Tailwind for
|
|
215
|
+
// classes the JIT build discovered — globals.css imports it. This import
|
|
216
|
+
// is the ONLY recompile trigger: the tailwind .html scan inputs are
|
|
217
|
+
// excluded from Vite's watcher (their .html change events would force
|
|
218
|
+
// full browser reloads). Only touched when the build actually changed
|
|
219
|
+
// tailwind content, so unchanged rebuilds cause no CSS recompile.
|
|
220
|
+
if (result?._tailwindChanged) {
|
|
221
|
+
fs.writeFileSync(
|
|
222
|
+
path.join(buildDirectory, 'tailwind-candidates.css'),
|
|
223
|
+
`/* Generated by Lowdefy build — rewritten on page changes to trigger CSS recompilation */\n/* ${Date.now()} */\n`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
194
226
|
jitLogger.info(
|
|
195
227
|
{ spin: 'succeed', color: 'white' },
|
|
196
228
|
`Built page "${pageId}" in ${formatDuration(Date.now() - startTime)}.`
|
|
@@ -201,4 +233,85 @@ async function buildPageIfNeeded({ pageId, buildDirectory, configDirectory }) {
|
|
|
201
233
|
}
|
|
202
234
|
}
|
|
203
235
|
|
|
236
|
+
// Collect every client _js hash the page references. jsMapParser reduces a _js
|
|
237
|
+
// operator to either { _js: "<hash>" } or { _js: { fn: "<hash>", args } }, and
|
|
238
|
+
// keeps args verbatim — so a _js nested inside another's args survives as its
|
|
239
|
+
// own node and must be descended into, or its hash is dropped and the client
|
|
240
|
+
// throws "_js function not found".
|
|
241
|
+
function collectJsHashes(node, hashes) {
|
|
242
|
+
if (type.isArray(node)) {
|
|
243
|
+
for (const item of node) collectJsHashes(item, hashes);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!type.isObject(node)) return;
|
|
247
|
+
if (Object.prototype.hasOwnProperty.call(node, '_js')) {
|
|
248
|
+
const inner = node._js;
|
|
249
|
+
if (type.isString(inner)) {
|
|
250
|
+
hashes.add(inner);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (type.isObject(inner) && type.isString(inner.fn)) {
|
|
254
|
+
hashes.add(inner.fn);
|
|
255
|
+
collectJsHashes(inner.args, hashes);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const value of Object.values(node)) collectJsHashes(value, hashes);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Icons are discovered by detectMissingIcons on the pre-parse page, so a name
|
|
264
|
+
// appearing only inside a _js body is real but is a hash in the served config.
|
|
265
|
+
// Reproduce that surface: scan the served config plus the page's own client _js
|
|
266
|
+
// source strings, and keep only names present in dynamicIconData (which holds
|
|
267
|
+
// only JIT-discovered icons — static ones are already in the client bundle).
|
|
268
|
+
function scopeDynamicIcons({ pageConfig, scopedJsMap, dynamicIconData }) {
|
|
269
|
+
if (Object.keys(dynamicIconData).length === 0) return undefined;
|
|
270
|
+
const scanText = [JSON.stringify(pageConfig), ...Object.values(scopedJsMap)].join('\n');
|
|
271
|
+
const found = {};
|
|
272
|
+
for (const regex of Object.values(iconPackages)) {
|
|
273
|
+
for (const match of scanText.matchAll(regex)) {
|
|
274
|
+
const name = match[1];
|
|
275
|
+
if (dynamicIconData[name] && !found[name]) {
|
|
276
|
+
found[name] = dynamicIconData[name];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return Object.keys(found).length > 0 ? found : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Scope this page's JIT-discovered enrichment out of the persistent build
|
|
284
|
+
// context so jitPageHandler can fold it into the page-config response the client
|
|
285
|
+
// already awaits — removing the two secondary fetches that stalled first paint.
|
|
286
|
+
// buildContext defaults to the module-private cachedBuildContext (re-read on
|
|
287
|
+
// every call, so it tracks invalidation resets); tests pass a stub.
|
|
288
|
+
export function getPageJitEnrichment({ pageConfig, buildContext = cachedBuildContext }) {
|
|
289
|
+
// No build context (before the first build, or after an invalidation reset)
|
|
290
|
+
// means nothing JIT-discovered to fold — the page serves what the static
|
|
291
|
+
// client bundle already carries.
|
|
292
|
+
if (!buildContext) return {};
|
|
293
|
+
|
|
294
|
+
const clientJsMap = buildContext.jsMap.client ?? {};
|
|
295
|
+
const hashes = new Set();
|
|
296
|
+
collectJsHashes(pageConfig, hashes);
|
|
297
|
+
|
|
298
|
+
const scopedJsMap = {};
|
|
299
|
+
for (const hash of hashes) {
|
|
300
|
+
if (Object.prototype.hasOwnProperty.call(clientJsMap, hash)) {
|
|
301
|
+
scopedJsMap[hash] = clientJsMap[hash];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const jsEntries =
|
|
306
|
+
Object.keys(scopedJsMap).length > 0 ? generateClientJsModule(scopedJsMap) : undefined;
|
|
307
|
+
|
|
308
|
+
const dynamicIcons = scopeDynamicIcons({
|
|
309
|
+
pageConfig,
|
|
310
|
+
scopedJsMap,
|
|
311
|
+
dynamicIconData: buildContext.dynamicIconData ?? {},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return { jsEntries, dynamicIcons };
|
|
315
|
+
}
|
|
316
|
+
|
|
204
317
|
export default buildPageIfNeeded;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
|
|
19
|
+
|
|
20
|
+
import createHandleError from './createHandleError.js';
|
|
21
|
+
|
|
22
|
+
// This sink is the sole writer of `error.handled`. Two callers read the flag and
|
|
23
|
+
// neither can set it for itself: runRoutine's catch guards on `!error.handled` to
|
|
24
|
+
// avoid logging a nested error twice, and the browser's handleError skips the
|
|
25
|
+
// round-trip to /api/client-error when it is set. runRoutine deliberately gave up
|
|
26
|
+
// its own assignment on the strength of this function, so the tests below pin the
|
|
27
|
+
// assignment and — more importantly — its position ahead of everything that can
|
|
28
|
+
// throw.
|
|
29
|
+
|
|
30
|
+
const keyMap = { key_1: { key: 'root.pages[0:home]', '~r': 'ref_1', '~l': 5 } };
|
|
31
|
+
const refMap = { ref_1: { path: 'pages/home.yaml' } };
|
|
32
|
+
|
|
33
|
+
function testContext({ logger, readConfigFile, configDirectory } = {}) {
|
|
34
|
+
return {
|
|
35
|
+
configDirectory,
|
|
36
|
+
logger: logger ?? { error: jest.fn() },
|
|
37
|
+
readConfigFile:
|
|
38
|
+
readConfigFile ??
|
|
39
|
+
jest.fn(async (path) => {
|
|
40
|
+
if (path === 'keyMap.json') return keyMap;
|
|
41
|
+
if (path === 'refMap.json') return refMap;
|
|
42
|
+
return null;
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
console.error = jest.fn();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('handleError marks the error handled and resolves its location', async () => {
|
|
52
|
+
const context = testContext();
|
|
53
|
+
const handleError = createHandleError({ context });
|
|
54
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
55
|
+
|
|
56
|
+
await handleError(error);
|
|
57
|
+
|
|
58
|
+
expect(error.handled).toBe(true);
|
|
59
|
+
expect(error.source).toBe('pages/home.yaml:5');
|
|
60
|
+
expect(error.config).toBe('root.pages[0:home]');
|
|
61
|
+
expect(context.logger.error).toHaveBeenCalledWith(error);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('handleError marks a LowdefyInternalError handled even though it never gets a source', async () => {
|
|
65
|
+
// The Decision 7 case. Location resolution is deliberately skipped for an
|
|
66
|
+
// internal error, so `source` stays undefined - which is why the client's
|
|
67
|
+
// already-logged gate reads `handled` and not `source`. Keyed on source, this
|
|
68
|
+
// error was POSTed back to /api/client-error and logged a second time.
|
|
69
|
+
const context = testContext();
|
|
70
|
+
const handleError = createHandleError({ context });
|
|
71
|
+
const error = new LowdefyInternalError('Unexpected condition.');
|
|
72
|
+
|
|
73
|
+
await handleError(error);
|
|
74
|
+
|
|
75
|
+
expect(error.handled).toBe(true);
|
|
76
|
+
expect(error.source).toBeUndefined();
|
|
77
|
+
expect(context.readConfigFile).not.toHaveBeenCalled();
|
|
78
|
+
expect(context.logger.error).toHaveBeenCalledWith(error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('handleError marks the error handled before it logs, so a throwing logger cannot skip the flag', async () => {
|
|
82
|
+
const logger = {
|
|
83
|
+
error: jest.fn(() => {
|
|
84
|
+
throw new Error('Log drain unreachable.');
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
const context = testContext({ logger });
|
|
88
|
+
const handleError = createHandleError({ context });
|
|
89
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
90
|
+
|
|
91
|
+
await handleError(error);
|
|
92
|
+
|
|
93
|
+
// The sink swallows its own failure - reaching it is what makes the error
|
|
94
|
+
// already-logged, so the flag must survive the log call failing. Were it set
|
|
95
|
+
// after the log, this error would cross the wire unflagged and be logged a
|
|
96
|
+
// second time from the browser.
|
|
97
|
+
expect(error.handled).toBe(true);
|
|
98
|
+
expect(logger.error).toHaveBeenCalled();
|
|
99
|
+
expect(console.error).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('handleError marks the error handled when location resolution rejects', async () => {
|
|
103
|
+
const readConfigFile = jest.fn(async () => {
|
|
104
|
+
throw new Error('keyMap.json unreadable.');
|
|
105
|
+
});
|
|
106
|
+
const context = testContext({ readConfigFile });
|
|
107
|
+
const handleError = createHandleError({ context });
|
|
108
|
+
const error = new ConfigError('Bad config.', { configKey: 'key_1' });
|
|
109
|
+
|
|
110
|
+
await handleError(error);
|
|
111
|
+
|
|
112
|
+
expect(error.handled).toBe(true);
|
|
113
|
+
// ConfigError's constructor initialises source to null; resolution failing
|
|
114
|
+
// leaves it there rather than assigning.
|
|
115
|
+
expect(error.source).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('handleError marks a plain Error handled', async () => {
|
|
119
|
+
const context = testContext();
|
|
120
|
+
const handleError = createHandleError({ context });
|
|
121
|
+
const error = new Error('Something broke.');
|
|
122
|
+
|
|
123
|
+
await handleError(error);
|
|
124
|
+
|
|
125
|
+
expect(error.handled).toBe(true);
|
|
126
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
19
|
+
import { serializer } from '@lowdefy/helpers';
|
|
20
|
+
|
|
21
|
+
const secret = 'planted-dev-secret-value';
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
const destination = {
|
|
25
|
+
write(line) {
|
|
26
|
+
lines.push(line);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// The CLI's dev terminal is parsed from this logger's output, so it must keep the full default
|
|
31
|
+
// serialization; route the real logger to memory to read the exact emitted line.
|
|
32
|
+
jest.unstable_mockModule('@lowdefy/logger/node', () => ({
|
|
33
|
+
createNodeLogger: (options) => createNodeLogger({ ...options, destination }),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
process.env.LOWDEFY_SECRET_API_KEY = secret;
|
|
37
|
+
|
|
38
|
+
const { default: createLogger } = await import('./createLogger.js');
|
|
39
|
+
|
|
40
|
+
function createAxiosError() {
|
|
41
|
+
const error = new Error(`Request failed for key ${secret}`);
|
|
42
|
+
error.name = 'AxiosError';
|
|
43
|
+
error.code = 'ERR_BAD_REQUEST';
|
|
44
|
+
error.config = {
|
|
45
|
+
auth: { username: 'svc-user', password: 'planted-config-password' },
|
|
46
|
+
params: { api_key: 'planted-config-param' },
|
|
47
|
+
headers: { Authorization: 'Bearer planted-config-header' },
|
|
48
|
+
};
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('dev logger writes an axios error with full default serialization and no scrub', () => {
|
|
53
|
+
const error = createAxiosError();
|
|
54
|
+
createLogger().error({ err: error }, error.message);
|
|
55
|
+
expect(lines).toHaveLength(1);
|
|
56
|
+
const { err, msg } = JSON.parse(lines[0]);
|
|
57
|
+
expect(err).toEqual(JSON.parse(JSON.stringify(serializer.serialize(error)['~e'])));
|
|
58
|
+
expect(err.config.auth.password).toBe('planted-config-password');
|
|
59
|
+
expect(err.config.params.api_key).toBe('planted-config-param');
|
|
60
|
+
expect(err.config.headers.Authorization).toBe('Bearer planted-config-header');
|
|
61
|
+
expect(msg).toBe(`Request failed for key ${secret}`);
|
|
62
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createSecretScrubber, getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
18
|
+
|
|
19
|
+
// Dev logs stay unscrubbed, but _error reads this off the context like every
|
|
20
|
+
// other server package, so a thrown message behaves identically in every mode.
|
|
21
|
+
const scrubSecrets = createSecretScrubber({ secrets: getSecretsFromEnv() });
|
|
22
|
+
|
|
23
|
+
export default scrubSecrets;
|
package/manager/getContext.mjs
CHANGED
|
@@ -22,18 +22,16 @@ import { hideBin } from 'yargs/helpers';
|
|
|
22
22
|
import pino from 'pino';
|
|
23
23
|
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
24
24
|
import checkMockUserWarning from './processes/checkMockUserWarning.mjs';
|
|
25
|
-
import compileCss from './processes/compileCss.mjs';
|
|
26
25
|
import initialBuild from './processes/initialBuild.mjs';
|
|
27
26
|
import installPlugins from './processes/installPlugins.mjs';
|
|
28
27
|
import lowdefyBuild from './processes/lowdefyBuild.mjs';
|
|
29
|
-
import nextBuild from './processes/nextBuild.mjs';
|
|
30
28
|
import readDotEnv from './processes/readDotEnv.mjs';
|
|
31
29
|
import reloadClients from './processes/reloadClients.mjs';
|
|
32
30
|
import restartServer from './processes/restartServer.mjs';
|
|
33
31
|
import shutdownServer from './processes/shutdownServer.mjs';
|
|
34
32
|
import startWatchers from './processes/startWatchers.mjs';
|
|
35
33
|
|
|
36
|
-
import
|
|
34
|
+
import getViteBin from './utils/getViteBin.mjs';
|
|
37
35
|
|
|
38
36
|
const argv = yargs(hideBin(process.argv)).array('watch').array('watchIgnore').argv;
|
|
39
37
|
|
|
@@ -42,7 +40,7 @@ async function getContext() {
|
|
|
42
40
|
|
|
43
41
|
const context = {
|
|
44
42
|
bin: {
|
|
45
|
-
|
|
43
|
+
vite: getViteBin(),
|
|
46
44
|
},
|
|
47
45
|
directories: {
|
|
48
46
|
build: path.resolve(process.cwd(), './build'),
|
|
@@ -56,7 +54,7 @@ async function getContext() {
|
|
|
56
54
|
destination: pino.destination({ dest: 1, sync: true }),
|
|
57
55
|
}),
|
|
58
56
|
options: {
|
|
59
|
-
port: argv.port ?? env.PORT ?? 3000,
|
|
57
|
+
port: Number(argv.port ?? env.PORT ?? 3000),
|
|
60
58
|
refResolver: argv.refResolver ?? env.LOWDEFY_BUILD_REF_RESOLVER,
|
|
61
59
|
watch:
|
|
62
60
|
argv.watch ?? env.LOWDEFY_SERVER_DEV_WATCH ? JSON.parse(env.LOWDEFY_SERVER_DEV_WATCH) : [],
|
|
@@ -87,8 +85,6 @@ async function getContext() {
|
|
|
87
85
|
}
|
|
88
86
|
};
|
|
89
87
|
|
|
90
|
-
context.compileCss = compileCss(context);
|
|
91
|
-
context.nextBuild = nextBuild(context);
|
|
92
88
|
context.readDotEnv = readDotEnv(context);
|
|
93
89
|
context.reloadClients = reloadClients(context);
|
|
94
90
|
context.restartServer = restartServer(context);
|