slicejs-web-framework 3.4.2 → 3.4.4
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/.opencode/opencode.json +14 -14
- package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +468 -468
- package/Slice/Components/Structural/Controller/Controller.js +1271 -1198
- package/Slice/Components/Structural/Debugger/Debugger.js +1503 -1503
- package/Slice/Components/Structural/EventManager/EventManager.js +366 -366
- package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +126 -5
- package/Slice/Components/Structural/Logger/Logger.js +225 -225
- package/Slice/Components/Structural/Router/Router.js +775 -775
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +106 -106
- package/Slice/Slice.js +650 -627
- package/Slice/tests/build-singleton.test.js +244 -244
- package/Slice/tests/bundle-v2-runtime-contract.test.js +738 -738
- package/Slice/tests/context-patch-use.test.js +95 -95
- package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
- package/Slice/tests/public-env-typed-accessors.test.js +66 -66
- package/api/index.js +281 -281
- package/api/utils/publicEnvResolver.js +123 -123
- package/package.json +44 -44
- package/slicejs-cli/.github/workflows/ci.yml +43 -0
- package/slicejs-cli/LICENSE +21 -0
- package/slicejs-cli/assets/Slice.js-logo.png +0 -0
- package/slicejs-cli/client.js +740 -0
- package/slicejs-cli/commands/Print.js +164 -0
- package/slicejs-cli/commands/Validations.js +92 -0
- package/slicejs-cli/commands/build/build.js +40 -0
- package/slicejs-cli/commands/buildProduction/buildProduction.js +577 -0
- package/slicejs-cli/commands/bundle/bundle.js +236 -0
- package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +55 -0
- package/slicejs-cli/commands/createComponent/createComponent.js +128 -0
- package/slicejs-cli/commands/deleteComponent/deleteComponent.js +81 -0
- package/slicejs-cli/commands/doctor/doctor.js +516 -0
- package/slicejs-cli/commands/getComponent/getComponent.js +695 -0
- package/slicejs-cli/commands/init/init.js +473 -0
- package/slicejs-cli/commands/listComponents/listComponents.js +172 -0
- package/slicejs-cli/commands/startServer/startServer.js +240 -0
- package/slicejs-cli/commands/startServer/watchServer.js +66 -0
- package/slicejs-cli/commands/types/types.js +583 -0
- package/slicejs-cli/commands/utils/LocalCliDelegation.js +53 -0
- package/slicejs-cli/commands/utils/PackageManager.js +148 -0
- package/slicejs-cli/commands/utils/PathHelper.js +75 -0
- package/slicejs-cli/commands/utils/VersionChecker.js +169 -0
- package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +2525 -0
- package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +933 -0
- package/slicejs-cli/commands/utils/loadConfig.js +31 -0
- package/slicejs-cli/commands/utils/sliceScripts.js +48 -0
- package/slicejs-cli/commands/utils/updateManager.js +471 -0
- package/slicejs-cli/package.json +71 -0
- package/slicejs-cli/playwright.config.js +51 -0
- package/slicejs-cli/pnpm-lock.yaml +1629 -0
- package/slicejs-cli/tests/build-command-integration.test.js +87 -0
- package/slicejs-cli/tests/build-mangling-e2e.test.js +290 -0
- package/slicejs-cli/tests/build-production-e2e.test.js +140 -0
- package/slicejs-cli/tests/builder-edge-cases.test.js +322 -0
- package/slicejs-cli/tests/bundle-generate-e2e.test.js +115 -0
- package/slicejs-cli/tests/bundle-generator.test.js +691 -0
- package/slicejs-cli/tests/bundle-v2-register-output.test.js +470 -0
- package/slicejs-cli/tests/bundling-dependency-edges.test.js +127 -0
- package/slicejs-cli/tests/bundling-imports-unit.test.js +267 -0
- package/slicejs-cli/tests/client-launcher-contract.test.js +211 -0
- package/slicejs-cli/tests/commands-component-crud.test.js +128 -0
- package/slicejs-cli/tests/commands-doctor.test.js +152 -0
- package/slicejs-cli/tests/commands-version-checker.test.js +37 -0
- package/slicejs-cli/tests/component-registry-parse.test.js +34 -0
- package/slicejs-cli/tests/dependency-analyzer.test.js +24 -0
- package/slicejs-cli/tests/e2e/bundles.spec.js +91 -0
- package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +56 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +136 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +149 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +45 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +106 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +5 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +158 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +33 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +56 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +83 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +164 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +167 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +116 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +44 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +180 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +20 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +181 -0
- package/slicejs-cli/tests/e2e/fixtures/components/registry.json +12 -0
- package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +65 -0
- package/slicejs-cli/tests/e2e/navigation.spec.js +44 -0
- package/slicejs-cli/tests/e2e/render.spec.js +34 -0
- package/slicejs-cli/tests/e2e/serve.mjs +264 -0
- package/slicejs-cli/tests/e2e/shared-deps.spec.js +61 -0
- package/slicejs-cli/tests/e2e/unminified.spec.js +33 -0
- package/slicejs-cli/tests/e2e-serve.test.js +148 -0
- package/slicejs-cli/tests/fixtures/components.js +8 -0
- package/slicejs-cli/tests/fixtures/sliceConfig.json +74 -0
- package/slicejs-cli/tests/getcomponent.test.js +435 -0
- package/slicejs-cli/tests/helpers/setup.js +102 -0
- package/slicejs-cli/tests/init-command-contract.test.js +101 -0
- package/slicejs-cli/tests/init-e2e.test.js +82 -0
- package/slicejs-cli/tests/init-project-isolation.test.js +98 -0
- package/slicejs-cli/tests/local-cli-delegation.test.js +81 -0
- package/slicejs-cli/tests/package-manager.test.js +127 -0
- package/slicejs-cli/tests/path-helper.test.js +206 -0
- package/slicejs-cli/tests/perf-budget.test.js +84 -0
- package/slicejs-cli/tests/postinstall-command.test.js +103 -0
- package/slicejs-cli/tests/property-mangling-regression.test.js +182 -0
- package/slicejs-cli/tests/types-breakage.test.js +491 -0
- package/slicejs-cli/tests/types-generator-errors.test.js +361 -0
- package/slicejs-cli/tests/types-generator.test.js +348 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { withTestProject } from './helpers/setup.js';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import {
|
|
8
|
+
checkNodeVersion,
|
|
9
|
+
checkDirectoryStructure,
|
|
10
|
+
checkConfig,
|
|
11
|
+
checkComponents,
|
|
12
|
+
checkPackageManagerSetup,
|
|
13
|
+
} from '../commands/doctor/doctor.js';
|
|
14
|
+
|
|
15
|
+
describe('doctor checks', () => {
|
|
16
|
+
test('checkNodeVersion passes on the supported runtime', async () => {
|
|
17
|
+
const r = await checkNodeVersion();
|
|
18
|
+
assert.equal(r.pass, true);
|
|
19
|
+
assert.match(r.message, /Node\.js version/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('checkDirectoryStructure passes on the starter project', async () => {
|
|
23
|
+
await withTestProject(async () => {
|
|
24
|
+
const r = await checkDirectoryStructure();
|
|
25
|
+
assert.equal(r.pass, true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('checkDirectoryStructure fails when src/ is missing', async () => {
|
|
30
|
+
await withTestProject(async (root) => {
|
|
31
|
+
await fs.remove(path.join(root, 'src'));
|
|
32
|
+
const r = await checkDirectoryStructure();
|
|
33
|
+
assert.equal(r.pass, false);
|
|
34
|
+
assert.match(r.message, /Missing directories/);
|
|
35
|
+
assert.match(r.message, /src\//);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('checkConfig passes for a valid sliceConfig.json', async () => {
|
|
40
|
+
await withTestProject(async () => {
|
|
41
|
+
const r = await checkConfig();
|
|
42
|
+
assert.equal(r.pass, true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('checkConfig fails on invalid JSON', async () => {
|
|
47
|
+
await withTestProject(async (root) => {
|
|
48
|
+
await fs.writeFile(path.join(root, 'src', 'sliceConfig.json'), '{ not valid json ');
|
|
49
|
+
const r = await checkConfig();
|
|
50
|
+
assert.equal(r.pass, false);
|
|
51
|
+
assert.match(r.message, /invalid JSON/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('checkConfig fails when paths.components is missing', async () => {
|
|
56
|
+
await withTestProject(async (root) => {
|
|
57
|
+
await fs.writeJson(path.join(root, 'src', 'sliceConfig.json'), { server: { port: 3000 } });
|
|
58
|
+
const r = await checkConfig();
|
|
59
|
+
assert.equal(r.pass, false);
|
|
60
|
+
assert.match(r.message, /missing paths\.components/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('checkComponents reports OK when every component folder has its .js', async () => {
|
|
65
|
+
await withTestProject(async () => {
|
|
66
|
+
const r = await checkComponents();
|
|
67
|
+
assert.equal(r.pass, true);
|
|
68
|
+
assert.match(r.message, /components checked/);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('checkComponents warns when a component folder lacks its .js file', async () => {
|
|
73
|
+
await withTestProject(async (root) => {
|
|
74
|
+
// AppShell ships with AppShell.js; remove it to simulate a broken component.
|
|
75
|
+
const broken = path.join(root, 'src', 'Components', 'AppComponents', 'Broken');
|
|
76
|
+
await fs.ensureDir(broken); // directory with no Broken.js
|
|
77
|
+
const r = await checkComponents();
|
|
78
|
+
assert.equal(r.warn, true);
|
|
79
|
+
assert.match(r.message, /missing files/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('checkPackageManagerSetup', () => {
|
|
85
|
+
async function makeProject({ pkg, files = [] } = {}) {
|
|
86
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-doctor-pm-'));
|
|
87
|
+
if (pkg) await fs.writeJson(path.join(dir, 'package.json'), pkg);
|
|
88
|
+
for (const f of files) await fs.outputFile(path.join(dir, f), '');
|
|
89
|
+
return dir;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('passes on a consistent pnpm project with local CLI', async () => {
|
|
93
|
+
const dir = await makeProject({
|
|
94
|
+
pkg: { name: 'x', packageManager: 'pnpm@11.0.0' },
|
|
95
|
+
files: ['pnpm-lock.yaml', 'node_modules/slicejs-cli/package.json']
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
const r = await checkPackageManagerSetup(dir);
|
|
99
|
+
assert.equal(r.pass, true);
|
|
100
|
+
} finally { await fs.remove(dir); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('warns on mixed lockfiles', async () => {
|
|
104
|
+
const dir = await makeProject({
|
|
105
|
+
pkg: { name: 'x', packageManager: 'pnpm@11.0.0' },
|
|
106
|
+
files: ['pnpm-lock.yaml', 'package-lock.json', 'node_modules/slicejs-cli/package.json']
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
const r = await checkPackageManagerSetup(dir);
|
|
110
|
+
assert.equal(r.warn, true);
|
|
111
|
+
assert.match(r.message, /Mixed lockfiles/);
|
|
112
|
+
assert.match(r.suggestion, /pnpm-lock\.yaml/);
|
|
113
|
+
} finally { await fs.remove(dir); }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('warns when packageManager field is missing', async () => {
|
|
117
|
+
const dir = await makeProject({
|
|
118
|
+
pkg: { name: 'x' },
|
|
119
|
+
files: ['pnpm-lock.yaml', 'node_modules/slicejs-cli/package.json']
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const r = await checkPackageManagerSetup(dir);
|
|
123
|
+
assert.equal(r.warn, true);
|
|
124
|
+
assert.match(r.message, /packageManager/);
|
|
125
|
+
} finally { await fs.remove(dir); }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('warns when packageManager field disagrees with the lockfile', async () => {
|
|
129
|
+
const dir = await makeProject({
|
|
130
|
+
pkg: { name: 'x', packageManager: 'pnpm@11.0.0' },
|
|
131
|
+
files: ['package-lock.json', 'node_modules/slicejs-cli/package.json']
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
const r = await checkPackageManagerSetup(dir);
|
|
135
|
+
assert.equal(r.warn, true);
|
|
136
|
+
assert.match(r.message, /packageManager is "pnpm" but the lockfile is package-lock\.json/);
|
|
137
|
+
} finally { await fs.remove(dir); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('warns when slicejs-cli is not installed locally', async () => {
|
|
141
|
+
const dir = await makeProject({
|
|
142
|
+
pkg: { name: 'x', packageManager: 'pnpm@11.0.0' },
|
|
143
|
+
files: ['pnpm-lock.yaml']
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
const r = await checkPackageManagerSetup(dir);
|
|
147
|
+
assert.equal(r.warn, true);
|
|
148
|
+
assert.match(r.message, /not installed locally/);
|
|
149
|
+
assert.match(r.suggestion, /-D slicejs-cli/);
|
|
150
|
+
} finally { await fs.remove(dir); }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import versionChecker from '../commands/utils/VersionChecker.js';
|
|
4
|
+
|
|
5
|
+
describe('VersionChecker.compareVersions', () => {
|
|
6
|
+
test('detects an outdated version', () => {
|
|
7
|
+
assert.equal(versionChecker.compareVersions('1.0.0', '1.0.1'), 'outdated');
|
|
8
|
+
assert.equal(versionChecker.compareVersions('1.2.0', '2.0.0'), 'outdated');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('detects a newer (local) version', () => {
|
|
12
|
+
assert.equal(versionChecker.compareVersions('2.0.0', '1.9.9'), 'newer');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('detects an up-to-date version', () => {
|
|
16
|
+
assert.equal(versionChecker.compareVersions('3.5.0', '3.5.0'), 'current');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('handles version strings of differing length', () => {
|
|
20
|
+
assert.equal(versionChecker.compareVersions('1.0', '1.0.0'), 'current');
|
|
21
|
+
assert.equal(versionChecker.compareVersions('1.0.0', '1.0.0.1'), 'outdated');
|
|
22
|
+
assert.equal(versionChecker.compareVersions('1.0.1', '1.0'), 'newer');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('returns null when a version is missing', () => {
|
|
26
|
+
assert.equal(versionChecker.compareVersions(null, '1.0.0'), null);
|
|
27
|
+
assert.equal(versionChecker.compareVersions('1.0.0', undefined), null);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('VersionChecker.getCurrentVersions', () => {
|
|
32
|
+
test('reads the CLI version from the package.json (no network)', async () => {
|
|
33
|
+
const current = await versionChecker.getCurrentVersions();
|
|
34
|
+
assert.ok(current, 'should resolve current versions');
|
|
35
|
+
assert.match(current.cli, /^\d+\.\d+\.\d+/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
|
|
6
|
+
|
|
7
|
+
test('generateTypesFile parses JSON from components.js (no eval)', async () => {
|
|
8
|
+
const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const srcDir = path.join(tmpRoot, 'src');
|
|
12
|
+
const { generateTypesFile } = await import('../commands/types/types.js');
|
|
13
|
+
const result = await generateTypesFile({
|
|
14
|
+
projectRoot: tmpRoot,
|
|
15
|
+
outputPath: path.join(srcDir, 'slice-build.generated.d.ts')
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.ok(result.componentsProcessed > 0);
|
|
19
|
+
assert.equal(fs.existsSync(result.outputPath), true);
|
|
20
|
+
|
|
21
|
+
const declaration = fs.readFileSync(result.outputPath, 'utf8');
|
|
22
|
+
assert.match(declaration, /export interface ButtonProps/);
|
|
23
|
+
} finally {
|
|
24
|
+
await cleanupTestProject(tmpRoot);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('Validations componentExists with JSON.parse (no eval)', async () => {
|
|
29
|
+
await withTestProject(async (tmpDir) => {
|
|
30
|
+
const validations = (await import('../commands/Validations.js')).default;
|
|
31
|
+
assert.equal(validations.componentExists('Button'), true);
|
|
32
|
+
assert.equal(validations.componentExists('NonExistent'), false);
|
|
33
|
+
}, { visualComponents: ['Button'] });
|
|
34
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import DependencyAnalyzer from '../commands/utils/bundling/DependencyAnalyzer.js';
|
|
4
|
+
|
|
5
|
+
test('parseComponentsConfig returns components map', () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const analyzer = new DependencyAnalyzer(import.meta.url);
|
|
8
|
+
const content = `const components = {"Button": "Visual", "FetchManager": "Service"};\nexport default components;`;
|
|
9
|
+
|
|
10
|
+
// Act
|
|
11
|
+
const result = analyzer.parseComponentsConfig(content);
|
|
12
|
+
|
|
13
|
+
// Assert
|
|
14
|
+
assert.deepEqual(result, { Button: 'Visual', FetchManager: 'Service' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('parseComponentsConfig throws for invalid config', () => {
|
|
18
|
+
// Arrange
|
|
19
|
+
const analyzer = new DependencyAnalyzer(import.meta.url);
|
|
20
|
+
const content = `const notComponents = {"Button": "Visual"};`;
|
|
21
|
+
|
|
22
|
+
// Act / Assert
|
|
23
|
+
assert.throws(() => analyzer.parseComponentsConfig(content), /components object not found/);
|
|
24
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
|
|
4
|
+
// Recursively collect every emitted bundle file referenced by the config.
|
|
5
|
+
// Only real bundle artifacts (slice-bundle.*.js) — not dependency identifiers
|
|
6
|
+
// that happen to end in `.js` (e.g. vendorShared.dependencies).
|
|
7
|
+
function collectBundleFiles(node, acc = new Set()) {
|
|
8
|
+
if (typeof node === 'string') {
|
|
9
|
+
if (node.startsWith('slice-bundle.') && node.endsWith('.js')) acc.add(node);
|
|
10
|
+
return acc;
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(node)) {
|
|
13
|
+
for (const n of node) collectBundleFiles(n, acc);
|
|
14
|
+
return acc;
|
|
15
|
+
}
|
|
16
|
+
if (node && typeof node === 'object') {
|
|
17
|
+
for (const v of Object.values(node)) collectBundleFiles(v, acc);
|
|
18
|
+
return acc;
|
|
19
|
+
}
|
|
20
|
+
return acc;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getConfig(request) {
|
|
24
|
+
const res = await request.get('/bundles/bundle.config.json');
|
|
25
|
+
expect(res.status()).toBe(200);
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test.describe('bundle quality & content (served production artifacts)', () => {
|
|
30
|
+
test('bundle.config.json is well-formed and maps every route', async ({ request }) => {
|
|
31
|
+
const cfg = await getConfig(request);
|
|
32
|
+
expect(cfg.production).toBe(true);
|
|
33
|
+
expect(cfg.format).toBe('v2');
|
|
34
|
+
expect(cfg.minified).toBe(true);
|
|
35
|
+
expect(cfg.obfuscated).toBe(true);
|
|
36
|
+
expect(cfg.bundles.framework, 'framework bundle present').toBeTruthy();
|
|
37
|
+
expect(cfg.bundles.critical, 'critical bundle present').toBeTruthy();
|
|
38
|
+
|
|
39
|
+
for (const route of ['/', '/about', '/404']) {
|
|
40
|
+
expect(Array.isArray(cfg.routeBundles[route]), `route ${route} is mapped`).toBe(true);
|
|
41
|
+
expect(cfg.routeBundles[route].length).toBeGreaterThan(0);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('every referenced bundle is served as valid, contract-compliant JS', async ({ request }) => {
|
|
46
|
+
const cfg = await getConfig(request);
|
|
47
|
+
const files = [...collectBundleFiles(cfg.bundles)];
|
|
48
|
+
expect(files.length).toBeGreaterThan(0);
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const res = await request.get(`/bundles/${file}`);
|
|
52
|
+
expect(res.status(), `${file} is served`).toBe(200);
|
|
53
|
+
expect(res.headers()['content-type'] || '').toMatch(/javascript/);
|
|
54
|
+
|
|
55
|
+
const code = await res.text();
|
|
56
|
+
expect(() => parse(code, { sourceType: 'module', plugins: ['jsx'] }), `${file} is valid JS`).not.toThrow();
|
|
57
|
+
// v2 runtime contract.
|
|
58
|
+
expect(code, `${file} exports SLICE_BUNDLE_META`).toContain('SLICE_BUNDLE_META');
|
|
59
|
+
expect(code, `${file} exports registerAll`).toContain('registerAll');
|
|
60
|
+
// No unresolved relative import may leak into a production bundle.
|
|
61
|
+
expect(/\bfrom\s+['"]\.\.?\//.test(code), `${file} leaks a relative import`).toBe(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the framework bundle carries the structural runtime', async ({ request }) => {
|
|
66
|
+
const code = await (await request.get('/bundles/slice-bundle.framework.js')).text();
|
|
67
|
+
expect(code).toContain('Controller');
|
|
68
|
+
expect(code).toContain('registerAll');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('all starter component classes are registered across the bundles', async ({ request }) => {
|
|
72
|
+
const cfg = await getConfig(request);
|
|
73
|
+
const files = [...collectBundleFiles(cfg.bundles)];
|
|
74
|
+
let combined = '';
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
combined += await (await request.get(`/bundles/${file}`)).text();
|
|
77
|
+
}
|
|
78
|
+
// Component names are emitted as string literals in the register* calls and
|
|
79
|
+
// survive minification (mangle.properties is off, strings are untouched).
|
|
80
|
+
for (const comp of ['AppShell', 'Navbar', 'MultiRoute', 'HomeSection', 'Button', 'AboutSection', 'NotFound']) {
|
|
81
|
+
expect(combined.includes(`"${comp}"`), `${comp} is registered in a bundle`).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('bundles are genuinely minified', async ({ request }) => {
|
|
86
|
+
const code = await (await request.get('/bundles/slice-bundle.framework.js')).text();
|
|
87
|
+
const longestLine = Math.max(...code.split('\n').map((l) => l.length));
|
|
88
|
+
expect(longestLine, 'minified output has long lines').toBeGreaterThan(300);
|
|
89
|
+
expect(code, 'comments are stripped').not.toContain('/**');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
function trackErrors(page) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));
|
|
6
|
+
page.on('pageerror', (e) => errors.push(String(e)));
|
|
7
|
+
return errors;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test.describe('default-export shared dependency', () => {
|
|
11
|
+
test('/defaultdep resolves a default export at runtime', async ({ page }) => {
|
|
12
|
+
const errors = trackErrors(page);
|
|
13
|
+
await page.goto('/defaultdep');
|
|
14
|
+
|
|
15
|
+
await expect(page.locator('slice-defaultdeppage')).toBeAttached();
|
|
16
|
+
await expect(page.locator('slice-defaultdeppage')).toHaveAttribute('data-cfg-title', 'Configured');
|
|
17
|
+
await expect(page.locator('slice-defaultdeppage')).toHaveAttribute(
|
|
18
|
+
'data-cfg-tagline',
|
|
19
|
+
'default-export-works'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.describe('component CSS application', () => {
|
|
27
|
+
test('/cssprobe applies the bundled component stylesheet', async ({ page }) => {
|
|
28
|
+
const errors = trackErrors(page);
|
|
29
|
+
await page.goto('/cssprobe');
|
|
30
|
+
|
|
31
|
+
const marker = page.locator('slice-cssprobepage .css-probe-marker');
|
|
32
|
+
await expect(marker).toBeVisible();
|
|
33
|
+
await expect(marker).toHaveCSS('color', 'rgb(7, 113, 219)');
|
|
34
|
+
await expect(marker).toHaveCSS('font-weight', '700');
|
|
35
|
+
|
|
36
|
+
expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test.describe('transitive dependency of a shared module', () => {
|
|
41
|
+
// mid.js (imported by the component) itself imports leaf.js. The bundler must
|
|
42
|
+
// recursively inline leaf.js and bind its exports inside mid's scope, or the
|
|
43
|
+
// page breaks at runtime.
|
|
44
|
+
test('/transitive renders and the transitively-imported helper works', async ({ page }) => {
|
|
45
|
+
const errors = trackErrors(page);
|
|
46
|
+
await page.goto('/transitive');
|
|
47
|
+
|
|
48
|
+
await expect(page.locator('slice-transitivepage')).toBeAttached();
|
|
49
|
+
await expect(page.locator('slice-transitivepage')).toHaveAttribute(
|
|
50
|
+
'data-transitive',
|
|
51
|
+
'mid(leaf-value)[leaf:leaf-value]'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export default class FetchManager {
|
|
2
|
+
constructor(props) {
|
|
3
|
+
const { baseUrl, timeout } = props;
|
|
4
|
+
if (baseUrl !== undefined) {
|
|
5
|
+
this.baseUrl = baseUrl;
|
|
6
|
+
}
|
|
7
|
+
this.methods = ['GET', 'POST', 'PUT', 'DELETE'];
|
|
8
|
+
this.lastRequest = null;
|
|
9
|
+
this.cacheEnabled = false;
|
|
10
|
+
this.defaultHeaders = {};
|
|
11
|
+
timeout ? (this.timeout = timeout) : (this.timeout = 10000);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async request(
|
|
15
|
+
method,
|
|
16
|
+
data,
|
|
17
|
+
endpoint,
|
|
18
|
+
onRequestSuccess,
|
|
19
|
+
onRequestError,
|
|
20
|
+
refetchOnError = false,
|
|
21
|
+
requestOptions = {}
|
|
22
|
+
) {
|
|
23
|
+
if (!this.methods.includes(method)) throw new Error('Invalid method');
|
|
24
|
+
if (data && typeof data !== 'object') throw new Error('Invalid data, not JSON');
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
|
|
27
|
+
let options;
|
|
28
|
+
if (method !== 'GET') {
|
|
29
|
+
options = {
|
|
30
|
+
method: method,
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
...this.defaultHeaders,
|
|
34
|
+
...requestOptions.headers,
|
|
35
|
+
},
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
options = {
|
|
40
|
+
method: method,
|
|
41
|
+
headers: {
|
|
42
|
+
...this.defaultHeaders,
|
|
43
|
+
...requestOptions.headers,
|
|
44
|
+
},
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (data) {
|
|
50
|
+
options.body = JSON.stringify(data);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let loading;
|
|
54
|
+
if (!slice.controller.getComponent('Loading')) {
|
|
55
|
+
loading = await slice.build('Loading', { sliceId: 'Loading' });
|
|
56
|
+
} else {
|
|
57
|
+
loading = slice.controller.getComponent('Loading');
|
|
58
|
+
}
|
|
59
|
+
loading.start();
|
|
60
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout || 10000);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
let response;
|
|
64
|
+
|
|
65
|
+
// Check if cache is enabled and a cached response exists
|
|
66
|
+
if (this.cacheEnabled && this.lastRequest && this.lastRequest.endpoint === endpoint) {
|
|
67
|
+
loading.stop();
|
|
68
|
+
return this.lastRequest.output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.baseUrl !== undefined) {
|
|
72
|
+
response = await fetch(this.baseUrl + endpoint, options);
|
|
73
|
+
} else {
|
|
74
|
+
response = await fetch(endpoint, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
if (typeof onRequestSuccess === 'function') {
|
|
79
|
+
onRequestSuccess(data, response);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
if (typeof onRequestError === 'function') {
|
|
83
|
+
onRequestError(data, response);
|
|
84
|
+
}
|
|
85
|
+
if (refetchOnError) {
|
|
86
|
+
// Retry once on error; pass false to avoid infinite recursion when
|
|
87
|
+
// the endpoint keeps failing.
|
|
88
|
+
return await this.request(
|
|
89
|
+
method,
|
|
90
|
+
data,
|
|
91
|
+
endpoint,
|
|
92
|
+
onRequestSuccess,
|
|
93
|
+
onRequestError,
|
|
94
|
+
false,
|
|
95
|
+
requestOptions
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let output = await response.json();
|
|
101
|
+
loading.stop();
|
|
102
|
+
|
|
103
|
+
// Cache the parsed response if cache is enabled
|
|
104
|
+
if (this.cacheEnabled) {
|
|
105
|
+
this.lastRequest = { data, output, endpoint };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return output;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// slice.logger.logError signature is (componentSliceId, message, error).
|
|
111
|
+
if (error.message === 'Failed to fetch') {
|
|
112
|
+
slice.logger.logError('FetchManager', 'Lost internet connection', error);
|
|
113
|
+
} else {
|
|
114
|
+
slice.logger.logError('FetchManager', 'Request failed', error);
|
|
115
|
+
}
|
|
116
|
+
loading.stop();
|
|
117
|
+
throw error;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Enable or disable caching of responses
|
|
124
|
+
enableCache() {
|
|
125
|
+
this.cacheEnabled = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
disableCache() {
|
|
129
|
+
this.cacheEnabled = false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Set default headers for all requests
|
|
133
|
+
setDefaultHeaders(headers) {
|
|
134
|
+
this.defaultHeaders = headers;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export default class IndexedDbManager {
|
|
2
|
+
constructor(props = {}) {
|
|
3
|
+
// Slice builds services with a single props object, e.g.
|
|
4
|
+
// slice.build('IndexedDbManager', { databaseName, storeName }). The legacy
|
|
5
|
+
// positional form `new IndexedDbManager(dbName, storeName)` still works.
|
|
6
|
+
if (typeof props === 'string') {
|
|
7
|
+
this.databaseName = props;
|
|
8
|
+
this.storeName = arguments[1];
|
|
9
|
+
} else {
|
|
10
|
+
this.databaseName = props.databaseName;
|
|
11
|
+
this.storeName = props.storeName;
|
|
12
|
+
}
|
|
13
|
+
this.db = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async openDatabase() {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const request = indexedDB.open(this.databaseName);
|
|
19
|
+
|
|
20
|
+
request.onupgradeneeded = (event) => {
|
|
21
|
+
const db = event.target.result;
|
|
22
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
23
|
+
db.createObjectStore(this.storeName, {
|
|
24
|
+
keyPath: 'id',
|
|
25
|
+
autoIncrement: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
request.onsuccess = (event) => {
|
|
31
|
+
this.db = event.target.result;
|
|
32
|
+
resolve(this.db);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
request.onerror = (event) => {
|
|
36
|
+
reject(new Error(`Error opening IndexedDB: ${event.target.error}`));
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
closeDatabase() {
|
|
42
|
+
if (this.db) {
|
|
43
|
+
this.db.close();
|
|
44
|
+
this.db = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async addItem(item) {
|
|
49
|
+
const db = await this.openDatabase();
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
52
|
+
const store = transaction.objectStore(this.storeName);
|
|
53
|
+
const request = store.add(item);
|
|
54
|
+
|
|
55
|
+
request.onsuccess = () => {
|
|
56
|
+
resolve(request.result);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
request.onerror = (event) => {
|
|
60
|
+
reject(new Error(`Error adding item to IndexedDB: ${event.target.error}`));
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async updateItem(item) {
|
|
66
|
+
const db = await this.openDatabase();
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
69
|
+
const store = transaction.objectStore(this.storeName);
|
|
70
|
+
const request = store.put(item);
|
|
71
|
+
|
|
72
|
+
request.onsuccess = () => {
|
|
73
|
+
resolve(request.result);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
request.onerror = (event) => {
|
|
77
|
+
reject(new Error(`Error updating item in IndexedDB: ${event.target.error}`));
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getItem(id) {
|
|
83
|
+
const db = await this.openDatabase();
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
86
|
+
const store = transaction.objectStore(this.storeName);
|
|
87
|
+
const request = store.get(id);
|
|
88
|
+
|
|
89
|
+
request.onsuccess = () => {
|
|
90
|
+
resolve(request.result);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
request.onerror = (event) => {
|
|
94
|
+
reject(new Error(`Error getting item from IndexedDB: ${event.target.error}`));
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async deleteItem(id) {
|
|
100
|
+
const db = await this.openDatabase();
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
103
|
+
const store = transaction.objectStore(this.storeName);
|
|
104
|
+
const request = store.delete(id);
|
|
105
|
+
|
|
106
|
+
request.onsuccess = () => {
|
|
107
|
+
resolve();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
request.onerror = (event) => {
|
|
111
|
+
reject(new Error(`Error deleting item from IndexedDB: ${event.target.error}`));
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getAllItems() {
|
|
117
|
+
const db = await this.openDatabase();
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const transaction = db.transaction([this.storeName], 'readonly');
|
|
120
|
+
const store = transaction.objectStore(this.storeName);
|
|
121
|
+
const request = store.getAll();
|
|
122
|
+
|
|
123
|
+
request.onsuccess = () => {
|
|
124
|
+
resolve(request.result);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
request.onerror = (event) => {
|
|
128
|
+
reject(new Error(`Error getting items from IndexedDB: ${event.target.error}`));
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async clearItems() {
|
|
134
|
+
const db = await this.openDatabase();
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
137
|
+
const store = transaction.objectStore(this.storeName);
|
|
138
|
+
const request = store.clear();
|
|
139
|
+
|
|
140
|
+
request.onsuccess = () => {
|
|
141
|
+
resolve();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
request.onerror = (event) => {
|
|
145
|
+
reject(new Error(`Error clearing items in IndexedDB: ${event.target.error}`));
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|